// A deliberately small service worker: it exists so iOS treats this as an installable // app and so the shell survives a flaky wifi moment. It does NOT try to make the player // work offline - the audio, the library and the state all live on the mouse, and a // cached answer for any of those would be a lie. const SHELL = "musikdelphin-shell-v1"; // There is no cover cache any more; `activate` below deletes every cache that is not // SHELL, which retires the two generations of cover cache this worker used to keep. self.addEventListener("install", (event) => { event.waitUntil( caches .open(SHELL) .then((cache) => cache.addAll([ "/", "/manifest.webmanifest", "/dolphin-mascot.png", "/fonts/nunito.css", "/fonts/nunito-latin.woff2", "/fonts/nunito-latin-ext.woff2", ]), ) .then(() => self.skipWaiting()), ); }); self.addEventListener("activate", (event) => { event.waitUntil( caches .keys() .then((names) => Promise.all(names.filter((n) => n !== SHELL).map((n) => caches.delete(n))), ) .then(() => self.clients.claim()), ); }); self.addEventListener("fetch", (event) => { const request = event.request; if (request.method !== "GET") return; const url = new URL(request.url); if (url.origin !== self.location.origin) return; // Cover art is NOT cached here, and that is deliberate. This worker only ever // registers on localhost or over HTTPS (see the README), which on this setup means // the kiosk on the device itself - where the backend is the same machine, and a // Cache Storage lookup plus a revalidating fetch plus a cache write is strictly more // work than just asking for the file. // // It was measured, after a stale-while-revalidate version of this handler made // searching visibly worse. Typing "conni" over a 343-album library, keydown to // painted, worst keystroke: 5580 ms through this worker against 461 ms with // Network.setBypassServiceWorker on. The main thread was not running any JavaScript // during those stalls - it sat waiting while ImageDecodeTask blocked on bytes this // worker was serialising through a single thread. // // If the player ever moves behind a TLS proxy for the tablet, revisit: caching covers // is worth something over wifi, and would want a budget rather than every cover on // every load. // Everything else under /api is live state. Never cache it, never serve it stale. if (url.pathname.startsWith("/api/")) return; // The shell: network first so a rebuilt frontend is picked up on the next load, // falling back to the cache when the mouse is off or out of range. event.respondWith( fetch(request) .then((response) => { if (response.ok) { const copy = response.clone(); caches.open(SHELL).then((cache) => cache.put(request, copy)); } return response; }) .catch(async () => { const hit = await caches.match(request); // A client-side app: any in-scope navigation resolves to the one document. return hit ?? (await caches.match("/")) ?? Response.error(); }), ); });