// A tab left open across a release still runs the old build, and the old build's lazy chunks are gone // from the server the moment the new one is deployed: its first lazy import (the download dialog, a // module it had not opened yet) answers 404, and the whole app fell to the crash screen. Measured on // prod 2026-09-19 — `GET /assets/DownloadDialog-….js 404` at 00:59, an hour after 0.89 went out. // // Vite announces such a failure as `vite:preloadError` (for the import itself as well as for its // preloaded dependencies); cancelling it and reloading puts the tab on the current build. Once per // minute at most: a chunk that is missing from the NEW build too would otherwise reload forever, so // the second failure inside the minute is let through to the error boundary, which says so. import { log } from "./log"; const KEY = "sf.staleBuildReload"; const WINDOW_MS = 60_000; let reloading = false; /** True once a reload for a newer build is under way: whatever fails to render in the meantime is * that missing chunk, and the error boundary must not report it as a crash. */ export function reloadingForNewBuild(): boolean { return reloading; } function lastReload(): number { try { return Number(sessionStorage.getItem(KEY) ?? 0) || 0; } catch { return 0; } } /** Returns the uninstall (for tests: the listener is on `window`, which outlives a module reset). */ export function installStaleBuildReload( reload: () => void = () => window.location.reload(), ): () => void { const onError = (event: Event) => { if (reloading) { event.preventDefault(); return; } if (Date.now() - lastReload() < WINDOW_MS) return; try { sessionStorage.setItem(KEY, String(Date.now())); } catch { // No storage (a private window): without the marker a reload could repeat, so don't. return; } event.preventDefault(); reloading = true; log.warn("app.stale_build", "a chunk of this build is gone from the server; reloading"); reload(); }; window.addEventListener("vite:preloadError", onError); return () => window.removeEventListener("vite:preloadError", onError); }