feat(clerk-js): Proactive session token refresh#7317
Conversation
🦋 Changeset detectedLatest commit: 6da6883 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
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 |
📝 WalkthroughWalkthroughSession token handling was refactored to a stale-while-revalidate model with proactive background refresh. SessionTokenCache.get now returns a TokenCacheGetResult object with an 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Tip 🧪 Unit Test Generation v2 is now available!We have significantly improved our unit test generation capabilities. To enable: Add this to your reviews:
finishing_touches:
unit_tests:
enabled: trueTry it out by using the Have feedback? Share your thoughts on our Discord thread! Comment |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
@clerk/agent-toolkit
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/dev-cli
@clerk/expo
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
Ephem
left a comment
There was a problem hiding this comment.
I like the latest changes! Using the poller for the background revalidation makes sense to me. I think there's still some torny parts to untangle, and besides the latest comments, something still feels harder than it should be here, so I wonder if we've found the correct mental model for this yet?
I have a few early thoughts, let's chat!
|
|
||
| if (expiresSoon) { | ||
| // Token expired or dangerously close to expiration - force synchronous refresh | ||
| if (remainingTtl <= POLLER_INTERVAL_IN_MS / 1000) { |
There was a problem hiding this comment.
Default LEEWAY was 10s, and SYNC_LEEWAY was 5s, so if I'm reading this correctly, we used to sync fetch when less than 15s was remaining? Now this value is 5s?
I wonder if 5s is enough considering latency and all other factors?
However we change this it's a breaking change so let's remember to document it properly in the changelog when we've figured it out.
There was a problem hiding this comment.
Right. Before we would do a blocking fetch under the 15s, now we use the stale value when it's under 15s but more than 5 seconds. The idea there is that the poller would async fetch the token before we get to the 5s. If that doesn't occur then we would force a sync fetch.
There was a problem hiding this comment.
And yes, this is a breaking change so it's going to be part of Core 3
| } | ||
|
|
||
| return value.entry; | ||
| const effectiveLeeway = Math.max(leeway, MIN_REMAINING_TTL_IN_SECONDS); |
There was a problem hiding this comment.
This part is a bit weird. MIN_REMAINING_TTL_IN_SECONDS is 15s, and default LEEWAY is 10s, but the Math.max ensures we never go below 15s right?
If this is what we want(?), I think we should increase the LEEWAY to 15s as well, that way we document that "you can only raise this".
Thinking more on it, this PR now changes the public leewayInSeconds to only apply if you also use the internal refreshTokenOnFocus option? That seems off.
There was a problem hiding this comment.
🤔 In agreement here, this effectively forces leeway to always be equal to or greater than MIN_REMAINING_TTL_IN_SECONDS, do we want that value to be 5 or 15 going forward?
| try { | ||
| const token = await this.clerk.session.getToken(); | ||
| // Use refreshIfStale to fetch fresh token when cached token is within leeway period | ||
| const token = await this.clerk.session.getToken({ refreshIfStale: true }); |
There was a problem hiding this comment.
This also affects the refreshTokenOnFocus, but do we want that? Because those cases can be a race to finish setting the cookie before any external data fetching runs, it seems prudent to use the available token then?
| // Prefer synchronous read to avoid microtask overhead when token is already resolved | ||
| const cachedToken = cacheResult.entry.resolvedToken ?? (await cacheResult.entry.tokenResolver); |
There was a problem hiding this comment.
As I mentioned in chat, when I made the comment about this I didn't think it through fully. To get the full benefits, we'd have to make sure _getToken/getToken itself to be synchronous when data is already available (or maybe more likely add the .status and .value/.reason fields to the promise).
This likely only makes sense if/when we decide to expose this promise to the end user so they can use it though.
I see you've used resolvedToken as the way to be able to have a background refetch running while still reading the currently cached token though so this still has immediate value. That was a bit unclear at first, but I think it makes sense, will need to think some more on it.
There was a problem hiding this comment.
Agreed. And yeah, right now the value is key to allow for returning the stale value, while we wait for the poller to do it's thing
d24d455 to
12a12f5
Compare
| shouldDispatchTokenUpdate, | ||
| leewayInSeconds, | ||
| ), | ||
| }); |
There was a problem hiding this comment.
Why are we triggering the same method here?
- Remove leewayInSeconds from GetTokenOptions type - Delete gettoken-leeway-minimum.md upgrade doc - Update changeset to major (breaking change) - Add comments explaining refreshLeadTime and onRefresh re-registration - Rename upgrade doc title to "proactive background refresh" - Simplify internal token cache to use default threshold
|
!allow-major |
|
!snapshot |
|
Hey @jacekradko - the snapshot version command generated the following package versions:
Tip: Use the snippet copy button below to quickly install the required packages. npm i @clerk/agent-toolkit@0.2.9-snapshot.v20260129035546 --save-exact
npm i @clerk/astro@3.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/backend@3.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/chrome-extension@3.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/clerk-js@6.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/dev-cli@1.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/expo@3.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/expo-passkeys@1.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/express@2.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/fastify@2.7.0-snapshot.v20260129035546 --save-exact
npm i @clerk/localizations@4.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/msw@0.0.1-snapshot.v20260129035546 --save-exact
npm i @clerk/nextjs@7.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/nuxt@2.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/react@6.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/react-router@3.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/shared@4.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/tanstack-react-start@1.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/testing@2.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/ui@1.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/upgrade@2.0.0-snapshot.v20260129035546 --save-exact
npm i @clerk/vue@2.0.0-snapshot.v20260129035546 --save-exact |
|
!snapshot |
|
Hey @jacekradko - the snapshot version command generated the following package versions:
Tip: Use the snippet copy button below to quickly install the required packages. npm i @clerk/agent-toolkit@0.2.9-snapshot.v20260130015346 --save-exact
npm i @clerk/astro@3.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/backend@3.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/chrome-extension@3.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/clerk-js@6.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/dev-cli@1.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/expo@3.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/expo-passkeys@1.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/express@2.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/fastify@2.7.0-snapshot.v20260130015346 --save-exact
npm i @clerk/localizations@4.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/msw@0.0.1-snapshot.v20260130015346 --save-exact
npm i @clerk/nextjs@7.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/nuxt@2.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/react@6.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/react-router@3.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/shared@4.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/tanstack-react-start@1.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/testing@2.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/ui@1.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/upgrade@2.0.0-snapshot.v20260130015346 --save-exact
npm i @clerk/vue@2.0.0-snapshot.v20260130015346 --save-exact |
Description
This PR implements proactive token refresh for session tokens. Instead of waiting for
getToken()calls to trigger refresh when a token is close to expiration, we schedule a timer when tokens are cached that fires proactively before the leeway period begins.Fixes: USER-4087
Dashboard testing PR: https://github.com/clerk/dashboard/pull/7990
How It Works
Key Behaviors
Benefits
getToken()call frequencyBroadcastChannelImplementation Details
Token Cache (
tokenCache.ts)onRefreshcallback on cache entries for proactive refresh(TTL - leeway - 2s)when tokens are cachedBroadcastChannelSession (
Session.ts)#refreshTokenInBackground(): Fires token refresh without blocking#backgroundRefreshInProgressSet prevents duplicate concurrent refreshesonRefreshcallback passed when caching tokensresolvedTokenon cache entries enables synchronous reads (avoids microtask overhead)Constants
Breaking Changes
leewayInSecondsoption: TheleewayInSecondsoption has been removed fromgetToken(). Token refresh timing is now handled automatically by the proactive refresh system. A codemod is available via@clerk/upgradeto remove this option from existing code.Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change
Summary by CodeRabbit
New Features
Breaking Changes
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.