/** * Browser wiring for the "server went away" recovery path. * * Owns the DOM half that `preload-error.ts` deliberately does not: the health * probe against the origin serving this bundle, and the reconnect overlay shown * while it is unreachable. The overlay is plain DOM with inline styles rather * than a React surface because it has to render when the app's own route chunks * can no longer be fetched. */ import { installPreloadErrorHandler, type PreloadErrorEnvironment } from "./preload-error"; import { reportRendererHandledError } from "./crash-reporting"; const OVERLAY_ID = "executor-server-disconnected"; const RELOAD_FLAG_KEY = "executor:preload-reloaded"; /** The unauthenticated health endpoint every Executor server exposes. */ const probeServer = async (): Promise => { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-raw-fetch -- boundary: fetch rejects when the server is down, which is precisely the signal being read try { const response = await fetch("/api/health", { cache: "no-store" }); return response.ok && (await response.text()).trim() === "ok"; } catch { return false; } }; const showDisconnectedOverlay = (): void => { if (document.getElementById(OVERLAY_ID)) return; const overlay = document.createElement("div"); overlay.id = OVERLAY_ID; overlay.style.cssText = [ "position:fixed", "inset:0", "z-index:2147483647", "display:grid", "place-items:center", "gap:0.5rem", "background:light-dark(#f7f7f4,#0a0a0a)", "color:light-dark(#18181b,#fafafa)", "color-scheme:light dark", "font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif", "text-align:center", ].join(";"); const title = document.createElement("div"); title.textContent = "Lost connection to the Executor server"; title.style.cssText = "font-size:0.95rem;font-weight:600"; const detail = document.createElement("div"); detail.textContent = "RECONNECTING…"; detail.style.cssText = [ "font-family:ui-monospace,SFMono-Regular,Menlo,monospace", "font-size:0.72rem", "letter-spacing:0.08em", "opacity:0.6", ].join(";"); const stack = document.createElement("div"); stack.style.cssText = "display:grid;gap:0.5rem;justify-items:center"; stack.append(title, detail); overlay.append(stack); document.body.append(overlay); }; const sessionFlag = (): Pick | null => { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: sessionStorage throws when storage is blocked or full try { return window.sessionStorage; } catch { return null; } }; /** * The real browser half of the environment `respondToPreloadError` consumes. * Exported so a test can check the mapping itself — an inverted probe or a * reload guard that never latches would leave every behavioural test green * while the app reload-loops. */ export const browserEnvironment = (): PreloadErrorEnvironment => { const storage = sessionFlag(); return { target: window, probeServer, showDisconnected: showDisconnectedOverlay, reload: () => window.location.reload(), // Routed through the same reporter the rest of the UI uses, so a chunk that // stays missing with a healthy server arrives as a HANDLED error carrying // its surface and action — not as a bare global `reportError`, which the // crash reporter files as an unhandled crash with that context dropped. report: (error) => reportRendererHandledError(error, { surface: "renderer", action: "load-route-chunk", message: "A route chunk stayed unreachable with the server answering", }), // Without storage the guard degrades to "never auto-reload", which is the // safe direction: a missing chunk gets reported instead of looping. readReloadFlag: () => (storage ? storage.getItem(RELOAD_FLAG_KEY) !== null : true), writeReloadFlag: () => storage?.setItem(RELOAD_FLAG_KEY, "1"), delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), }; }; /** * Install the preload-error recovery path. Safe to call outside a browser * (SSR/tests), where it is a no-op. */ export const installServerDisconnectedRecovery = (): void => { if (typeof window === "undefined") return; installPreloadErrorHandler(browserEnvironment()); };