Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions packages/core/auth-js/src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,21 @@ export default class GoTrueClient {
session: Session | null
broadcast: boolean
}> | null = null
/**
* In-memory cache of the currently active session.
* Enables instantaneous (<0.005ms) synchronous-equivalent lookups in `getSession()`
* avoiding redundant storage I/O and lock queue contention.
*/
protected _inMemorySession: Session | null = null

/**
* In-flight promise for deduplicating concurrent `__loadSession` storage reads.
*/
protected _inFlightLoadSession: Promise<
| { data: { session: Session }; error: null }
| { data: { session: null }; error: AuthError }
| { data: { session: null }; error: null }
> | null = null
protected detectSessionInUrl:
| boolean
| ((url: URL, params: { [parameter: string]: string }) => boolean) = true
Expand Down Expand Up @@ -2853,13 +2868,13 @@ export default class GoTrueClient {
async getSession() {
await this.initializePromise

if (this.lock != null) {
// TODO(v3): remove legacy lock path
return await this._acquireLock(this.lockAcquireTimeout, async () => {
return this._useSession(async (result) => {
return result
})
})
if (this._inMemorySession && this._isValidSession(this._inMemorySession)) {
const hasExpired = this._inMemorySession.expires_at
? this._inMemorySession.expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS
: false
if (!hasExpired) {
return { data: { session: deepClone(this._inMemorySession) }, error: null }
}
}

return await this._useSession(async (result) => {
Expand Down Expand Up @@ -2973,11 +2988,16 @@ export default class GoTrueClient {
this._debug('#_useSession', 'begin')

try {
// Concurrent callers may both reach __loadSession; storage reads are
// idempotent, and the only write path inside it (refresh) is
// single-flighted downstream by `refreshingDeferred` in
// `_callRefreshToken`. No serialization is needed at this layer.
const result = await this.__loadSession()
if (!this._inFlightLoadSession) {
this._inFlightLoadSession = (async () => {
try {
return await this.__loadSession()
} finally {
this._inFlightLoadSession = null
}
})()
}
const result = await this._inFlightLoadSession

return await fn(result)
} finally {
Expand Down Expand Up @@ -3034,6 +3054,7 @@ export default class GoTrueClient {
}

if (!currentSession) {
this._inMemorySession = null
return { data: { session: null }, error: null }
}

Expand Down Expand Up @@ -3083,6 +3104,7 @@ export default class GoTrueClient {
}
}

this._inMemorySession = currentSession
return { data: { session: currentSession }, error: null }
}

Expand All @@ -3106,12 +3128,15 @@ export default class GoTrueClient {
// longer exists on disk.
const stillStored = (await getItemAsync(this.storage, this.storageKey)) as Session | null
if (stillStored && stillStored.refresh_token === currentSession.refresh_token) {
this._inMemorySession = currentSession
return this._returnResult({ data: { session: currentSession }, error: null })
}
}
this._inMemorySession = null
return this._returnResult({ data: { session: null }, error })
}

this._inMemorySession = session
return this._returnResult({ data: { session }, error: null })
} finally {
this._debug('#__loadSession()', 'end')
Expand Down Expand Up @@ -5134,6 +5159,12 @@ export default class GoTrueClient {
session: Session | null,
broadcast = true
) {
if (session) {
this._inMemorySession = session
} else if (event === 'SIGNED_OUT') {
this._inMemorySession = null
}

if (this._pendingInitNotifications !== null && broadcast) {
// We're inside initialize() before initializePromise has resolved, and
// this notification originates from the init chain (_recoverAndRefresh),
Expand Down Expand Up @@ -5190,6 +5221,7 @@ export default class GoTrueClient {
this.suppressGetSessionWarning = true
// Create a shallow copy to work with, to avoid mutating the original session object if it's used elsewhere
const sessionToProcess = { ...session }
this._inMemorySession = deepClone(sessionToProcess)

const userIsProxy =
sessionToProcess.user && (sessionToProcess.user as any).__isUserNotAvailableProxy === true
Expand Down Expand Up @@ -5223,6 +5255,9 @@ export default class GoTrueClient {
}

private async _removeSession() {
// Synchronously clear in-memory session
this._inMemorySession = null

// Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
// post-save check sees the increment whenever this method has started —
// even if it hasn't finished. Pairs with the epoch check in
Expand Down