sw.js (3.1 KB)
1 // Caches the app shell (so it works offline) and map tiles (so panning/zooming 2 // reuses previously-viewed tiles instead of refetching). Leaflet is vendored in 3 // the shell, so map tiles are the only remaining network dependency. 4 5 const SHELL_CACHE = "app-shell-v8"; 6 const TILE_CACHE = "map-tiles-v1"; // CORS fetches so the cached responses aren't opaque 7 const KEEP = new Set([SHELL_CACHE, TILE_CACHE]); 8 9 // Every file the app needs to boot with no network. 10 const SHELL = [ 11 "./", 12 "index.html", 13 "manifest.json", 14 "resources/icon.png", 15 "resources/format.js", 16 "resources/qr.js", 17 "resources/pdf.js", 18 "resources/flyer.js", 19 "resources/panel.js", 20 "resources/map.js", 21 "resources/app.js", 22 "resources/nav.js", 23 "resources/styles.css", 24 "resources/leaflet/leaflet-nogap.js", 25 "resources/demo.json", 26 "resources/leaflet/leaflet.js", 27 "resources/leaflet/leaflet.css", 28 ]; 29 30 // Real (gitignored) data; overrides demo.json when present, but often absent. Cached 31 // best-effort so a 404 can't fail the install — the app boots fine on demo.json alone. 32 const OPTIONAL = ["resources/events.json"]; 33 34 self.addEventListener("install", (e) => { 35 self.skipWaiting(); 36 // Atomic precache of the shell: any failure rejects, so the prior shell cache 37 // survives a bad install. waitUntil resolves only once the full shell is cached, 38 // so activate won't drop the old version until this one is complete. 39 e.waitUntil(caches.open(SHELL_CACHE).then((cache) => 40 cache.addAll(SHELL).then(() => 41 Promise.all(OPTIONAL.map((u) => cache.add(u).catch(() => {}))) 42 ) 43 )); 44 }); 45 46 self.addEventListener("activate", (e) => e.waitUntil( 47 Promise.all([ 48 self.clients.claim(), 49 // drop old cache versions (e.g. superseded shells/tiles) 50 caches.keys().then((keys) => Promise.all( 51 keys.filter((k) => !KEEP.has(k)).map((k) => caches.delete(k)) 52 )), 53 ]) 54 )); 55 56 function isCacheableTile(url) { 57 return url.hostname.endsWith("basemaps.cartocdn.com"); // map tiles 58 } 59 60 // network-first for same-origin app files: fresh when online, cached when not, 61 // so edits show up on reload but the app still boots offline. 62 async function shellResponse(request) { 63 const cache = await caches.open(SHELL_CACHE); 64 try { 65 const resp = await fetch(request); 66 if (resp.ok) cache.put(request, resp.clone()); 67 return resp; 68 } catch { 69 const hit = await cache.match(request) || await cache.match("index.html"); 70 if (hit) return hit; 71 throw new Error("offline and not cached"); 72 } 73 } 74 75 // cache-first for map tiles: serve a cached copy instantly, else fetch and store. 76 async function tileResponse(url) { 77 const cache = await caches.open(TILE_CACHE); 78 const hit = await cache.match(url.href); 79 if (hit) return hit; 80 const resp = await fetch(url.href, { mode: "cors", credentials: "omit" }); 81 if (resp.ok) cache.put(url.href, resp.clone()); 82 return resp; 83 } 84 85 self.addEventListener("fetch", (event) => { 86 if (event.request.method !== "GET") return; 87 const url = new URL(event.request.url); 88 89 if (isCacheableTile(url)) { 90 event.respondWith(tileResponse(url)); 91 return; 92 } 93 // same-origin navigations + assets -> app shell 94 if (url.origin === self.location.origin) { 95 event.respondWith(shellResponse(event.request)); 96 } 97 });