Skip to content

refactor(js): extract the refresh scheduler from TokenCache#9042

Open
jacekradko wants to merge 2 commits into
mainfrom
jacek/sdk-140-extract-refresh-scheduler
Open

refactor(js): extract the refresh scheduler from TokenCache#9042
jacekradko wants to merge 2 commits into
mainfrom
jacek/sdk-140-extract-refresh-scheduler

Conversation

@jacekradko

@jacekradko jacekradko commented Jun 30, 2026

Copy link
Copy Markdown
Member

Step 4 of the TokenCache decouple (SDK-117): lifts both setTimeout timers (expiration cleanup + proactive refresh) out of tokenCache.ts into a createRefreshScheduler collaborator with an injected clock, matching the tokenStore/keyResolver shape from the earlier steps. Timers stay setTimeout-based and the clock only supplies now(), so the existing ~30 fake-timer tests pass unchanged.

Two things worth a close read:

Refresh delays are now recomputed from the token's absolute exp (exp - now() - leeway - lead) rather than a fixed expiresIn measured from cache-set time. Identical for freshly fetched tokens (iat == now), but for a token hydrated from the session cookie before the tab loaded, today's code schedules the proactive refresh a full lifetime out (past expiry, so it never fires proactively); the recompute fixes that. refreshScheduler.test.ts pins it down (a past-issuance token fires at 13s, not 43s).

deleteKey is both the expiration-timer callback and the resolver chain's .catch, so its scheduler.cancel(key) stays inside the store.get(key) === value identity guard. Otherwise a stale rejected resolver could disarm the replacement entry's live timers. A new tokenCache test covers that path.

Deliberately deferred to a follow-up: the startup catch-up sweep, re-arm on visibility/online (overlaps AuthCookieService's existing focus/visibility refresh), and failure backoff.

Closes SDK-140.

Summary by CodeRabbit

  • New Features

    • Session token background refresh scheduling now uses the token’s absolute expiry to ensure proactive refresh runs before expiration, including after page load.
    • Token caching now centralizes refresh/expiration timing for more consistent behavior.
  • Bug Fixes

    • Prevents refresh from being missed when tokens are expired or near expiry.
    • Fixes stale async token updates overwriting newer cache entries without correctly cancelling prior refresh callbacks.
    • Ensures overwritten or replaced tokens don’t trigger outdated refreshes.

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jun 30, 2026 2:21am
swingset Ready Ready Preview, Comment Jun 30, 2026 2:21am

Request Review

@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a7ade3d

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

This PR includes changesets to release 4 packages
Name Type
@clerk/clerk-js Patch
@clerk/chrome-extension Patch
@clerk/electron Patch
@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

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: c5df91b4-28e6-4f71-ad0e-cbe121082e22

📥 Commits

Reviewing files that changed from the base of the PR and between 46fcf73 and a7ade3d.

📒 Files selected for processing (4)
  • packages/clerk-js/src/core/__tests__/tokenCache.test.ts
  • packages/clerk-js/src/core/refreshScheduler.ts
  • packages/clerk-js/src/core/tokenCache.ts
  • packages/clerk-js/src/core/tokenStore.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/clerk-js/src/core/tokenStore.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/clerk-js/src/core/tests/tokenCache.test.ts
  • packages/clerk-js/src/core/refreshScheduler.ts
  • packages/clerk-js/src/core/tokenCache.ts

📝 Walkthrough

Walkthrough

Adds a refresh scheduler for per-key token expiration and proactive refresh timing, wires MemoryTokenCache through it, and updates tests and release notes. The cache now computes refresh timing from absolute expiry for cookie-restored tokens.

Changes

RefreshScheduler abstraction and token cache integration

Layer / File(s) Summary
RefreshScheduler contracts and implementation
packages/clerk-js/src/core/refreshScheduler.ts
Defines the clock and scheduler interfaces, timer helpers, per-key timer storage, cancellation methods, and scheduling based on absolute expiry.
tokenCache integration with RefreshScheduler
packages/clerk-js/src/core/tokenCache.ts, packages/clerk-js/src/core/tokenStore.ts
Replaces direct timer ownership in the cache with the scheduler, injects a clock, removes the refresh threshold constant, and updates cache cleanup and overwrite handling.
Tests and release note
packages/clerk-js/src/core/__tests__/refreshScheduler.test.ts, packages/clerk-js/src/core/__tests__/tokenCache.test.ts, .changeset/sdk-140-refresh-scheduler.md
Adds scheduler timing and cancellation tests, token cache overwrite and expiry timing tests, and a changeset describing the refresh timing behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Poem

🐇 I hopped through timers, quick and neat,
Absolute expiry set the beat.
Cookie tokens woke up on time,
No more refreshes after chime.
The cache now listens to the clock! ⏰

🚥 Pre-merge checks | ✅ 5
✅ 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 accurately describes the main refactor: extracting refresh scheduling out of TokenCache.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@pkg-pr-new

pkg-pr-new Bot commented Jun 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9042

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9042

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9042

@clerk/expo

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9042

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9042

@clerk/upgrade

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

@clerk/vue

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

commit: a7ade3d

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-01T20:12:14.394Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on a7ade3d.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

65-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid any in the unref() path.

This helper can stay cross-runtime without dropping type safety; a small guard is enough to call unref() only on Node timer handles.

♻️ Proposed refactor
+interface UnrefTimer {
+  unref(): void;
+}
+
+const hasUnref = (
+  value: ReturnType<typeof setTimeout>,
+): value is ReturnType<typeof setTimeout> & UnrefTimer =>
+  typeof value === 'object' && value !== null && 'unref' in value && typeof value.unref === 'function';
+
 const armTimer = (callback: () => void, delayMs: number): ReturnType<typeof setTimeout> => {
   const id = setTimeout(callback, delayMs);
-  if (typeof (id as any).unref === 'function') {
-    (id as any).unref();
+  if (hasUnref(id)) {
+    id.unref();
   }
   return id;
 };

As per coding guidelines, "Avoid any type - prefer unknown when type is uncertain, then narrow with type guards" and "No any types without justification in code review".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/clerk-js/src/core/refreshScheduler.ts` around lines 65 - 69, The
armTimer helper in refreshScheduler currently uses an any cast to call unref()
on the timer handle, which violates the no-any guideline. Update armTimer to use
a type-safe narrowing approach (for example, a small guard on the setTimeout
result) so unref() is only called when the handle actually supports it, while
keeping the cross-runtime behavior intact.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/clerk-js/src/core/refreshScheduler.ts`:
- Around line 65-69: The armTimer helper in refreshScheduler currently uses an
any cast to call unref() on the timer handle, which violates the no-any
guideline. Update armTimer to use a type-safe narrowing approach (for example, a
small guard on the setTimeout result) so unref() is only called when the handle
actually supports it, while keeping the cross-runtime behavior intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: bf4cce79-ae41-43b1-9ece-833e22a0cc19

📥 Commits

Reviewing files that changed from the base of the PR and between c5697d7 and 46fcf73.

📒 Files selected for processing (5)
  • .changeset/sdk-140-refresh-scheduler.md
  • packages/clerk-js/src/core/__tests__/refreshScheduler.test.ts
  • packages/clerk-js/src/core/__tests__/tokenCache.test.ts
  • packages/clerk-js/src/core/refreshScheduler.ts
  • packages/clerk-js/src/core/tokenCache.ts

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.

1 participant