diff --git a/client/src/main.tsx b/client/src/main.tsx index 075e0d6..438aa74 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -9,6 +9,7 @@ import { getLoginUrl } from "./const"; import "./index.css"; const queryClient = new QueryClient(); +const ASSET_REFRESH_KEY = "asset-recovery-reloaded"; const redirectToLoginIfUnauthorized = (error: unknown) => { if (!(error instanceof TRPCClientError)) return; @@ -21,6 +22,60 @@ const redirectToLoginIfUnauthorized = (error: unknown) => { window.location.href = getLoginUrl(); }; +function reloadForStaleAsset(reason: string) { + if (typeof window === "undefined") return; + + const alreadyReloaded = window.sessionStorage.getItem(ASSET_REFRESH_KEY) === "1"; + if (alreadyReloaded) { + console.error("[Asset Recovery] stale asset still failing after reload", reason); + return; + } + + window.sessionStorage.setItem(ASSET_REFRESH_KEY, "1"); + console.warn("[Asset Recovery] reloading page due to stale asset failure:", reason); + window.location.reload(); +} + +function clearAssetRecoveryFlag() { + if (typeof window === "undefined") return; + window.sessionStorage.removeItem(ASSET_REFRESH_KEY); +} + +if (typeof window !== "undefined") { + window.addEventListener("load", () => { + clearAssetRecoveryFlag(); + }, { once: true }); + + window.addEventListener("vite:preloadError", (event) => { + const customEvent = event as Event & { payload?: unknown; preventDefault: () => void }; + customEvent.preventDefault(); + reloadForStaleAsset(String(customEvent.payload ?? "vite preload error")); + }); + + window.addEventListener("error", (event) => { + const target = event.target; + if (!(target instanceof HTMLLinkElement || target instanceof HTMLScriptElement)) { + return; + } + + const assetUrl = target instanceof HTMLLinkElement ? target.href : target.src; + if (assetUrl.includes("/assets/")) { + reloadForStaleAsset(assetUrl); + } + }, true); + + window.addEventListener("unhandledrejection", (event) => { + const reason = event.reason instanceof Error ? event.reason.message : String(event.reason ?? ""); + if ( + reason.includes("Failed to fetch dynamically imported module") || + reason.includes("Importing a module script failed") || + reason.includes("Unable to preload CSS") + ) { + reloadForStaleAsset(reason); + } + }); +} + queryClient.getQueryCache().subscribe(event => { if (event.type === "updated" && event.action.type === "error") { const error = event.query.state.error; diff --git a/server/_core/static.ts b/server/_core/static.ts index 4295920..91f711f 100644 --- a/server/_core/static.ts +++ b/server/_core/static.ts @@ -13,9 +13,17 @@ export function serveStatic(app: Express) { ); } - app.use(express.static(distPath)); + app.use(express.static(distPath, { index: false })); + + app.use("*", (req, res) => { + // Missing files under /assets or any path with an extension must return 404. + // Falling back to index.html causes browsers to report MIME errors on stale chunks. + const requestPath = req.originalUrl.split("?")[0]; + if (path.extname(requestPath)) { + res.status(404).type("text/plain").send("Not found"); + return; + } - app.use("*", (_req, res) => { res.sendFile(path.resolve(distPath, "index.html")); }); } diff --git a/server/worker.ts b/server/worker.ts index 6438057..17d676b 100644 --- a/server/worker.ts +++ b/server/worker.ts @@ -35,9 +35,14 @@ async function workOnce() { async function main() { console.log(`[worker] ${workerId} started`); for (;;) { - const hasWorked = await workOnce(); - if (!hasWorked) { - await sleep(ENV.backgroundTaskPollMs); + try { + const hasWorked = await workOnce(); + if (!hasWorked) { + await sleep(ENV.backgroundTaskPollMs); + } + } catch (error) { + console.error("[worker] loop error", error); + await sleep(Math.max(ENV.backgroundTaskPollMs, 3_000)); } } }