Compare commits

...

2 Commits

Author SHA1 Message Date
BOHA
88d5d43448 chore(release): v2.4.8 - deploy-skew self-healing (stale chunk auto-reload + cache headers)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:32:49 +02:00
BOHA
4745af3639 fix(spa): self-heal stale clients after deploys (vite:preloadError + cache headers)
After a release the old hashed chunks are deleted, so an open tab from
the previous build failed its next lazy-page import until a manual
refresh. Three-part fix per the Vite deploy guide:

- vite:preloadError listener reloads the page once (10s sessionStorage
  guard against loops) so stale tabs heal themselves
- static cache headers: /assets/* immutable for a year (content-hashed
  names), index.html no-cache so new visits always see fresh chunk refs
- missing /assets/* now 404 instead of getting the SPA index.html
  fallback (HTML-as-JS masked the failure)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:31:59 +02:00
4 changed files with 37 additions and 3 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "app-ts",
"version": "2.4.7",
"version": "2.4.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "app-ts",
"version": "2.4.7",
"version": "2.4.8",
"license": "ISC",
"dependencies": {
"@anthropic-ai/sdk": "^0.102.0",

View File

@@ -1,6 +1,6 @@
{
"name": "app-ts",
"version": "2.4.7",
"version": "2.4.8",
"description": "",
"main": "dist/server.js",
"scripts": {

View File

@@ -4,6 +4,22 @@ import { BrowserRouter } from "react-router-dom";
import App from "./App";
import { ThemeProvider } from "./context/ThemeContext";
// Deploy-skew recovery (official Vite mechanism): after a release the old
// hashed chunks are deleted, so a tab still running the previous build fails
// its next lazy-page import. Vite emits `vite:preloadError` for exactly this —
// swallow the error and reload once, which loads the fresh index.html and new
// chunk names. The timestamp guard prevents a reload loop when a chunk is
// genuinely unloadable (e.g. offline): a second failure within 10 s falls
// through to the normal error path.
window.addEventListener("vite:preloadError", (event) => {
const KEY = "vite-preload-reloaded-at";
const last = Number(sessionStorage.getItem(KEY) || 0);
if (Date.now() - last < 10_000) return;
sessionStorage.setItem(KEY, String(Date.now()));
event.preventDefault();
window.location.reload();
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter

View File

@@ -206,12 +206,30 @@ async function start() {
root: path.join(__dirname, "..", "dist-client"),
prefix: "/",
wildcard: false,
// Deploy-skew caching contract (per the Vite deploy guide):
// - /assets/* names are content-hashed → safe to cache forever
// (a changed file always gets a NEW name, so "immutable" is correct).
// - index.html (and any non-hashed file) must always be revalidated,
// otherwise a cached copy keeps referencing deleted old chunks.
setHeaders: (res, filePath) => {
if (filePath.includes(`${path.sep}assets${path.sep}`)) {
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
} else {
res.setHeader("Cache-Control", "no-cache");
}
},
});
app.setNotFoundHandler((request, reply) => {
if (request.url.startsWith("/api/")) {
return reply.status(404).send({ success: false, error: "Not found" });
}
// A missing hashed asset (old chunk after a deploy) must 404, NOT get
// the SPA index.html fallback — HTML-as-JS masks the failure and breaks
// the client's vite:preloadError reload recovery.
if (request.url.startsWith("/assets/")) {
return reply.status(404).send();
}
return reply.sendFile("index.html");
});
}