Skip to content

Commit 71023f2

Browse files
committed
feat(shared): backport intelligent retries to loadClerkJsScript
Enhanced loadClerkJsScript and loadClerkUiScript with intelligent error detection and retry logic: - Added hasScriptRequestError() function that uses Performance API to detect failed script loads by checking transferSize, decodedBodySize, responseEnd, and responseStatus - If an existing script has a request error, remove it and retry loading - If waiting for an existing script times out, remove it and allow retry - Added error event listener to bail out early on script load failures instead of waiting for full timeout - Cache scriptUrl to check for errors before attempting to wait for existing script This prevents indefinite hangs when script loads fail and enables automatic recovery from transient network issues.
1 parent 94bcb1d commit 71023f2

File tree

4 files changed

+90
-17
lines changed

4 files changed

+90
-17
lines changed

packages/shared/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ throw new Error(
33
);
44

55
export {};
6+
// Force rebuild for explicit exports (replacing wildcard)

packages/shared/src/loadClerkJsScript.ts

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,51 @@ function isClerkGlobalProperlyLoaded(prop: 'Clerk' | '__internal_ClerkUiCtor'):
5555
const isClerkProperlyLoaded = () => isClerkGlobalProperlyLoaded('Clerk');
5656
const isClerkUiProperlyLoaded = () => isClerkGlobalProperlyLoaded('__internal_ClerkUiCtor');
5757

58+
/**
59+
* Checks if an existing script has a request error using Performance API.
60+
*
61+
* @param scriptUrl - The URL of the script to check.
62+
* @returns True if the script has failed to load due to a network/HTTP error.
63+
*/
64+
function hasScriptRequestError(scriptUrl: string): boolean {
65+
if (typeof window === 'undefined' || !window.performance) {
66+
return false;
67+
}
68+
69+
const entries = performance.getEntriesByName(scriptUrl, 'resource') as PerformanceResourceTiming[];
70+
71+
if (entries.length === 0) {
72+
return false;
73+
}
74+
75+
const scriptEntry = entries[entries.length - 1];
76+
77+
// transferSize === 0 with responseEnd === 0 indicates network failure
78+
// transferSize === 0 with responseEnd > 0 might be a 4xx/5xx error or blocked request
79+
if (scriptEntry.transferSize === 0 && scriptEntry.decodedBodySize === 0) {
80+
// If there was no response at all, it's definitely an error
81+
if (scriptEntry.responseEnd === 0) {
82+
return true;
83+
}
84+
// If we got a response but no content, likely an HTTP error (4xx/5xx)
85+
if (scriptEntry.responseEnd > 0 && scriptEntry.responseStart > 0) {
86+
return true;
87+
}
88+
89+
if ('responseStatus' in scriptEntry) {
90+
const status = (scriptEntry as any).responseStatus;
91+
if (status >= 400) {
92+
return true;
93+
}
94+
if (scriptEntry.responseStatus === 0) {
95+
return true;
96+
}
97+
}
98+
}
99+
100+
return false;
101+
}
102+
58103
/**
59104
* Hotloads the Clerk JS script with robust failure detection.
60105
*
@@ -88,20 +133,30 @@ export const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions): Promis
88133
return null;
89134
}
90135

91-
const existingScript = document.querySelector<HTMLScriptElement>('script[data-clerk-js-script]');
92-
93-
if (existingScript) {
94-
return waitForPredicateWithTimeout(timeout, isClerkProperlyLoaded, rejectWith());
95-
}
96-
97136
if (!opts?.publishableKey) {
98137
errorThrower.throwMissingPublishableKeyError();
99138
return null;
100139
}
101140

141+
const scriptUrl = clerkJsScriptUrl(opts);
142+
const existingScript = document.querySelector<HTMLScriptElement>('script[data-clerk-js-script]');
143+
144+
if (existingScript) {
145+
if (hasScriptRequestError(scriptUrl)) {
146+
existingScript.remove();
147+
} else {
148+
try {
149+
await waitForPredicateWithTimeout(timeout, isClerkProperlyLoaded, rejectWith(), existingScript);
150+
return null;
151+
} catch {
152+
existingScript.remove();
153+
}
154+
}
155+
}
156+
102157
const loadPromise = waitForPredicateWithTimeout(timeout, isClerkProperlyLoaded, rejectWith());
103158

104-
loadScript(clerkJsScriptUrl(opts), {
159+
loadScript(scriptUrl, {
105160
async: true,
106161
crossOrigin: 'anonymous',
107162
nonce: opts.nonce,
@@ -125,19 +180,30 @@ export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promis
125180
return null;
126181
}
127182

128-
const existingScript = document.querySelector<HTMLScriptElement>('script[data-clerk-ui-script]');
129-
130-
if (existingScript) {
131-
return waitForPredicateWithTimeout(timeout, isClerkUiProperlyLoaded, rejectWith());
132-
}
133-
134183
if (!opts?.publishableKey) {
135184
errorThrower.throwMissingPublishableKeyError();
136185
return null;
137186
}
138187

188+
const scriptUrl = clerkUiScriptUrl(opts);
189+
const existingScript = document.querySelector<HTMLScriptElement>('script[data-clerk-ui-script]');
190+
191+
if (existingScript) {
192+
if (hasScriptRequestError(scriptUrl)) {
193+
existingScript.remove();
194+
} else {
195+
try {
196+
await waitForPredicateWithTimeout(timeout, isClerkUiProperlyLoaded, rejectWith(), existingScript);
197+
return null;
198+
} catch {
199+
existingScript.remove();
200+
}
201+
}
202+
}
203+
139204
const loadPromise = waitForPredicateWithTimeout(timeout, isClerkUiProperlyLoaded, rejectWith());
140-
loadScript(clerkUiScriptUrl(opts), {
205+
206+
loadScript(scriptUrl, {
141207
async: true,
142208
crossOrigin: 'anonymous',
143209
nonce: opts.nonce,
@@ -223,6 +289,7 @@ function waitForPredicateWithTimeout(
223289
timeoutMs: number,
224290
predicate: () => boolean,
225291
rejectWith: Error,
292+
existingScript?: HTMLScriptElement,
226293
): Promise<HTMLScriptElement | null> {
227294
return new Promise((resolve, reject) => {
228295
let resolved = false;
@@ -232,6 +299,12 @@ function waitForPredicateWithTimeout(
232299
clearInterval(pollInterval);
233300
};
234301

302+
// Bail out early if the script fails to load, instead of waiting for the entire timeout
303+
existingScript?.addEventListener('error', () => {
304+
cleanup(timeoutId, pollInterval);
305+
reject(rejectWith);
306+
});
307+
235308
const checkAndResolve = () => {
236309
if (resolved) {
237310
return;

packages/shared/src/loadScript.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ export async function loadScript(src = '', opts: LoadScriptOptions): Promise<HTM
3636
script.defer = defer || false;
3737

3838
script.addEventListener('load', () => {
39-
console.log('this loaded ', src);
40-
4139
script.remove();
4240
resolve(script);
4341
});
@@ -56,7 +54,6 @@ export async function loadScript(src = '', opts: LoadScriptOptions): Promise<HTM
5654

5755
return retry(load, {
5856
shouldRetry: (_, iterations) => {
59-
console.log('nikos 3', _, iterations);
6057
return iterations <= 5;
6158
},
6259
});

packages/shared/src/types/environment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { APIKeysSettingsResource } from './apiKeysSettings';
22
import type { AuthConfigResource } from './authConfig';
33
import type { CommerceSettingsResource } from './commerceSettings';
4+
import type { EnableEnvironmentSettingParams } from './devtools';
45
import type { DisplayConfigResource } from './displayConfig';
56
import type { OrganizationSettingsResource } from './organizationSettings';
67
import type { ProtectConfigResource } from './protectConfig';
@@ -23,4 +24,5 @@ export interface EnvironmentResource extends ClerkResource {
2324
maintenanceMode: boolean;
2425
clientDebugMode: boolean;
2526
__internal_toSnapshot: () => EnvironmentJSONSnapshot;
27+
__internal_enableEnvironmentSetting: (params: EnableEnvironmentSettingParams) => Promise<void>;
2628
}

0 commit comments

Comments
 (0)