Skip to content

Commit adf971b

Browse files
committed
feat(GoTrueClient): Move Lock into separate classe
- Improve code organization and testability - Optimize promise handling with Promise.allSettled - Simplify lock and queue management logic - Maintain existing functionality with minor improvements
1 parent 9bdc023 commit adf971b

File tree

3 files changed

+87
-94
lines changed

3 files changed

+87
-94
lines changed

src/GoTrueClient.ts

+21-94
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import type {
108108
JwtHeader,
109109
} from './lib/types'
110110
import { stringToUint8Array } from './lib/base64url'
111+
import { LockClient } from './lib/lock-client'
111112

112113
polyfillGlobalThis() // Make "globalThis" available
113114

@@ -175,9 +176,7 @@ export default class GoTrueClient {
175176
protected hasCustomAuthorizationHeader = false
176177
protected suppressGetSessionWarning = false
177178
protected fetch: Fetch
178-
protected lock: LockFunc
179-
protected lockAcquired = false
180-
protected pendingInLock: Promise<any>[] = []
179+
protected lock: LockClient
181180

182181
/**
183182
* Used to broadcast state change events to other tabs listening.
@@ -219,17 +218,16 @@ export default class GoTrueClient {
219218
this.url = settings.url
220219
this.headers = settings.headers
221220
this.fetch = resolveFetch(settings.fetch)
222-
this.lock = settings.lock || lockNoOp
223221
this.detectSessionInUrl = settings.detectSessionInUrl
224222
this.flowType = settings.flowType
225223
this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader
226224

227225
if (settings.lock) {
228-
this.lock = settings.lock
226+
this.lock = new LockClient(settings.lock, settings.storageKey, this._debug)
229227
} else if (isBrowser() && globalThis?.navigator?.locks) {
230-
this.lock = navigatorLock
228+
this.lock = new LockClient(navigatorLock, settings.storageKey, this._debug)
231229
} else {
232-
this.lock = lockNoOp
230+
this.lock = new LockClient(lockNoOp, settings.storageKey, this._debug)
233231
}
234232
this.jwks = { keys: [] }
235233
this.jwks_cached_at = Number.MIN_SAFE_INTEGER
@@ -301,7 +299,7 @@ export default class GoTrueClient {
301299
}
302300

303301
this.initializePromise = (async () => {
304-
return await this._acquireLock(-1, async () => {
302+
return await this.lock.acquireLock(-1, async () => {
305303
return await this._initialize()
306304
})
307305
})()
@@ -596,7 +594,7 @@ export default class GoTrueClient {
596594
async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
597595
await this.initializePromise
598596

599-
return this._acquireLock(-1, async () => {
597+
return this.lock.acquireLock(-1, async () => {
600598
return this._exchangeCodeForSession(authCode)
601599
})
602600
}
@@ -863,7 +861,7 @@ export default class GoTrueClient {
863861
async reauthenticate(): Promise<AuthResponse> {
864862
await this.initializePromise
865863

866-
return await this._acquireLock(-1, async () => {
864+
return await this.lock.acquireLock(-1, async () => {
867865
return await this._reauthenticate()
868866
})
869867
}
@@ -947,7 +945,7 @@ export default class GoTrueClient {
947945
async getSession() {
948946
await this.initializePromise
949947

950-
const result = await this._acquireLock(-1, async () => {
948+
const result = await this.lock.acquireLock(-1, async () => {
951949
return this._useSession(async (result) => {
952950
return result
953951
})
@@ -956,77 +954,6 @@ export default class GoTrueClient {
956954
return result
957955
}
958956

959-
/**
960-
* Acquires a global lock based on the storage key.
961-
*/
962-
private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
963-
this._debug('#_acquireLock', 'begin', acquireTimeout)
964-
965-
try {
966-
if (this.lockAcquired) {
967-
const last = this.pendingInLock.length
968-
? this.pendingInLock[this.pendingInLock.length - 1]
969-
: Promise.resolve()
970-
971-
const result = (async () => {
972-
await last
973-
return await fn()
974-
})()
975-
976-
this.pendingInLock.push(
977-
(async () => {
978-
try {
979-
await result
980-
} catch (e: any) {
981-
// we just care if it finished
982-
}
983-
})()
984-
)
985-
986-
return result
987-
}
988-
989-
return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
990-
this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
991-
992-
try {
993-
this.lockAcquired = true
994-
995-
const result = fn()
996-
997-
this.pendingInLock.push(
998-
(async () => {
999-
try {
1000-
await result
1001-
} catch (e: any) {
1002-
// we just care if it finished
1003-
}
1004-
})()
1005-
)
1006-
1007-
await result
1008-
1009-
// keep draining the queue until there's nothing to wait on
1010-
while (this.pendingInLock.length) {
1011-
const waitOn = [...this.pendingInLock]
1012-
1013-
await Promise.all(waitOn)
1014-
1015-
this.pendingInLock.splice(0, waitOn.length)
1016-
}
1017-
1018-
return await result
1019-
} finally {
1020-
this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
1021-
1022-
this.lockAcquired = false
1023-
}
1024-
})
1025-
} finally {
1026-
this._debug('#_acquireLock', 'end')
1027-
}
1028-
}
1029-
1030957
/**
1031958
* Use instead of {@link #getSession} inside the library. It is
1032959
* semantically usually what you want, as getting a session involves some
@@ -1095,7 +1022,7 @@ export default class GoTrueClient {
10951022
> {
10961023
this._debug('#__loadSession()', 'begin')
10971024

1098-
if (!this.lockAcquired) {
1025+
if (!this.lock.lockAcquired) {
10991026
this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
11001027
}
11011028

@@ -1182,7 +1109,7 @@ export default class GoTrueClient {
11821109

11831110
await this.initializePromise
11841111

1185-
const result = await this._acquireLock(-1, async () => {
1112+
const result = await this.lock.acquireLock(-1, async () => {
11861113
return await this._getUser()
11871114
})
11881115

@@ -1244,7 +1171,7 @@ export default class GoTrueClient {
12441171
): Promise<UserResponse> {
12451172
await this.initializePromise
12461173

1247-
return await this._acquireLock(-1, async () => {
1174+
return await this.lock.acquireLock(-1, async () => {
12481175
return await this._updateUser(attributes, options)
12491176
})
12501177
}
@@ -1311,7 +1238,7 @@ export default class GoTrueClient {
13111238
}): Promise<AuthResponse> {
13121239
await this.initializePromise
13131240

1314-
return await this._acquireLock(-1, async () => {
1241+
return await this.lock.acquireLock(-1, async () => {
13151242
return await this._setSession(currentSession)
13161243
})
13171244
}
@@ -1383,7 +1310,7 @@ export default class GoTrueClient {
13831310
async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
13841311
await this.initializePromise
13851312

1386-
return await this._acquireLock(-1, async () => {
1313+
return await this.lock.acquireLock(-1, async () => {
13871314
return await this._refreshSession(currentSession)
13881315
})
13891316
}
@@ -1590,7 +1517,7 @@ export default class GoTrueClient {
15901517
async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
15911518
await this.initializePromise
15921519

1593-
return await this._acquireLock(-1, async () => {
1520+
return await this.lock.acquireLock(-1, async () => {
15941521
return await this._signOut(options)
15951522
})
15961523
}
@@ -1653,7 +1580,7 @@ export default class GoTrueClient {
16531580
;(async () => {
16541581
await this.initializePromise
16551582

1656-
await this._acquireLock(-1, async () => {
1583+
await this.lock.acquireLock(-1, async () => {
16571584
this._emitInitialSession(id)
16581585
})
16591586
})()
@@ -2197,7 +2124,7 @@ export default class GoTrueClient {
21972124
this._debug('#_autoRefreshTokenTick()', 'begin')
21982125

21992126
try {
2200-
await this._acquireLock(0, async () => {
2127+
await this.lock.acquireLock(0, async () => {
22012128
try {
22022129
const now = Date.now()
22032130

@@ -2296,7 +2223,7 @@ export default class GoTrueClient {
22962223
// the lock first asynchronously
22972224
await this.initializePromise
22982225

2299-
await this._acquireLock(-1, async () => {
2226+
await this.lock.acquireLock(-1, async () => {
23002227
if (document.visibilityState !== 'visible') {
23012228
this._debug(
23022229
methodName,
@@ -2432,7 +2359,7 @@ export default class GoTrueClient {
24322359
* {@see GoTrueMFAApi#verify}
24332360
*/
24342361
private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
2435-
return this._acquireLock(-1, async () => {
2362+
return this.lock.acquireLock(-1, async () => {
24362363
try {
24372364
return await this._useSession(async (result) => {
24382365
const { data: sessionData, error: sessionError } = result
@@ -2475,7 +2402,7 @@ export default class GoTrueClient {
24752402
* {@see GoTrueMFAApi#challenge}
24762403
*/
24772404
private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
2478-
return this._acquireLock(-1, async () => {
2405+
return this.lock.acquireLock(-1, async () => {
24792406
try {
24802407
return await this._useSession(async (result) => {
24812408
const { data: sessionData, error: sessionError } = result
@@ -2561,7 +2488,7 @@ export default class GoTrueClient {
25612488
* {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
25622489
*/
25632490
private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
2564-
return this._acquireLock(-1, async () => {
2491+
return this.lock.acquireLock(-1, async () => {
25652492
return await this._useSession(async (result) => {
25662493
const {
25672494
data: { session },

src/lib/lock-client.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { LockFunc } from './types'
2+
3+
export class LockClient {
4+
private pendingInLock: Promise<any>[] = []
5+
constructor(
6+
private lock: LockFunc,
7+
private storageKey: string = '',
8+
private _debug: (...args: any[]) => void
9+
) {}
10+
11+
/**
12+
* status of the lock
13+
*/
14+
public lockAcquired = false
15+
16+
/**
17+
* Acquires a global lock based on the storage key.
18+
*/
19+
public async acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
20+
this._debug('#_acquireLock', 'begin', acquireTimeout)
21+
22+
try {
23+
if (this.lockAcquired) return this._handleExistingLock(fn)
24+
return this._handleNewLock(acquireTimeout, fn)
25+
} finally {
26+
this._debug('#_acquireLock', 'end')
27+
}
28+
}
29+
30+
private async _handleExistingLock<R>(fn: () => Promise<R>): Promise<R> {
31+
const lastPending = this.pendingInLock[this.pendingInLock.length - 1] || Promise.resolve()
32+
await lastPending
33+
return await fn()
34+
}
35+
36+
private async _handleNewLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
37+
return this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
38+
this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
39+
40+
try {
41+
this.lockAcquired = true
42+
const result = fn()
43+
44+
// make sure fn is the last for this batch
45+
this.pendingInLock.push(result)
46+
await this._drainPendingQueue()
47+
48+
// result have already completed, just unwrap the promise now.
49+
return await result
50+
} finally {
51+
this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
52+
this.lockAcquired = false
53+
}
54+
})
55+
}
56+
57+
private async _drainPendingQueue(): Promise<void> {
58+
while (this.pendingInLock.length) {
59+
const batch = [...this.pendingInLock]
60+
// guaranteed that promise will be completed with either resolved or rejected
61+
await Promise.allSettled(batch)
62+
this.pendingInLock.splice(0, batch.length)
63+
}
64+
}
65+
}

tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"rootDir": "src",
99
"sourceMap": true,
1010
"target": "ES2017",
11+
"lib": ["es2020", "DOM"],
1112

1213
"strict": true,
1314

0 commit comments

Comments
 (0)