@@ -20,7 +20,9 @@ import { createSessionCookie } from './cookies/session';
2020import { getCookieSuffix } from './cookieSuffix' ;
2121import type { DevBrowser } from './devBrowser' ;
2222import { createDevBrowser } from './devBrowser' ;
23- import { SessionCookiePoller } from './SessionCookiePoller' ;
23+ import type { SafeLockReturn } from './safeLock' ;
24+ import { SafeLock } from './safeLock' ;
25+ import { REFRESH_SESSION_TOKEN_LOCK_KEY , SessionCookiePoller } from './SessionCookiePoller' ;
2426
2527// TODO(@dimkl): make AuthCookieService singleton since it handles updating cookies using a poller
2628// and we need to avoid updating them concurrently.
@@ -41,11 +43,12 @@ import { SessionCookiePoller } from './SessionCookiePoller';
4143 * - handleUnauthenticatedDevBrowser(): resets dev browser in case of invalid dev browser
4244 */
4345export class AuthCookieService {
44- private poller : SessionCookiePoller | null = null ;
45- private clientUat : ClientUatCookieHandler ;
46- private sessionCookie : SessionCookieHandler ;
4746 private activeCookie : ReturnType < typeof createCookieHandler > ;
47+ private clientUat : ClientUatCookieHandler ;
4848 private devBrowser : DevBrowser ;
49+ private poller : SessionCookiePoller | null = null ;
50+ private sessionCookie : SessionCookieHandler ;
51+ private tokenRefreshLock : SafeLockReturn ;
4952
5053 public static async create (
5154 clerk : Clerk ,
@@ -66,6 +69,11 @@ export class AuthCookieService {
6669 private instanceType : InstanceType ,
6770 private clerkEventBus : ReturnType < typeof createClerkEventBus > ,
6871 ) {
72+ // Create shared lock for cross-tab token refresh coordination.
73+ // This lock is used by both the poller and the focus handler to prevent
74+ // concurrent token fetches across tabs.
75+ this . tokenRefreshLock = SafeLock ( REFRESH_SESSION_TOKEN_LOCK_KEY ) ;
76+
6977 // set cookie on token update
7078 eventBus . on ( events . TokenUpdate , ( { token } ) => {
7179 this . updateSessionCookie ( token && token . getRawString ( ) ) ;
@@ -77,14 +85,14 @@ export class AuthCookieService {
7785 this . refreshTokenOnFocus ( ) ;
7886 this . startPollingForToken ( ) ;
7987
80- this . clientUat = createClientUatCookie ( cookieSuffix ) ;
81- this . sessionCookie = createSessionCookie ( cookieSuffix ) ;
8288 this . activeCookie = createActiveContextCookie ( ) ;
89+ this . clientUat = createClientUatCookie ( cookieSuffix ) ;
8390 this . devBrowser = createDevBrowser ( {
84- frontendApi : clerk . frontendApi ,
85- fapiClient,
8691 cookieSuffix,
92+ fapiClient,
93+ frontendApi : clerk . frontendApi ,
8794 } ) ;
95+ this . sessionCookie = createSessionCookie ( cookieSuffix ) ;
8896 }
8997
9098 public async setup ( ) {
@@ -126,7 +134,7 @@ export class AuthCookieService {
126134
127135 public startPollingForToken ( ) {
128136 if ( ! this . poller ) {
129- this . poller = new SessionCookiePoller ( ) ;
137+ this . poller = new SessionCookiePoller ( this . tokenRefreshLock ) ;
130138 this . poller . startPollingForSessionToken ( ( ) => this . refreshSessionToken ( ) ) ;
131139 }
132140 }
@@ -147,7 +155,11 @@ export class AuthCookieService {
147155 // is updated as part of the scheduled microtask. Our existing event-based mechanism to update the cookie schedules a task, and so the cookie
148156 // is updated too late and not guaranteed to be fresh before the refetch occurs.
149157 // While online `.schedule()` executes synchronously and immediately, ensuring the above mechanism will not break.
150- void this . refreshSessionToken ( { updateCookieImmediately : true } ) ;
158+ //
159+ // We use the shared lock to coordinate with the poller and other tabs, preventing
160+ // concurrent token fetches when multiple tabs become visible or when focus events
161+ // fire while the poller is already refreshing the token.
162+ void this . tokenRefreshLock . acquireLockAndRun ( ( ) => this . refreshSessionToken ( { updateCookieImmediately : true } ) ) ;
151163 }
152164 } ) ;
153165 }
0 commit comments