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.
83 lines
2.8 KiB
JavaScript
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;
|
|
}
|
|
}
|