From 3b506d3c07caa19d9ba73c1c467173e5203bffb7 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Wed, 4 May 2022 11:49:50 +0800 Subject: [PATCH 01/10] fix: localStorage sync --- src/GoTrueClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index c6b79c4a0..4e6a2fa6d 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -184,7 +184,7 @@ export default class GoTrueClient { * @param password The user's password. * @param refreshToken A valid refresh token that was returned on login. * @param provider One of the providers supported by GoTrue. - * @param redirectTo A URL to send the user to after they are confirmed (OAuth logins only). + * @param redirectTo A URL to send the user to after they are confirmed (OAuth logins only). * @param shouldCreateUser A boolean flag to indicate whether to automatically create a user on magiclink / otp sign-ins if the user doesn't exist. Defaults to true. * @param scopes A space-separated list of scopes granted to the OAuth application. */ @@ -404,7 +404,7 @@ export default class GoTrueClient { ...this.currentSession, access_token, token_type: 'bearer', - user: this.user() + user: this.user(), } this._notifyAllSubscribers('TOKEN_REFRESHED') @@ -752,7 +752,7 @@ export default class GoTrueClient { if (e.key === STORAGE_KEY) { const newSession = JSON.parse(String(e.newValue)) if (newSession?.currentSession?.access_token) { - this._recoverAndRefresh() + this._saveSession(newSession.currentSession) this._notifyAllSubscribers('SIGNED_IN') } else { this._removeSession() From 4284a0cf1c88d3d06fe529d9408fc4bc9fef0b65 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Wed, 4 May 2022 12:00:08 +0800 Subject: [PATCH 02/10] fix: recover on visibility change --- src/GoTrueClient.ts | 21 +++++++++++++++++---- src/lib/constants.ts | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 4e6a2fa6d..fe103a7de 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -1,6 +1,6 @@ import GoTrueApi from './GoTrueApi' import { isBrowser, getParameterByName, uuid } from './lib/helpers' -import { GOTRUE_URL, DEFAULT_HEADERS, STORAGE_KEY } from './lib/constants' +import { GOTRUE_URL, DEFAULT_HEADERS, STORAGE_KEY, EXPIRY_MARGIN } from './lib/constants' import { polyfillGlobalThis } from './lib/polyfills' import { Fetch } from './lib/fetch' @@ -101,6 +101,7 @@ export default class GoTrueClient { this._recoverSession() this._recoverAndRefresh() this._listenForMultiTabEvents() + this._handleVisibilityChange() if (settings.detectSessionInUrl && isBrowser() && !!getParameterByName('access_token')) { // Handle the OAuth redirect @@ -619,7 +620,7 @@ export default class GoTrueClient { const { currentSession, expiresAt } = data const timeNow = Math.round(Date.now() / 1000) - if (expiresAt >= timeNow && currentSession?.user) { + if (expiresAt >= timeNow + EXPIRY_MARGIN && currentSession?.user) { this._saveSession(currentSession) this._notifyAllSubscribers('SIGNED_IN') } @@ -643,7 +644,7 @@ export default class GoTrueClient { const { currentSession, expiresAt } = data const timeNow = Math.round(Date.now() / 1000) - if (expiresAt < timeNow) { + if (expiresAt < timeNow + EXPIRY_MARGIN) { if (this.autoRefreshToken && currentSession.refresh_token) { const { error } = await this._callRefreshToken(currentSession.refresh_token) if (error) { @@ -703,7 +704,7 @@ export default class GoTrueClient { if (expiresAt) { const timeNow = Math.round(Date.now() / 1000) const expiresIn = expiresAt - timeNow - const refreshDurationBeforeExpires = expiresIn > 60 ? 60 : 0.5 + const refreshDurationBeforeExpires = expiresIn > EXPIRY_MARGIN ? EXPIRY_MARGIN : 0.5 this._startAutoRefreshToken((expiresIn - refreshDurationBeforeExpires) * 1000) } @@ -764,4 +765,16 @@ export default class GoTrueClient { console.error('_listenForMultiTabEvents', error) } } + + private _handleVisibilityChange() { + try { + window?.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + this._recoverAndRefresh() + } + }) + } catch (error) { + console.error('_handleVisibilityChange', error) + } + } } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 667eeeeac..cb485af93 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -2,7 +2,7 @@ import { version } from './version' export const GOTRUE_URL = 'http://localhost:9999' export const AUDIENCE = '' export const DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version}` } -export const EXPIRY_MARGIN = 60 * 1000 +export const EXPIRY_MARGIN = 60 // in seconds export const STORAGE_KEY = 'supabase.auth.token' export const COOKIE_OPTIONS = { name: 'sb', From c64d8a21985fe4c39cb26cb6e6c28a4686bc1aa1 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Wed, 4 May 2022 12:11:26 +0800 Subject: [PATCH 03/10] fix: retry when failed to fetch --- src/GoTrueClient.ts | 18 ++++++++++++++++-- src/lib/constants.ts | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index fe103a7de..b5b5069cb 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -1,6 +1,12 @@ import GoTrueApi from './GoTrueApi' import { isBrowser, getParameterByName, uuid } from './lib/helpers' -import { GOTRUE_URL, DEFAULT_HEADERS, STORAGE_KEY, EXPIRY_MARGIN } from './lib/constants' +import { + GOTRUE_URL, + DEFAULT_HEADERS, + STORAGE_KEY, + EXPIRY_MARGIN, + OFFLINE_RETRY_INTERVAL, +} from './lib/constants' import { polyfillGlobalThis } from './lib/polyfills' import { Fetch } from './lib/fetch' @@ -649,6 +655,10 @@ export default class GoTrueClient { const { error } = await this._callRefreshToken(currentSession.refresh_token) if (error) { console.log(error.message) + if (error.message === 'Failed to fetch') { + setTimeout(() => this._recoverAndRefresh(), OFFLINE_RETRY_INTERVAL * 1000) + return + } await this._removeSession() } } else { @@ -735,7 +745,11 @@ export default class GoTrueClient { if (this.refreshTokenTimer) clearTimeout(this.refreshTokenTimer) if (value <= 0 || !this.autoRefreshToken) return - this.refreshTokenTimer = setTimeout(() => this._callRefreshToken(), value) + this.refreshTokenTimer = setTimeout(async () => { + const { error } = await this._callRefreshToken() + if (error?.message === 'Failed to fetch') + this._startAutoRefreshToken(OFFLINE_RETRY_INTERVAL * 1000) + }, value) if (typeof this.refreshTokenTimer.unref === 'function') this.refreshTokenTimer.unref() } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index cb485af93..b556082fb 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -3,6 +3,7 @@ export const GOTRUE_URL = 'http://localhost:9999' export const AUDIENCE = '' export const DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version}` } export const EXPIRY_MARGIN = 60 // in seconds +export const OFFLINE_RETRY_INTERVAL = 5 // in seconds export const STORAGE_KEY = 'supabase.auth.token' export const COOKIE_OPTIONS = { name: 'sb', From e7822f90cc80a132a64d6ab0031bb88d19bbdbea Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Wed, 4 May 2022 12:54:20 +0800 Subject: [PATCH 04/10] chore: clearTimeout for retires --- src/GoTrueClient.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index b5b5069cb..bcb196f4e 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -656,7 +656,11 @@ export default class GoTrueClient { if (error) { console.log(error.message) if (error.message === 'Failed to fetch') { - setTimeout(() => this._recoverAndRefresh(), OFFLINE_RETRY_INTERVAL * 1000) + if (this.refreshTokenTimer) clearTimeout(this.refreshTokenTimer) + this.refreshTokenTimer = setTimeout( + () => this._recoverAndRefresh(), + OFFLINE_RETRY_INTERVAL * 1000 + ) return } await this._removeSession() From c8c53d7141fcc228d7da77a33603611f10e4ec18 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Thu, 5 May 2022 15:10:38 +0800 Subject: [PATCH 05/10] fix: more robust network failure handling --- src/GoTrueClient.ts | 11 ++++++----- src/lib/constants.ts | 3 ++- src/lib/fetch.ts | 24 ++++++++---------------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index bcb196f4e..567fddad5 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -5,7 +5,8 @@ import { DEFAULT_HEADERS, STORAGE_KEY, EXPIRY_MARGIN, - OFFLINE_RETRY_INTERVAL, + NETWORK_FAILURE, + NETWORK_FAILURE_RETRY_INTERVAL, } from './lib/constants' import { polyfillGlobalThis } from './lib/polyfills' import { Fetch } from './lib/fetch' @@ -655,11 +656,11 @@ export default class GoTrueClient { const { error } = await this._callRefreshToken(currentSession.refresh_token) if (error) { console.log(error.message) - if (error.message === 'Failed to fetch') { + if (error.message === NETWORK_FAILURE) { if (this.refreshTokenTimer) clearTimeout(this.refreshTokenTimer) this.refreshTokenTimer = setTimeout( () => this._recoverAndRefresh(), - OFFLINE_RETRY_INTERVAL * 1000 + NETWORK_FAILURE_RETRY_INTERVAL * 1000 ) return } @@ -751,8 +752,8 @@ export default class GoTrueClient { this.refreshTokenTimer = setTimeout(async () => { const { error } = await this._callRefreshToken() - if (error?.message === 'Failed to fetch') - this._startAutoRefreshToken(OFFLINE_RETRY_INTERVAL * 1000) + if (error?.message === NETWORK_FAILURE) + this._startAutoRefreshToken(NETWORK_FAILURE_RETRY_INTERVAL * 1000) }, value) if (typeof this.refreshTokenTimer.unref === 'function') this.refreshTokenTimer.unref() } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b556082fb..6c5c6d919 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -3,7 +3,8 @@ export const GOTRUE_URL = 'http://localhost:9999' export const AUDIENCE = '' export const DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version}` } export const EXPIRY_MARGIN = 60 // in seconds -export const OFFLINE_RETRY_INTERVAL = 5 // in seconds +export const NETWORK_FAILURE = 'NETWORK_FAILURE' +export const NETWORK_FAILURE_RETRY_INTERVAL = 1 // in seconds export const STORAGE_KEY = 'supabase.auth.token' export const COOKIE_OPTIONS = { name: 'sb', diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index 015d12336..bc6f23c67 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -1,3 +1,5 @@ +import { NETWORK_FAILURE } from './constants' + export type Fetch = typeof fetch export interface FetchOptions { @@ -9,21 +11,6 @@ export interface FetchOptions { export type RequestMethodType = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' -const _getErrorMessage = (err: any): string => - err.msg || err.message || err.error_description || err.error || JSON.stringify(err) - -const handleError = (error: any, reject: any) => { - if (typeof error.json !== 'function') { - return reject(error) - } - error.json().then((err: any) => { - return reject({ - message: _getErrorMessage(err), - status: error?.status || 500, - }) - }) -} - const _getRequestParams = (method: RequestMethodType, options?: FetchOptions, body?: object) => { const params: { [k: string]: any } = { method, headers: options?.headers || {} } @@ -52,7 +39,12 @@ async function _handleRequest( return result.json() }) .then((data) => resolve(data)) - .catch((error) => handleError(error, reject)) + .catch(() => + reject({ + message: NETWORK_FAILURE, + status: null, + }) + ) }) } From 0875ffa84827d699481199d13902032b7eb7cc15 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Thu, 5 May 2022 15:15:36 +0800 Subject: [PATCH 06/10] chore: default expiry margin to 10s --- src/lib/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6c5c6d919..d2d589376 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -2,7 +2,7 @@ import { version } from './version' export const GOTRUE_URL = 'http://localhost:9999' export const AUDIENCE = '' export const DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version}` } -export const EXPIRY_MARGIN = 60 // in seconds +export const EXPIRY_MARGIN = 10 // in seconds export const NETWORK_FAILURE = 'NETWORK_FAILURE' export const NETWORK_FAILURE_RETRY_INTERVAL = 1 // in seconds export const STORAGE_KEY = 'supabase.auth.token' From de9df53df0d48134ac6113fb9e6959d68034cce0 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Thu, 5 May 2022 15:31:51 +0800 Subject: [PATCH 07/10] chore: move localstorage acess to helpers --- src/GoTrueClient.ts | 40 ++++++++++++++-------------------------- src/lib/helpers.ts | 36 ++++++++++++++++++++++++++++++++++++ src/lib/types.ts | 11 +++++++++++ 3 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 567fddad5..4f0ed550b 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -1,5 +1,13 @@ import GoTrueApi from './GoTrueApi' -import { isBrowser, getParameterByName, uuid } from './lib/helpers' +import { + isBrowser, + getParameterByName, + uuid, + setItemAsync, + removeItemAsync, + getItemSynchronously, + getItemAsync, +} from './lib/helpers' import { GOTRUE_URL, DEFAULT_HEADERS, @@ -23,6 +31,7 @@ import type { UserCredentials, VerifyOTPParams, OpenIDConnectCredentials, + SupportedStorage, } from './lib/types' polyfillGlobalThis() // Make "globalThis" available @@ -36,17 +45,6 @@ const DEFAULT_OPTIONS = { headers: DEFAULT_HEADERS, } -type AnyFunction = (...args: any[]) => any -type MaybePromisify = T | Promise - -type PromisifyMethods = { - [K in keyof T]: T[K] extends AnyFunction - ? (...args: Parameters) => MaybePromisify> - : T[K] -} - -type SupportedStorage = PromisifyMethods> - export default class GoTrueClient { /** * Namespace for the GoTrue API methods. @@ -618,12 +616,7 @@ export default class GoTrueClient { */ private _recoverSession() { try { - const json = isBrowser() && this.localStorage?.getItem(STORAGE_KEY) - if (!json || typeof json !== 'string') { - return null - } - - const data = JSON.parse(json) + const data = getItemSynchronously(this.localStorage, STORAGE_KEY) const { currentSession, expiresAt } = data const timeNow = Math.round(Date.now() / 1000) @@ -642,12 +635,7 @@ export default class GoTrueClient { */ private async _recoverAndRefresh() { try { - const json = isBrowser() && (await this.localStorage.getItem(STORAGE_KEY)) - if (!json) { - return null - } - - const data = JSON.parse(json) + const data = await getItemAsync(this.localStorage, STORAGE_KEY) const { currentSession, expiresAt } = data const timeNow = Math.round(Date.now() / 1000) @@ -732,14 +720,14 @@ export default class GoTrueClient { private _persistSession(currentSession: Session) { const data = { currentSession, expiresAt: currentSession.expires_at } - isBrowser() && this.localStorage.setItem(STORAGE_KEY, JSON.stringify(data)) + setItemAsync(this.localStorage, STORAGE_KEY, data) } private async _removeSession() { this.currentSession = null this.currentUser = null if (this.refreshTokenTimer) clearTimeout(this.refreshTokenTimer) - isBrowser() && (await this.localStorage.removeItem(STORAGE_KEY)) + removeItemAsync(this.localStorage, STORAGE_KEY) } /** diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index c5b97e03e..85c7486b8 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -1,4 +1,5 @@ import crossFetch from 'cross-fetch' +import { SupportedStorage } from './types' export function expiresAt(expiresIn: number) { const timeNow = Math.round(Date.now() / 1000) @@ -39,3 +40,38 @@ export const resolveFetch = (customFetch?: Fetch): Fetch => { } return (...args) => _fetch(...args) } + +// LocalStorage helpers +export const setItemAsync = async ( + storage: SupportedStorage, + key: string, + data: any +): Promise => { + isBrowser() && (await storage?.setItem(key, JSON.stringify(data))) +} + +export const getItemAsync = async (storage: SupportedStorage, key: string): Promise => { + const value = isBrowser() && (await storage?.getItem(key)) + if (!value) return null + try { + return JSON.parse(value) + } catch { + return value + } +} + +export const getItemSynchronously = (storage: SupportedStorage, key: string): any | null => { + const value = isBrowser() && storage?.getItem(key) + if (!value || typeof value !== 'string') { + return null + } + try { + return JSON.parse(value) + } catch { + return value + } +} + +export const removeItemAsync = async (storage: SupportedStorage, key: string): Promise => { + isBrowser() && (await storage?.removeItem(key)) +} diff --git a/src/lib/types.ts b/src/lib/types.ts index ebb74a2fe..762795229 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -209,3 +209,14 @@ export interface OpenIDConnectCredentials { client_id?: string issuer?: string } + +type AnyFunction = (...args: any[]) => any +type MaybePromisify = T | Promise + +type PromisifyMethods = { + [K in keyof T]: T[K] extends AnyFunction + ? (...args: Parameters) => MaybePromisify> + : T[K] +} + +export type SupportedStorage = PromisifyMethods> From 2b24a23f38d888a54e073cc99d4c9c400471bac3 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Thu, 5 May 2022 15:44:53 +0800 Subject: [PATCH 08/10] fix: localstorage null --- src/GoTrueClient.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 4f0ed550b..2215a848e 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -617,6 +617,7 @@ export default class GoTrueClient { private _recoverSession() { try { const data = getItemSynchronously(this.localStorage, STORAGE_KEY) + if (!data) return null const { currentSession, expiresAt } = data const timeNow = Math.round(Date.now() / 1000) @@ -636,6 +637,7 @@ export default class GoTrueClient { private async _recoverAndRefresh() { try { const data = await getItemAsync(this.localStorage, STORAGE_KEY) + if (!data) return null const { currentSession, expiresAt } = data const timeNow = Math.round(Date.now() / 1000) From 4d253b688c5a2a6dc2c2006a48897e17a0fa8c49 Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Thu, 5 May 2022 17:06:34 +0800 Subject: [PATCH 09/10] fix: error handling --- src/GoTrueClient.ts | 5 ++++- src/lib/constants.ts | 2 +- src/lib/fetch.ts | 25 +++++++++++++++++++------ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 2215a848e..9eeb3526b 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -753,7 +753,6 @@ export default class GoTrueClient { */ private _listenForMultiTabEvents() { if (!this.multiTab || !isBrowser() || !window?.addEventListener) { - // console.debug('Auth multi-tab support is disabled.') return false } @@ -776,6 +775,10 @@ export default class GoTrueClient { } private _handleVisibilityChange() { + if (!this.multiTab || !isBrowser() || !window?.addEventListener) { + return false + } + try { window?.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index d2d589376..3fd771868 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -3,7 +3,7 @@ export const GOTRUE_URL = 'http://localhost:9999' export const AUDIENCE = '' export const DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version}` } export const EXPIRY_MARGIN = 10 // in seconds -export const NETWORK_FAILURE = 'NETWORK_FAILURE' +export const NETWORK_FAILURE = 'Request Failed' export const NETWORK_FAILURE_RETRY_INTERVAL = 1 // in seconds export const STORAGE_KEY = 'supabase.auth.token' export const COOKIE_OPTIONS = { diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index bc6f23c67..93f332593 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -11,6 +11,24 @@ export interface FetchOptions { export type RequestMethodType = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' +const _getErrorMessage = (err: any): string => + err.msg || err.message || err.error_description || err.error || JSON.stringify(err) + +const handleError = (error: any, reject: any) => { + if (!error?.status) { + return reject({ message: NETWORK_FAILURE }) + } + if (typeof error.json !== 'function') { + return reject(error) + } + error.json().then((err: any) => { + return reject({ + message: _getErrorMessage(err), + status: error?.status || 500, + }) + }) +} + const _getRequestParams = (method: RequestMethodType, options?: FetchOptions, body?: object) => { const params: { [k: string]: any } = { method, headers: options?.headers || {} } @@ -39,12 +57,7 @@ async function _handleRequest( return result.json() }) .then((data) => resolve(data)) - .catch(() => - reject({ - message: NETWORK_FAILURE, - status: null, - }) - ) + .catch((error) => handleError(error, reject)) }) } From 9b651039ee0a35699b2cdee53e64caf48208a0ad Mon Sep 17 00:00:00 2001 From: thorwebdev Date: Thu, 5 May 2022 18:31:47 +0800 Subject: [PATCH 10/10] chore: exponential backoff --- src/GoTrueClient.ts | 20 +++++++++++++++----- src/lib/constants.ts | 7 +++++-- src/lib/fetch.ts | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 9eeb3526b..bdb1c3c8a 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -14,7 +14,6 @@ import { STORAGE_KEY, EXPIRY_MARGIN, NETWORK_FAILURE, - NETWORK_FAILURE_RETRY_INTERVAL, } from './lib/constants' import { polyfillGlobalThis } from './lib/polyfills' import { Fetch } from './lib/fetch' @@ -66,6 +65,7 @@ export default class GoTrueClient { protected multiTab: boolean protected stateChangeEmitters: Map = new Map() protected refreshTokenTimer?: ReturnType + protected networkRetries: number = 0 /** * Create a new client for use in the browser. @@ -643,19 +643,24 @@ export default class GoTrueClient { if (expiresAt < timeNow + EXPIRY_MARGIN) { if (this.autoRefreshToken && currentSession.refresh_token) { + this.networkRetries++ const { error } = await this._callRefreshToken(currentSession.refresh_token) if (error) { console.log(error.message) - if (error.message === NETWORK_FAILURE) { + if ( + error.message === NETWORK_FAILURE.ERROR_MESSAGE && + this.networkRetries < NETWORK_FAILURE.MAX_RETRIES + ) { if (this.refreshTokenTimer) clearTimeout(this.refreshTokenTimer) this.refreshTokenTimer = setTimeout( () => this._recoverAndRefresh(), - NETWORK_FAILURE_RETRY_INTERVAL * 1000 + NETWORK_FAILURE.RETRY_INTERVAL ** this.networkRetries * 100 // exponential backoff ) return } await this._removeSession() } + this.networkRetries = 0 } else { this._removeSession() } @@ -741,9 +746,14 @@ export default class GoTrueClient { if (value <= 0 || !this.autoRefreshToken) return this.refreshTokenTimer = setTimeout(async () => { + this.networkRetries++ const { error } = await this._callRefreshToken() - if (error?.message === NETWORK_FAILURE) - this._startAutoRefreshToken(NETWORK_FAILURE_RETRY_INTERVAL * 1000) + if (!error) this.networkRetries = 0 + if ( + error?.message === NETWORK_FAILURE.ERROR_MESSAGE && + this.networkRetries < NETWORK_FAILURE.MAX_RETRIES + ) + this._startAutoRefreshToken(NETWORK_FAILURE.RETRY_INTERVAL ** this.networkRetries * 100) // exponential backoff }, value) if (typeof this.refreshTokenTimer.unref === 'function') this.refreshTokenTimer.unref() } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 3fd771868..d8bdd5bb7 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -3,8 +3,11 @@ export const GOTRUE_URL = 'http://localhost:9999' export const AUDIENCE = '' export const DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version}` } export const EXPIRY_MARGIN = 10 // in seconds -export const NETWORK_FAILURE = 'Request Failed' -export const NETWORK_FAILURE_RETRY_INTERVAL = 1 // in seconds +export const NETWORK_FAILURE = { + ERROR_MESSAGE: 'Request Failed', + MAX_RETRIES: 10, + RETRY_INTERVAL: 2, // in deciseconds +} export const STORAGE_KEY = 'supabase.auth.token' export const COOKIE_OPTIONS = { name: 'sb', diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index 93f332593..68d24e528 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -16,7 +16,7 @@ const _getErrorMessage = (err: any): string => const handleError = (error: any, reject: any) => { if (!error?.status) { - return reject({ message: NETWORK_FAILURE }) + return reject({ message: NETWORK_FAILURE.ERROR_MESSAGE }) } if (typeof error.json !== 'function') { return reject(error)