Skip to content

Commit

Permalink
feat: provide default storage when persistSession is false or `loca…
Browse files Browse the repository at this point in the history
…lStorage` 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.
  • Loading branch information
hf committed Sep 19, 2023
1 parent 1f41e5d commit 9324fa5
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 49 deletions.
67 changes: 27 additions & 40 deletions src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, Subscription> = new Map()
protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
protected visibilityChangedCallback: (() => Promise<any>) | null = null
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}

/**
Expand Down
25 changes: 23 additions & 2 deletions src/lib/local-storage.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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]
},
}
}
19 changes: 13 additions & 6 deletions test/GoTrueClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
3 changes: 2 additions & 1 deletion test/lib/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down

0 comments on commit 9324fa5

Please sign in to comment.