All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s
Der SW rief bisher im Install-Handler self.skipWaiting() auf — der neue SW übersprang damit die "waiting"-Phase und aktivierte sofort. pwaStore.onUpdateFound feuerte trotzdem auf statechange= "installed" + vorhandenem controller und setzte updateAvailable= true. Ergebnis: Toast erschien, obwohl der SW bereits übernommen hatte, und der Klick auf "Neu laden" löste durch das Timing einen neuen Update-Zyklus aus → Endlosschleife, v.a. im Incognito-Mode wo jede Session neu installiert. Jetzt klassisches Pattern: SW wartet in "installed"-Zustand bis der User den Toast bestätigt; pwaStore.reload() postet SKIP_WAITING an den wartenden SW, lauscht auf controllerchange und reloadet dann erst. Ohne diese Trennung ist der Toast semantisch kaputt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
2.2 KiB
TypeScript
68 lines
2.2 KiB
TypeScript
class PwaStore {
|
|
updateAvailable = $state(false);
|
|
private registration: ServiceWorkerRegistration | null = null;
|
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
async init(): Promise<void> {
|
|
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();
|