From 9324fa5d593edf9c26cfcbea61221c97ec75f376 Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Tue, 19 Sep 2023 18:37:55 +0200 Subject: [PATCH] feat: provide default storage when `persistSession` is false or `localStorage` is not supported (#774) Provides a default memory-based, per client, storage interface when the `persistSession` is set to false (which previously used a similar approach) or when the `storage` option is not set and the current environment is not a browser or has no `localStorage` registered on `globalThis`. With this PR, the warning that was often emitted in server-side environments is gone, as those environments now gracefully and safely fall back to using per-client memory storage. --- src/GoTrueClient.ts | 67 ++++++++++++++++----------------------- src/lib/local-storage.ts | 25 +++++++++++++-- test/GoTrueClient.test.ts | 19 +++++++---- test/lib/clients.ts | 3 +- 4 files changed, 65 insertions(+), 49 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 7fa4a9741..adaad53ab 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -30,7 +30,7 @@ import { supportsLocalStorage, parseParametersFromURL, } from './lib/helpers' -import localStorageAdapter from './lib/local-storage' +import { localStorageAdapter, memoryLocalStorageAdapter } from './lib/local-storage' import { polyfillGlobalThis } from './lib/polyfills' import { version } from './lib/version' import { LockAcquireTimeoutError } from './lib/locks' @@ -123,17 +123,12 @@ export default class GoTrueClient { */ protected storageKey: string - /** - * The session object for the currently logged in user. If null, it means there isn't a logged-in user. - * Only used if persistSession is false. - */ - protected inMemorySession: Session | null - protected flowType: AuthFlowType protected autoRefreshToken: boolean protected persistSession: boolean protected storage: SupportedStorage + protected memoryStorage: { [key: string]: string } | null = null protected stateChangeEmitters: Map = new Map() protected autoRefreshTicker: ReturnType | null = null protected visibilityChangedCallback: (() => Promise) | null = null @@ -183,11 +178,9 @@ export default class GoTrueClient { this.logger = settings.debug } - this.inMemorySession = null + this.persistSession = settings.persistSession this.storageKey = settings.storageKey this.autoRefreshToken = settings.autoRefreshToken - this.persistSession = settings.persistSession - this.storage = settings.storage || localStorageAdapter this.admin = new GoTrueAdminApi({ url: settings.url, headers: settings.headers, @@ -211,11 +204,20 @@ export default class GoTrueClient { getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this), } - if (this.persistSession && this.storage === localStorageAdapter && !supportsLocalStorage()) { - console.warn( - `No storage option exists to persist the session, which may result in unexpected behavior when using auth. - If you want to set persistSession to true, please provide a storage option or you may set persistSession to false to disable this warning.` - ) + if (this.persistSession) { + if (settings.storage) { + this.storage = settings.storage + } else { + if (supportsLocalStorage()) { + this.storage = localStorageAdapter + } else { + this.memoryStorage = {} + this.storage = memoryLocalStorageAdapter(this.memoryStorage) + } + } + } else { + this.memoryStorage = {} + this.storage = memoryLocalStorageAdapter(this.memoryStorage) } if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) { @@ -970,22 +972,17 @@ export default class GoTrueClient { try { let currentSession: Session | null = null - if (this.persistSession) { - const maybeSession = await getItemAsync(this.storage, this.storageKey) + const maybeSession = await getItemAsync(this.storage, this.storageKey) - this._debug('#getSession()', 'session from storage', maybeSession) + this._debug('#getSession()', 'session from storage', maybeSession) - if (maybeSession !== null) { - if (this._isValidSession(maybeSession)) { - currentSession = maybeSession - } else { - this._debug('#getSession()', 'session from storage is not valid') - await this._removeSession() - } + if (maybeSession !== null) { + if (this._isValidSession(maybeSession)) { + currentSession = maybeSession + } else { + this._debug('#getSession()', 'session from storage is not valid') + await this._removeSession() } - } else { - currentSession = this.inMemorySession - this._debug('#getSession()', 'session from memory', currentSession) } if (!currentSession) { @@ -1787,13 +1784,7 @@ export default class GoTrueClient { private async _saveSession(session: Session) { this._debug('#_saveSession()', session) - if (!this.persistSession) { - this.inMemorySession = session - } - - if (this.persistSession && session.expires_at) { - await this._persistSession(session) - } + await this._persistSession(session) } private _persistSession(currentSession: Session) { @@ -1805,11 +1796,7 @@ export default class GoTrueClient { private async _removeSession() { this._debug('#_removeSession()') - if (this.persistSession) { - await removeItemAsync(this.storage, this.storageKey) - } else { - this.inMemorySession = null - } + await removeItemAsync(this.storage, this.storageKey) } /** diff --git a/src/lib/local-storage.ts b/src/lib/local-storage.ts index ceea1aed1..d103f5649 100644 --- a/src/lib/local-storage.ts +++ b/src/lib/local-storage.ts @@ -1,7 +1,10 @@ import { supportsLocalStorage } from './helpers' import { SupportedStorage } from './types' -const localStorageAdapter: SupportedStorage = { +/** + * Provides safe access to the globalThis.localStorage property. + */ +export const localStorageAdapter: SupportedStorage = { getItem: (key) => { if (!supportsLocalStorage()) { return null @@ -25,4 +28,22 @@ const localStorageAdapter: SupportedStorage = { }, } -export default localStorageAdapter +/** + * Returns a localStorage-like object that stores the key-value pairs in + * memory. + */ +export function memoryLocalStorageAdapter(store: { [key: string]: string } = {}): SupportedStorage { + return { + getItem: (key) => { + return store[key] || null + }, + + setItem: (key, value) => { + store[key] = value + }, + + removeItem: (key) => { + delete store[key] + }, + } +} diff --git a/test/GoTrueClient.test.ts b/test/GoTrueClient.test.ts index 86d0e539f..b2ae4efd8 100644 --- a/test/GoTrueClient.test.ts +++ b/test/GoTrueClient.test.ts @@ -177,12 +177,19 @@ describe('GoTrueClient', () => { expired.setMinutes(expired.getMinutes() - 1) const expiredSeconds = Math.floor(expired.getTime() / 1000) - // @ts-expect-error 'Allow access to protected inMemorySession' - authWithSession.inMemorySession = { - // @ts-expect-error 'Allow access to protected inMemorySession' - ...authWithSession.inMemorySession, - expires_at: expiredSeconds, - } + // @ts-expect-error 'Allow access to protected storage' + const storage = authWithSession.storage + + // @ts-expect-error 'Allow access to protected storageKey' + const storageKey = authWithSession.storageKey + + await storage.setItem( + storageKey, + JSON.stringify({ + ...JSON.parse((await storage.getItem(storageKey)) || 'null'), + expires_at: expiredSeconds, + }) + ) // wait 1 seconds before calling getSession() await new Promise((r) => setTimeout(r, 1000)) diff --git a/test/lib/clients.ts b/test/lib/clients.ts index 7295f810e..e042b7212 100644 --- a/test/lib/clients.ts +++ b/test/lib/clients.ts @@ -46,7 +46,8 @@ export const authClient = new GoTrueClient({ export const authClientWithSession = new GoTrueClient({ url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, autoRefreshToken: false, - persistSession: false, + persistSession: true, + storage: new MemoryStorage(), }) export const authSubscriptionClient = new GoTrueClient({