feat(clerk-js): Stale-while-revalidate session token
Description
Return a cached token and schedule background refresh if the token is read while in the expiration leeway timeframe.
Fixes: USER-4087
Checklist
- [ ]
pnpm testruns as expected. - [ ]
pnpm buildruns 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
Release Notes
-
New Features
- Added
refreshIfStaleoption to token retrieval API, enabling proactive fresh token fetching when cached token is within refresh leeway. - Implemented stale-while-revalidate behavior: immediately returns cached token while automatically refreshing in the background.
- Added
-
Improvements
- Enhanced token expiration handling with improved leeway calculations for more reliable token freshness guarantees.
✏️ Tip: You can customize this high-level summary in your review settings.
⚠️ No Changeset found
Latest commit: 441532faf7642b4ab2ceb6fe9814e0baef44e66c
Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.
This PR includes no changesets
When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types
Click here to learn what changesets are, and how to add one.
Click here if you're a maintainer who wants to add a changeset to this PR
Walkthrough
The pull request introduces stale-while-revalidate (SWR) semantics to the token caching system. Instead of immediately evicting stale tokens, the cache now returns them with a needsRefresh flag, enabling immediate token availability while triggering background refresh. This prevents artificial latency when tokens are accessed in hot paths.
Changes
| Cohort / File(s) | Summary |
|---|---|
Token cache core logic packages/clerk-js/src/core/tokenCache.ts |
Modified get() to return TokenCacheGetResult object with entry and needsRefresh flag. Implements SWR by returning stale tokens with refresh signals instead of evicting them. Added resolvedToken field to cache entries for synchronous reads and imported POLLER_INTERVAL_IN_MS for TTL alignment. |
Session token retrieval packages/clerk-js/src/core/resources/Session.ts |
Added refreshIfStale option to token retrieval and refactored token fetching into private methods: #fetchToken, #createTokenResolver, and #dispatchTokenEvents. Updated to handle new cache result shape and resolve tokens from either pre-resolved values or token resolvers. |
Token type definitions packages/shared/src/types/session.ts |
Added refreshIfStale?: boolean option to GetTokenOptions and reordered fields for consistency. |
Session cookie polling packages/clerk-js/src/core/auth/SessionCookiePoller.ts |
Exported new constant POLLER_INTERVAL_IN_MS (5 seconds) and updated internal polling timer to use it. |
Cookie service packages/clerk-js/src/core/auth/AuthCookieService.ts |
Updated to use getToken({ refreshIfStale: true }) for proactive token refresh when updating cookies. |
Test suite packages/clerk-js/src/core/__tests__/tokenCache.test.ts, packages/clerk-js/src/core/resources/__tests__/Session.test.ts |
Updated all cache lookup assertions to access result?.entry and result?.needsRefresh. Added extensive new test cases verifying SWR behavior, leeway handling, token resolution timing, and multi-session isolation with the new cache API shape. |
Sequence Diagram
sequenceDiagram
participant Client
participant Session
participant TokenCache
participant Poller
participant Backend
Client->>Session: getToken({ refreshIfStale: true })
activate Session
Session->>TokenCache: get(cacheKey)
activate TokenCache
alt Token exists and valid
TokenCache->>TokenCache: Check remaining TTL vs leeway
alt Within leeway threshold
TokenCache-->>Session: { entry, needsRefresh: true }
else Valid with buffer
TokenCache-->>Session: { entry, needsRefresh: false }
end
else Token stale or missing
TokenCache-->>Session: undefined
end
deactivate TokenCache
alt Token returned with needsRefresh=true
Session->>Session: Return cached token immediately
Session-->>Client: token
par Background Refresh
Session->>Backend: Fetch fresh token
Backend-->>Session: New token
Session->>TokenCache: Update cache with fresh token
Session->>Poller: Signal refresh complete
end
else No token or refreshIfStale=false
Session->>Backend: Fetch fresh token
Backend-->>Session: New token
Session->>TokenCache: Cache new token + resolver
Session-->>Client: token
end
deactivate Session
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
-
Token cache API change: The
get()method's return type signature change fromTokenCacheEntry | undefinedtoTokenCacheGetResult | undefinedrequires verification across all call sites and test assertions. -
SWR state machine logic: The leeway and TTL calculations in
tokenCache.ts(includingneedsRefreshdetermination based on remaining TTL, poller interval alignment, and edge cases) warrant careful review. -
Session token refactoring: The introduction of private methods
#fetchToken,#createTokenResolver, and#dispatchTokenEventsconsolidates token fetching; verify event dispatch order and resolver lifecycle. - Multi-path token resolution: Ensure token resolution from pre-resolved values vs. awaiting resolvers is handled consistently across all code paths.
- Test coverage density: Substantial test additions for SWR scenarios, leeway variations, and concurrent requests; verify test assumptions align with intended behavior.
Poem
🐰 Hops of joy with tokens swift,
No more waiting—here's the gift!
Stale but spry, we fetch anew,
While background gnomes refresh for you! ✨
Pre-merge checks and finishing touches
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The title 'feat(clerk-js): Stale-while-revalidate session token' accurately describes the main feature being implemented - adding stale-while-revalidate behavior for session tokens. |
| Linked Issues check | ✅ Passed | All objectives from USER-4087 are met: cache entries are no longer deleted during leeway [tokenCache.ts], cached tokens are returned even within leeway [Session.ts], background refresh is performed asynchronously [SessionCookiePoller.ts, Session.ts], and latency is eliminated by immediate token return. |
| Out of Scope Changes check | ✅ Passed | All changes are directly scoped to implementing stale-while-revalidate behavior: core cache logic [tokenCache.ts], session token handling [Session.ts], cookie polling [SessionCookiePoller.ts], cookie service updates [AuthCookieService.ts], type definitions [session.ts], and comprehensive test coverage aligning with the feature. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
✨ 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
feat/stale-while-revalidate-token
Comment @coderabbitai help to get the list of available commands and usage tips.
The latest updates on your projects. Learn more about Vercel for GitHub.
| Project | Deployment | Review | Updated (UTC) |
|---|---|---|---|
| clerk-js-sandbox | Preview, Comment | Dec 17, 2025 1:38pm |
@clerk/agent-toolkit
npm i https://pkg.pr.new/@clerk/agent-toolkit@7317
@clerk/astro
npm i https://pkg.pr.new/@clerk/astro@7317
@clerk/backend
npm i https://pkg.pr.new/@clerk/backend@7317
@clerk/chrome-extension
npm i https://pkg.pr.new/@clerk/chrome-extension@7317
@clerk/clerk-js
npm i https://pkg.pr.new/@clerk/clerk-js@7317
@clerk/dev-cli
npm i https://pkg.pr.new/@clerk/dev-cli@7317
@clerk/expo
npm i https://pkg.pr.new/@clerk/expo@7317
@clerk/expo-passkeys
npm i https://pkg.pr.new/@clerk/expo-passkeys@7317
@clerk/express
npm i https://pkg.pr.new/@clerk/express@7317
@clerk/fastify
npm i https://pkg.pr.new/@clerk/fastify@7317
@clerk/localizations
npm i https://pkg.pr.new/@clerk/localizations@7317
@clerk/nextjs
npm i https://pkg.pr.new/@clerk/nextjs@7317
@clerk/nuxt
npm i https://pkg.pr.new/@clerk/nuxt@7317
@clerk/react
npm i https://pkg.pr.new/@clerk/react@7317
@clerk/react-router
npm i https://pkg.pr.new/@clerk/react-router@7317
@clerk/shared
npm i https://pkg.pr.new/@clerk/shared@7317
@clerk/tanstack-react-start
npm i https://pkg.pr.new/@clerk/tanstack-react-start@7317
@clerk/testing
npm i https://pkg.pr.new/@clerk/testing@7317
@clerk/ui
npm i https://pkg.pr.new/@clerk/ui@7317
@clerk/upgrade
npm i https://pkg.pr.new/@clerk/upgrade@7317
@clerk/vue
npm i https://pkg.pr.new/@clerk/vue@7317
commit: 441532f