class PwaStore { updateAvailable = $state(false); private registration: ServiceWorkerRegistration | null = null; private pollTimer: ReturnType | null = null; async init(): Promise { if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return; try { this.registration = await navigator.serviceWorker.ready; } catch { return; } if (!this.registration) return; // Wenn beim Mount schon ein neuer SW installiert und aktiv wartet, // zeigen wir den Toast direkt an. 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 ein 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 — entweder war es nur eine Toast-Anzeige, oder // der SW ist schon aktiv. In beiden Fällen reicht ein Reload. location.reload(); return; } // Klassisches Pattern: User-Klick → SKIP_WAITING an den wartenden // SW → controllerchange feuert, wenn der neue SW übernimmt → dann // reloaden wir die Seite, damit sie unter dem neuen SW läuft. navigator.serviceWorker.addEventListener( 'controllerchange', () => location.reload(), { once: true } ); waiting.postMessage({ type: 'SKIP_WAITING' }); } dismiss(): void { this.updateAvailable = false; } } export const pwaStore = new PwaStore();