Skip to content

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

Open
jacekradko wants to merge 1 commit into
mainfrom
jacek/sdk-140-extract-refresh-scheduler
Open

refactor(js): extract the refresh scheduler from TokenCache#9042
jacekradko wants to merge 1 commit 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 refreshes are now scheduled more reliably based on the token’s actual expiry time, helping background refresh happen before expiration after page load.
    • Token caching now uses a more consistent timing source for refresh and expiration handling.
  • Bug Fixes

    • Improved handling of expired or nearly expired tokens so refresh timers aren’t missed.
    • Fixed stale async token updates from interfering with newer cached tokens and their refresh timers.

@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: 46fcf73

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

📝 Walkthrough

Walkthrough

Adds a new RefreshScheduler module with an injectable Clock interface that manages per-key expiration and proactive refresh timers. MemoryTokenCache is updated to use this scheduler instead of direct setTimeout/clearTimeout calls, fixing refresh timing for tokens restored from session cookies on page load.

Changes

RefreshScheduler abstraction and token cache integration

Layer / File(s) Summary
RefreshScheduler contracts and implementation
packages/clerk-js/src/core/refreshScheduler.ts
Defines Clock and RefreshScheduler interfaces, refresh timing constants, armTimer/clearHandles helpers, and the createRefreshScheduler factory with a per-key timer Map.
tokenCache integration with RefreshScheduler
packages/clerk-js/src/core/tokenCache.ts
Imports Clock/createRefreshScheduler, removes timer-ID fields from TokenCacheValue, removes BACKGROUND_REFRESH_THRESHOLD_IN_SECONDS, injects clock and scheduler into MemoryTokenCache, and replaces all direct setTimeout/clearTimeout calls with scheduler.schedule/cancel/cancelAll.
Tests and changeset
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 full test suite for createRefreshScheduler timing and cancellation semantics, adds a stale-rejection overwrite-protection test in tokenCache, and adds a patch changeset documenting the fix.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • clerk/javascript#8860: Modifies the same tokenCache.ts functions (get, clear, setInternal) for internal timer or storage mechanics changes.

Suggested reviewers

  • wobsoriano

🐇 A new scheduler hops into view,
Timers computed from expiry true.
Cookie-restored tokens no longer late,
Refresh fires before the expiration date!
No more missed alarms — the clock's set right. 🕐✨

🚥 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 summarizes the main refactor: extracting refresh scheduling from TokenCache into a new scheduler.
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: 46fcf73

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-06-30T02:24:08.983Z

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 46fcf73.

@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