Files
paliad/frontend/public/sw.js
m 0800ba97f3 fix(sw, assets, install): bypass HTTP cache + revalidate assets + mobile-only install banner (t-paliad-043 step 3)
After step 2 deployed the IIFE-wrapped bundles, m's browser still saw
the broken page because /assets/projects.js was being served from the
local HTTP cache (no Cache-Control, just heuristic freshness from
Last-Modified). Even after the new SW activated and cleared its own
caches, its cacheFirst handler did `fetch(req)` which goes through the
browser HTTP cache — re-fed the SW cache from the stale bundle and the
loop perpetuated forever.

Three mutually reinforcing fixes:

1. SW cacheFirst now does `fetch(req, { cache: "reload" })` for the
   network leg. Forces the network fetch to bypass the browser's HTTP
   cache, so the SW always seeds its own cache from a true network read.

2. Go static handlers for /assets/* and /icons/* set
   `Cache-Control: no-cache, must-revalidate`. Combined with the
   Last-Modified that http.FileServer already emits, browsers send
   If-Modified-Since and the server replies 304 when unchanged — fast
   for repeat loads, fresh on every deploy. Users without a SW (or after
   the kill-switch unregistered theirs) now also pick up new bundles
   immediately.

3. pwa-install.ts gates the install banner on
   `(min-width: 768px)` — same breakpoint the BottomNav and other
   mobile-shell elements use. Desktop partners no longer get an install
   prompt covering their work area.
2026-04-26 14:37:02 +02:00

83 lines
2.8 KiB
JavaScript

// Paliad service worker. Cache strategy:
// /assets/* + /icons/* → cache-first (immutable per deploy)
// /api/* → network-first (fall back to cached snapshot)
// everything else → network passthrough
//
// CACHE_VERSION is rewritten to "v<build-epoch-ms>" by frontend/build.ts on
// every deploy. The activate handler deletes ANY cache whose name doesn't
// match — covers both prior versioned caches (v17143…) and any pre-versioning
// cache name (paliad-v1-static, t-paliad-043 kill-switch survivors). This is
// what guarantees a stale `/assets/projects.js` from a previous deploy gets
// purged the moment the new SW activates, instead of lingering until the user
// manually clears site data.
const CACHE_VERSION = "__PALIAD_BUILD_VERSION__";
const STATIC_CACHE = `${CACHE_VERSION}-static`;
self.addEventListener("install", () => {
// skipWaiting so the new SW takes over the moment install completes,
// rather than waiting for every tab to close. Combined with clients.claim
// in activate, this means a deploy reaches users on their next navigation.
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(
keys.filter((k) => k !== STATIC_CACHE).map((k) => caches.delete(k)),
);
await self.clients.claim();
})(),
);
});
self.addEventListener("fetch", (event) => {
const req = event.request;
if (req.method !== "GET") return;
const url = new URL(req.url);
if (url.origin !== self.location.origin) return;
if (url.pathname.startsWith("/assets/") || url.pathname.startsWith("/icons/")) {
event.respondWith(cacheFirst(req));
return;
}
if (url.pathname.startsWith("/api/")) {
event.respondWith(networkFirst(req));
return;
}
});
async function cacheFirst(req) {
const cache = await caches.open(STATIC_CACHE);
const cached = await cache.match(req);
if (cached) return cached;
try {
// cache: "reload" forces the network leg to BYPASS the browser HTTP
// cache. Without this, a stale /assets/projects.js sitting in the
// browser's disk cache from a previous deploy would be returned to us,
// we'd cache it again, and the user would be stuck on the old bundle
// forever — exactly the failure mode that caused t-paliad-043.
const res = await fetch(req, { cache: "reload" });
if (res && res.ok) cache.put(req, res.clone());
return res;
} catch (err) {
if (cached) return cached;
throw err;
}
}
async function networkFirst(req) {
try {
return await fetch(req);
} catch (err) {
const cache = await caches.open(STATIC_CACHE);
const cached = await cache.match(req);
if (cached) return cached;
throw err;
}
}