// Standard Service-Worker-Update-Pattern (Workbox-Style, web.dev „The // Service Worker Lifecycle"): Der SW ruft im Install-Handler NICHT // skipWaiting() auf. Bei einem Update landet der neue SW im waiting- // Status, wir zeigen dem User einen Toast. Klickt er „Neu laden", // posten wir SKIP_WAITING an den wartenden SW, warten auf den // controllerchange und reloaden einmalig — das refreshing-Flag // verhindert den klassischen Doppel-Reload, wenn der User zusätzlich // manuell F5 drückt. class PwaStore { updateAvailable = $state(false); private registration: ServiceWorkerRegistration | null = null; private pollTimer: ReturnType | null = null; private refreshing = false; async init(): Promise { if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return; navigator.serviceWorker.addEventListener('controllerchange', () => { if (this.refreshing) return; this.refreshing = true; location.reload(); }); try { this.registration = await navigator.serviceWorker.ready; } catch { return; } if (!this.registration) return; // Waiting-SW beim Mount = echtes, vom Browser als neu erkanntes // Update (gleiche Bytes hätten keinen waiting-Slot erzeugt). if (this.registration.waiting) { this.updateAvailable = true; } this.registration.addEventListener('updatefound', () => this.onUpdateFound()); // Alle 30 Minuten aktiv nach Updates fragen, damit der User sie auch // mitbekommt, wenn er die Seite lange offen lässt ohne zu navigieren. this.pollTimer = setInterval(() => { void this.registration?.update().catch(() => {}); }, 30 * 60_000); } private onUpdateFound(): void { const installing = this.registration?.installing; if (!installing) return; installing.addEventListener('statechange', () => { // 'installed' UND laufender controller = Update für bestehenden Tab. // (Ohne controller wäre das die erste Installation, kein Update.) if (installing.state === 'installed' && navigator.serviceWorker.controller) { this.updateAvailable = true; } }); } reload(): void { this.updateAvailable = false; const waiting = this.registration?.waiting; if (!waiting) { // Kein wartender SW — reicht ein normaler Reload. this.refreshing = true; location.reload(); return; } // SKIP_WAITING an den wartenden SW → activate → controllerchange → // der Listener in init() führt den Reload aus. waiting.postMessage({ type: 'SKIP_WAITING' }); } dismiss(): void { this.updateAvailable = false; } } export const pwaStore = new PwaStore();