fix(pwa): Reload-Loop beim Zombie-Cleanup beseitigt
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 29s
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 29s
Der Commit 1bec054 hatte einen globalen controllerchange-Listener in
init(), der bei jedem Event location.reload() auslöste. In Kombination
mit der Zombie-Aufräumung (silent SKIP_WAITING) ergab das einen
Endlos-Loop: Seite lädt → Zombie erkannt → SKIP_WAITING → controller-
change → Reload → neue Seite mit frischem Zombie → usw.
Fix: Der controllerchange-Listener wird nur noch scoped aus reload()
heraus gesetzt ({ once: true }) — also genau dann, wenn der User auf
„Neu laden" geklickt hat und einen Reload tatsächlich will. Beim
silent Zombie-Cleanup gibt es keinen Listener, die Seite läuft
einfach nahtlos unter dem neuen (funktional identischen) SW weiter.
Regression-Test sichert ab, dass fireControllerChange() nach silent
SKIP_WAITING location.reload() NICHT aufruft.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,21 +9,19 @@
|
||||
// als „neues Update" interpretieren und den Toast bei jedem Reload
|
||||
// erneut zeigen. Wir fragen darum per MessageChannel GET_VERSION an
|
||||
// beiden SWs, vergleichen und räumen identische Bytes still auf.
|
||||
//
|
||||
// Kritisch: Der Reload beim controllerchange darf NUR durch User-Klick
|
||||
// passieren, nicht automatisch beim silent Cleanup — sonst ergibt der
|
||||
// Zombie-Refresh einen Endlos-Reload-Loop, weil der Browser jede neue
|
||||
// Seite wieder mit frischem Zombie ausstattet.
|
||||
class PwaStore {
|
||||
updateAvailable = $state(false);
|
||||
private registration: ServiceWorkerRegistration | null = null;
|
||||
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private refreshing = false;
|
||||
|
||||
async init(): Promise<void> {
|
||||
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 {
|
||||
@@ -64,13 +62,12 @@ class PwaStore {
|
||||
queryVersion(active)
|
||||
]);
|
||||
if (waitingVersion && activeVersion && waitingVersion === activeVersion) {
|
||||
// Bit-identischer Zombie — ohne User-Toast aufräumen. Der neue
|
||||
// SW wird zur Active, controllerchange feuert, init()-Listener
|
||||
// triggert einen einzigen Reload.
|
||||
// Bit-identischer Zombie: silent aufräumen, KEIN reload — die Seite
|
||||
// läuft nahtlos unter dem neuen SW weiter (funktional identisch).
|
||||
waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
return;
|
||||
}
|
||||
// Versions-Unterschied oder unbekannt: User entscheidet.
|
||||
// Versions-Unterschied oder unbekannt: User entscheidet via Toast.
|
||||
this.updateAvailable = true;
|
||||
}
|
||||
|
||||
@@ -78,11 +75,17 @@ class PwaStore {
|
||||
this.updateAvailable = false;
|
||||
const waiting = this.registration?.waiting;
|
||||
if (!waiting) {
|
||||
this.refreshing = true;
|
||||
// Kein wartender SW — reicht ein normaler Reload.
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
// SKIP_WAITING → activate → controllerchange → init()-Listener reloadet.
|
||||
// Klassisches Pattern: User-Klick → SKIP_WAITING → controllerchange
|
||||
// feuert, wenn der neue SW übernimmt → dann reloaden wir einmalig.
|
||||
navigator.serviceWorker.addEventListener(
|
||||
'controllerchange',
|
||||
() => location.reload(),
|
||||
{ once: true }
|
||||
);
|
||||
waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user