fix(auth): only clear inFlightRefresh if it still points to the settling promise

This commit is contained in:
kolaente 2026-06-19 22:40:58 +02:00
parent 0949f4c854
commit 5ce9135eba
2 changed files with 39 additions and 3 deletions

View File

@ -120,4 +120,34 @@ describe('refreshToken in-flight dedup', () => {
expect(localStorage.getItem('token')).toBeNull() expect(localStorage.getItem('token')).toBeNull()
}) })
it('an older refresh settling does not clobber a newer in-flight one', async () => {
// Refresh A starts and stays in flight.
const pA = refreshToken(true)
expect(postCallCount).toBe(1)
const resolveA = resolvePost
// User logs out, which drops the in-flight reference to A.
removeToken()
// Refresh B starts; it must claim the in-flight slot.
const pB = refreshToken(true)
expect(postCallCount).toBe(2)
const resolveB = resolvePost
// A settles after B started. Its cleanup must NOT null the in-flight
// slot, since that slot now belongs to B. Without the `=== p` guard,
// A's .finally would clobber B and let a concurrent caller fire a
// second parallel POST.
resolveA?.({data: {token: FAKE_TOKEN}})
await pA
// A concurrent caller while B is still in flight must dedup to B —
// no third POST.
const pB2 = refreshToken(true)
expect(postCallCount).toBe(2)
resolveB?.({data: {token: FAKE_TOKEN}})
await Promise.all([pB, pB2])
})
}) })

View File

@ -63,10 +63,16 @@ export async function refreshToken(persist: boolean): Promise<void> {
if (inFlightRefresh) { if (inFlightRefresh) {
return inFlightRefresh return inFlightRefresh
} }
inFlightRefresh = doRefresh(persist).finally(() => { const p = doRefresh(persist)
inFlightRefresh = null inFlightRefresh = p
// Only clear if it still points to this promise — a logout (or a newer
// refresh started after it) may have replaced inFlightRefresh meanwhile.
p.finally(() => {
if (inFlightRefresh === p) {
inFlightRefresh = null
}
}) })
return inFlightRefresh return p
} }
async function doRefresh(persist: boolean): Promise<void> { async function doRefresh(persist: boolean): Promise<void> {