fix(sw): kill-switch SW to unstick users with broken cached bundle (t-paliad-043 step 1)
Emergency: t-paliad-042 shipped a service worker that cached a broken /assets/projects.js (crashes on init with "d is not a function"), making /projects show the empty state. Mobile Safari users have no devtools to manually unregister the SW. Replace sw.js with a self-destructing variant: on activate, delete every cache, unregister itself, and force every open client to navigate to a fresh page. /sw.js is served with no-cache headers so browsers refetch on the next navigation and propagate the kill-switch automatically. Step 2 (separate commit): fix the projects.js bundle bug, then ship a properly versioned SW that evicts stale caches on every deploy.
This commit is contained in:
@@ -1,70 +1,39 @@
|
||||
// Paliad service worker — minimal cache-first for /assets/* and /icons/*,
|
||||
// network-first for /api/*, network passthrough for everything else.
|
||||
// Bumping CACHE_VERSION purges the previous cache on activation.
|
||||
const CACHE_VERSION = "paliad-v1";
|
||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||
// Paliad service worker — KILL-SWITCH (t-paliad-043).
|
||||
// The previous SW (t-paliad-042 paliad-v1) cached a broken /assets/projects.js
|
||||
// bundle that crashes on init ("d is not a function"), making /projects appear
|
||||
// empty. Mobile Safari users have no devtools to unregister manually, so this
|
||||
// SW is shipped specifically to self-destruct on activation:
|
||||
// 1. unregister itself
|
||||
// 2. delete every cache it ever opened
|
||||
// 3. force every open client to reload from the network
|
||||
//
|
||||
// Browsers re-fetch sw.js on every navigation (we send Cache-Control: no-cache
|
||||
// from the Go handler), so this propagates the moment a user opens any page.
|
||||
// A proper versioned SW will land in a follow-up commit (Step 2) once the
|
||||
// underlying bundle bug is fixed.
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
// Activate the new SW as soon as it is installed; matters when
|
||||
// CACHE_VERSION changes so users don't keep stale assets.
|
||||
self.addEventListener("install", () => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(
|
||||
keys
|
||||
.filter((k) => k !== STATIC_CACHE && k.startsWith("paliad-"))
|
||||
.map((k) => caches.delete(k)),
|
||||
);
|
||||
await self.clients.claim();
|
||||
const cacheNames = await caches.keys();
|
||||
await Promise.all(cacheNames.map((n) => caches.delete(n)));
|
||||
await self.registration.unregister();
|
||||
const clientsList = await self.clients.matchAll({ type: "window" });
|
||||
for (const client of clientsList) {
|
||||
try {
|
||||
client.navigate(client.url);
|
||||
} catch {
|
||||
// Some browsers reject navigate() on cross-origin or detached clients —
|
||||
// safe to ignore; next manual reload will pick up the unregistered state.
|
||||
}
|
||||
}
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// HTML navigations + everything else: pass through to the network.
|
||||
});
|
||||
|
||||
async function cacheFirst(req) {
|
||||
const cache = await caches.open(STATIC_CACHE);
|
||||
const cached = await cache.match(req);
|
||||
if (cached) return cached;
|
||||
try {
|
||||
const res = await fetch(req);
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Fetch handler intentionally absent: with no cache and pending unregister,
|
||||
// every request goes straight to the network.
|
||||
|
||||
Reference in New Issue
Block a user