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 });