All files / src/scripts chunk-reload.ts

100% Statements 19/19
100% Branches 16/16
100% Functions 3/3
100% Lines 15/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63                            1x 1x               14x 12x 12x   10x 14x                 6x     7x         6x 6x     7x 4x         5x           1x  
// Self-heal stale tabs after a deploy.
//
// Astro fingerprints client JS/CSS as `/_astro/[hash].js`. Each deploy mints
// new hashes and deletes the old files. A tab opened before a deploy still
// references the old hashes; when it later soft-navigates (ClientRouter) or
// prefetches, the request for the now-deleted chunk 404s, the script fails to
// boot, and the page goes blank. One full reload fetches fresh HTML with
// current hashes and fixes it.
//
// Resource-load errors ('error' on a <script>/<link>) do NOT bubble, so we
// listen in the capture phase on window. The listener is registered once on
// import; window survives ClientRouter swaps, so it persists across SPA navs
// without needing `data-astro-rerun`.
 
const RELOAD_KEY = "__chunkReloadAt";
const RELOAD_COOLDOWN_MS = 10_000;
 
/**
 * True only for a failed load of one of our own fingerprinted bundle assets
 * (a <script>/<link> whose src/href contains `/_astro/`, regardless of
 * origin). False for images, inline script errors, or elements with no URL.
 */
export function isStaleChunkError(target: EventTarget | null): boolean {
	if (!(target instanceof Element)) return false;
	const tag = target.tagName;
	if (tag !== "SCRIPT" && tag !== "LINK") return false;
	const url =
		target.getAttribute("src") ?? target.getAttribute("href") ?? "";
	return url.includes("/_astro/");
}
 
/**
 * Register a capture-phase 'error' listener that reloads the page once when a
 * fingerprinted `/_astro/*` asset 404s. A sessionStorage timestamp guards
 * against reload storms: at most one reload per RELOAD_COOLDOWN_MS.
 */
export function installChunkReloadGuard(win: Window = window): void {
	win.addEventListener(
		"error",
		(event) => {
			if (!isStaleChunkError(event.target)) return;
 
			// Loop-guard. sessionStorage can throw (Safari private mode); a
			// failure to read/write must not swallow the reload — better to
			// risk a rare extra reload than leave the user on a blank page.
			try {
				const last = Number(
					win.sessionStorage.getItem(RELOAD_KEY) ?? "0",
				);
				if (Date.now() - last < RELOAD_COOLDOWN_MS) return;
				win.sessionStorage.setItem(RELOAD_KEY, String(Date.now()));
			} catch {
				// ignore storage failures and fall through to reload
			}
 
			win.location.reload();
		},
		true,
	);
}
 
installChunkReloadGuard();