Merge: kill-switch SW (t-paliad-043 step 1) — emergency unstick

This commit is contained in:
m
2026-04-26 14:26:01 +02:00

View File

@@ -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.