') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(js): Coalesce concurrent first/second factor preparations by tmilewski · Pull Request #9225 · clerk/javascript · GitHub
Skip to content

fix(js): Coalesce concurrent first/second factor preparations - #9225

Merged
tmilewski merged 14 commits into
mainfrom
tom/fix-concurrent-second-factor-prepare
Jul 27, 2026
Merged

fix(js): Coalesce concurrent first/second factor preparations#9225
tmilewski merged 14 commits into
mainfrom
tom/fix-concurrent-second-factor-prepare

Conversation

@tmilewski

@tmilewskitmilewski commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

  • Coalesce identical concurrent first- and second-factor preparation requests during sign-in, sign-up, and user-profile verification
  • Keep requests for different factors and authentication attempts independent

Why

Concurrent consumers could prepare the same verification factor more than once, sending duplicate one-time codes and invalidating the code the user received first.

Testing

  • Added Vitest coverage for identical and distinct preparations, retries, future APIs, cross-attempt isolation, sign-up and profile verification, and stalled-request expiry
  • Verified the Clerk JS test suite, declaration build, and lint checks

@vercel

vercelBot commented Jul 23, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentJul 23, 2026 7:36pm
swingsetReadyReadyPreview, CommentJul 23, 2026 7:36pm

Request Review

@changeset-bot

changeset-botBot commented Jul 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 30e121c

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

This PR includes changesets to release 4 packages
NameType
@clerk/clerk-jsPatch
@clerk/chrome-extensionPatch
@clerk/electronPatch
@clerk/expoPatch

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

@pkg-pr-new

pkg-pr-newBot commented Jul 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/electron

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

@clerk/electron-passkeys

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

@clerk/eslint-plugin

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

@clerk/expo

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

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@9225

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: 30e121c

@tmilewskitmilewski changed the title Coalesce concurrent sign-in factor preparationsfix(js): Coalesce concurrent sign-in factor preparationsJul 23, 2026
@tmilewskitmilewski self-assigned this Jul 23, 2026
@github-actions

github-actionsBot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-23T19:37:44.465Z

Summary

MetricCount
Packages analyzed19
Packages with changes1
🔴 Breaking changes0
🟡 Non-breaking changes1
🟢 Additions0

🤖 This report was reviewed by claude-sonnet-4-6.


@clerk/backend

Current version: 3.12.0
Recommended bump: MINOR → 3.13.0

Subpath ./proxy

🟡 Non-breaking Changes (1)

Modified: FrontendApiProxyOptions.fapiUrl
- fapiUrl?: string;

Static analyzer: Removed property FrontendApiProxyOptions.fapiUrl

🤖 AI review (reclassified as non-breaking) (85%): The removed property fapiUrl was optional (fapiUrl?: string), so no existing well-typed consumer was required to provide it; removing it only prevents consumers from passing it, which would have been silently ignored anyway (excess property checks aside, but as an interface input field removal it means callers that did pass it will now get a compile error). Wait — callers who explicitly set fapiUrl on an object of type FrontendApiProxyOptions will get a type error after removal.


Report generated by Break Check

Last ran on 30e121c.

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

BaseResource now supports optional coalescing for concurrent POST mutations, including deterministic request keys, pending-request TTL handling, abort controllers, and AbortSignal forwarding. Verification preparation calls across sign-in, sign-up, email, phone, and web3 flows enable this behavior. Tests cover shared requests, differing inputs, cleanup after completion or failure, TTL expiry, and signal propagation. A patch Changesets entry was added.

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

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly summarizes the main change: coalescing concurrent factor-preparation requests.
Description check✅ PassedThe description matches the implemented changes and testing coverage described in the PR.

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

coderabbitai[bot]

This comment was marked as resolved.

@tmilewskitmilewski changed the title fix(js): Coalesce concurrent sign-in factor preparationsfix(js): Coalesce concurrent first/second factor preparationsJul 23, 2026

@coderabbitaicoderabbitaiBot 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/resources/__tests__/Web3Wallet.test.ts (1)

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

Replace the untyped deferred response.

Use the concrete response shape so this fixture catches invalid mock payloads.

Proposed fix
- const deferred = createDeferredPromise<any>();+ const deferred = createDeferredPromise<{ response: Web3WalletJSON }>();

As per coding guidelines, “Avoid any type - prefer unknown when type is uncertain.”

🤖 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/resources/__tests__/Web3Wallet.test.ts` at line
37, Replace the any type in the createDeferredPromise fixture within Web3Wallet
tests with the concrete response type expected by the tested wallet resource.
Ensure the deferred promise and its resolved mock payload are type-checked
against that response shape, rather than using unknown or another untyped
fallback.

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/resources/__tests__/Web3Wallet.test.ts`:
- Line 37: Replace the any type in the createDeferredPromise fixture within
Web3Wallet tests with the concrete response type expected by the tested wallet
resource. Ensure the deferred promise and its resolved mock payload are
type-checked against that response shape, rather than using unknown or another
untyped fallback.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c614ce2-8737-427f-a57b-1c6decd84a9b

📥 Commits

Reviewing files that changed from the base of the PR and between 59bf726 and 30e121c.

📒 Files selected for processing (4)
  • packages/clerk-js/src/core/resources/Base.ts
  • packages/clerk-js/src/core/resources/__tests__/EmailAddress.test.ts
  • packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts
  • packages/clerk-js/src/core/resources/__tests__/Web3Wallet.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go(manual)
  • clerk/dashboard(manual)
  • clerk/accounts(manual)
  • clerk/backoffice(manual)
  • clerk/clerk(manual)
  • clerk/clerk-docs(manual)
  • clerk/cloudflare-workers(manual)
  • clerk/clerk-ios(auto-detected)
  • clerk/cli(auto-detected)
  • clerk/clerk-android(auto-detected)
💤 Files with no reviewable changes (1)
  • packages/clerk-js/src/core/resources/Base.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/clerk-js/src/core/resources/tests/SignIn.test.ts

@tmilewski
tmilewski requested a review from a teamJuly 23, 2026 20:46
@tmilewski
tmilewski merged commit e35d971 into mainJul 27, 2026
55 checks passed
@tmilewski
tmilewski deleted the tom/fix-concurrent-second-factor-prepare branch July 27, 2026 17:50
Sign up for freeto 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.

2 participants

@tmilewski@dstaley