A tab names the account it was claimed for on every request, beacon and upload (X-Siftlode-Expect-Account); current_user and /auth/logout refuse a mismatch with 401 account_changed and keep the session, so a tab left showing the account signed in before reloads instead of writing to the new one. /auth/login clears a connect marker left by an abandoned connect, and a link completes only on a session that still holds. The prefs merge reads the row under a lock. A tab's sessionStorage records its own owner. A page the account loses is given up, address bar and remembered page included. The panel layouts are preferences; the storage contract also refuses a key handed to the Storage API or to usePref outside its kind. A refused sign-in as someone else and a failed sign-out say so. The chromium-only e2e specs are in the gate register, the member walk waits for its requests, and devices.spec checks the save itself.
56 lines
2.1 KiB
TypeScript
56 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";
|
|
import { LS } from "./storage";
|
|
|
|
const KEY = LS.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);
|
|
}
|