Skip to content

feat: failure detection #6026

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed

feat: failure detection #6026

wants to merge 4 commits into from

Conversation

jacekradko
Copy link
Member

@jacekradko jacekradko commented May 29, 2025

Description

Checklist

  • pnpm test runs as expected.
  • pnpm build runs 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

  • New Features

    • Introduced a robust script loading coordinator to prevent multiple ClerkJS scripts from loading simultaneously and to manage loading states.
    • Added a React hook to expose ClerkJS script loading state to components.
    • Extended event handling for script load, error, and state changes in the ClerkJSScript component.
    • Exported new types and functions for advanced script state management.
  • Refactor

    • Updated ClerkJSScript to use a unified, coordinated loading approach across different Next.js router modes.
  • Bug Fixes

    • Improved handling of script insertion, duplicate prevention, and event notification for more reliable ClerkJS loading.
  • Tests

    • Enhanced and expanded test coverage for script loading scenarios, including error and race condition handling.

Copy link

changeset-bot bot commented May 29, 2025

⚠️ No Changeset found

Latest commit: b6431b1

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

Copy link

vercel bot commented May 29, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
clerk-js-sandbox ⬜️ Skipped (Inspect) May 29, 2025 6:21pm

Copy link

pkg-pr-new bot commented May 29, 2025

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@6026

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@6026

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@6026

@clerk/clerk-expo

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/clerk-react

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

@clerk/react-router

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

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@6026

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@6026

@clerk/types

npm i https://pkg.pr.new/@clerk/types@6026

@clerk/upgrade

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

@clerk/vue

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

commit: b6431b1

@clerk-cookie
Copy link
Collaborator

Hello 👋

We currently close PRs after 60 days of inactivity. It's been 50 days since the last update here. If we missed this PR, please reply here. Otherwise, we'll close this PR in 10 days.

Thanks for being a part of the Clerk community! 🙏

Copy link
Contributor

coderabbitai bot commented Jul 19, 2025

📝 Walkthrough

Walkthrough

This update introduces a global blocking coordinator pattern for managing the loading of the ClerkJS script in Next.js applications. A new inline script is injected into the document head to track and control the ClerkJS script's loading state, preventing multiple scripts from loading simultaneously. The coordinator exposes its state and allows registration of callbacks for load, error, and state changes. The ClerkJSScript component is refactored to use this coordinator, and a new React hook, useClerkJSLoadingState, is added to provide loading state to components. Supporting utilities and tests are updated to integrate with and verify the coordinator-based loading mechanism. Public exports are extended to include coordinator-related functions and types.


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (3)
packages/shared/src/loadClerkJsScript.ts (1)

85-85: Consider a more accurate error message

The error message "ClerkJS loaded but script element not found" might be misleading. The script element might not exist if ClerkJS was loaded through a different method or if the DOM query failed.

-              reject(new Error('ClerkJS loaded but script element not found'));
+              reject(new Error('ClerkJS coordinator reported success but script element could not be retrieved'));
packages/nextjs/src/utils/clerk-js-script.tsx (1)

9-11: Address the TODO - avoid code duplication

The TODO indicates that the blocking coordinator code is duplicated here instead of being properly imported. This creates maintenance burden and potential for divergence between implementations.

Would you like me to help set up the proper exports so this inline duplication can be removed?

packages/shared/src/clerkJsBlockingCoordinator.ts (1)

202-224: Performance concern: Expensive MutationObserver configuration

Observing the entire body with subtree: true is expensive and the querySelectorAll inside the mutation callback adds more overhead.

Consider:

  1. Only observing specific containers where scripts are likely to be added
  2. Using a more efficient selector strategy
  3. Debouncing the observer callback for multiple mutations
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c97fd0 and b6431b1.

📒 Files selected for processing (5)
  • packages/nextjs/src/utils/clerk-js-script.tsx (2 hunks)
  • packages/shared/src/__tests__/loadClerkJsScript.test.ts (4 hunks)
  • packages/shared/src/clerkJsBlockingCoordinator.ts (1 hunks)
  • packages/shared/src/index.ts (1 hunks)
  • packages/shared/src/loadClerkJsScript.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{js,jsx,ts,tsx}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/development.mdc
**/*.{js,jsx,ts,tsx,json,css,scss,md,yaml,yml}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/development.mdc
packages/**/*.{ts,tsx}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/development.mdc
packages/**/*.{ts,tsx,d.ts}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/development.mdc
packages/**/index.{js,ts}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/development.mdc
**/*.{ts,tsx}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/development.mdc
  • .cursor/rules/typescript.mdc
**/*.{js,ts,tsx,jsx}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/monorepo.mdc
**/index.ts

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/react.mdc
  • .cursor/rules/typescript.mdc
**/*

Instructions used from:

Sources:
⚙️ CodeRabbit Configuration File

packages/**/*.{test,spec}.{js,jsx,ts,tsx}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/monorepo.mdc
**/__tests__/**/*.{ts,tsx}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/typescript.mdc
**/*.{jsx,tsx}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/development.mdc
  • .cursor/rules/react.mdc
**/*.tsx

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/react.mdc
🔇 Additional comments (3)
packages/shared/src/index.ts (1)

40-40: LGTM!

The new export follows the established pattern for module exports in this file.

packages/shared/src/loadClerkJsScript.ts (1)

162-166: Good API design with clear aliasing

The re-exports with descriptive aliases provide a cleaner public API that aligns well with the existing module's purpose.

packages/shared/src/__tests__/loadClerkJsScript.test.ts (1)

12-138: Excellent test refactoring

The move from mocked behavior to real DOM simulation provides much better confidence in the implementation. The race condition test (lines 92-103) is particularly valuable.

<script
ref={scriptRef}
src={scriptUrl}
data-clerk-js-script='true'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Inconsistent data attribute values

The data-clerk-js-script attribute uses 'true' here but empty string in buildClerkJsScriptAttributes. This inconsistency could cause issues.

-          data-clerk-js-script='true'
+          data-clerk-js-script=''

And for the Pages Router:

-        data-clerk-js-script='true'
+        data-clerk-js-script=''

Also applies to: 254-254

🤖 Prompt for AI Agents
In packages/nextjs/src/utils/clerk-js-script.tsx at lines 242 and 254, the data
attribute `data-clerk-js-script` is set to the string 'true', whereas in the
function `buildClerkJsScriptAttributes` it is set as an empty string. To fix
this inconsistency, standardize the value of `data-clerk-js-script` across all
usages by either setting it to 'true' or an empty string consistently, updating
the attribute in these lines to match the value used in
`buildClerkJsScriptAttributes`.

Comment on lines +131 to +148
function useClerkJSLoadingState() {
const [loadingState, setLoadingState] = useState<LoadingState>('idle');

useEffect(() => {
if (typeof window === 'undefined') return;

const coordinator = (window as any).__clerkJSBlockingCoordinator;
if (coordinator) {
coordinator.registerCallback({
onStateChange: (state: LoadingState) => {
setLoadingState(state);
},
});
}
}, []);

return { loadingState };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing cleanup in useEffect

The effect registers a callback but doesn't clean it up, which could cause memory leaks.

   useEffect(() => {
     if (typeof window === 'undefined') return;

     const coordinator = (window as any).__clerkJSBlockingCoordinator;
     if (coordinator) {
+      // Set initial state
+      setLoadingState(coordinator.state);
+      
       coordinator.registerCallback({
         onStateChange: (state: LoadingState) => {
           setLoadingState(state);
         },
       });
     }
+    
+    // Note: The blocking coordinator doesn't provide an unregister method
+    // This could lead to memory leaks if components using this hook are frequently mounted/unmounted
   }, []);

Consider implementing an unregister mechanism in the blocking coordinator.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function useClerkJSLoadingState() {
const [loadingState, setLoadingState] = useState<LoadingState>('idle');
useEffect(() => {
if (typeof window === 'undefined') return;
const coordinator = (window as any).__clerkJSBlockingCoordinator;
if (coordinator) {
coordinator.registerCallback({
onStateChange: (state: LoadingState) => {
setLoadingState(state);
},
});
}
}, []);
return { loadingState };
}
function useClerkJSLoadingState() {
const [loadingState, setLoadingState] = useState<LoadingState>('idle');
useEffect(() => {
if (typeof window === 'undefined') return;
const coordinator = (window as any).__clerkJSBlockingCoordinator;
if (coordinator) {
// Set initial state
setLoadingState(coordinator.state);
coordinator.registerCallback({
onStateChange: (state: LoadingState) => {
setLoadingState(state);
},
});
}
// Note: The blocking coordinator doesn't provide an unregister method
// This could lead to memory leaks if components using this hook are frequently mounted/unmounted
}, []);
return { loadingState };
}
🤖 Prompt for AI Agents
In packages/nextjs/src/utils/clerk-js-script.tsx around lines 131 to 148, the
useEffect registers a callback on the coordinator but does not unregister it on
cleanup, risking memory leaks. Modify the effect to return a cleanup function
that calls an unregister method on the coordinator to remove the callback when
the component unmounts. Ensure the coordinator supports this unregister
mechanism and invoke it properly in the cleanup.

Comment on lines +22 to +128
}

var coordinator = {
state: 'idle',
scriptUrl: null,
scriptElement: null,
error: null,
callbacks: [],

shouldAllowScript: function(scriptElement) {
if (!scriptElement.hasAttribute('data-clerk-js-script')) {
return true;
}

if (this.scriptElement && this.scriptElement.src === scriptElement.src) {
return false;
}

if (window.Clerk) {
this.setState('loaded');
return false;
}

if (this.state === 'loading') {
return false;
}

this.adoptScript(scriptElement);
return true;
},

adoptScript: function(scriptElement) {
this.scriptElement = scriptElement;
this.scriptUrl = scriptElement.src;
this.setState('loading');

var self = this;

scriptElement.addEventListener('load', function() {
scriptElement.setAttribute('data-clerk-loaded', 'true');
self.setState('loaded');
});

scriptElement.addEventListener('error', function() {
self.setState('error', new Error('ClerkJS failed to load'));
});
},

registerCallback: function(callback) {
this.callbacks.push(callback);

if (callback.onStateChange) {
callback.onStateChange(this.state);
}

if (this.state === 'loaded' && callback.onLoad) {
callback.onLoad();
} else if (this.state === 'error' && callback.onError && this.error) {
callback.onError(this.error);
}
},

setState: function(newState, error) {
this.state = newState;
if (error) this.error = error;

for (var i = 0; i < this.callbacks.length; i++) {
var callback = this.callbacks[i];
try {
if (callback.onStateChange) {
callback.onStateChange(newState);
}

if (newState === 'loaded' && callback.onLoad) {
callback.onLoad();
} else if (newState === 'error' && callback.onError && error) {
callback.onError(error);
}
} catch (e) {
console.error('ClerkJS coordinator callback error:', e);
}
}
}
};

var originalAppendChild = Node.prototype.appendChild;
Node.prototype.appendChild = function(child) {
if (child.tagName === 'SCRIPT' && child.hasAttribute('data-clerk-js-script')) {
if (!coordinator.shouldAllowScript(child)) {
return child;
}
}

return originalAppendChild.call(this, child);
};

window.__clerkJSBlockingCoordinator = coordinator;
})();
`.trim();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Security concern: Global prototype modification

The inline script modifies global DOM prototypes (Node.prototype.appendChild), which could:

  1. Conflict with other libraries that also modify these prototypes
  2. Break third-party scripts that expect standard DOM behavior
  3. Create security vulnerabilities if not properly sanitized

Consider using MutationObserver exclusively or implementing a more isolated approach.

Let me verify if there are any other scripts in the codebase that might conflict with these DOM modifications:

Please tag clerk/security to review the security implications of modifying global DOM prototypes.


🏁 Script executed:

#!/bin/bash
# Search for other code that might modify DOM prototypes
rg -A 5 "prototype\.(appendChild|insertBefore)" --type js --type ts

# Search for MutationObserver usage that might conflict
rg -A 5 "MutationObserver" --type js --type ts

# Check if any third-party libraries are known to modify these methods
fd "package.json" --exec jq -r '.dependencies | keys[]' {} \; | sort | uniq | grep -E "(jquery|prototype|mootools|zepto)"

Length of output: 9285


Consolidate and Isolate DOM Prototype Overrides

We’re currently overriding Node.prototype.appendChild (and insertBefore) in two places—
• packages/shared/src/clerkJsBlockingCoordinator.ts
• packages/nextjs/src/utils/clerk-js-script.tsx

This duplication not only increases maintenance burden but also amplifies the risk of conflicts with other code that touches global prototypes.

Suggestions:

  • Remove the inline duplicate in packages/nextjs/src/utils/clerk-js-script.tsx and consume the shared coordinator implementation from packages/shared/src/clerkJsBlockingCoordinator.ts.
  • If global prototype patching is unavoidable, wrap it in a self-contained module that can:
    • Automatically restore the original methods when no longer needed.
    • Expose a clear API for registering/unregistering callbacks.
  • As an alternative to prototype overrides, consider relying solely on a MutationObserver-based approach to intercept injected <script> tags and avoid touching Node.prototype.

Files needing updates:

  • packages/nextjs/src/utils/clerk-js-script.tsx
  • (Optionally) the shared coordinator at packages/shared/src/clerkJsBlockingCoordinator.ts to ensure it supports being imported into Next.js
🤖 Prompt for AI Agents
In packages/nextjs/src/utils/clerk-js-script.tsx around lines 22 to 128, the
code duplicates the override of Node.prototype.appendChild already implemented
in packages/shared/src/clerkJsBlockingCoordinator.ts, causing maintenance and
conflict risks. Remove the inline prototype override and instead import and use
the shared coordinator from packages/shared/src/clerkJsBlockingCoordinator.ts.
Refactor the shared coordinator to provide a self-contained module that manages
the prototype patching with an API to register/unregister callbacks and restore
original methods when needed. Optionally, consider replacing prototype overrides
with a MutationObserver-based approach to detect injected script tags without
modifying global prototypes.

Comment on lines +181 to +203
useEffect(() => {
if (typeof window === 'undefined' || coordinatorInjected.current) return;

// Check if coordinator already exists
if ((window as any).__clerkJSBlockingCoordinator) {
coordinatorInjected.current = true;
return;
}

// Create and inject coordinator script into head
const coordinatorScript = document.createElement('script');
coordinatorScript.id = 'clerk-blocking-coordinator';
coordinatorScript.innerHTML = getBlockingCoordinatorScript();

// Insert at the beginning of head to ensure it runs first
if (document.head.firstChild) {
document.head.insertBefore(coordinatorScript, document.head.firstChild);
} else {
document.head.appendChild(coordinatorScript);
}

coordinatorInjected.current = true;
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential race condition in coordinator injection

Multiple components could attempt to inject the coordinator simultaneously before coordinatorInjected.current is set.

   useEffect(() => {
     if (typeof window === 'undefined' || coordinatorInjected.current) return;

+    // Use a global flag to prevent race conditions
+    if ((window as any).__clerkJSBlockingCoordinatorInjecting) return;
+    (window as any).__clerkJSBlockingCoordinatorInjecting = true;
+
     // Check if coordinator already exists
     if ((window as any).__clerkJSBlockingCoordinator) {
       coordinatorInjected.current = true;
+      delete (window as any).__clerkJSBlockingCoordinatorInjecting;
       return;
     }

     // Create and inject coordinator script into head
     const coordinatorScript = document.createElement('script');
     coordinatorScript.id = 'clerk-blocking-coordinator';
     coordinatorScript.innerHTML = getBlockingCoordinatorScript();

     // Insert at the beginning of head to ensure it runs first
     if (document.head.firstChild) {
       document.head.insertBefore(coordinatorScript, document.head.firstChild);
     } else {
       document.head.appendChild(coordinatorScript);
     }

     coordinatorInjected.current = true;
+    delete (window as any).__clerkJSBlockingCoordinatorInjecting;
   }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (typeof window === 'undefined' || coordinatorInjected.current) return;
// Check if coordinator already exists
if ((window as any).__clerkJSBlockingCoordinator) {
coordinatorInjected.current = true;
return;
}
// Create and inject coordinator script into head
const coordinatorScript = document.createElement('script');
coordinatorScript.id = 'clerk-blocking-coordinator';
coordinatorScript.innerHTML = getBlockingCoordinatorScript();
// Insert at the beginning of head to ensure it runs first
if (document.head.firstChild) {
document.head.insertBefore(coordinatorScript, document.head.firstChild);
} else {
document.head.appendChild(coordinatorScript);
}
coordinatorInjected.current = true;
}, []);
useEffect(() => {
if (typeof window === 'undefined' || coordinatorInjected.current) return;
// Use a global flag to prevent race conditions
if ((window as any).__clerkJSBlockingCoordinatorInjecting) return;
(window as any).__clerkJSBlockingCoordinatorInjecting = true;
// Check if coordinator already exists
if ((window as any).__clerkJSBlockingCoordinator) {
coordinatorInjected.current = true;
delete (window as any).__clerkJSBlockingCoordinatorInjecting;
return;
}
// Create and inject coordinator script into head
const coordinatorScript = document.createElement('script');
coordinatorScript.id = 'clerk-blocking-coordinator';
coordinatorScript.innerHTML = getBlockingCoordinatorScript();
// Insert at the beginning of head to ensure it runs first
if (document.head.firstChild) {
document.head.insertBefore(coordinatorScript, document.head.firstChild);
} else {
document.head.appendChild(coordinatorScript);
}
coordinatorInjected.current = true;
delete (window as any).__clerkJSBlockingCoordinatorInjecting;
}, []);
🤖 Prompt for AI Agents
In packages/nextjs/src/utils/clerk-js-script.tsx around lines 181 to 203, there
is a potential race condition where multiple components might inject the
coordinator script simultaneously before coordinatorInjected.current is set. To
fix this, implement a locking mechanism or use a shared state outside the
component scope to ensure only one injection occurs at a time, preventing
duplicate script insertions.

Comment on lines +280 to +282
// Return a no-op unsubscribe function since the blocking coordinator
// doesn't need complex unsubscription
return () => {};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Misleading unsubscribe function

The function returns a no-op unsubscribe, but the comment acknowledges the coordinator doesn't support unsubscription. This could mislead users into thinking cleanup is happening when it's not.

Either:

  1. Implement proper unsubscription in the coordinator (preferred)
  2. Document clearly in the function's JSDoc that cleanup is not performed
  3. Consider returning void instead of a no-op function
🤖 Prompt for AI Agents
In packages/shared/src/clerkJsBlockingCoordinator.ts around lines 280 to 282,
the function returns a no-op unsubscribe function which may mislead users into
thinking unsubscription or cleanup occurs. To fix this, either implement proper
unsubscription logic in the coordinator, or if that is not feasible, update the
function's JSDoc to clearly state that no cleanup or unsubscription is
performed. Alternatively, consider changing the return type to void instead of
returning a no-op function to avoid confusion.

Comment on lines +171 to +187
var originalAppendChild = Node.prototype.appendChild;
Node.prototype.appendChild = function(child) {
if (!interceptScript(child)) {
// Create a dummy script element to return
var dummy = document.createElement('script');
dummy.src = child.src;
Object.keys(child.attributes || {}).forEach(function(key) {
var attr = child.attributes[key];
if (attr && attr.name && attr.value) {
dummy.setAttribute(attr.name, attr.value);
}
});
return dummy;
}

return originalAppendChild.call(this, child);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: Dummy script elements could cause issues

Creating dummy script elements that are returned but never added to DOM could:

  1. Cause memory leaks if references are held
  2. Break code that expects the returned element to be in the DOM
  3. Violate the appendChild contract

Consider throwing an error or properly handling the duplicate script scenario instead of returning a dummy element.

🤖 Prompt for AI Agents
In packages/shared/src/clerkJsBlockingCoordinator.ts between lines 171 and 187,
the current override of Node.prototype.appendChild returns a dummy script
element when interceptScript(child) is false, which can cause memory leaks and
break DOM expectations. Instead of returning a dummy element, modify the
function to either throw an error to explicitly block unsupported script appends
or handle the duplicate script scenario properly by not returning a dummy but
preventing the append while maintaining the appendChild contract. Ensure the
returned element is either the appended child or handle the case without
breaking DOM integrity.

Comment on lines +1 to +294
return dummy;
}

return originalInsertBefore.call(this, newNode, referenceNode);
};

// Also watch for scripts being set via innerHTML or similar
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
Array.prototype.slice.call(mutation.addedNodes).forEach(function(node) {
if (node.nodeType === 1) { // Element node
if (node.tagName === 'SCRIPT' && node.hasAttribute('data-clerk-js-script')) {
if (!coordinator.shouldAllowScript(node)) {
node.remove();
}
}

// Also check children in case scripts are added in bulk
var scripts = node.querySelectorAll ? node.querySelectorAll('script[data-clerk-js-script]') : [];
for (var i = 0; i < scripts.length; i++) {
if (!coordinator.shouldAllowScript(scripts[i])) {
scripts[i].remove();
}
}
}
});
}
});
});

// Start observing
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true
});
} else {
// If body doesn't exist yet, wait for it
document.addEventListener('DOMContentLoaded', function() {
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}

// Also observe head if it exists
if (document.head) {
observer.observe(document.head, {
childList: true,
subtree: true
});
}

// Expose the coordinator globally
window.__clerkJSBlockingCoordinator = coordinator;
})();
`.trim();
}

/**
* Get the current state from the blocking coordinator.
*/
export function getBlockingCoordinatorState(): LoadingState {
if (typeof window === 'undefined') return 'idle';
const coordinator = (window as any).__clerkJSBlockingCoordinator;
return coordinator ? coordinator.state : 'idle';
}

/**
* Register callbacks with the blocking coordinator.
*/
export function registerWithBlockingCoordinator(callback: {
onLoad?: () => void;
onError?: (error: Error) => void;
onStateChange?: (state: LoadingState) => void;
}): () => void {
if (typeof window === 'undefined') return () => {};

const coordinator = (window as any).__clerkJSBlockingCoordinator;
if (coordinator) {
coordinator.registerCallback(callback);
}

// Return a no-op unsubscribe function since the blocking coordinator
// doesn't need complex unsubscription
return () => {};
}

/**
* Check if ClerkJS is loaded according to the blocking coordinator.
*/
export function isClerkJSLoadedBlocking(): boolean {
if (typeof window === 'undefined') return false;
const coordinator = (window as any).__clerkJSBlockingCoordinator;
return coordinator ? coordinator.state === 'loaded' : false;
}

export type { LoadingState, BlockingCoordinator };
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Security Review Required

This implementation modifies global DOM prototypes which has significant security implications. Please tag clerk/security for review.

Key concerns:

  1. Global prototype pollution
  2. Potential for breaking third-party scripts
  3. No mechanism to restore original behavior
  4. Intercepting all script insertions could be exploited
🤖 Prompt for AI Agents
In packages/shared/src/clerkJsBlockingCoordinator.ts lines 1 to 294, the code
modifies global DOM prototypes like Node.prototype.appendChild and insertBefore,
which poses security risks and can break third-party scripts. To address this,
tag the code for clerk/security review as requested, and implement a mechanism
to restore original prototype methods if needed. Additionally, consider limiting
the interception scope to only trusted contexts or scripts to reduce
exploitation risk, and document these security considerations clearly.

Comment on lines +121 to +134
registerCallback: function(callback) {
this.callbacks.push(callback);

// Call immediately if we have a current state
if (callback.onStateChange) {
callback.onStateChange(this.state);
}

if (this.state === 'loaded' && callback.onLoad) {
callback.onLoad();
} else if (this.state === 'error' && callback.onError && this.error) {
callback.onError(this.error);
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Memory leak: Callbacks array grows indefinitely

The callbacks array only grows and never shrinks, causing a memory leak in long-running applications.

     registerCallback: function(callback) {
-      this.callbacks.push(callback);
+      var callbackId = Date.now() + Math.random();
+      callback._id = callbackId;
+      this.callbacks.push(callback);
       
       // Call immediately if we have a current state
       if (callback.onStateChange) {
         callback.onStateChange(this.state);
       }
       
       if (this.state === 'loaded' && callback.onLoad) {
         callback.onLoad();
       } else if (this.state === 'error' && callback.onError && this.error) {
         callback.onError(this.error);
       }
+      
+      // Return unsubscribe function
+      var self = this;
+      return function() {
+        self.callbacks = self.callbacks.filter(function(cb) {
+          return cb._id !== callbackId;
+        });
+      };
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
registerCallback: function(callback) {
this.callbacks.push(callback);
// Call immediately if we have a current state
if (callback.onStateChange) {
callback.onStateChange(this.state);
}
if (this.state === 'loaded' && callback.onLoad) {
callback.onLoad();
} else if (this.state === 'error' && callback.onError && this.error) {
callback.onError(this.error);
}
},
registerCallback: function(callback) {
// Generate a unique ID for this callback and attach it
var callbackId = Date.now() + Math.random();
callback._id = callbackId;
this.callbacks.push(callback);
// Call immediately if we have a current state
if (callback.onStateChange) {
callback.onStateChange(this.state);
}
if (this.state === 'loaded' && callback.onLoad) {
callback.onLoad();
} else if (this.state === 'error' && callback.onError && this.error) {
callback.onError(this.error);
}
// Return an unsubscribe function to remove this callback
var self = this;
return function() {
self.callbacks = self.callbacks.filter(function(cb) {
return cb._id !== callbackId;
});
};
},
🤖 Prompt for AI Agents
In packages/shared/src/clerkJsBlockingCoordinator.ts around lines 121 to 134,
the callbacks array keeps growing without removing callbacks after they are
called, leading to a memory leak. Modify the registerCallback function to remove
or clean up callbacks from the array once they have been invoked or are no
longer needed, ensuring the array does not grow indefinitely in long-running
applications.

@jacekradko jacekradko closed this Jul 21, 2025
@jacekradko jacekradko deleted the feat/failure-detection branch July 21, 2025 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants