@@ -38,6 +38,7 @@ let cachedSessionTokenWasAuthenticated = false
3838let cachedRateLimit : RateLimitInfo | null = null
3939let remainingBeforeRequest : number | null = null
4040let refreshInFlight : Promise < void > | null = null
41+ let sessionCacheGeneration = 0
4142
4243function dispatchRateLimitUpdate ( ) : void {
4344 if ( typeof window !== 'undefined' ) {
@@ -90,6 +91,7 @@ function surfaceHourlyLimit(parsedError: ServerErrorBody | null): never {
9091// through the opaque path.
9192async function fetchChatJWT (
9293 authBearer : string ,
94+ cacheGeneration : number ,
9395) : Promise < { key : string ; expiresAt : number | null } | null > {
9496 let response : Response
9597 try {
@@ -124,6 +126,7 @@ async function fetchChatJWT(
124126
125127 const parsedError = parseErrorBody ( await response . text ( ) )
126128 if ( isHourlyLimit ( response . status , parsedError ) ) {
129+ if ( cacheGeneration !== sessionCacheGeneration ) return null
127130 surfaceHourlyLimit ( parsedError )
128131 }
129132 return null
@@ -134,6 +137,8 @@ async function fetchSessionToken(): Promise<string> {
134137 return DEV_API_KEY
135138 }
136139
140+ const cacheGeneration = sessionCacheGeneration
141+
137142 // If the user was previously signed in, wait for Clerk to initialize
138143 // the auth token manager before fetching — otherwise we'd get an
139144 // anonymous free-tier key that gets cached until expiry.
@@ -164,6 +169,9 @@ async function fetchSessionToken(): Promise<string> {
164169 )
165170 }
166171 }
172+ if ( cacheGeneration !== sessionCacheGeneration ) {
173+ return fetchSessionToken ( )
174+ }
167175 const usedAuthHeader = authBearer !== null
168176
169177 // If the cached token was fetched anonymously but we now have an
@@ -196,7 +204,10 @@ async function fetchSessionToken(): Promise<string> {
196204 // Anonymous users (and signed-in users without an active subscription) fall
197205 // back to the opaque /api/keys/chat path below.
198206 if ( authBearer ) {
199- const jwt = await fetchChatJWT ( authBearer )
207+ const jwt = await fetchChatJWT ( authBearer , cacheGeneration )
208+ if ( cacheGeneration !== sessionCacheGeneration ) {
209+ return fetchSessionToken ( )
210+ }
200211 if ( jwt !== null ) {
201212 cachedSessionToken = jwt . key
202213 cachedSessionTokenWasAuthenticated = true
@@ -216,9 +227,15 @@ async function fetchSessionToken(): Promise<string> {
216227 const response = await fetch ( `${ API_BASE_URL } /api/keys/chat` , {
217228 headers,
218229 } )
230+ if ( cacheGeneration !== sessionCacheGeneration ) {
231+ return fetchSessionToken ( )
232+ }
219233
220234 if ( ! response . ok ) {
221235 const errorText = await response . text ( )
236+ if ( cacheGeneration !== sessionCacheGeneration ) {
237+ return fetchSessionToken ( )
238+ }
222239 logError ( 'Failed to fetch session token from server' , undefined , {
223240 component : 'tinfoil-client' ,
224241 action : 'fetchSessionToken' ,
@@ -242,6 +259,9 @@ async function fetchSessionToken(): Promise<string> {
242259 }
243260
244261 const data = await response . json ( )
262+ if ( cacheGeneration !== sessionCacheGeneration ) {
263+ return fetchSessionToken ( )
264+ }
245265 cachedSessionToken = data . key
246266 cachedSessionTokenWasAuthenticated = usedAuthHeader
247267 if ( data . expires_at ) {
@@ -295,14 +315,16 @@ export function snapshotAndDecrementRemaining(): void {
295315export async function refreshRateLimit ( ) : Promise < void > {
296316 if ( refreshInFlight ) return refreshInFlight
297317
298- refreshInFlight = ( async ( ) => {
318+ const refresh = ( async ( ) => {
319+ const refreshGeneration = sessionCacheGeneration
299320 const snapshot = remainingBeforeRequest
300321 remainingBeforeRequest = null
301322 cachedSessionToken = null
302323 cachedSessionTokenExpiresAt = null
303324 try {
304325 await fetchSessionToken ( )
305326 if (
327+ refreshGeneration === sessionCacheGeneration &&
306328 snapshot !== null &&
307329 cachedRateLimit &&
308330 cachedRateLimit . remaining >= snapshot
@@ -318,31 +340,38 @@ export async function refreshRateLimit(): Promise<void> {
318340 component : 'tinfoil-client' ,
319341 action : 'refreshRateLimit' ,
320342 } )
321- } finally {
322- refreshInFlight = null
323343 }
324344 } ) ( )
345+ refreshInFlight = refresh
325346
326- return refreshInFlight
347+ try {
348+ await refresh
349+ } finally {
350+ if ( refreshInFlight === refresh ) {
351+ refreshInFlight = null
352+ }
353+ }
327354}
328355
329356export function resetTinfoilClient ( ) : void {
357+ sessionCacheGeneration ++
330358 clientInstance = null
331359 secureClient = null
332360 lastSessionToken = null
333361 cachedSessionToken = null
334362 cachedSessionTokenExpiresAt = null
335363 cachedSessionTokenWasAuthenticated = false
336- remainingBeforeRequest = null
337364 cachedRateLimit = null
338365 remainingBeforeRequest = null
339366 refreshInFlight = null
340367}
341368
342369export function invalidateSessionCache ( ) : void {
370+ sessionCacheGeneration ++
343371 cachedSessionToken = null
344372 cachedSessionTokenExpiresAt = null
345373 cachedSessionTokenWasAuthenticated = false
374+ remainingBeforeRequest = null
346375 if ( cachedRateLimit !== null ) {
347376 cachedRateLimit = null
348377 dispatchRateLimitUpdate ( )
0 commit comments