Skip to content

Commit b9b1d39

Browse files
committed
feat: introduce experimental split user and session storage
1 parent 9748dd9 commit b9b1d39

File tree

4 files changed

+122
-36
lines changed

4 files changed

+122
-36
lines changed

src/GoTrueClient.ts

+89-7
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ import {
3333
uuid,
3434
retryable,
3535
sleep,
36-
supportsLocalStorage,
3736
parseParametersFromURL,
3837
getCodeChallengeAndMethod,
38+
userNotAvailableProxy,
39+
supportsLocalStorage,
3940
} from './lib/helpers'
40-
import { localStorageAdapter, memoryLocalStorageAdapter } from './lib/local-storage'
41+
import { memoryLocalStorageAdapter } from './lib/local-storage'
4142
import { polyfillGlobalThis } from './lib/polyfills'
4243
import { version } from './lib/version'
4344
import { LockAcquireTimeoutError, navigatorLock } from './lib/locks'
@@ -97,7 +98,10 @@ import type {
9798

9899
polyfillGlobalThis() // Make "globalThis" available
99100

100-
const DEFAULT_OPTIONS: Omit<Required<GoTrueClientOptions>, 'fetch' | 'storage' | 'lock'> = {
101+
const DEFAULT_OPTIONS: Omit<
102+
Required<GoTrueClientOptions>,
103+
'fetch' | 'storage' | 'userStorage' | 'lock'
104+
> = {
101105
url: GOTRUE_URL,
102106
storageKey: STORAGE_KEY,
103107
autoRefreshToken: true,
@@ -144,6 +148,10 @@ export default class GoTrueClient {
144148
protected autoRefreshToken: boolean
145149
protected persistSession: boolean
146150
protected storage: SupportedStorage
151+
/**
152+
* @experimental
153+
*/
154+
protected userStorage: SupportedStorage | null = null
147155
protected memoryStorage: { [key: string]: string } | null = null
148156
protected stateChangeEmitters: Map<string, Subscription> = new Map()
149157
protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
@@ -236,12 +244,16 @@ export default class GoTrueClient {
236244
this.storage = settings.storage
237245
} else {
238246
if (supportsLocalStorage()) {
239-
this.storage = localStorageAdapter
247+
this.storage = globalThis.localStorage
240248
} else {
241249
this.memoryStorage = {}
242250
this.storage = memoryLocalStorageAdapter(this.memoryStorage)
243251
}
244252
}
253+
254+
if (settings.userStorage) {
255+
this.userStorage = settings.userStorage
256+
}
245257
} else {
246258
this.memoryStorage = {}
247259
this.storage = memoryLocalStorageAdapter(this.memoryStorage)
@@ -1119,7 +1131,20 @@ export default class GoTrueClient {
11191131
)
11201132

11211133
if (!hasExpired) {
1122-
if (this.storage.isServer) {
1134+
if (this.userStorage) {
1135+
const maybeUser: { user?: User | null } | null = (await getItemAsync(
1136+
this.userStorage,
1137+
this.storageKey + '-user'
1138+
)) as any
1139+
1140+
if (maybeUser?.user) {
1141+
currentSession.user = maybeUser.user
1142+
} else {
1143+
currentSession.user = userNotAvailableProxy()
1144+
}
1145+
}
1146+
1147+
if (this.storage.isServer && currentSession.user) {
11231148
let suppressWarning = this.suppressGetSessionWarning
11241149
const proxySession: Session = new Proxy(currentSession, {
11251150
get: (target: any, prop: string, receiver: any) => {
@@ -1911,7 +1936,47 @@ export default class GoTrueClient {
19111936
this._debug(debugName, 'begin')
19121937

19131938
try {
1914-
const currentSession = await getItemAsync(this.storage, this.storageKey)
1939+
const currentSession: Session = (await getItemAsync(this.storage, this.storageKey)) as any
1940+
1941+
if (this.userStorage) {
1942+
let maybeUser: { user: User | null } | null = (await getItemAsync(
1943+
this.userStorage,
1944+
this.storageKey + '-user'
1945+
)) as any
1946+
1947+
if (!this.storage.isServer && Object.is(this.storage, this.userStorage) && !maybeUser) {
1948+
// storage and userStorage are the same storage medium, for example
1949+
// window.localStorage if userStorage does not have the user from
1950+
// storage stored, store it first thereby migrating the user object
1951+
// from storage -> userStorage
1952+
1953+
maybeUser = { user: currentSession.user }
1954+
await setItemAsync(this.userStorage, this.storageKey + '-user', maybeUser)
1955+
}
1956+
1957+
currentSession.user = maybeUser?.user ?? userNotAvailableProxy()
1958+
} else if (currentSession && !currentSession.user) {
1959+
// user storage is not set, let's check if it was previously enabled so
1960+
// we bring back the storage as it should be
1961+
1962+
if (!currentSession.user) {
1963+
// test if userStorage was previously enabled and the storage medium was the same, to move the user back under the same key
1964+
const separateUser: { user: User | null } | null = (await getItemAsync(
1965+
this.storage,
1966+
this.storageKey + '-user'
1967+
)) as any
1968+
1969+
if (separateUser && separateUser?.user) {
1970+
currentSession.user = separateUser.user
1971+
1972+
await removeItemAsync(this.storage, this.storageKey + '-user')
1973+
await setItemAsync(this.storage, this.storageKey, currentSession)
1974+
} else {
1975+
currentSession.user = userNotAvailableProxy()
1976+
}
1977+
}
1978+
}
1979+
19151980
this._debug(debugName, 'session from storage', currentSession)
19161981

19171982
if (!this._isValidSession(currentSession)) {
@@ -2061,13 +2126,30 @@ export default class GoTrueClient {
20612126
// _saveSession is always called whenever a new session has been acquired
20622127
// so we can safely suppress the warning returned by future getSession calls
20632128
this.suppressGetSessionWarning = true
2064-
await setItemAsync(this.storage, this.storageKey, session)
2129+
2130+
if (this.userStorage) {
2131+
await setItemAsync(this.userStorage, this.storageKey + '-user', { user: session.user })
2132+
2133+
const clone = structuredClone(session) as any // cast intentional as we're deleting the `user` property of a required type below
2134+
delete clone.user
2135+
2136+
console.log('@@@@@@@@@@@@@@@@@@@@@@@', clone)
2137+
2138+
await setItemAsync(this.storage, this.storageKey, clone)
2139+
} else {
2140+
await setItemAsync(this.storage, this.storageKey, session)
2141+
}
20652142
}
20662143

20672144
private async _removeSession() {
20682145
this._debug('#_removeSession()')
20692146

20702147
await removeItemAsync(this.storage, this.storageKey)
2148+
2149+
if (this.userStorage) {
2150+
await removeItemAsync(this.storage, this.storageKey + '-user')
2151+
}
2152+
20712153
await this._notifyAllSubscribers('SIGNED_OUT', null)
20722154
}
20732155

src/lib/helpers.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { API_VERSION_HEADER_NAME } from './constants'
2-
import { SupportedStorage } from './types'
2+
import { SupportedStorage, User } from './types'
33

44
export function expiresAt(expiresIn: number) {
55
const timeNow = Math.round(Date.now() / 1000)
@@ -344,3 +344,23 @@ export function parseResponseAPIVersion(response: Response) {
344344
return null
345345
}
346346
}
347+
348+
export function userNotAvailableProxy(): User {
349+
return new Proxy({} as User, {
350+
get: (_target: any, prop: string) => {
351+
throw new Error(
352+
`@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Accessing the "${prop}" property of the session object is not supported. Please use getUser() instead.`
353+
)
354+
},
355+
set: (_target: any, prop: string) => {
356+
throw new Error(
357+
`@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Setting the "${prop}" property of the session object is not supported. Please use getUser() to fetch a user object you can manipulate.`
358+
)
359+
},
360+
deleteProperty: (_target: any, prop: string) => {
361+
throw new Error(
362+
`@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Deleting the "${prop}" property of the session object is not supported. Please use getUser() to fetch a user object you can manipulate.`
363+
)
364+
},
365+
})
366+
}

src/lib/local-storage.ts

-28
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,5 @@
1-
import { supportsLocalStorage } from './helpers'
21
import { SupportedStorage } from './types'
32

4-
/**
5-
* Provides safe access to the globalThis.localStorage property.
6-
*/
7-
export const localStorageAdapter: SupportedStorage = {
8-
getItem: (key) => {
9-
if (!supportsLocalStorage()) {
10-
return null
11-
}
12-
13-
return globalThis.localStorage.getItem(key)
14-
},
15-
setItem: (key, value) => {
16-
if (!supportsLocalStorage()) {
17-
return
18-
}
19-
20-
globalThis.localStorage.setItem(key, value)
21-
},
22-
removeItem: (key) => {
23-
if (!supportsLocalStorage()) {
24-
return
25-
}
26-
27-
globalThis.localStorage.removeItem(key)
28-
},
29-
}
30-
313
/**
324
* Returns a localStorage-like object that stores the key-value pairs in
335
* memory.

src/lib/types.ts

+12
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ export type GoTrueClientOptions = {
6969
persistSession?: boolean
7070
/* Provide your own local storage implementation to use instead of the browser's local storage. */
7171
storage?: SupportedStorage
72+
/**
73+
* Stores the user object in a separate storage location from the rest of the session data. When non-null, `storage` will only store a JSON object containing the access and refresh token and some adjacent metadata, while `userStorage` will only contain the user object under the key `storageKey + '-user'`.
74+
*
75+
* When this option is set and cookie storage is used, `getSession()` and other functions that load a session from the cookie store might not return back a user. It's very important to always use `getUser()` to fetch a user object in those scenarios.
76+
*
77+
* @experimental
78+
*/
79+
userStorage?: SupportedStorage
7280
/* A custom fetch implementation. */
7381
fetch?: Fetch
7482
/* If set to 'pkce' PKCE flow. Defaults to the 'implicit' flow otherwise */
@@ -252,6 +260,10 @@ export interface Session {
252260
*/
253261
expires_at?: number
254262
token_type: string
263+
264+
/**
265+
* When using a separate user storage, accessing properties of this object will throw an error.
266+
*/
255267
user: User
256268
}
257269

0 commit comments

Comments
 (0)