const INSTALL_KEY = "__agentNativeRouteChunkRecoveryInstalled"; const INTENDED_NAV_MAX_AGE_MS = 15_000; // Last-resort reload bookkeeping. Persisted in sessionStorage so the cooldown // survives the reload it triggers (the in-memory closure is destroyed), with a // window-scoped fallback for environments where sessionStorage throws. const STALE_CHUNK_RELOAD_AT_KEY = "__agentNativeStaleChunkReloadAt"; const STALE_CHUNK_RELOAD_COOLDOWN_MS = 10_000; export interface RouteChunkRecoveryState { intendedHref: string | null; intendedAt: number; routeModuleFailureAt: number; recoveryHref: string | null; recovering: boolean; } export function createRouteChunkRecoveryState(): RouteChunkRecoveryState { return { intendedHref: null, intendedAt: 0, routeModuleFailureAt: 0, recoveryHref: null, recovering: false, }; } export function isRouteModuleReloadMessage(value: unknown): boolean { return ( typeof value === "string" && /Error loading route module `[^`]+`, reloading page\.\.\./.test(value) ); } export function isDynamicImportFailureMessage(value: unknown): boolean { if (typeof value !== "string") return false; const message = value.toLowerCase(); return ( message.includes("failed to fetch dynamically imported module") || message.includes("error loading dynamically imported module") || message.includes("importing a module script failed") || message.includes("failed to fetch dynamically imported") ); } export function rememberIntendedNavigation( state: RouteChunkRecoveryState, href: string, now = Date.now(), ): void { state.intendedHref = href; state.intendedAt = now; } export function getFreshIntendedNavigation( state: RouteChunkRecoveryState, currentHref: string, now = Date.now(), ): string | null { if (!state.intendedHref) return null; if (now - state.intendedAt > INTENDED_NAV_MAX_AGE_MS) return null; if (state.intendedHref === currentHref) return null; return state.intendedHref; } function anchorFromTarget( target: EventTarget | null, ): HTMLAnchorElement | null { let node = target as HTMLElement | null; while (node) { if ( node.tagName?.toUpperCase() === "A" && typeof (node as HTMLAnchorElement).href === "string" ) { return node as HTMLAnchorElement; } node = node.parentElement; } return null; } function sameOriginHref(win: Window, href: string): string | null { try { const url = new URL(href, win.location.href); return url.origin === win.location.origin ? url.href : null; } catch { return null; } } export function intendedHrefFromClick( win: Window, event: MouseEvent, ): string | null { if (event.defaultPrevented) return null; if (event.button !== 0) return null; if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { return null; } const anchor = anchorFromTarget(event.target); if (!anchor) return null; if (anchor.hasAttribute("download")) return null; const target = anchor.getAttribute("target"); if (target && target !== "_self") return null; return sameOriginHref(win, anchor.href); } function hardNavigate(win: Window, href: string): void { try { win.location.assign(href); } catch { win.location.href = href; } } function isAgentNativeDesktop(win: Window): boolean { return /AgentNativeDesktop/i.test(win.navigator?.userAgent || ""); } function readStaleChunkReloadAt(win: Window): number { try { const raw = ( win as unknown as { sessionStorage?: Storage } ).sessionStorage?.getItem(STALE_CHUNK_RELOAD_AT_KEY); const parsed = raw == null ? NaN : Number(raw); if (Number.isFinite(parsed)) return parsed; } catch {} const mem = (win as unknown as Record)[ STALE_CHUNK_RELOAD_AT_KEY ]; return typeof mem === "number" ? mem : 0; } function markStaleChunkReload(win: Window, now: number): void { (win as unknown as Record)[STALE_CHUNK_RELOAD_AT_KEY] = now; try { (win as unknown as { sessionStorage?: Storage }).sessionStorage?.setItem( STALE_CHUNK_RELOAD_AT_KEY, String(now), ); } catch {} } /** * Last resort when a stale lazy chunk fails to load for the *current* route — * an old tab whose hashed chunk filenames no longer exist after a deploy — and * there is no fresh cross-route navigation to recover to. A single guarded * reload pulls a fresh index.html plus chunk manifest. A sessionStorage cooldown * prevents a reload loop when the chunk is genuinely unreachable (e.g. offline), * letting the error surface to Sentry as before in that case. * * Returns true when a reload was triggered. */ export function reloadForStaleChunk( win: Window | undefined = typeof window === "undefined" ? undefined : window, now = Date.now(), ): boolean { if (!win?.location) return false; // Desktop webviews intentionally stay open across deploys; a forced reload // reads as a random tab refresh, matching recoverToIntendedNavigation(). if (isAgentNativeDesktop(win)) return false; const lastReloadAt = readStaleChunkReloadAt(win); if ( lastReloadAt > 0 && now - lastReloadAt <= STALE_CHUNK_RELOAD_COOLDOWN_MS ) { return false; } markStaleChunkReload(win, now); hardNavigate(win, win.location.href); return true; } /** * Recover when a caught error (e.g. a `React.lazy` rejection surfaced to an * error boundary) is a stale dynamic-import failure. No-op and returns false * for any other error so callers can fall through to their normal handling. */ export function recoverFromStaleChunkError( error: unknown, win: Window | undefined = typeof window === "undefined" ? undefined : window, ): boolean { const message = error instanceof Error ? error.message : String(error ?? ""); if (!isDynamicImportFailureMessage(message)) return false; return reloadForStaleChunk(win); } function recoverToIntendedNavigation( win: Window, state: RouteChunkRecoveryState, ): boolean { const target = getFreshIntendedNavigation(state, win.location.href); const sameCurrentTarget = !target && state.intendedHref === win.location.href && Date.now() - state.intendedAt <= INTENDED_NAV_MAX_AGE_MS ? state.intendedHref : null; const recoveryTarget = target ?? sameCurrentTarget; if (!recoveryTarget) return false; state.recovering = true; state.recoveryHref = recoveryTarget; // Keep the desktop shell mounted, but replace only the route that failed to // load. A current-page reload remains suppressed below when there is no // intended cross-route destination to recover. if (isAgentNativeDesktop(win)) { hardNavigate(win, recoveryTarget); return true; } try { win.history.replaceState(win.history.state, "", recoveryTarget); } catch {} hardNavigate(win, recoveryTarget); return true; } function recoverFromDynamicImportFailure( win: Window, state: RouteChunkRecoveryState, message: string, ): boolean { if (!isDynamicImportFailureMessage(message)) return false; state.routeModuleFailureAt = Date.now(); if (recoverToIntendedNavigation(win, state)) return true; return reloadForStaleChunk(win); } function patchHistoryMethod( win: Window, state: RouteChunkRecoveryState, method: "pushState" | "replaceState", ): void { const original = win.history[method]; win.history[method] = function patchedHistoryMethod(...args) { if (typeof args[2] === "string" || args[2] instanceof URL) { const href = sameOriginHref(win, String(args[2])); if (href) rememberIntendedNavigation(state, href); } return original.apply(this, args); }; } function patchReload(win: Window, state: RouteChunkRecoveryState): void { const originalReload = win.location.reload.bind(win.location); const patchedReload = function patchedReload() { if ( isAgentNativeDesktop(win) && Date.now() - state.routeModuleFailureAt <= 1_000 ) { return; } if ( state.recoveryHref && Date.now() - state.routeModuleFailureAt <= 1_000 ) { hardNavigate(win, state.recoveryHref); return; } if ( Date.now() - state.routeModuleFailureAt <= 1_000 && recoverToIntendedNavigation(win, state) ) { return; } originalReload(); }; try { Object.defineProperty(win.location, "reload", { configurable: true, value: patchedReload, }); } catch { try { win.location.reload = patchedReload; } catch {} } } export function installRouteChunkRecovery( win: Window | undefined = typeof window === "undefined" ? undefined : window, ) { const consoleRef = (win as unknown as { console?: Console } | undefined) ?.console; if ( !win?.document || !win.location || !win.history || typeof win.addEventListener !== "function" || !consoleRef ) { return; } const installedTarget = win as unknown as Record; if (installedTarget[INSTALL_KEY]) return; installedTarget[INSTALL_KEY] = true; const state = createRouteChunkRecoveryState(); win.document.addEventListener( "click", (event) => { const href = intendedHrefFromClick(win, event); if (href) rememberIntendedNavigation(state, href); }, true, ); patchHistoryMethod(win, state, "pushState"); patchHistoryMethod(win, state, "replaceState"); patchReload(win, state); win.addEventListener("unhandledrejection", (event) => { const reason = (event as PromiseRejectionEvent).reason; const message = String(reason?.message || reason || ""); if (recoverFromDynamicImportFailure(win, state, message)) { event.preventDefault(); } }); win.addEventListener("error", (event) => { const errorEvent = event as ErrorEvent; const message = String( errorEvent.error?.message || errorEvent.message || "", ); if (recoverFromDynamicImportFailure(win, state, message)) { event.preventDefault(); } }); // React Router catches stale route-module import failures and reloads the // current URL. Its console message is the only signal exposed before reload. const originalError = consoleRef.error.bind(consoleRef); try { consoleRef.error = (...args: unknown[]) => { if (args.some(isRouteModuleReloadMessage)) { state.routeModuleFailureAt = Date.now(); recoverToIntendedNavigation(win, state); } originalError(...args); }; } catch {} }