Files
siftlode/frontend/src/lib/staleBuild.ts
T
peter 51e98bfc38 fix(app): reload onto a new build instead of crashing on a missing chunk
A tab left open across a release asked for a lazy chunk the server no
longer has (prod: DownloadDialog 404 an hour after 0.89) and fell to the
crash screen. `vite:preloadError` now reloads the tab once a minute at
most; a second failure is let through to the error boundary, which now
wraps every entry — the public /watch, /privacy and /terms pages crashed
to a blank page.
2026-09-19 04:51:31 +02:00

55 lines
2.1 KiB
TypeScript

// 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);
}