1/* Service Worker für den Lead-Scanner (Scope /lead-scanner/): 2 * App-Oberfläche und Skripte werden nach dem ersten Aufruf zwischengespeichert, damit die App 3 * am Messestand auch ohne Netz startet. Scans selbst puffert die App in IndexedDB. 4 * 5 * 22.09.2026: Kaltstart ohne Netz endete auf der Browser-Fehlerseite. Grund war zum einen, dass 6 * der Start der Hauptseite alle Caches löschte (siehe src/main.tsx), zum anderen, dass `install` 7 * nichts vorab holte: Hülle und Chunks landeten erst beim nächsten Durchlauf im Cache. Jetzt legt 8 * `install` die Hülle vorab ab, und die Seite meldet nach dem Laden ihre geladenen Skripte und 9 * Styles per `postMessage({type: "cache-assets", urls: [...]})` nach. Nach EINEM Online-Besuch ist 10 * damit alles für den Kaltstart da. */ 11const CACHE = "hk-lead-scanner-v3"; 12/** Wie lange ein Seitenaufruf auf das Netz wartet, bevor die Hülle aus dem Cache kommt. */ 13const NAV_TIMEOUT_MS = 3000; 14const ZEITLIMIT = Symbol("zeitlimit"); 15const SHELL = "/lead-scanner/__shell"; 16 17// Wurzel des Scopes â daher kommt die App-Hülle (index.html der SPA) 18function shellRequestUrl() { 19 try { 20 return new URL("./", self.registration.scope).toString(); 21 } catch { 22 return "/lead-scanner/"; 23 } 24} 25 26self.addEventListener("install", (event) => { 27 event.waitUntil((async () => { 28 // Hülle vorab holen. Ein Fehler hier darf die Installation nicht scheitern lassen. 29 try { 30 const res = await fetch(shellRequestUrl(), { cache: "reload" }); 31 if (res && res.ok) { 32 const cache = await caches.open(CACHE); 33 await cache.put(SHELL, res.clone()); 34 } 35 } catch { /* offline installiert â die Hülle kommt beim ersten Seitenaufruf */ } 36 await self.skipWaiting(); 37 })()); 38}); 39 40self.addEventListener("activate", (event) => { 41 event.waitUntil((async () => { 42 const keys = await caches.keys(); 43 await Promise.all(keys.filter((k) => k.startsWith("hk-lead-scanner-") && k !== CACHE).map((k) => caches.delete(k))); 44 await self.clients.claim(); 45 })()); 46}); 47 48/* Die Seite schickt nach dem Laden die Liste aller geladenen gleichnamigen (same-origin) Skripte 49 * und Styles. Einzeln ablegen statt `cache.addAll`, damit eine einzelne fehlschlagende URL nicht 50 * den ganzen Durchlauf verwirft. */ 51self.addEventListener("message", (event) => { 52 const data = event.data; 53 if (!data || data.type !== "cache-assets" || !Array.isArray(data.urls)) return; 54 event.waitUntil((async () => { 55 const cache = await caches.open(CACHE); 56 for (const url of data.urls) { 57 try { 58 if (typeof url !== "string") continue; 59 const abs = new URL(url, self.location.origin); 60 if (abs.origin !== self.location.origin) continue; 61 if (await cache.match(abs.href)) continue; 62 const res = await fetch(abs.href, { credentials: "same-origin" }); 63 if (res && res.ok) await cache.put(abs.href, res.clone()); 64 } catch { /* diese eine URL überspringen */ } 65 } 66 })()); 67}); 68 69self.addEventListener("fetch", (event) => { 70 const req = event.request; 71 if (req.method !== "GET") return; 72 const url = new URL(req.url); 73 if (url.origin !== self.location.origin) return; // Supabase & Co. immer live 74 75 // Seitenaufrufe: Netz zuerst, aber höchstens NAV_TIMEOUT_MS lang. Am Messestand ist âNetz ohne 76 // Durchsatzâ häufiger als gar kein Netz: fetch() scheitert dann nicht, sondern hängt, und das 77 // Neuladen der Scanner-App blieb weiÃ. Nach dem Zeitlimit kommt die zuletzt gesehene Hülle aus 78 // dem Cache; die Netzantwort läuft im Hintergrund weiter und frischt den Cache auf, wenn sie 79 // noch eintrifft. Abgelegt wird nur eine erfolgreiche Antwort (fresh.ok) â eine 404- oder 80 // 500-Seite darf die funktionierende Hülle nicht überschreiben. 81 if (req.mode === "navigate") { 82 let ablage = Promise.resolve(); 83 const netz = fetch(req).then((fresh) => { 84 if (fresh && fresh.ok) { 85 // Kopie sofort ziehen, bevor der Browser den Körper liest; das Ablegen wartet nicht 86 // auf die Antwort an die Seite. 87 const kopie = fresh.clone(); 88 ablage = caches.open(CACHE).then((cache) => cache.put(SHELL, kopie)).catch(() => undefined); 89 } 90 return fresh; 91 });
92 // Den Hintergrund-Abruf samt Ablage am Leben halten, auch wenn die Antwort schon aus dem 93 // Cache kam. 94 event.waitUntil(netz.then(() => ablage, () => undefined)); 95 event.respondWith((async () => { 96 const cache = await caches.open(CACHE); 97 const ausCache = async () => (await cache.match(SHELL)) || (await cache.match(req)) || null; 98 const zeitlimit = new Promise((resolve) => setTimeout(() => resolve(ZEITLIMIT), NAV_TIMEOUT_MS)); 99 try { 100 const erstes = await Promise.race([netz, zeitlimit]); 101 if (erstes !== ZEITLIMIT) return erstes; 102 // Netz zu langsam: Hülle zeigen, falls vorhanden â sonst doch auf das Netz warten. 103 const huelle = await ausCache(); 104 if (huelle) return huelle; 105 return await netz; 106 } catch { 107 return (await ausCache()) || Response.error(); 108 } 109 })()); 110 return; 111 } 112 113 // Gebaute Skripte/Styles sind mit Hash versehen: Cache zuerst, sonst Netz und merken 114 const cacheable = url.pathname.startsWith("/assets/") || /\.(js|css|png|svg|woff2?|jpg|webp|mp3|json)$/i.test(url.pathname); 115 if (!cacheable) return; 116 event.respondWith((async () => { 117 const cache = await caches.open(CACHE); 118 const hit = await cache.match(req); 119 if (hit) return hit; 120 try { 121 const fresh = await fetch(req); 122 if (fresh.ok) cache.put(req, fresh.clone()); 123 return fresh; 124 } catch { 125 return Response.error(); 126 } 127 })()); 128});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.