2026-04-18 17:57:51 +02:00
|
|
|
// 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.
|
2026-04-17 19:38:00 +02:00
|
|
|
class PwaStore {
|
|
|
|
|
updateAvailable = $state(false);
|
|
|
|
|
private registration: ServiceWorkerRegistration | null = null;
|
|
|
|
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
2026-04-18 17:57:51 +02:00
|
|
|
private refreshing = false;
|
2026-04-17 19:38:00 +02:00
|
|
|
|
|
|
|
|
async init(): Promise<void> {
|
|
|
|
|
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return;
|
2026-04-18 17:57:51 +02:00
|
|
|
|
|
|
|
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
|
|
|
if (this.refreshing) return;
|
|
|
|
|
this.refreshing = true;
|
|
|
|
|
location.reload();
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-17 19:38:00 +02:00
|
|
|
try {
|
|
|
|
|
this.registration = await navigator.serviceWorker.ready;
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!this.registration) return;
|
|
|
|
|
|
2026-04-18 17:57:51 +02:00
|
|
|
// 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;
|
2026-04-17 19:38:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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', () => {
|
2026-04-18 17:57:51 +02:00
|
|
|
// 'installed' UND laufender controller = Update für bestehenden Tab.
|
2026-04-17 19:38:00 +02:00
|
|
|
// (Ohne controller wäre das die erste Installation, kein Update.)
|
2026-04-18 17:57:51 +02:00
|
|
|
if (installing.state === 'installed' && navigator.serviceWorker.controller) {
|
2026-04-17 19:38:00 +02:00
|
|
|
this.updateAvailable = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reload(): void {
|
|
|
|
|
this.updateAvailable = false;
|
2026-04-18 17:27:04 +02:00
|
|
|
const waiting = this.registration?.waiting;
|
|
|
|
|
if (!waiting) {
|
2026-04-18 17:57:51 +02:00
|
|
|
// Kein wartender SW — reicht ein normaler Reload.
|
|
|
|
|
this.refreshing = true;
|
2026-04-18 17:27:04 +02:00
|
|
|
location.reload();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-18 17:57:51 +02:00
|
|
|
// SKIP_WAITING an den wartenden SW → activate → controllerchange →
|
|
|
|
|
// der Listener in init() führt den Reload aus.
|
2026-04-18 17:27:04 +02:00
|
|
|
waiting.postMessage({ type: 'SKIP_WAITING' });
|
2026-04-17 19:38:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dismiss(): void {
|
|
|
|
|
this.updateAvailable = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const pwaStore = new PwaStore();
|