fix(web): reload once when a deployed chunk fails to import
After a deploy, an open tab still references the previous build's content-hashed chunks and the first lazy navigation dies with "Failed to fetch dynamically imported module". Handle Vite's vite:preloadError by reloading onto the current build, guarded to at most one reload per minute so a persistently missing chunk cannot reload-loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bcc78e7332
commit
8dc2dcbc13
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { installPreloadErrorReload } from "./reloadOnPreloadError";
|
||||
|
||||
function harness(now: () => number) {
|
||||
const reload = vi.fn();
|
||||
const storage = new Map<string, string>();
|
||||
return {
|
||||
reload,
|
||||
deps: {
|
||||
reload,
|
||||
now,
|
||||
getItem: (k: string) => storage.get(k) ?? null,
|
||||
setItem: (k: string, v: string) => storage.set(k, v),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("installPreloadErrorReload", () => {
|
||||
it("reloads when a dynamically imported chunk fails to load", () => {
|
||||
const { reload, deps } = harness(() => 1_000_000);
|
||||
installPreloadErrorReload(deps);
|
||||
|
||||
window.dispatchEvent(new Event("vite:preloadError"));
|
||||
expect(reload).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not reload again within the loop-guard window", () => {
|
||||
let clock = 1_000_000;
|
||||
const { reload, deps } = harness(() => clock);
|
||||
|
||||
installPreloadErrorReload(deps);
|
||||
window.dispatchEvent(new Event("vite:preloadError"));
|
||||
expect(reload).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Post-reload page still hits the error seconds later: give up instead
|
||||
// of reload-looping.
|
||||
clock += 5_000;
|
||||
installPreloadErrorReload(deps);
|
||||
window.dispatchEvent(new Event("vite:preloadError"));
|
||||
expect(reload).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reloads again once the guard window has passed (a later deploy)", () => {
|
||||
let clock = 1_000_000;
|
||||
const { reload, deps } = harness(() => clock);
|
||||
|
||||
installPreloadErrorReload(deps);
|
||||
window.dispatchEvent(new Event("vite:preloadError"));
|
||||
expect(reload).toHaveBeenCalledTimes(1);
|
||||
|
||||
clock += 10 * 60_000;
|
||||
installPreloadErrorReload(deps);
|
||||
window.dispatchEvent(new Event("vite:preloadError"));
|
||||
expect(reload).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
// After a deploy, an open tab's code still references the previous build's
|
||||
// content-hashed chunks; the first lazy navigation then fails with
|
||||
// "Failed to fetch dynamically imported module". Vite reports that as a
|
||||
// window "vite:preloadError" event — reload once so the tab picks up the
|
||||
// current build, with a time guard so a persistently broken chunk cannot
|
||||
// cause a reload loop.
|
||||
|
||||
const GUARD_KEY = "silo:preload-error-reload-at";
|
||||
const GUARD_WINDOW_MS = 60_000;
|
||||
|
||||
interface PreloadErrorReloadDeps {
|
||||
reload: () => void;
|
||||
now: () => number;
|
||||
getItem: (key: string) => string | null;
|
||||
setItem: (key: string, value: string) => void;
|
||||
}
|
||||
|
||||
export function installPreloadErrorReload(deps?: Partial<PreloadErrorReloadDeps>): void {
|
||||
const reload = deps?.reload ?? (() => window.location.reload());
|
||||
const now = deps?.now ?? Date.now;
|
||||
const getItem = deps?.getItem ?? ((k: string) => sessionStorage.getItem(k));
|
||||
const setItem = deps?.setItem ?? ((k: string, v: string) => sessionStorage.setItem(k, v));
|
||||
|
||||
window.addEventListener("vite:preloadError", (event) => {
|
||||
const lastReloadAt = Number(getItem(GUARD_KEY) ?? 0);
|
||||
if (now() - lastReloadAt < GUARD_WINDOW_MS) {
|
||||
// We already reloaded moments ago and the chunk is still missing;
|
||||
// let the failure surface instead of reload-looping.
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setItem(GUARD_KEY, String(now()));
|
||||
reload();
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { installPreloadErrorReload } from "./lib/reloadOnPreloadError";
|
||||
import "./app.css";
|
||||
|
||||
installPreloadErrorReload();
|
||||
|
||||
const root = document.getElementById("root");
|
||||
if (root === null) throw new Error("Root element #root not found");
|
||||
createRoot(root).render(<App />);
|
||||
|
||||
Reference in New Issue
Block a user