export const DEV_CLIENT_LOAD_ATTEMPTS = 10; export const DEV_CLIENT_RETRY_DELAY_MS = 500; export type DevClientLoadOutcome = 'loaded' | 'aborted' | 'failed'; export interface DevClientLoadDeps { load: () => Promise; isAborted: () => boolean; wait: (delayMs: number) => Promise; onRetry: (attempt: number, error: unknown) => void; onGiveUp: (attempts: number, error: unknown) => void; } /** * The dev client is served by a watch-mode build, so a load can fail for * reasons that resolve on their own: a rebuild in flight, or a shutdown that * aborted the navigation. Failing hard skipped every window listener * registered after the load and left a blank window with no message, so this * retries and always reports instead of throwing. */ export async function loadDevelopmentClientWithRetry(deps: DevClientLoadDeps): Promise { for (let attempt = 1; attempt <= DEV_CLIENT_LOAD_ATTEMPTS; attempt += 1) { if (deps.isAborted()) { return 'aborted'; } try { await deps.load(); return 'loaded'; } catch (error) { if (deps.isAborted()) { return 'aborted'; } if (attempt === DEV_CLIENT_LOAD_ATTEMPTS) { deps.onGiveUp(attempt, error); return 'failed'; } deps.onRetry(attempt, error); await deps.wait(DEV_CLIENT_RETRY_DELAY_MS); } } return 'failed'; }