diff --git a/apps/web/src/lib/chunkReloadGuard.test.ts b/apps/web/src/lib/chunkReloadGuard.test.ts index 632e8ec8cf0dc..716dbb3fc4fbc 100644 --- a/apps/web/src/lib/chunkReloadGuard.test.ts +++ b/apps/web/src/lib/chunkReloadGuard.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vite-plus/test"; -import { clearChunkReloadGuard, reloadOnceForChunkLoadError } from "./chunkReloadGuard"; +import { reloadOnceForChunkLoadError } from "./chunkReloadGuard"; function createStorageStub(): Storage { const store = new Map(); @@ -25,21 +25,20 @@ describe("reloadOnceForChunkLoadError", () => { const storage = createStorageStub(); const reload = vi.fn(); - expect(reloadOnceForChunkLoadError(() => storage, reload)).toBe(true); + expect(reloadOnceForChunkLoadError(() => storage, reload, "1.0.0")).toBe(true); expect(reload).toHaveBeenCalledTimes(1); - expect(reloadOnceForChunkLoadError(() => storage, reload)).toBe(false); + expect(reloadOnceForChunkLoadError(() => storage, reload, "1.0.0")).toBe(false); expect(reload).toHaveBeenCalledTimes(1); }); - it("reloads again after a successful boot cleared the guard", () => { + it("reloads again once a deploy changed the build", () => { const storage = createStorageStub(); const reload = vi.fn(); - reloadOnceForChunkLoadError(() => storage, reload); - clearChunkReloadGuard(() => storage); + reloadOnceForChunkLoadError(() => storage, reload, "1.0.0"); - expect(reloadOnceForChunkLoadError(() => storage, reload)).toBe(true); + expect(reloadOnceForChunkLoadError(() => storage, reload, "1.0.1")).toBe(true); expect(reload).toHaveBeenCalledTimes(2); }); @@ -51,6 +50,5 @@ describe("reloadOnceForChunkLoadError", () => { expect(reloadOnceForChunkLoadError(blocked, reload)).toBe(false); expect(reload).not.toHaveBeenCalled(); - expect(() => clearChunkReloadGuard(blocked)).not.toThrow(); }); }); diff --git a/apps/web/src/lib/chunkReloadGuard.ts b/apps/web/src/lib/chunkReloadGuard.ts index c83e0e71e0f4c..0cad00232eb6b 100644 --- a/apps/web/src/lib/chunkReloadGuard.ts +++ b/apps/web/src/lib/chunkReloadGuard.ts @@ -1,15 +1,17 @@ // Deploys and desktop server swaps can invalidate lazy chunks; one reload fetches the fresh index. +// Keyed by build and kept across boots: a chunk loaded after boot can fail on every visit. const CHUNK_RELOAD_GUARD_KEY = "t3code:chunk-load-reloaded"; export function reloadOnceForChunkLoadError( getStorage: () => Storage = () => window.sessionStorage, reload: () => void = () => window.location.reload(), + build = import.meta.env.APP_VERSION, ): boolean { let alreadyReloaded: boolean; try { const storage = getStorage(); - alreadyReloaded = storage.getItem(CHUNK_RELOAD_GUARD_KEY) === "1"; - if (!alreadyReloaded) storage.setItem(CHUNK_RELOAD_GUARD_KEY, "1"); + alreadyReloaded = storage.getItem(CHUNK_RELOAD_GUARD_KEY) === build; + if (!alreadyReloaded) storage.setItem(CHUNK_RELOAD_GUARD_KEY, build); } catch { // Without storage, the reload guard cannot prevent an infinite loop. return false; @@ -18,9 +20,3 @@ export function reloadOnceForChunkLoadError( reload(); return true; } - -export function clearChunkReloadGuard(getStorage: () => Storage = () => window.sessionStorage) { - try { - getStorage().removeItem(CHUNK_RELOAD_GUARD_KEY); - } catch {} -} diff --git a/apps/web/src/main.test.ts b/apps/web/src/main.test.ts new file mode 100644 index 0000000000000..56be08671e331 --- /dev/null +++ b/apps/web/src/main.test.ts @@ -0,0 +1,37 @@ +import { expect, it, vi } from "vite-plus/test"; + +vi.mock("./appInit", () => ({ startAppServices: () => ({ load: async () => {} }) })); +vi.mock("./AppRoot", () => ({ AppRoot: () => null })); +vi.mock("./i18n/runtime", () => ({ whenLocaleReady: async () => {} })); +vi.mock("react-dom/client", () => ({ default: { createRoot: () => ({ render: () => {} }) } })); + +// Boots main in a fresh page that shares the tab's sessionStorage, as a reload does. +async function bootThenFailLazyChunk(sessionStorage: Map) { + vi.resetModules(); + const reload = vi.fn().mockName("location.reload"); + const page = Object.assign(new EventTarget(), { + sessionStorage: { + getItem: (key: string) => sessionStorage.get(key) ?? null, + setItem: (key: string, value: string) => sessionStorage.set(key, value), + removeItem: (key: string) => sessionStorage.delete(key), + }, + location: { reload }, + }); + vi.stubGlobal("window", page); + vi.stubGlobal("document", { getElementById: () => ({}) }); + try { + const { startup } = await import("./main"); + await startup; + page.dispatchEvent(new Event("vite:preloadError", { cancelable: true })); + return reload; + } finally { + vi.unstubAllGlobals(); + } +} + +it("reloads only once when a lazy chunk fails after every boot", async () => { + const sessionStorage = new Map(); + + expect(await bootThenFailLazyChunk(sessionStorage)).toHaveBeenCalledTimes(1); + expect(await bootThenFailLazyChunk(sessionStorage)).not.toHaveBeenCalled(); +}); diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 4469f9cbf6ba3..112de4848170c 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -9,16 +9,14 @@ import { isElectron } from "./env"; import { startAppServices } from "./appInit"; import { AppRoot } from "./AppRoot"; import { whenLocaleReady } from "./i18n/runtime"; -import { clearChunkReloadGuard, reloadOnceForChunkLoadError } from "./lib/chunkReloadGuard"; +import { reloadOnceForChunkLoadError } from "./lib/chunkReloadGuard"; const router = startAppServices(); const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined; -let chunkLoadFailed = false; let reloadScheduled = false; window.addEventListener("vite:preloadError", (event) => { - chunkLoadFailed = true; if (reloadOnceForChunkLoadError()) { reloadScheduled = true; event.preventDefault(); @@ -43,7 +41,6 @@ export const startup = Promise.all([ .then(([ManagedAuthShell]) => { // Route chunk failures resolve router.load() and surface later through the error boundary. if (reloadScheduled) return; - if (!chunkLoadFailed) clearChunkReloadGuard(); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( {ManagedAuthShell && clerkPublishableKey ? (