Skip to content

Conversation

@jacekradko
Copy link
Member

@jacekradko jacekradko commented Nov 24, 2025

Description

Fix race condition where multiple browser tabs could fetch session tokens simultaneously. The refreshTokenOnFocus handler now uses the same cross-tab lock as the session token poller, preventing duplicate API calls when switching between tabs or when focus events fire while another tab is already refreshing the token.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Bug Fixes

    • Token refreshes now coordinate across browser tabs to prevent duplicate refreshes; if locking fails, refresh proceeds in a safe degraded mode with warnings.
  • Refactor

    • Centralized cross-tab coordination and simplified polling to avoid redundant local locking.
  • Tests

    • Added comprehensive tests for lock coordination, polling start/stop behavior, and polling intervals.
  • Chores

    • Added a changeset documenting this patch release.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Nov 24, 2025

🦋 Changeset detected

Latest commit: 4a6769d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@clerk/clerk-js Patch
@clerk/chrome-extension Patch
@clerk/clerk-expo Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Nov 24, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
clerk-js-sandbox Ready Ready Preview Comment Nov 26, 2025 1:37am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Walkthrough

Implements cross-tab SafeLock with timeouts and degraded fallback; adds per-token locks to Session token fetching to prevent duplicate refreshes; removes locking from SessionCookiePoller; reorders AuthCookieService initialization; adds LruMap and tests for SafeLock, SessionCookiePoller, and LruMap; adds a changeset for the patch.

Changes

Cohort / File(s) Summary
Changeset
/.changeset/fix-token-refresh-race-condition.md
Adds a changeset documenting a patch release fixing multi-tab token refresh races via cross-tab locking.
SafeLock implementation & tests
packages/clerk-js/src/core/auth/safeLock.ts, packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts
Introduces LOCK_TIMEOUT_MS, navigator.locks path with AbortController timeout, fallback to browser-tabs-lock with timeout, degraded-mode execution with warning logs when locks time out/unavailable, beforeunload cleanup, and a typed acquireLockAndRun<T> API; tests cover Web Locks path, shared-lock sequencing, and degraded behavior.
Session token locking
packages/clerk-js/src/core/resources/Session.ts
Adds per-token LruMap of SafeLock instances via getTokenLock, fast-path cache check, lock-wrapped fetch with double-checking cache after lock, immediate caching of in-flight promise to dedupe concurrent requests, and preserved event emissions/logging around token resolution.
SessionCookiePoller simplification & tests
packages/clerk-js/src/core/auth/SessionCookiePoller.ts, packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
Removes internal SafeLock usage and lock key constant; poller now directly invokes refresh callback (coordination handled in Session.getToken). Adds tests for start/stop behavior, interval scheduling, and callback sequencing using fake timers.
AuthCookieService tweaks
packages/clerk-js/src/core/auth/AuthCookieService.ts
Reorders private member initialization (moves clientUatCookie and sessionCookie after activeCookie), swaps argument order when initializing dev browser, and adjusts constructor initialization ordering.
LruMap implementation & tests
packages/clerk-js/src/utils/lru-map.ts, packages/clerk-js/src/utils/__tests__/lru-map.test.ts
Adds LruMap<K,V> extending Map with LRU eviction by maxSize; overrides get and set to maintain recency and evict oldest entries when full. Tests validate eviction, recency updates, and edge cases.

Sequence Diagram(s)

sequenceDiagram
    participant TabA as Browser Tab A
    participant TabB as Browser Tab B
    participant Lock as SafeLock (per-token)
    participant API as Token API

    rect rgb(245,250,255)
    note over TabA,TabB: Both tabs request same token (poller/focus/user)
    TabA->>Lock: acquireLockAndRun(tokenId, refreshCb)
    TabB->>Lock: acquireLockAndRun(tokenId, refreshCb)
    end

    rect rgb(220,255,220)
    note over Lock,API: TabA obtains lock, performs refresh
    Lock->>TabA: lock granted
    TabA->>API: fetch token
    API-->>TabA: token returned
    TabA->>TabA: cache token & emit events
    Lock-->>TabA: release
    end

    rect rgb(255,245,220)
    note over Lock,TabB: TabB acquires lock later and re-checks cache
    Lock->>TabB: lock granted
    TabB->>TabB: sees cached token → skip API
    Lock-->>TabB: release
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review safeLock.ts for AbortController timeout correctness, degraded-path logging, and beforeunload cleanup.
  • Inspect Session.ts for correct double-checked locking, in-flight promise caching, event emission order, and LruMap usage.
  • Verify SessionCookiePoller behavior after lock removal and test coverage.
  • Quick check of AuthCookieService initialization reorder for side effects.
  • Validate LruMap edge cases and tests.

Poem

🐰 I tuck the latch and guard the gate,
Tabs line up, they do not race,
One fetch hops, the others wait,
Cached tokens warm the burrowed space,
Cheerful locks keep cookies safe.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: fixing a race condition when fetching tokens across tabs.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/token-poller-race-condition

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 24, 2025

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7304

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7304

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7304

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7304

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7304

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7304

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@7304

@clerk/clerk-expo

npm i https://pkg.pr.new/@clerk/clerk-expo@7304

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7304

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7304

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7304

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7304

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7304

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7304

@clerk/clerk-react

npm i https://pkg.pr.new/@clerk/clerk-react@7304

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7304

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@7304

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7304

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7304

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7304

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@7304

@clerk/types

npm i https://pkg.pr.new/@clerk/types@7304

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7304

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7304

commit: 4a6769d

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/clerk-js/src/core/auth/safeLock.ts (1)

16-38: Fix inconsistent return type behavior.

The acquireLockAndRun function has inconsistent return behavior:

  • Navigator.locks path (line 25-28): Returns false on error via .catch()
  • Fallback path (line 31-37): Returns undefined implicitly when lock acquisition fails (after line 37)

This creates ambiguity for callers: should they check for false, undefined, or both to detect failure?

Consider one of these approaches:

Option 1 (Recommended): Return a consistent sentinel value

   const acquireLockAndRun = async (cb: () => Promise<unknown>) => {
     if ('locks' in navigator && isSecureContext) {
       const controller = new AbortController();
       const lockTimeout = setTimeout(() => controller.abort(), 4999);
       return await navigator.locks
         .request(key, { signal: controller.signal }, async () => {
           clearTimeout(lockTimeout);
           return await cb();
         })
         .catch(() => {
           // browser-tabs-lock never seems to throw, so we are mirroring the behavior here
           return false;
         });
     }
 
     if (await lock.acquireLock(key, 5000)) {
       try {
         return await cb();
       } finally {
         await lock.releaseLock(key);
       }
     }
+    
+    // Return false when lock acquisition fails to match navigator.locks behavior
+    return false;
   };

Option 2: Use a Result/Option type for clearer semantics

This would make success/failure explicit but requires more extensive changes.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cfaa021 and c694bae.

📒 Files selected for processing (4)
  • .changeset/fix-token-refresh-race-condition.md (1 hunks)
  • packages/clerk-js/src/core/auth/AuthCookieService.ts (6 hunks)
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts (1 hunks)
  • packages/clerk-js/src/core/auth/safeLock.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

All code must pass ESLint checks with the project's configuration

Files:

  • packages/clerk-js/src/core/auth/safeLock.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts
**/*.{js,jsx,ts,tsx,json,md,yml,yaml}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use Prettier for consistent code formatting

Files:

  • packages/clerk-js/src/core/auth/safeLock.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

TypeScript is required for all packages

Files:

  • packages/clerk-js/src/core/auth/safeLock.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Follow established naming conventions (PascalCase for components, camelCase for variables)

Prefer importing types from @clerk/shared/types instead of the deprecated @clerk/types alias

Files:

  • packages/clerk-js/src/core/auth/safeLock.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts
packages/**/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

packages/**/src/**/*.{ts,tsx,js,jsx}: Maintain comprehensive JSDoc comments for public APIs
Use tree-shaking friendly exports
Validate all inputs and sanitize outputs
All public APIs must be documented with JSDoc
Use dynamic imports for optional features
Provide meaningful error messages to developers
Include error recovery suggestions where applicable
Log errors appropriately for debugging
Lazy load components and features when possible
Implement proper caching strategies
Use efficient data structures and algorithms
Implement proper logging with different levels

Files:

  • packages/clerk-js/src/core/auth/safeLock.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts
**/*.ts?(x)

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use proper TypeScript error types

Files:

  • packages/clerk-js/src/core/auth/safeLock.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/typescript.mdc)

**/*.{ts,tsx}: Always define explicit return types for functions, especially public APIs
Use proper type annotations for variables and parameters where inference isn't clear
Avoid any type - prefer unknown when type is uncertain, then narrow with type guards
Implement type guards for unknown types using the pattern function isType(value: unknown): value is Type
Use interface for object shapes that might be extended
Use type for unions, primitives, and computed types
Prefer readonly properties for immutable data structures
Use private for internal implementation details in classes
Use protected for inheritance hierarchies
Use public explicitly for clarity in public APIs
Use mixins for shared behavior across unrelated classes in TypeScript
Use generic constraints with bounded type parameters like <T extends { id: string }>
Use utility types like Omit, Partial, and Pick for data transformation instead of manual type construction
Use discriminated unions instead of boolean flags for state management and API responses
Use mapped types for transforming object types
Use conditional types for type-level logic
Leverage template literal types for string manipulation at the type level
Use ES6 imports/exports consistently
Use default exports sparingly, prefer named exports
Document functions with JSDoc comments including @param, @returns, @throws, and @example tags
Create custom error classes that extend Error for specific error types
Use the Result pattern for error handling instead of throwing exceptions
Use optional chaining (?.) and nullish coalescing (??) operators for safe property access
Let TypeScript infer obvious types to reduce verbosity
Use const assertions with as const for literal types
Use satisfies operator for type checking without widening types
Declare readonly arrays and objects for immutable data structures
Use spread operator and array spread for immutable updates instead of mutations
Use lazy loading for large types...

Files:

  • packages/clerk-js/src/core/auth/safeLock.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts
🧬 Code graph analysis (2)
packages/clerk-js/src/core/auth/AuthCookieService.ts (4)
packages/clerk-js/src/core/auth/cookies/clientUat.ts (2)
  • ClientUatCookieHandler (12-15)
  • createClientUatCookie (23-65)
packages/clerk-js/src/core/auth/SessionCookiePoller.ts (2)
  • SessionCookiePoller (9-42)
  • REFRESH_SESSION_TOKEN_LOCK_KEY (6-6)
packages/clerk-js/src/core/auth/cookies/session.ts (2)
  • SessionCookieHandler (10-14)
  • createSessionCookie (28-59)
packages/clerk-js/src/core/auth/safeLock.ts (2)
  • SafeLockReturn (3-5)
  • SafeLock (7-41)
packages/clerk-js/src/core/auth/SessionCookiePoller.ts (1)
packages/clerk-js/src/core/auth/safeLock.ts (2)
  • SafeLockReturn (3-5)
  • SafeLock (7-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (5)
packages/clerk-js/src/core/auth/SessionCookiePoller.ts (1)

6-6: Good: Exporting the lock key enables cross-component coordination.

Exporting REFRESH_SESSION_TOKEN_LOCK_KEY allows AuthCookieService to create a shared lock using the same key, which is essential for the cross-tab synchronization mechanism.

packages/clerk-js/src/core/auth/AuthCookieService.ts (4)

72-75: Excellent: Clear documentation of the locking mechanism.

The comment clearly explains the purpose of the tokenRefreshLock and how it coordinates cross-tab token refresh. This helps future maintainers understand the synchronization strategy.


137-137: Good: Shared lock injected into poller.

Passing this.tokenRefreshLock to the SessionCookiePoller constructor ensures both the poller and the focus handler use the same cross-tab lock, which is essential for preventing the race condition described in the PR objectives.


158-162: Good: Focus handler now uses the shared lock.

Wrapping the refreshSessionToken call in this.tokenRefreshLock.acquireLockAndRun() prevents concurrent token fetches when multiple tabs gain focus simultaneously or when focus events fire while the poller is already refreshing.

The updated comment clearly explains the cross-tab coordination mechanism.


47-47: Properties are properly initialized in the constructor — no issue exists.

The properties clientUat and sessionCookie are declared without explicit initialization on lines 47 and 50, but they are both assigned values in the constructor (lines ~88–95 via createClientUatCookie() and createSessionCookie()). TypeScript's strictPropertyInitialization rule only flags properties that are declared without initialization AND never assigned in the constructor. Since these properties are assigned before any use, they comply with strict mode requirements and require no changes.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
packages/clerk-js/src/core/auth/safeLock.ts (2)

42-46: Memory leak: event listener is never removed.

Each call to SafeLock() adds a new beforeunload listener that is never cleaned up. If SafeLock is called multiple times (e.g., during hot reloads in development), listeners accumulate.

Consider returning a cleanup function or ensuring SafeLock is only instantiated once per key:

 export function SafeLock(key: string): SafeLockReturn {
   const lock = new Lock();

+  const handleBeforeUnload = async () => {
+    await lock.releaseLock(key);
+  };
+
   // Release any held locks when the tab is closing to prevent deadlocks
   // eslint-disable-next-line @typescript-eslint/no-misused-promises
-  window.addEventListener('beforeunload', async () => {
-    await lock.releaseLock(key);
-  });
+  window.addEventListener('beforeunload', handleBeforeUnload);

Since this is scoped to single-instance usage (via AuthCookieService), this is low-risk but worth noting for future reuse.


48-74: Consider adding explicit return type annotation for type safety.

Per coding guidelines, explicit return types should be defined for functions. While TypeScript can infer the return type, an explicit annotation would improve clarity and catch type mismatches earlier.

-  const acquireLockAndRun = async (cb: () => Promise<unknown>) => {
+  const acquireLockAndRun = async (cb: () => Promise<unknown>): Promise<unknown> => {
packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts (2)

82-96: Consider testing lock acquisition failure scenario.

The tests cover the happy path well, but there's no test for when acquireLockAndRun returns false (timeout/failure). This edge case is part of the SafeLockReturn contract.

it('handles lock acquisition timeout gracefully', async () => {
  const sharedLock: SafeLockReturn = {
    acquireLockAndRun: vi.fn().mockResolvedValue(false), // Lock timeout
  };

  const poller = new SessionCookiePoller(sharedLock);
  const callback = vi.fn().mockResolvedValue(undefined);

  poller.startPollingForSessionToken(callback);

  // Callback should not be invoked when lock returns false
  expect(callback).not.toHaveBeenCalled();

  poller.stopPollingForSessionToken();
});

100-121: Unnecessary async on test function.

The test doesn't use await at the top level. While not harmful, removing async would be cleaner.

-    it('allows restart after stop', async () => {
+    it('allows restart after stop', () => {
packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts (1)

83-104: Test name is misleading - mock doesn't actually simulate sequential execution.

The test "mock lock can simulate sequential execution" fires both promises concurrently via Promise.all. The mock implementation executes callbacks in parallel, not sequentially as a real lock would. This doesn't validate actual sequential behavior.

Consider either:

  1. Renaming to clarify it tests concurrent invocation tracking
  2. Or implementing actual sequential simulation with a queue
-    it('mock lock can simulate sequential execution', async () => {
+    it('tracks multiple concurrent lock invocations', async () => {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c694bae and b991f29.

📒 Files selected for processing (5)
  • packages/clerk-js/src/core/auth/AuthCookieService.ts (6 hunks)
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts (1 hunks)
  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts (1 hunks)
  • packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts (1 hunks)
  • packages/clerk-js/src/core/auth/safeLock.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

All code must pass ESLint checks with the project's configuration

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
**/*.{js,jsx,ts,tsx,json,md,yml,yaml}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use Prettier for consistent code formatting

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

TypeScript is required for all packages

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Follow established naming conventions (PascalCase for components, camelCase for variables)

Prefer importing types from @clerk/shared/types instead of the deprecated @clerk/types alias

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
packages/**/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

packages/**/src/**/*.{ts,tsx,js,jsx}: Maintain comprehensive JSDoc comments for public APIs
Use tree-shaking friendly exports
Validate all inputs and sanitize outputs
All public APIs must be documented with JSDoc
Use dynamic imports for optional features
Provide meaningful error messages to developers
Include error recovery suggestions where applicable
Log errors appropriately for debugging
Lazy load components and features when possible
Implement proper caching strategies
Use efficient data structures and algorithms
Implement proper logging with different levels

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

**/*.{test,spec}.{ts,tsx,js,jsx}: Unit tests are required for all new functionality
Verify proper error handling and edge cases
Include tests for all new features

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts
**/*.ts?(x)

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use proper TypeScript error types

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
**/*.{test,spec,e2e}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use real Clerk instances for integration tests

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/typescript.mdc)

**/*.{ts,tsx}: Always define explicit return types for functions, especially public APIs
Use proper type annotations for variables and parameters where inference isn't clear
Avoid any type - prefer unknown when type is uncertain, then narrow with type guards
Implement type guards for unknown types using the pattern function isType(value: unknown): value is Type
Use interface for object shapes that might be extended
Use type for unions, primitives, and computed types
Prefer readonly properties for immutable data structures
Use private for internal implementation details in classes
Use protected for inheritance hierarchies
Use public explicitly for clarity in public APIs
Use mixins for shared behavior across unrelated classes in TypeScript
Use generic constraints with bounded type parameters like <T extends { id: string }>
Use utility types like Omit, Partial, and Pick for data transformation instead of manual type construction
Use discriminated unions instead of boolean flags for state management and API responses
Use mapped types for transforming object types
Use conditional types for type-level logic
Leverage template literal types for string manipulation at the type level
Use ES6 imports/exports consistently
Use default exports sparingly, prefer named exports
Document functions with JSDoc comments including @param, @returns, @throws, and @example tags
Create custom error classes that extend Error for specific error types
Use the Result pattern for error handling instead of throwing exceptions
Use optional chaining (?.) and nullish coalescing (??) operators for safe property access
Let TypeScript infer obvious types to reduce verbosity
Use const assertions with as const for literal types
Use satisfies operator for type checking without widening types
Declare readonly arrays and objects for immutable data structures
Use spread operator and array spread for immutable updates instead of mutations
Use lazy loading for large types...

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts
  • packages/clerk-js/src/core/auth/AuthCookieService.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
🧬 Code graph analysis (2)
packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts (2)
packages/clerk-js/src/core/auth/safeLock.ts (1)
  • SafeLockReturn (6-15)
packages/clerk-js/src/core/auth/SessionCookiePoller.ts (1)
  • SessionCookiePoller (27-60)
packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts (1)
packages/clerk-js/src/core/auth/safeLock.ts (2)
  • SafeLock (39-77)
  • SafeLockReturn (6-15)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (34)
  • GitHub Check: Integration Tests (nextjs, chrome, 15, RQ)
  • GitHub Check: Integration Tests (quickstart, chrome, 16)
  • GitHub Check: Integration Tests (billing, chrome, RQ)
  • GitHub Check: Integration Tests (nextjs, chrome, 16)
  • GitHub Check: Integration Tests (machine, chrome)
  • GitHub Check: Integration Tests (localhost, chrome)
  • GitHub Check: Integration Tests (quickstart, chrome, 15)
  • GitHub Check: Integration Tests (vue, chrome)
  • GitHub Check: Integration Tests (machine, chrome, RQ)
  • GitHub Check: Integration Tests (react-router, chrome)
  • GitHub Check: Integration Tests (nextjs, chrome, 14)
  • GitHub Check: Integration Tests (expo-web, chrome)
  • GitHub Check: Integration Tests (astro, chrome)
  • GitHub Check: Integration Tests (tanstack-react-start, chrome)
  • GitHub Check: Integration Tests (custom, chrome)
  • GitHub Check: Integration Tests (nextjs, chrome, 15)
  • GitHub Check: Integration Tests (sessions:staging, chrome)
  • GitHub Check: Integration Tests (nuxt, chrome)
  • GitHub Check: Integration Tests (handshake, chrome)
  • GitHub Check: Integration Tests (handshake:staging, chrome)
  • GitHub Check: Integration Tests (ap-flows, chrome)
  • GitHub Check: Integration Tests (elements, chrome)
  • GitHub Check: Integration Tests (billing, chrome)
  • GitHub Check: Integration Tests (sessions, chrome)
  • GitHub Check: Integration Tests (express, chrome)
  • GitHub Check: Integration Tests (generic, chrome)
  • GitHub Check: Publish with pkg-pr-new
  • GitHub Check: Unit Tests (22, shared, clerk-js, RQ)
  • GitHub Check: Unit Tests (22, **)
  • GitHub Check: Static analysis
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (11)
packages/clerk-js/src/core/auth/safeLock.ts (2)

3-15: JSDoc documentation added - addresses past review feedback.

The interface now has proper JSDoc documentation explaining the cross-tab lock coordination purpose and the acquireLockAndRun method behavior including the timeout return value.


17-39: JSDoc documentation added - addresses past review feedback.

The function now has comprehensive JSDoc including purpose, parameters, return type, and a practical example demonstrating cross-tab coordination.

packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts (2)

6-14: LGTM - Clean test setup.

Proper use of beforeEach/afterEach for fake timer management and mock restoration.


16-64: Good coverage for shared lock coordination pattern.

These tests effectively validate the core fix - that an external lock can be injected and shared between the poller and focus handler, which is the key mechanism for preventing the race condition.

packages/clerk-js/src/core/auth/__tests__/safeLock.test.ts (2)

6-23: LGTM - Good interface contract tests.

These tests validate the API surface and mockability of SafeLockReturn, which is important for the dependency injection pattern used in SessionCookiePoller.


25-45: Good defensive testing with environment check.

The conditional skip for environments without Web Locks API is appropriate. The test validates the callback is invoked and the result propagates correctly.

packages/clerk-js/src/core/auth/AuthCookieService.ts (5)

23-25: LGTM - Clean imports for the new lock coordination feature.

Proper separation of type import (SafeLockReturn) from value imports (SafeLock, REFRESH_SESSION_TOKEN_LOCK_KEY).


51-54: Good documentation for the shared lock member.

The JSDoc clearly explains the purpose of tokenRefreshLock for cross-tab coordination.


75-78: Lock creation is appropriately positioned.

Creating the lock early in the constructor ensures it's available before refreshTokenOnFocus() and startPollingForToken() are called.


138-142: Correctly passes shared lock to poller.

The poller now receives the same tokenRefreshLock that the focus handler uses, enabling cross-tab coordination.


152-168: Core fix: Focus handler now uses shared lock - LGTM.

This is the key change that fixes the race condition. By wrapping refreshSessionToken in tokenRefreshLock.acquireLockAndRun(), the focus handler will coordinate with both the poller and other tabs, preventing duplicate API calls.

The comment clearly explains the coordination mechanism.

@jacekradko jacekradko requested a review from brkalow November 25, 2025 03:07
Comment on lines 75 to 78
// Create shared lock for cross-tab token refresh coordination.
// This lock is used by both the poller and the focus handler to prevent
// concurrent token fetches across tabs.
this.tokenRefreshLock = SafeLock(REFRESH_SESSION_TOKEN_LOCK_KEY);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I'm thinking we should add the lock in getToken() itself, otherwise we'll have to patch any other places getToken() is used, and users won't have this taken care of out of the box

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted the implementation to live inside getToken()

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/clerk-js/src/core/resources/Session.ts (1)

45-64: Potential memory leak: tokenLocks Map grows unboundedly.

The tokenLocks Map accumulates entries for each unique tokenId (session + org + template combination) but never removes them. Over a long-lived session where users switch organizations or use different JWT templates, this could lead to memory growth.

Consider adding a cleanup mechanism, such as:

  • Using a WeakMap if the key could be an object
  • Adding a maximum size with LRU eviction
  • Clearing entries when the session ends

For now, this is low risk since each entry is lightweight (just a SafeLock instance), but worth noting for long-term maintenance.

packages/clerk-js/src/core/auth/safeLock.ts (1)

18-22: Multiple beforeunload listeners accumulate with each SafeLock instance.

Since SafeLock is called once per unique tokenId (via getTokenLock in Session.ts), a new beforeunload listener is registered for each lock instance. Over time with many different token types, this accumulates listeners.

Consider:

  1. Moving the listener registration outside the function (module-level, once)
  2. Tracking all lock instances in a module-level registry and releasing them in a single listener
  3. Using { once: true } isn't suitable here since we need it to fire every time
+const lockRegistry = new Map<string, Lock>();
+
+// Single listener to release all locks on tab close
+window.addEventListener('beforeunload', async () => {
+  for (const [key, lock] of lockRegistry) {
+    await lock.releaseLock(key);
+  }
+});
+
 export function SafeLock(key: string) {
   const lock = new Lock();
-
-  // Release any held locks when the tab is closing to prevent deadlocks
-  // eslint-disable-next-line @typescript-eslint/no-misused-promises
-  window.addEventListener('beforeunload', async () => {
-    await lock.releaseLock(key);
-  });
+  lockRegistry.set(key, lock);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b991f29 and ca20b97.

📒 Files selected for processing (5)
  • .changeset/fix-token-refresh-race-condition.md (1 hunks)
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts (2 hunks)
  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts (1 hunks)
  • packages/clerk-js/src/core/auth/safeLock.ts (1 hunks)
  • packages/clerk-js/src/core/resources/Session.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/clerk-js/src/core/auth/SessionCookiePoller.ts
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

All code must pass ESLint checks with the project's configuration

Files:

  • packages/clerk-js/src/core/resources/Session.ts
  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
**/*.{js,jsx,ts,tsx,json,md,yml,yaml}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use Prettier for consistent code formatting

Files:

  • packages/clerk-js/src/core/resources/Session.ts
  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

TypeScript is required for all packages

Files:

  • packages/clerk-js/src/core/resources/Session.ts
  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Follow established naming conventions (PascalCase for components, camelCase for variables)

Prefer importing types from @clerk/shared/types instead of the deprecated @clerk/types alias

Files:

  • packages/clerk-js/src/core/resources/Session.ts
  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
packages/**/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

packages/**/src/**/*.{ts,tsx,js,jsx}: Maintain comprehensive JSDoc comments for public APIs
Use tree-shaking friendly exports
Validate all inputs and sanitize outputs
All public APIs must be documented with JSDoc
Use dynamic imports for optional features
Provide meaningful error messages to developers
Include error recovery suggestions where applicable
Log errors appropriately for debugging
Lazy load components and features when possible
Implement proper caching strategies
Use efficient data structures and algorithms
Implement proper logging with different levels

Files:

  • packages/clerk-js/src/core/resources/Session.ts
  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
**/*.ts?(x)

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use proper TypeScript error types

Files:

  • packages/clerk-js/src/core/resources/Session.ts
  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/typescript.mdc)

**/*.{ts,tsx}: Always define explicit return types for functions, especially public APIs
Use proper type annotations for variables and parameters where inference isn't clear
Avoid any type - prefer unknown when type is uncertain, then narrow with type guards
Implement type guards for unknown types using the pattern function isType(value: unknown): value is Type
Use interface for object shapes that might be extended
Use type for unions, primitives, and computed types
Prefer readonly properties for immutable data structures
Use private for internal implementation details in classes
Use protected for inheritance hierarchies
Use public explicitly for clarity in public APIs
Use mixins for shared behavior across unrelated classes in TypeScript
Use generic constraints with bounded type parameters like <T extends { id: string }>
Use utility types like Omit, Partial, and Pick for data transformation instead of manual type construction
Use discriminated unions instead of boolean flags for state management and API responses
Use mapped types for transforming object types
Use conditional types for type-level logic
Leverage template literal types for string manipulation at the type level
Use ES6 imports/exports consistently
Use default exports sparingly, prefer named exports
Document functions with JSDoc comments including @param, @returns, @throws, and @example tags
Create custom error classes that extend Error for specific error types
Use the Result pattern for error handling instead of throwing exceptions
Use optional chaining (?.) and nullish coalescing (??) operators for safe property access
Let TypeScript infer obvious types to reduce verbosity
Use const assertions with as const for literal types
Use satisfies operator for type checking without widening types
Declare readonly arrays and objects for immutable data structures
Use spread operator and array spread for immutable updates instead of mutations
Use lazy loading for large types...

Files:

  • packages/clerk-js/src/core/resources/Session.ts
  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
  • packages/clerk-js/src/core/auth/safeLock.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

**/*.{test,spec}.{ts,tsx,js,jsx}: Unit tests are required for all new functionality
Verify proper error handling and edge cases
Include tests for all new features

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
**/*.{test,spec,e2e}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use real Clerk instances for integration tests

Files:

  • packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts
🧬 Code graph analysis (2)
packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts (1)
packages/clerk-js/src/core/auth/SessionCookiePoller.ts (1)
  • SessionCookiePoller (11-39)
packages/clerk-js/src/core/auth/safeLock.ts (1)
packages/clerk-js/src/utils/debug.ts (1)
  • debugLogger (150-179)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Build Packages
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
.changeset/fix-token-refresh-race-condition.md (1)

1-14: LGTM!

The changeset provides clear and comprehensive documentation of the race condition fix, including the key implementation details: cross-tab locking, per-tokenId coordination, double-checked locking pattern, and graceful timeout handling.

packages/clerk-js/src/core/auth/__tests__/SessionCookiePoller.test.ts (1)

1-142: LGTM!

Comprehensive test coverage for SessionCookiePoller with well-structured tests covering:

  • Immediate callback execution on start
  • Prevention of concurrent polling sessions
  • Proper stop and restart behavior
  • Correct scheduling that waits for callback completion

The use of fake timers and proper cleanup in each test is appropriate.

packages/clerk-js/src/core/resources/Session.ts (1)

416-473: Well-implemented double-checked locking pattern.

The implementation correctly:

  1. Checks cache before acquiring lock (fast path)
  2. Re-checks cache after acquiring lock (prevents duplicate fetches when another tab populated it)
  3. Caches the promise immediately to prevent concurrent in-tab duplicates
  4. Holds the lock until the token is resolved and cached
packages/clerk-js/src/core/auth/safeLock.ts (1)

29-59: Solid lock acquisition implementation with appropriate degraded mode handling.

The implementation correctly:

  • Uses Web Locks API in secure contexts with abort-based timeout
  • Falls back to localStorage-based locking in non-secure contexts
  • Ensures operations complete in degraded mode rather than failing silently
  • Properly releases locks in the fallback path via finally block

The generic return type <T> preserves type safety for callers.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/clerk-js/src/core/resources/Session.ts (1)

38-64: Per-token SafeLock map looks good; consider lifecycle/typing polish

The per-tokenId SafeLock registry is a reasonable way to allow parallelism across token types while preventing duplicate fetches for the same token. One minor consideration: tokenLocks is module-global and never pruned, so in very long-lived apps with many distinct tokenIds this Map could grow without bound. In practice tokenId cardinality is likely low, but if this ever becomes a concern, you could consider:

  • Clearing entries when a session is ended/removed, or
  • Introducing a small LRU or size cap.

You might also consider giving getTokenLock an explicit return type (ReturnType<typeof SafeLock>) and extracting that into a type TokenLock = ReturnType<typeof SafeLock>; alias to make the intent clearer.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 04b08a6 and f94b214.

📒 Files selected for processing (1)
  • packages/clerk-js/src/core/resources/Session.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

All code must pass ESLint checks with the project's configuration

Files:

  • packages/clerk-js/src/core/resources/Session.ts
**/*.{js,jsx,ts,tsx,json,md,yml,yaml}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use Prettier for consistent code formatting

Files:

  • packages/clerk-js/src/core/resources/Session.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

TypeScript is required for all packages

Files:

  • packages/clerk-js/src/core/resources/Session.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Follow established naming conventions (PascalCase for components, camelCase for variables)

Prefer importing types from @clerk/shared/types instead of the deprecated @clerk/types alias

Files:

  • packages/clerk-js/src/core/resources/Session.ts
packages/**/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

packages/**/src/**/*.{ts,tsx,js,jsx}: Maintain comprehensive JSDoc comments for public APIs
Use tree-shaking friendly exports
Validate all inputs and sanitize outputs
All public APIs must be documented with JSDoc
Use dynamic imports for optional features
Provide meaningful error messages to developers
Include error recovery suggestions where applicable
Log errors appropriately for debugging
Lazy load components and features when possible
Implement proper caching strategies
Use efficient data structures and algorithms
Implement proper logging with different levels

Files:

  • packages/clerk-js/src/core/resources/Session.ts
**/*.ts?(x)

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use proper TypeScript error types

Files:

  • packages/clerk-js/src/core/resources/Session.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/typescript.mdc)

**/*.{ts,tsx}: Always define explicit return types for functions, especially public APIs
Use proper type annotations for variables and parameters where inference isn't clear
Avoid any type - prefer unknown when type is uncertain, then narrow with type guards
Implement type guards for unknown types using the pattern function isType(value: unknown): value is Type
Use interface for object shapes that might be extended
Use type for unions, primitives, and computed types
Prefer readonly properties for immutable data structures
Use private for internal implementation details in classes
Use protected for inheritance hierarchies
Use public explicitly for clarity in public APIs
Use mixins for shared behavior across unrelated classes in TypeScript
Use generic constraints with bounded type parameters like <T extends { id: string }>
Use utility types like Omit, Partial, and Pick for data transformation instead of manual type construction
Use discriminated unions instead of boolean flags for state management and API responses
Use mapped types for transforming object types
Use conditional types for type-level logic
Leverage template literal types for string manipulation at the type level
Use ES6 imports/exports consistently
Use default exports sparingly, prefer named exports
Document functions with JSDoc comments including @param, @returns, @throws, and @example tags
Create custom error classes that extend Error for specific error types
Use the Result pattern for error handling instead of throwing exceptions
Use optional chaining (?.) and nullish coalescing (??) operators for safe property access
Let TypeScript infer obvious types to reduce verbosity
Use const assertions with as const for literal types
Use satisfies operator for type checking without widening types
Declare readonly arrays and objects for immutable data structures
Use spread operator and array spread for immutable updates instead of mutations
Use lazy loading for large types...

Files:

  • packages/clerk-js/src/core/resources/Session.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Build Packages
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (2)
packages/clerk-js/src/core/resources/Session.ts (2)

394-407: Fast-path cache read before acquiring the lock is correct

The “check cache, then lock” flow looks solid: you avoid unnecessary lock acquisition on hot cache hits, preserve the existing TokenUpdate emission for cached tokens, and still normalize empty raw strings to null. This aligns well with the new cross-tab locking behavior without adding contention to the common case.


409-465: skipCache semantics are intentional and working correctly—no action needed

Verification confirms the behavior matches the review's description and is intentional:

  1. Behavior verified: When skipCache=true, the code explicitly bypasses both pre-lock and post-lock cache reads (lines 395, 415: skipCache ? undefined : SessionTokenCache.get()), while still caching the result (line 450). Concurrent skipCache=true calls for the same tokenId will each trigger API requests rather than deduping in-flight promises.

  2. Intent is explicit: The conditional checks at lines 395 and 415 confirm this is deliberate—not an accidental oversight.

  3. Usage context: Only one call site uses skipCache=true (packages/clerk-js/src/core/clerk.ts:2774) during outage recovery. This scenario doesn't have multiple concurrent calls, so the edge case doesn't manifest in practice.

The implementation is sound: skipCache means "bypass my local cache read for this call" (ensuring fresh data), while still caching results for future calls (and for skipCache=false calls in other tabs). The double-check-lock pattern correctly prevents duplicate fetches between skipCache=false callers.

Comment on lines +449 to +450
// Cache the promise immediately to prevent concurrent calls from triggering duplicate requests
SessionTokenCache.set({ tokenId, tokenResolver });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's interesting that we have this here yet it doesn't solve the race condition. I suppose if N getToken calls are fired at the same time it would happen.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
.changeset/fix-token-refresh-race-condition.md (1)

5-5: Refine the message to focus more on user impact than implementation.

While this is already much improved, the mention of "cross-tab lock" remains implementation-focused. Consider rephrasing to emphasize the user-visible benefit (proper synchronization) rather than the mechanism (locking).

Suggested alternative:

-Fix race condition where multiple browser tabs could fetch session tokens simultaneously. `getToken()` now uses a cross-tab lock to coordinate token refresh operations
+Fix race condition where multiple browser tabs could fetch session tokens simultaneously. Token refresh operations are now properly coordinated across tabs.

This version emphasizes what users care about (proper synchronization) without describing the technical mechanism. Aligns with the feedback that release notes should be "light on implementation details and focus on the user impact."

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f94b214 and be3c3a9.

📒 Files selected for processing (1)
  • .changeset/fix-token-refresh-race-condition.md (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
packages/clerk-js/src/utils/__tests__/lru-map.test.ts (1)

5-73: Test coverage is comprehensive for core LRU behaviors.

The tests cover the essential LRU functionality: basic operations, eviction, MRU promotion, updates, and edge case with maxSize=1.

Consider adding tests for these edge cases:

  1. maxSize of 0: The implementation's while (this.size >= this.maxSize) loop could behave unexpectedly or loop infinitely if maxSize is 0.
  2. Storing undefined as a value: The get() implementation uses if (value !== undefined) to decide whether to refresh access, meaning entries with undefined values won't be promoted to MRU position.
+ it('handles storing undefined as a value', () => {
+   const map = new LruMap<string, number | undefined>(3);
+   map.set('a', undefined);
+   map.set('b', 2);
+   map.set('c', 3);
+   
+   // Access 'a' - should still exist but won't be promoted due to implementation
+   expect(map.get('a')).toBeUndefined();
+   expect(map.has('a')).toBe(true);
+ });
+
+ it('throws or handles maxSize of 0', () => {
+   // Document expected behavior
+   const map = new LruMap<string, number>(0);
+   // This could cause issues - verify expected behavior
+ });
packages/clerk-js/src/utils/lru-map.ts (3)

10-17: Edge case: entries with undefined values won't receive MRU promotion.

The condition if (value !== undefined) prevents MRU promotion for entries that store undefined as a value. If a key maps to undefined, get() returns it but doesn't refresh its position, making it vulnerable to early eviction.

For the current usage (storing SafeLock objects), this isn't an issue. However, for a generic utility class, consider using super.has(key) instead:

 override get(key: K): V | undefined {
-  const value = super.get(key);
-  if (value !== undefined) {
+  if (super.has(key)) {
+    const value = super.get(key);
     super.delete(key);
     super.set(key, value);
+    return value;
   }
-  return value;
+  return undefined;
 }

5-8: Consider validating maxSize in the constructor.

A maxSize of 0 or negative would result in an LruMap that can never store entries (all sets immediately evict). While the implementation handles this gracefully due to the break condition, explicit validation would prevent misuse and clarify intent.

 constructor(private maxSize: number) {
   super();
+  if (maxSize < 1) {
+    throw new Error('LruMap maxSize must be at least 1');
+  }
 }

1-8: JSDoc could be enhanced with @param and @example tags.

Per coding guidelines, public APIs should include comprehensive JSDoc with @param, @returns, @throws, and @example tags.

 /**
  * A simple Map with LRU (Least Recently Used) eviction.
  * When the map exceeds maxSize, the oldest entries are removed.
+ *
+ * @template K - The type of map keys
+ * @template V - The type of map values
+ * @param maxSize - Maximum number of entries before eviction occurs (must be >= 1)
+ *
+ * @example
+ * const cache = new LruMap<string, number>(3);
+ * cache.set('a', 1);
+ * cache.get('a'); // Returns 1 and marks 'a' as most recently used
  */
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between be3c3a9 and 4a6769d.

📒 Files selected for processing (3)
  • packages/clerk-js/src/core/resources/Session.ts (3 hunks)
  • packages/clerk-js/src/utils/__tests__/lru-map.test.ts (1 hunks)
  • packages/clerk-js/src/utils/lru-map.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

All code must pass ESLint checks with the project's configuration

Files:

  • packages/clerk-js/src/utils/lru-map.ts
  • packages/clerk-js/src/utils/__tests__/lru-map.test.ts
  • packages/clerk-js/src/core/resources/Session.ts
**/*.{js,jsx,ts,tsx,json,md,yml,yaml}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use Prettier for consistent code formatting

Files:

  • packages/clerk-js/src/utils/lru-map.ts
  • packages/clerk-js/src/utils/__tests__/lru-map.test.ts
  • packages/clerk-js/src/core/resources/Session.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

TypeScript is required for all packages

Files:

  • packages/clerk-js/src/utils/lru-map.ts
  • packages/clerk-js/src/utils/__tests__/lru-map.test.ts
  • packages/clerk-js/src/core/resources/Session.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Follow established naming conventions (PascalCase for components, camelCase for variables)

Prefer importing types from @clerk/shared/types instead of the deprecated @clerk/types alias

Files:

  • packages/clerk-js/src/utils/lru-map.ts
  • packages/clerk-js/src/utils/__tests__/lru-map.test.ts
  • packages/clerk-js/src/core/resources/Session.ts
packages/**/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

packages/**/src/**/*.{ts,tsx,js,jsx}: Maintain comprehensive JSDoc comments for public APIs
Use tree-shaking friendly exports
Validate all inputs and sanitize outputs
All public APIs must be documented with JSDoc
Use dynamic imports for optional features
Provide meaningful error messages to developers
Include error recovery suggestions where applicable
Log errors appropriately for debugging
Lazy load components and features when possible
Implement proper caching strategies
Use efficient data structures and algorithms
Implement proper logging with different levels

Files:

  • packages/clerk-js/src/utils/lru-map.ts
  • packages/clerk-js/src/utils/__tests__/lru-map.test.ts
  • packages/clerk-js/src/core/resources/Session.ts
**/*.ts?(x)

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use proper TypeScript error types

Files:

  • packages/clerk-js/src/utils/lru-map.ts
  • packages/clerk-js/src/utils/__tests__/lru-map.test.ts
  • packages/clerk-js/src/core/resources/Session.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/typescript.mdc)

**/*.{ts,tsx}: Always define explicit return types for functions, especially public APIs
Use proper type annotations for variables and parameters where inference isn't clear
Avoid any type - prefer unknown when type is uncertain, then narrow with type guards
Implement type guards for unknown types using the pattern function isType(value: unknown): value is Type
Use interface for object shapes that might be extended
Use type for unions, primitives, and computed types
Prefer readonly properties for immutable data structures
Use private for internal implementation details in classes
Use protected for inheritance hierarchies
Use public explicitly for clarity in public APIs
Use mixins for shared behavior across unrelated classes in TypeScript
Use generic constraints with bounded type parameters like <T extends { id: string }>
Use utility types like Omit, Partial, and Pick for data transformation instead of manual type construction
Use discriminated unions instead of boolean flags for state management and API responses
Use mapped types for transforming object types
Use conditional types for type-level logic
Leverage template literal types for string manipulation at the type level
Use ES6 imports/exports consistently
Use default exports sparingly, prefer named exports
Document functions with JSDoc comments including @param, @returns, @throws, and @example tags
Create custom error classes that extend Error for specific error types
Use the Result pattern for error handling instead of throwing exceptions
Use optional chaining (?.) and nullish coalescing (??) operators for safe property access
Let TypeScript infer obvious types to reduce verbosity
Use const assertions with as const for literal types
Use satisfies operator for type checking without widening types
Declare readonly arrays and objects for immutable data structures
Use spread operator and array spread for immutable updates instead of mutations
Use lazy loading for large types...

Files:

  • packages/clerk-js/src/utils/lru-map.ts
  • packages/clerk-js/src/utils/__tests__/lru-map.test.ts
  • packages/clerk-js/src/core/resources/Session.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

**/*.{test,spec}.{ts,tsx,js,jsx}: Unit tests are required for all new functionality
Verify proper error handling and edge cases
Include tests for all new features

Files:

  • packages/clerk-js/src/utils/__tests__/lru-map.test.ts
**/*.{test,spec,e2e}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use real Clerk instances for integration tests

Files:

  • packages/clerk-js/src/utils/__tests__/lru-map.test.ts
🧬 Code graph analysis (2)
packages/clerk-js/src/utils/__tests__/lru-map.test.ts (1)
packages/clerk-js/src/utils/lru-map.ts (1)
  • LruMap (5-34)
packages/clerk-js/src/core/resources/Session.ts (9)
packages/clerk-js/src/utils/lru-map.ts (1)
  • LruMap (5-34)
packages/clerk-js/src/core/auth/safeLock.ts (1)
  • SafeLock (15-62)
packages/clerk-js/src/core/tokenCache.ts (1)
  • SessionTokenCache (413-413)
packages/clerk-js/src/utils/debug.ts (1)
  • debugLogger (150-179)
packages/clerk-js/src/core/events.ts (2)
  • eventBus (32-32)
  • events (7-15)
packages/clerk-js/rspack.config.js (1)
  • path (4-4)
packages/clerk-js/src/core/resources/Client.ts (1)
  • path (161-163)
packages/clerk-js/src/core/resources/User.ts (1)
  • path (111-113)
packages/clerk-js/src/core/resources/Token.ts (1)
  • Token (7-57)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (34)
  • GitHub Check: Integration Tests (quickstart, chrome, 15)
  • GitHub Check: Integration Tests (nextjs, chrome, 15, RQ)
  • GitHub Check: Integration Tests (quickstart, chrome, 16)
  • GitHub Check: Integration Tests (nextjs, chrome, 14)
  • GitHub Check: Integration Tests (react-router, chrome)
  • GitHub Check: Integration Tests (custom, chrome)
  • GitHub Check: Integration Tests (nextjs, chrome, 16)
  • GitHub Check: Integration Tests (nextjs, chrome, 15)
  • GitHub Check: Integration Tests (machine, chrome, RQ)
  • GitHub Check: Integration Tests (expo-web, chrome)
  • GitHub Check: Integration Tests (machine, chrome)
  • GitHub Check: Integration Tests (billing, chrome, RQ)
  • GitHub Check: Integration Tests (handshake:staging, chrome)
  • GitHub Check: Integration Tests (billing, chrome)
  • GitHub Check: Integration Tests (astro, chrome)
  • GitHub Check: Integration Tests (tanstack-react-start, chrome)
  • GitHub Check: Integration Tests (vue, chrome)
  • GitHub Check: Integration Tests (nuxt, chrome)
  • GitHub Check: Integration Tests (handshake, chrome)
  • GitHub Check: Integration Tests (localhost, chrome)
  • GitHub Check: Integration Tests (sessions, chrome)
  • GitHub Check: Integration Tests (sessions:staging, chrome)
  • GitHub Check: Integration Tests (generic, chrome)
  • GitHub Check: Integration Tests (express, chrome)
  • GitHub Check: Integration Tests (ap-flows, chrome)
  • GitHub Check: Integration Tests (elements, chrome)
  • GitHub Check: Publish with pkg-pr-new
  • GitHub Check: Unit Tests (22, **)
  • GitHub Check: Unit Tests (22, shared, clerk-js, RQ)
  • GitHub Check: Static analysis
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (4)
packages/clerk-js/src/core/resources/Session.ts (4)

46-64: Well-designed per-tokenId locking with bounded memory.

The implementation correctly uses LRU eviction to prevent unbounded growth while maintaining per-tokenId locks for parallel fetches of different token types.

The design is safe even if a SafeLock wrapper is evicted while the underlying browser lock is held—navigator.locks.request is keyed by the string key (clerk.lock.getToken.${tokenId}), not the wrapper object. A newly created SafeLock for the same tokenId will still correctly coordinate with the existing browser lock.


394-407: LGTM: Fast path avoids lock overhead for cache hits.

The unlocked cache check for immediate hits is the correct optimization pattern—it avoids cross-tab lock acquisition overhead for the common case where a valid token is already cached.


409-466: Double-checked locking pattern correctly implemented.

The implementation follows the standard pattern:

  1. Check cache without lock (fast path)
  2. Acquire lock on miss
  3. Re-check cache after lock (another tab may have populated it)
  4. Fetch and cache if still missing

One observation: when skipCache is true, the code still acquires the lock but always performs a fetch. This is intentional and correct—it ensures that even explicit cache-bypassing requests are serialized to prevent thundering herd on the API.


447-450: Based on my verification of the codebase, I can now provide the rewritten review comment:


The concern about rejected promises being cached is not valid — rejected promises are automatically cleaned up.

The tokenResolver promise is cached immediately at line 450, but if Token.create() rejects, the tokenCache.ts implementation automatically removes the entry via its .catch() handler (line 368: deleteKey()). This means failed token fetches do not persist in cache. On retry (line 130-137 of Session.ts), the cache miss triggers a fresh Token.create() call, allowing retries to succeed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants