Handle stale frontend assets and harden worker startup
这个提交包含在:
@@ -9,6 +9,7 @@ import { getLoginUrl } from "./const";
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
const ASSET_REFRESH_KEY = "asset-recovery-reloaded";
|
||||||
|
|
||||||
const redirectToLoginIfUnauthorized = (error: unknown) => {
|
const redirectToLoginIfUnauthorized = (error: unknown) => {
|
||||||
if (!(error instanceof TRPCClientError)) return;
|
if (!(error instanceof TRPCClientError)) return;
|
||||||
@@ -21,6 +22,60 @@ const redirectToLoginIfUnauthorized = (error: unknown) => {
|
|||||||
window.location.href = getLoginUrl();
|
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 => {
|
queryClient.getQueryCache().subscribe(event => {
|
||||||
if (event.type === "updated" && event.action.type === "error") {
|
if (event.type === "updated" && event.action.type === "error") {
|
||||||
const error = event.query.state.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"));
|
res.sendFile(path.resolve(distPath, "index.html"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,14 @@ async function workOnce() {
|
|||||||
async function main() {
|
async function main() {
|
||||||
console.log(`[worker] ${workerId} started`);
|
console.log(`[worker] ${workerId} started`);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const hasWorked = await workOnce();
|
try {
|
||||||
if (!hasWorked) {
|
const hasWorked = await workOnce();
|
||||||
await sleep(ENV.backgroundTaskPollMs);
|
if (!hasWorked) {
|
||||||
|
await sleep(ENV.backgroundTaskPollMs);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[worker] loop error", error);
|
||||||
|
await sleep(Math.max(ENV.backgroundTaskPollMs, 3_000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户