fix(pwa): Recovery-Zähler, Update-Suppression und Timer-Leaks beheben

Stale-Recovery zählt nur aufeinanderfolgende Fehler und wird nach Hard Recovery zurückgesetzt; Update-Checks respektieren „Später“, und PWA-Refresh-State sowie Recovery-Timer werden zuverlässig gesetzt bzw. beim Unmount aufgeräumt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 14:24:25 +02:00
parent bbd4281dcb
commit 7cf04b3357
3 changed files with 95 additions and 13 deletions
+40 -11
View File
@@ -57,8 +57,9 @@ function scheduleUpdateChecks(
registration.update().catch(() => {})
}, UPDATE_CHECK_INTERVAL_MS)
const versionIntervalId = window.setInterval(() => {
if (isUpdateSuppressed()) return
void isDeployedVersionNewer().then((outdated) => {
if (outdated) onOutdated()
if (outdated && !isUpdateSuppressed()) onOutdated()
})
}, VERSION_CHECK_INTERVAL_MS)
@@ -81,8 +82,18 @@ function reloadForServiceWorkerTakeover(): void {
export function usePwaUpdate() {
const cleanupRef = useRef<(() => void) | null>(null)
const hardRecoveryTimerRef = useRef<number | null>(null)
const setNeedRefreshRef = useRef<(value: boolean) => void>(() => {})
const reloadFallbackTimerRef = useRef<number | null>(null)
const forceRecoveryTimerRef = useRef<number | null>(null)
const setNeedRefreshRef = useRef<((value: boolean) => void) | null>(null)
const pendingNeedRefreshRef = useRef<boolean | null>(null)
const applyNeedRefresh = (value: boolean) => {
if (setNeedRefreshRef.current) {
setNeedRefreshRef.current(value)
return
}
pendingNeedRefreshRef.current = value
}
const {
needRefresh: [needRefresh, setNeedRefresh],
@@ -94,23 +105,28 @@ export function usePwaUpdate() {
},
onNeedRefresh() {
if (isUpdateSuppressed()) return
setNeedRefreshRef.current(true)
applyNeedRefresh(true)
},
onRegisteredSW(_swUrl: string, registration: ServiceWorkerRegistration | undefined) {
if (!registration) return
if (isUpdateSuppressed() || !registration.waiting) {
setNeedRefreshRef.current(false)
applyNeedRefresh(false)
}
cleanupRef.current?.()
cleanupRef.current = scheduleUpdateChecks(registration, () => {
setNeedRefreshRef.current(true)
if (isUpdateSuppressed()) return
applyNeedRefresh(true)
})
}
})
setNeedRefreshRef.current = setNeedRefresh
if (pendingNeedRefreshRef.current !== null) {
setNeedRefresh(pendingNeedRefreshRef.current)
pendingNeedRefreshRef.current = null
}
useEffect(() => {
if (isUpdateSuppressed()) {
@@ -126,9 +142,13 @@ export function usePwaUpdate() {
return () => {
cleanupRef.current?.()
cleanupRef.current = null
if (hardRecoveryTimerRef.current !== null) {
window.clearTimeout(hardRecoveryTimerRef.current)
hardRecoveryTimerRef.current = null
if (reloadFallbackTimerRef.current !== null) {
window.clearTimeout(reloadFallbackTimerRef.current)
reloadFallbackTimerRef.current = null
}
if (forceRecoveryTimerRef.current !== null) {
window.clearTimeout(forceRecoveryTimerRef.current)
forceRecoveryTimerRef.current = null
}
}
}, [setNeedRefresh])
@@ -140,11 +160,20 @@ export function usePwaUpdate() {
await updateServiceWorker(true)
await triggerServiceWorkerUpdate()
hardRecoveryTimerRef.current = window.setTimeout(() => {
if (reloadFallbackTimerRef.current !== null) {
window.clearTimeout(reloadFallbackTimerRef.current)
}
if (forceRecoveryTimerRef.current !== null) {
window.clearTimeout(forceRecoveryTimerRef.current)
}
reloadFallbackTimerRef.current = window.setTimeout(() => {
reloadFallbackTimerRef.current = null
reloadForServiceWorkerTakeover()
}, UPDATE_RELOAD_FALLBACK_MS)
window.setTimeout(() => {
forceRecoveryTimerRef.current = window.setTimeout(() => {
forceRecoveryTimerRef.current = null
void forcePwaRecovery()
}, UPDATE_HARD_RECOVERY_MS)
}