From 3d6f6393b37335522f74b892e4414649c074b7cb Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:27:04 +0200 Subject: [PATCH] =?UTF-8?q?fix(pwa):=20Endlos-Loop=20"Neue=20Version=20ver?= =?UTF-8?q?f=C3=BCgbar"=20beseitigt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/lib/client/pwa.svelte.ts | 17 ++++++++++++++++- src/service-worker.ts | 10 +++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/lib/client/pwa.svelte.ts b/src/lib/client/pwa.svelte.ts index bc125ac..e84d991 100644 --- a/src/lib/client/pwa.svelte.ts +++ b/src/lib/client/pwa.svelte.ts @@ -41,7 +41,22 @@ class PwaStore { reload(): void { this.updateAvailable = false; - location.reload(); + 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 { diff --git a/src/service-worker.ts b/src/service-worker.ts index 0bbc7e4..648acc0 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -20,7 +20,12 @@ self.addEventListener('install', (event) => { (async () => { const cache = await caches.open(SHELL_CACHE); await cache.addAll(SHELL_ASSETS); - await self.skipWaiting(); + // Kein self.skipWaiting() hier — der Client (pwaStore) fragt den + // User via UpdateToast, ob der neue SW sofort übernehmen soll, und + // schickt dann eine SKIP_WAITING-Message. Ohne diese Trennung + // würde pwaStore beim Install-Event fälschlich "Neue Version" + // zeigen (weil statechange='installed' + controller=alter SW), und + // der neue SW würde einen Tick später ungefragt übernehmen. })() ); }); @@ -91,6 +96,9 @@ self.addEventListener('message', (event) => { event.waitUntil(runSync(false)); } else if (data.type === 'sync-check') { event.waitUntil(runSync(true)); + } else if (data.type === 'SKIP_WAITING') { + // Wird vom pwaStore nach User-Klick auf "Neu laden" geschickt. + void self.skipWaiting(); } });