From 4b74ae3cc27b80b2cd555f3c4e900fdf7eb8974c Mon Sep 17 00:00:00 2001 From: cynarAI Date: Fri, 2 Oct 2026 18:01:19 +0000 Subject: [PATCH] fix(web): reload once when the module graph is aborted while booting The lazy-route recovery cannot help when the browser aborts a module of the static import graph: nothing of the app runs, the root stays empty, and the only trace is a console error. A Web E2E run hit this right after a page reload, when four modules of the shell failed with net::ERR_NETWORK_CHANGED and the shell never rendered. An inline script in the HTML entry now listens for the error event of the module script and reloads the page once, guarded by a session flag that the entry module clears after a successful boot. The existing preload recovery stays in place; it only covers Vite's chunk preloads of production builds. A new E2E spec aborts one module request during boot and one lazy route request, and expects the sign-in page to render in both cases. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u (cherry picked from commit e5b7e036e1a60e7e270592c4b9382ee23ed45ccb) --- apps/web/e2e/module-recovery.spec.ts | 38 ++++++++++++++++++++++++++++ apps/web/index.html | 20 +++++++++++++++ apps/web/src/main.tsx | 4 +++ 3 files changed, 62 insertions(+) create mode 100644 apps/web/e2e/module-recovery.spec.ts diff --git a/apps/web/e2e/module-recovery.spec.ts b/apps/web/e2e/module-recovery.spec.ts new file mode 100644 index 0000000..f557942 --- /dev/null +++ b/apps/web/e2e/module-recovery.spec.ts @@ -0,0 +1,38 @@ +import type { Page } from "@playwright/test"; +import { expect, test } from "@playwright/test"; + +/** Abort the first request for a module once, then let every later request through. */ +async function abortFirstRequest(page: Page, pattern: string) { + const counter = { aborted: 0 }; + await page.route(pattern, async (route) => { + if (counter.aborted === 0) { + counter.aborted += 1; + await route.abort("failed"); + return; + } + await route.continue(); + }); + return counter; +} + +test("the app reloads itself once when a module request aborts while booting", async ({ page }) => { + const counter = await abortFirstRequest(page, "**/src/App.tsx"); + + await page.goto("/sign-in", { waitUntil: "commit" }); + + await expect(page.getByRole("heading", { name: "Sign in to EvOo" })).toBeVisible(); + expect(counter.aborted).toBe(1); + expect(await page.evaluate(() => sessionStorage.getItem("rakazo:boot-reload"))).toBeNull(); +}); + +test("a lazy route recovers when its module request aborts", async ({ page }) => { + const counter = await abortFirstRequest(page, "**/src/pages/Auth.tsx"); + + await page.goto("/sign-in"); + + await expect(page.getByRole("heading", { name: "Sign in to EvOo" })).toBeVisible(); + expect(counter.aborted).toBe(1); + expect( + await page.evaluate(() => sessionStorage.getItem("rakazo:module-reload:pages/Auth")), + ).toBeNull(); +}); diff --git a/apps/web/index.html b/apps/web/index.html index 80df422..c626fbd 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -29,6 +29,26 @@ if (meta) meta.setAttribute("content", theme === "light" ? "#fafaf8" : "#07080a"); })(); +
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index a0bbd2c..1b03bf7 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -12,6 +12,10 @@ import { resolveUiLocale } from "./lib/ui-locale"; import "./styles.css"; markOnce("rk:renderer:module-evaluated"); +try { + // Set by the inline boot script in index.html before it reloads once. + sessionStorage.removeItem("rakazo:boot-reload"); +} catch {} installPreloadRecovery(); applyUiDirection(resolveUiLocale()); applyUiAppearance();