Skip to content

Commit f9b5aa2

Browse files
committed
wip
1 parent bbd948a commit f9b5aa2

File tree

6 files changed

+304
-143
lines changed

6 files changed

+304
-143
lines changed

packages/clerk-js/src/core/__tests__/clerk.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ describe('Clerk singleton', () => {
222222
try {
223223
const loadPromise = sut.load();
224224

225-
await vi.advanceTimersByTimeAsync(2000);
225+
await vi.runAllTimersAsync();
226226
await loadPromise;
227227
} finally {
228228
authCreateSpy.mockRestore();
@@ -254,10 +254,20 @@ describe('Clerk singleton', () => {
254254

255255
try {
256256
const loadPromise = sut.load();
257-
const expectationPromise = expect(loadPromise).rejects.toThrow(/Clerk: Network failure/);
258257

259-
await vi.advanceTimersByTimeAsync(2000);
260-
await expectationPromise;
258+
await vi.runAllTimersAsync();
259+
260+
try {
261+
await loadPromise;
262+
throw new Error('Expected load to throw');
263+
} catch (err) {
264+
expect(err).toBeInstanceOf(Error);
265+
expect((err as Error).message).toMatch(/Something went wrong initializing Clerk/);
266+
const cause = (err as Error).cause as any;
267+
expect(cause).toBeDefined();
268+
expect(cause.code).toBe('network_error');
269+
expect(cause.clerkRuntimeError).toBe(true);
270+
}
261271

262272
expect(mountSpy).toHaveBeenCalledTimes(2);
263273
expect(mockClientFetch).toHaveBeenCalledTimes(2);

packages/clerk-js/src/core/clerk.ts

Lines changed: 85 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ import { assertNoLegacyProp } from '../utils/assertNoLegacyProp';
136136
import { CLERK_ENVIRONMENT_STORAGE_ENTRY, SafeLocalStorage } from '../utils/localStorage';
137137
import { memoizeListenerCallback } from '../utils/memoizeStateListenerCallback';
138138
import { RedirectUrls } from '../utils/redirectUrls';
139+
import { withRetry } from '../utils/retry';
139140
import { AuthCookieService } from './auth/AuthCookieService';
140141
import { CaptchaHeartbeat } from './auth/CaptchaHeartbeat';
141142
import { CLERK_SATELLITE_URL, CLERK_SUFFIXED_COOKIES, CLERK_SYNCED, ERROR_CODES } from './constants';
@@ -2570,124 +2571,109 @@ export class Clerk implements ClerkInterface {
25702571

25712572
let initializationDegradedCounter = 0;
25722573

2573-
const wait = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
2574+
const initializeClerk = async (): Promise<void> => {
2575+
const initEnvironmentPromise = Environment.getInstance()
2576+
.fetch({ touch: shouldTouchEnv })
2577+
.then(res => this.updateEnvironment(res))
2578+
.catch(() => {
2579+
++initializationDegradedCounter;
2580+
const environmentSnapshot = SafeLocalStorage.getItem<EnvironmentJSONSnapshot | null>(
2581+
CLERK_ENVIRONMENT_STORAGE_ENTRY,
2582+
null,
2583+
);
25742584

2575-
const maxRetries = 2;
2576-
let retries = 0;
2577-
while (retries < maxRetries) {
2578-
try {
2579-
const initEnvironmentPromise = Environment.getInstance()
2580-
.fetch({ touch: shouldTouchEnv })
2581-
.then(res => this.updateEnvironment(res))
2582-
.catch(() => {
2583-
++initializationDegradedCounter;
2584-
const environmentSnapshot = SafeLocalStorage.getItem<EnvironmentJSONSnapshot | null>(
2585-
CLERK_ENVIRONMENT_STORAGE_ENTRY,
2586-
null,
2587-
);
2585+
if (environmentSnapshot) {
2586+
this.updateEnvironment(new Environment(environmentSnapshot));
2587+
}
2588+
});
25882589

2589-
if (environmentSnapshot) {
2590-
this.updateEnvironment(new Environment(environmentSnapshot));
2590+
const initClient = async () => {
2591+
return Client.getOrCreateInstance()
2592+
.fetch()
2593+
.then(res => this.updateClient(res))
2594+
.catch(async e => {
2595+
/**
2596+
* Only handle non 4xx errors, like 5xx errors and network errors.
2597+
*/
2598+
if (is4xxError(e)) {
2599+
throw e;
25912600
}
2592-
});
25932601

2594-
const initClient = async () => {
2595-
return Client.getOrCreateInstance()
2596-
.fetch()
2597-
.then(res => this.updateClient(res))
2598-
.catch(async e => {
2599-
/**
2600-
* Only handle non 4xx errors, like 5xx errors and network errors.
2601-
*/
2602-
if (is4xxError(e)) {
2603-
// bubble it up
2604-
throw e;
2605-
}
2606-
2607-
++initializationDegradedCounter;
2608-
2609-
const jwtInCookie = this.#authService?.getSessionCookie();
2610-
const localClient = createClientFromJwt(jwtInCookie);
2611-
2612-
this.updateClient(localClient);
2613-
2614-
/**
2615-
* In most scenarios we want the poller to stop while we are fetching a fresh token during an outage.
2616-
* We want to avoid having the below `getToken()` retrying at the same time as the poller.
2617-
*/
2618-
this.#authService?.stopPollingForToken();
2619-
2620-
// Attempt to grab a fresh token
2621-
await this.session
2622-
?.getToken({ skipCache: true })
2623-
// If the token fetch fails, let Clerk be marked as loaded and leave it up to the poller.
2624-
.catch(() => null)
2625-
.finally(() => {
2626-
this.#authService?.startPollingForToken();
2627-
});
2628-
2629-
// Allows for Clerk to be marked as loaded with the client and session created from the JWT.
2630-
return null;
2631-
});
2632-
};
2633-
2634-
const initComponents = () => {
2635-
if (Clerk.mountComponentRenderer && !this.#componentControls) {
2636-
this.#componentControls = Clerk.mountComponentRenderer(
2637-
this,
2638-
this.environment as Environment,
2639-
this.#options,
2640-
);
2641-
}
2642-
};
2602+
++initializationDegradedCounter;
26432603

2644-
const [, clientResult] = await allSettled([initEnvironmentPromise, initClient()]);
2604+
const jwtInCookie = this.#authService?.getSessionCookie();
2605+
const localClient = createClientFromJwt(jwtInCookie);
26452606

2646-
if (clientResult.status === 'rejected') {
2647-
const e = clientResult.reason;
2607+
this.updateClient(localClient);
26482608

2649-
if (isError(e, 'requires_captcha')) {
2650-
initComponents();
2651-
await initClient();
2652-
} else {
2653-
throw e;
2654-
}
2655-
}
2609+
/**
2610+
* In most scenarios we want the poller to stop while we are fetching a fresh token during an outage.
2611+
* We want to avoid having the below `getToken()` retrying at the same time as the poller.
2612+
*/
2613+
this.#authService?.stopPollingForToken();
2614+
2615+
await this.session
2616+
?.getToken({ skipCache: true })
2617+
.catch(() => null)
2618+
.finally(() => {
2619+
this.#authService?.startPollingForToken();
2620+
});
26562621

2657-
this.#authService?.setClientUatCookieForDevelopmentInstances();
2622+
return null;
2623+
});
2624+
};
26582625

2659-
if (await this.#redirectFAPIInitiatedFlow()) {
2660-
return;
2626+
const initComponents = () => {
2627+
if (Clerk.mountComponentRenderer && !this.#componentControls) {
2628+
this.#componentControls = Clerk.mountComponentRenderer(this, this.environment as Environment, this.#options);
26612629
}
2630+
};
26622631

2663-
initComponents();
2632+
const [, clientResult] = await allSettled([initEnvironmentPromise, initClient()]);
26642633

2665-
break;
2666-
} catch (err) {
2667-
if (!isValidBrowserOnline()) {
2668-
console.warn(err);
2669-
return;
2634+
if (clientResult.status === 'rejected') {
2635+
const e = clientResult.reason;
2636+
2637+
if (isError(e, 'requires_captcha')) {
2638+
initComponents();
2639+
await initClient();
2640+
} else {
2641+
throw e;
26702642
}
2643+
}
26712644

2672-
const isDevBrowserUnauthenticated = isError(err, 'dev_browser_unauthenticated');
2673-
const isNetworkError = isClerkRuntimeError(err) && err.code === 'network_error';
2645+
this.#authService?.setClientUatCookieForDevelopmentInstances();
26742646

2675-
if (!isDevBrowserUnauthenticated && !isNetworkError) {
2676-
throw err;
2677-
}
2647+
if (await this.#redirectFAPIInitiatedFlow()) {
2648+
return;
2649+
}
26782650

2679-
retries += 1;
2651+
initComponents();
2652+
};
26802653

2681-
if (isDevBrowserUnauthenticated) {
2682-
await this.#authService.handleUnauthenticatedDevBrowser();
2683-
}
2654+
try {
2655+
await withRetry(initializeClerk, {
2656+
jitter: true,
2657+
maxAttempts: 2,
2658+
shouldRetry: async error => {
2659+
if (!isValidBrowserOnline()) {
2660+
console.warn(error);
2661+
return false;
2662+
}
26842663

2685-
if (retries >= maxRetries) {
2686-
clerkErrorInitFailed(err);
2687-
}
2664+
const isDevBrowserUnauthenticated = isError(error as any, 'dev_browser_unauthenticated');
2665+
const isNetworkError = isClerkRuntimeError(error) && error.code === 'network_error';
26882666

2689-
await wait(Math.pow(2, retries) * 1_000);
2690-
}
2667+
if (isDevBrowserUnauthenticated && this.#authService) {
2668+
await this.#authService.handleUnauthenticatedDevBrowser();
2669+
return true;
2670+
}
2671+
2672+
return isNetworkError;
2673+
},
2674+
});
2675+
} catch (err) {
2676+
clerkErrorInitFailed(err);
26912677
}
26922678

26932679
this.#captchaHeartbeat = new CaptchaHeartbeat(this);

packages/clerk-js/src/core/errors.ts

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,46 +16,8 @@ export function clerkNetworkError(url: string, e: Error): never {
1616
throw new Error(`${errorPrefix} Network error at "${url}" - ${e}. Please try again.`);
1717
}
1818

19-
const formatErrorCause = (cause: unknown): string | null => {
20-
if (!cause) {
21-
return null;
22-
}
23-
24-
if (cause instanceof Error) {
25-
return cause.message;
26-
}
27-
28-
if (typeof cause === 'string') {
29-
return cause;
30-
}
31-
32-
if (typeof cause === 'number' || typeof cause === 'bigint' || typeof cause === 'boolean') {
33-
return cause.toString();
34-
}
35-
36-
if (typeof cause === 'symbol') {
37-
return cause.description ? `Symbol(${cause.description})` : 'Symbol()';
38-
}
39-
40-
if (typeof cause === 'function') {
41-
return cause.name ? `[Function ${cause.name}]` : '[Function anonymous]';
42-
}
43-
44-
if (typeof cause === 'object') {
45-
try {
46-
return JSON.stringify(cause);
47-
} catch {
48-
return Object.prototype.toString.call(cause);
49-
}
50-
}
51-
52-
return '[Unknown cause]';
53-
};
54-
5519
export function clerkErrorInitFailed(error?: unknown): never {
56-
const formattedCause = formatErrorCause(error);
57-
const cause = formattedCause ? ` Cause: ${formattedCause}` : '';
58-
throw new Error(`${errorPrefix} Something went wrong initializing Clerk.${cause}`);
20+
throw new Error(`${errorPrefix} Something went wrong initializing Clerk.`, { cause: error });
5921
}
6022

6123
export function clerkErrorDevInitFailed(msg = ''): never {

0 commit comments

Comments
 (0)