Skip to content

feat: Move Lock-related logic to LockManager class #1040

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
115 changes: 21 additions & 94 deletions src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import type {
JwtHeader,
} from './lib/types'
import { stringToUint8Array } from './lib/base64url'
import { LockClient } from './lib/lock-client'

polyfillGlobalThis() // Make "globalThis" available

Expand Down Expand Up @@ -175,9 +176,7 @@ export default class GoTrueClient {
protected hasCustomAuthorizationHeader = false
protected suppressGetSessionWarning = false
protected fetch: Fetch
protected lock: LockFunc
protected lockAcquired = false
protected pendingInLock: Promise<any>[] = []
protected lock: LockClient

/**
* Used to broadcast state change events to other tabs listening.
Expand Down Expand Up @@ -219,17 +218,16 @@ export default class GoTrueClient {
this.url = settings.url
this.headers = settings.headers
this.fetch = resolveFetch(settings.fetch)
this.lock = settings.lock || lockNoOp
this.detectSessionInUrl = settings.detectSessionInUrl
this.flowType = settings.flowType
this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader

if (settings.lock) {
this.lock = settings.lock
this.lock = new LockClient(settings.lock, settings.storageKey, this._debug)
} else if (isBrowser() && globalThis?.navigator?.locks) {
this.lock = navigatorLock
this.lock = new LockClient(navigatorLock, settings.storageKey, this._debug)
} else {
this.lock = lockNoOp
this.lock = new LockClient(lockNoOp, settings.storageKey, this._debug)
}
this.jwks = { keys: [] }
this.jwks_cached_at = Number.MIN_SAFE_INTEGER
Expand Down Expand Up @@ -301,7 +299,7 @@ export default class GoTrueClient {
}

this.initializePromise = (async () => {
return await this._acquireLock(-1, async () => {
return await this.lock.acquireLock(-1, async () => {
return await this._initialize()
})
})()
Expand Down Expand Up @@ -596,7 +594,7 @@ export default class GoTrueClient {
async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
await this.initializePromise

return this._acquireLock(-1, async () => {
return this.lock.acquireLock(-1, async () => {
return this._exchangeCodeForSession(authCode)
})
}
Expand Down Expand Up @@ -863,7 +861,7 @@ export default class GoTrueClient {
async reauthenticate(): Promise<AuthResponse> {
await this.initializePromise

return await this._acquireLock(-1, async () => {
return await this.lock.acquireLock(-1, async () => {
return await this._reauthenticate()
})
}
Expand Down Expand Up @@ -947,7 +945,7 @@ export default class GoTrueClient {
async getSession() {
await this.initializePromise

const result = await this._acquireLock(-1, async () => {
const result = await this.lock.acquireLock(-1, async () => {
return this._useSession(async (result) => {
return result
})
Expand All @@ -956,77 +954,6 @@ export default class GoTrueClient {
return result
}

/**
* Acquires a global lock based on the storage key.
*/
private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
this._debug('#_acquireLock', 'begin', acquireTimeout)

try {
if (this.lockAcquired) {
const last = this.pendingInLock.length
? this.pendingInLock[this.pendingInLock.length - 1]
: Promise.resolve()

const result = (async () => {
await last
return await fn()
})()

this.pendingInLock.push(
(async () => {
try {
await result
} catch (e: any) {
// we just care if it finished
}
})()
)

return result
}

return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)

try {
this.lockAcquired = true

const result = fn()

this.pendingInLock.push(
(async () => {
try {
await result
} catch (e: any) {
// we just care if it finished
}
})()
)

await result

// keep draining the queue until there's nothing to wait on
while (this.pendingInLock.length) {
const waitOn = [...this.pendingInLock]

await Promise.all(waitOn)

this.pendingInLock.splice(0, waitOn.length)
}

return await result
} finally {
this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)

this.lockAcquired = false
}
})
} finally {
this._debug('#_acquireLock', 'end')
}
}

/**
* Use instead of {@link #getSession} inside the library. It is
* semantically usually what you want, as getting a session involves some
Expand Down Expand Up @@ -1095,7 +1022,7 @@ export default class GoTrueClient {
> {
this._debug('#__loadSession()', 'begin')

if (!this.lockAcquired) {
if (!this.lock.lockAcquired) {
this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
}

Expand Down Expand Up @@ -1182,7 +1109,7 @@ export default class GoTrueClient {

await this.initializePromise

const result = await this._acquireLock(-1, async () => {
const result = await this.lock.acquireLock(-1, async () => {
return await this._getUser()
})

Expand Down Expand Up @@ -1244,7 +1171,7 @@ export default class GoTrueClient {
): Promise<UserResponse> {
await this.initializePromise

return await this._acquireLock(-1, async () => {
return await this.lock.acquireLock(-1, async () => {
return await this._updateUser(attributes, options)
})
}
Expand Down Expand Up @@ -1311,7 +1238,7 @@ export default class GoTrueClient {
}): Promise<AuthResponse> {
await this.initializePromise

return await this._acquireLock(-1, async () => {
return await this.lock.acquireLock(-1, async () => {
return await this._setSession(currentSession)
})
}
Expand Down Expand Up @@ -1383,7 +1310,7 @@ export default class GoTrueClient {
async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
await this.initializePromise

return await this._acquireLock(-1, async () => {
return await this.lock.acquireLock(-1, async () => {
return await this._refreshSession(currentSession)
})
}
Expand Down Expand Up @@ -1590,7 +1517,7 @@ export default class GoTrueClient {
async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
await this.initializePromise

return await this._acquireLock(-1, async () => {
return await this.lock.acquireLock(-1, async () => {
return await this._signOut(options)
})
}
Expand Down Expand Up @@ -1653,7 +1580,7 @@ export default class GoTrueClient {
;(async () => {
await this.initializePromise

await this._acquireLock(-1, async () => {
await this.lock.acquireLock(-1, async () => {
this._emitInitialSession(id)
})
})()
Expand Down Expand Up @@ -2197,7 +2124,7 @@ export default class GoTrueClient {
this._debug('#_autoRefreshTokenTick()', 'begin')

try {
await this._acquireLock(0, async () => {
await this.lock.acquireLock(0, async () => {
try {
const now = Date.now()

Expand Down Expand Up @@ -2296,7 +2223,7 @@ export default class GoTrueClient {
// the lock first asynchronously
await this.initializePromise

await this._acquireLock(-1, async () => {
await this.lock.acquireLock(-1, async () => {
if (document.visibilityState !== 'visible') {
this._debug(
methodName,
Expand Down Expand Up @@ -2432,7 +2359,7 @@ export default class GoTrueClient {
* {@see GoTrueMFAApi#verify}
*/
private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
return this._acquireLock(-1, async () => {
return this.lock.acquireLock(-1, async () => {
try {
return await this._useSession(async (result) => {
const { data: sessionData, error: sessionError } = result
Expand Down Expand Up @@ -2475,7 +2402,7 @@ export default class GoTrueClient {
* {@see GoTrueMFAApi#challenge}
*/
private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
return this._acquireLock(-1, async () => {
return this.lock.acquireLock(-1, async () => {
try {
return await this._useSession(async (result) => {
const { data: sessionData, error: sessionError } = result
Expand Down Expand Up @@ -2561,7 +2488,7 @@ export default class GoTrueClient {
* {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
*/
private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
return this._acquireLock(-1, async () => {
return this.lock.acquireLock(-1, async () => {
return await this._useSession(async (result) => {
const {
data: { session },
Expand Down
64 changes: 64 additions & 0 deletions src/lib/lock-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { LockFunc } from './types'

export class LockClient {
private pendingInLock: Promise<any>[] = []
constructor(
private lock: LockFunc,
private storageKey: string = '',
private _debug: (...args: any[]) => void
) {}

/**
* status of the lock
*/
public lockAcquired = false

/**
* Acquires a global lock based on the storage key.
*/
public async acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
this._debug('#_acquireLock', 'begin', acquireTimeout)

try {
if (this.lockAcquired) return this._handleExistingLock(fn)
return this._handleNewLock(acquireTimeout, fn)
} finally {
this._debug('#_acquireLock', 'end')
}
}

private async _handleExistingLock<R>(fn: () => Promise<R>): Promise<R> {
await this._drainPendingQueue()
return await fn()
}

private async _handleNewLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
return this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)

try {
this.lockAcquired = true
const result = fn()

// make sure fn is the last for this batch
this.pendingInLock.push(result)
await this._drainPendingQueue()

// result have already completed, just unwrap the promise now.
return await result
} finally {
this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
this.lockAcquired = false
}
})
}

private async _drainPendingQueue(): Promise<void> {
while (this.pendingInLock.length) {
const batch = [...this.pendingInLock]
// guaranteed that promise will be completed with either resolved or rejected
await Promise.allSettled(batch)
this.pendingInLock.splice(0, batch.length)
}
}
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"rootDir": "src",
"sourceMap": true,
"target": "ES2017",
"lib": ["es2020", "DOM"],

"strict": true,

Expand Down