Handle stale frontend assets and harden worker startup
这个提交包含在:
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,10 +35,15 @@ async function workOnce() {
|
||||
async function main() {
|
||||
console.log(`[worker] ${workerId} started`);
|
||||
for (;;) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户