From 8f36a704b786ccbf54c4b3f1803db467e3bb97db Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:13:10 +0000 Subject: [PATCH] feat: robust cache busting on deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Express: Cache-Control no-cache/no-store/must-revalidate for index.html, sw.js, manifest.json; immutable 1yr cache for hashed assets - HTML: meta http-equiv tags to prevent browser caching of entry point - Service Worker: version-based cache purge on activate, notifies clients of version change, updateViaCache: none on registration - main.tsx: SW registration with update detection, 30min update polling, auto-reload on new version activation - APISIX: route-level cache headers — no-cache for /, /sw.js, /manifest.json; immutable 1yr for /assets/* Co-Authored-By: Patrick Munis --- client/index.html | 29 +++++++++++++++++++------- client/public/sw.js | 29 ++++++++++++++++++-------- client/src/main.tsx | 35 ++++++++++++++++++++++++++++++++ infra/apisix/routes.yaml | 44 ++++++++++++++++++++++++++++++++++++++++ server/_core/vite.ts | 25 +++++++++++++++++++++-- server/index.ts | 22 +++++++++++++++++++- 6 files changed, 166 insertions(+), 18 deletions(-) diff --git a/client/index.html b/client/index.html index 41eab608..5ee7fad9 100644 --- a/client/index.html +++ b/client/index.html @@ -1,24 +1,39 @@ - + content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover" + /> 54Link POS Shell - + + + + - + - + @@ -27,7 +42,7 @@ + data-website-id="%VITE_ANALYTICS_WEBSITE_ID%" + > - diff --git a/client/public/sw.js b/client/public/sw.js index 1c8aa3df..56532f52 100644 --- a/client/public/sw.js +++ b/client/public/sw.js @@ -1,9 +1,10 @@ /** - * 54Link POS Shell -- Service Worker v4 + * 54Link POS Shell -- Service Worker v5 * Features: Web Push (failover/fraud/float/settlement), offline shell cache, - * background sync for offline TX queue, periodic sync for fraud status. + * background sync for offline TX queue, periodic sync for fraud status, + * version-based cache busting on deployment. */ -const CACHE_VERSION = "v5"; +const CACHE_VERSION = "v6"; const SHELL_CACHE = `54link-shell-${CACHE_VERSION}`; const API_CACHE = `54link-api-${CACHE_VERSION}`; const DATA_CACHE = `54link-data-${CACHE_VERSION}`; @@ -31,19 +32,31 @@ self.addEventListener("install", event => { }); self.addEventListener("activate", event => { + const KEEP = new Set([SHELL_CACHE, API_CACHE, DATA_CACHE]); event.waitUntil( caches .keys() .then(keys => Promise.all( keys - .filter( - k => - k.startsWith("54link-") && k !== SHELL_CACHE && k !== API_CACHE - ) - .map(k => caches.delete(k)) + .filter(k => !KEEP.has(k)) + .map(k => { + console.log(`[SW] Purging stale cache: ${k}`); + return caches.delete(k); + }) ) ) + .then(() => { + // Notify all open clients to refresh for the new version + self.clients.matchAll({ type: "window" }).then(clients => { + clients.forEach(client => + client.postMessage({ + type: "SW_VERSION_CHANGED", + version: CACHE_VERSION, + }) + ); + }); + }) ); self.clients.claim(); }); diff --git a/client/src/main.tsx b/client/src/main.tsx index af6bd822..70e466fa 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -59,6 +59,41 @@ if (savedTheme === "light") { document.documentElement.classList.add("light"); } +// ── Service Worker: register with cache-bust on version change ───────────── +if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker + .register("/sw.js", { scope: "/", updateViaCache: "none" }) + .then(reg => { + // Check for updates every 30 minutes + setInterval(() => reg.update(), 30 * 60 * 1000); + + reg.addEventListener("updatefound", () => { + const newWorker = reg.installing; + if (!newWorker) return; + newWorker.addEventListener("statechange", () => { + if ( + newWorker.state === "activated" && + navigator.serviceWorker.controller + ) { + // New SW activated — prompt user to reload for latest version + console.log("[SW] New version available, reloading..."); + window.location.reload(); + } + }); + }); + }) + .catch(err => console.error("[SW] Registration failed:", err)); + + // Listen for version-change messages from the SW + navigator.serviceWorker.addEventListener("message", event => { + if (event.data?.type === "SW_VERSION_CHANGED") { + console.log(`[SW] Updated to ${event.data.version}`); + } + }); + }); +} + createRoot(document.getElementById("root")!).render( diff --git a/infra/apisix/routes.yaml b/infra/apisix/routes.yaml index dc807402..eee2c080 100644 --- a/infra/apisix/routes.yaml +++ b/infra/apisix/routes.yaml @@ -3,6 +3,50 @@ # Rate limits: api_general=60/min, api_auth=10/min, api_tx=30/min routes: + # ── Cache-Busting: HTML entry point must never be cached ─────────────────── + - id: pos-shell-html + uri: / + upstream_id: pos-shell + plugins: + response-rewrite: + headers: + set: + Cache-Control: "no-cache, no-store, must-revalidate" + Pragma: "no-cache" + Expires: "0" + + - id: pos-shell-sw + uri: /sw.js + upstream_id: pos-shell + plugins: + response-rewrite: + headers: + set: + Cache-Control: "no-cache, no-store, must-revalidate" + Pragma: "no-cache" + Expires: "0" + + - id: pos-shell-manifest + uri: /manifest.json + upstream_id: pos-shell + plugins: + response-rewrite: + headers: + set: + Cache-Control: "no-cache, no-store, must-revalidate" + Pragma: "no-cache" + Expires: "0" + + # ── Hashed static assets: long-lived immutable cache ────────────────────── + - id: pos-shell-assets + uri: /assets/* + upstream_id: pos-shell + plugins: + response-rewrite: + headers: + set: + Cache-Control: "public, max-age=31536000, immutable" + # ── Main POS Shell App ───────────────────────────────────────────────────── - id: pos-shell-api uri: /api/* diff --git a/server/_core/vite.ts b/server/_core/vite.ts index 7c479711..df50ef1e 100644 --- a/server/_core/vite.ts +++ b/server/_core/vite.ts @@ -60,10 +60,31 @@ export function serveStatic(app: Express) { ); } - app.use(express.static(distPath)); + // Hashed assets (JS/CSS) get long-lived cache; everything else is short-lived + app.use( + express.static(distPath, { + maxAge: "1y", + immutable: true, + setHeaders(res, filePath) { + // index.html and non-hashed files must never be cached + if ( + filePath.endsWith(".html") || + filePath.endsWith("manifest.json") || + filePath.endsWith("sw.js") + ) { + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + res.setHeader("Pragma", "no-cache"); + res.setHeader("Expires", "0"); + } + }, + }) + ); - // fall through to index.html if the file doesn't exist + // fall through to index.html if the file doesn't exist (SPA routing) app.use("*", (_req, res) => { + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + res.setHeader("Pragma", "no-cache"); + res.setHeader("Expires", "0"); res.sendFile(path.resolve(distPath, "index.html")); }); } diff --git a/server/index.ts b/server/index.ts index 6c0f2a19..692cd71e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -17,10 +17,30 @@ async function startServer() { ? path.resolve(__dirname, "public") : path.resolve(__dirname, "..", "dist", "public"); - app.use(express.static(staticPath)); + // Hashed assets (JS/CSS) get long-lived cache; HTML/SW/manifest must never be cached + app.use( + express.static(staticPath, { + maxAge: "1y", + immutable: true, + setHeaders(res, filePath) { + if ( + filePath.endsWith(".html") || + filePath.endsWith("manifest.json") || + filePath.endsWith("sw.js") + ) { + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + res.setHeader("Pragma", "no-cache"); + res.setHeader("Expires", "0"); + } + }, + }) + ); // Handle client-side routing - serve index.html for all routes app.get("*", (_req, res) => { + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + res.setHeader("Pragma", "no-cache"); + res.setHeader("Expires", "0"); res.sendFile(path.join(staticPath, "index.html")); });