sw.js (3760B)
1 /* badge.virtualshack.io — offline shell 2 * 3 * Why this exists: DEF CON's network is hostile and congested, and the setup app is 4 * useless if you can't load it at the moment you need it. 5 * Load the site once before you travel and it keeps working after. 6 * 7 * Strategy: precache the whole (tiny) site on install, then serve cache-first and 8 * refresh in the background. Cache-first is deliberate — on a bad network, "instant 9 * and slightly stale" beats "spinner forever". 10 * 11 * Bump CACHE on every content change, otherwise returning visitors keep the old copy 12 * until the background refresh lands (i.e. one reload behind). 13 * 14 * Bump the ?v= on the stylesheet <link> in index.html to the SAME number at the same time. 15 * index.html is served no-cache but /assets/ is not, so a CSS edit otherwise sits behind 16 * Cloudflare's edge cache for hours after a deploy. Keeping the two numbers equal means 17 * there is one version to remember, not two. 18 */ 19 20 const CACHE = "onlyn00bs-v39"; 21 22 // Resolved against the worker's own location, so it's right whether the site is served at 23 // / (production) or under a path (a local preview). 24 const GALLERY_IMG = new URL("assets/gallery/", self.location).pathname; 25 26 const PRECACHE = [ 27 "./", 28 "index.html", 29 "assets/badge.css", 30 "assets/icon.svg", 31 "manifest.webmanifest", 32 // The setup app. Precached deliberately: this is THE page you need working on a hostile 33 // con network, and it is self-contained (no imports, no external assets), so one entry 34 // covers the whole flow. A --public build omits it and the cache.add 404s harmlessly 35 // (see allSettled below). 36 "provision/", 37 ]; 38 39 self.addEventListener("install", (e) => { 40 e.waitUntil((async () => { 41 const cache = await caches.open(CACHE); 42 // Individually, not addAll(): one 404 shouldn't fail the whole install and 43 // leave a recipient with no offline copy at all. 44 await Promise.allSettled(PRECACHE.map((url) => cache.add(new Request(url, { cache: "reload" })))); 45 await self.skipWaiting(); 46 })()); 47 }); 48 49 self.addEventListener("activate", (e) => { 50 e.waitUntil((async () => { 51 const keys = await caches.keys(); 52 await Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))); 53 await self.clients.claim(); 54 })()); 55 }); 56 57 self.addEventListener("fetch", (e) => { 58 const req = e.request; 59 if (req.method !== "GET") return; 60 const url = new URL(req.url); 61 if (url.origin !== self.location.origin) return; // never touch cross-origin 62 // Gallery photos are network-only. Everything else this worker sees gets cached as it's 63 // fetched, which is right for a ~2 MB site and wrong for a photo gallery: every photo 64 // anyone ever opened would sit in their cache until the next CACHE bump. The gallery 65 // page itself is still cached like any other page; only the images pass through. 66 if (url.pathname.startsWith(GALLERY_IMG)) return; 67 68 e.respondWith((async () => { 69 const cache = await caches.open(CACHE); 70 const hit = await cache.match(req, { ignoreSearch: true }); 71 72 const fresh = fetch(req) 73 .then((res) => { 74 if (res && res.ok && res.type === "basic") cache.put(req, res.clone()); 75 return res; 76 }) 77 .catch(() => null); 78 79 if (hit) return hit; // cache-first 80 81 const res = await fresh; 82 if (res) return res; 83 84 // Offline and never cached: hand navigations the shell rather than a browser error. 85 if (req.mode === "navigate") { 86 const shell = await cache.match("./", { ignoreSearch: true }) || await cache.match("index.html"); 87 if (shell) return shell; 88 } 89 return new Response("offline — this page was never cached", { 90 status: 503, 91 headers: { "Content-Type": "text/plain" }, 92 }); 93 })()); 94 });