Skip to content

Commit ae4f1de

Browse files
authored
Merge pull request #339 from pixtron/fix/issue-334
fix: resolve & reset deferred upon refresh error
2 parents 35827fc + 828b0b3 commit ae4f1de

File tree

3 files changed

+111
-16
lines changed

3 files changed

+111
-16
lines changed

src/GoTrueClient.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import type {
4444
SignInWithPasswordlessCredentials,
4545
AuthResponse,
4646
OAuthResponse,
47+
CallRefreshTokenResult,
4748
} from './lib/types'
4849

4950
polyfillGlobalThis() // Make "globalThis" available
@@ -82,10 +83,7 @@ export default class GoTrueClient {
8283
protected refreshTokenTimer?: ReturnType<typeof setTimeout>
8384
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
8485
protected networkRetries: number = 0
85-
protected refreshingDeferred: Deferred<{
86-
session: Session
87-
error: null
88-
}> | null = null
86+
protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
8987

9088
/**
9189
* Create a new client for use in the browser.
@@ -779,17 +777,14 @@ export default class GoTrueClient {
779777
}
780778
}
781779

782-
private async _callRefreshToken(refreshToken: string) {
783-
try {
784-
// refreshing is already in progress
785-
if (this.refreshingDeferred) {
786-
return await this.refreshingDeferred.promise
787-
}
780+
private async _callRefreshToken(refreshToken: string): Promise<CallRefreshTokenResult> {
781+
// refreshing is already in progress
782+
if (this.refreshingDeferred) {
783+
return this.refreshingDeferred.promise
784+
}
788785

789-
this.refreshingDeferred = new Deferred<{
790-
session: Session
791-
error: null
792-
}>()
786+
try {
787+
this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
793788

794789
if (!refreshToken) {
795790
throw new AuthSessionMissingError()
@@ -804,15 +799,21 @@ export default class GoTrueClient {
804799
const result = { session, error: null }
805800

806801
this.refreshingDeferred.resolve(result)
807-
this.refreshingDeferred = null
808802

809803
return result
810804
} catch (error) {
811805
if (isAuthError(error)) {
812-
return { session: null, error }
806+
const result = { session: null, error }
807+
808+
this.refreshingDeferred?.resolve(result)
809+
810+
return result
813811
}
814812

813+
this.refreshingDeferred?.reject(error)
815814
throw error
815+
} finally {
816+
this.refreshingDeferred = null
816817
}
817818
}
818819

src/lib/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,13 @@ type PromisifyMethods<T> = {
272272
}
273273

274274
export type SupportedStorage = PromisifyMethods<Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>>
275+
276+
export type CallRefreshTokenResult =
277+
| {
278+
session: Session
279+
error: null
280+
}
281+
| {
282+
session: null
283+
error: AuthError
284+
}

test/GoTrueClient.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AuthError } from '../src/lib/errors'
12
import {
23
authClient as auth,
34
authClientWithSession as authWithSession,
@@ -114,6 +115,89 @@ describe('GoTrueClient', () => {
114115
expect(refreshAccessTokenSpy).toBeCalledTimes(1)
115116
})
116117

118+
test('_callRefreshToken() should resolve all pending refresh requests and reset deferred upon AuthError', async () => {
119+
const { email, password } = mockUserCredentials()
120+
refreshAccessTokenSpy.mockImplementationOnce(() =>
121+
Promise.resolve({
122+
session: null,
123+
error: new AuthError('Something did not work as expected'),
124+
})
125+
)
126+
127+
const { error, session } = await authWithSession.signUp({
128+
email,
129+
password,
130+
})
131+
132+
expect(error).toBeNull()
133+
expect(session).not.toBeNull()
134+
135+
const [{ session: session1, error: error1 }, { session: session2, error: error2 }] =
136+
await Promise.all([
137+
// @ts-expect-error 'Allow access to private _callRefreshToken()'
138+
authWithSession._callRefreshToken(session?.refresh_token),
139+
// @ts-expect-error 'Allow access to private _callRefreshToken()'
140+
authWithSession._callRefreshToken(session?.refresh_token),
141+
])
142+
143+
expect(error1).toHaveProperty('message')
144+
expect(error2).toHaveProperty('message')
145+
expect(session1).toBeNull()
146+
expect(session2).toBeNull()
147+
148+
expect(refreshAccessTokenSpy).toBeCalledTimes(1)
149+
150+
// verify the deferred has been reset and successive calls can be made
151+
// @ts-expect-error 'Allow access to private _callRefreshToken()'
152+
const { session: session3, error: error3 } = await authWithSession._callRefreshToken(
153+
session!.refresh_token
154+
)
155+
156+
expect(error3).toBeNull()
157+
expect(session3).toHaveProperty('access_token')
158+
})
159+
160+
test('_callRefreshToken() should reject all pending refresh requests and reset deferred upon any non AuthError', async () => {
161+
const mockError = new Error('Something did not work as expected')
162+
163+
const { email, password } = mockUserCredentials()
164+
refreshAccessTokenSpy.mockImplementationOnce(() => Promise.reject(mockError))
165+
166+
const { error, session } = await authWithSession.signUp({
167+
email,
168+
password,
169+
})
170+
171+
expect(error).toBeNull()
172+
expect(session).not.toBeNull()
173+
174+
const [error1, error2] =
175+
await Promise.allSettled([
176+
// @ts-expect-error 'Allow access to private _callRefreshToken()'
177+
authWithSession._callRefreshToken(session?.refresh_token),
178+
// @ts-expect-error 'Allow access to private _callRefreshToken()'
179+
authWithSession._callRefreshToken(session?.refresh_token),
180+
])
181+
182+
expect(error1.status).toEqual('rejected')
183+
expect(error2.status).toEqual('rejected')
184+
185+
// status === 'rejected' above makes sure it is a PromiseRejectedResult
186+
expect((error1 as PromiseRejectedResult).reason).toEqual(mockError)
187+
expect((error1 as PromiseRejectedResult).reason).toEqual(mockError)
188+
189+
expect(refreshAccessTokenSpy).toBeCalledTimes(1)
190+
191+
// vreify the deferred has been reset and successive calls can be made
192+
// @ts-expect-error 'Allow access to private _callRefreshToken()'
193+
const { session: session3, error: error3 } = await authWithSession._callRefreshToken(
194+
session!.refresh_token
195+
)
196+
197+
expect(error3).toBeNull()
198+
expect(session3).toHaveProperty('access_token')
199+
})
200+
117201
test('getSessionFromUrl() can only be called from a browser', async () => {
118202
const { error, session } = await authWithSession.getSessionFromUrl()
119203

0 commit comments

Comments
 (0)