Files
kapteins-daagbok/client/src/hooks/usePwaUpdate.ts
T
elpatron 1373c11de8 fix(pwa): Kaltstart nach verpassten Updates stabilisieren
Service Worker übernimmt Updates zuverlässig (SKIP_WAITING, clientsClaim),
wartende Versionen werden beim Start angewendet und veraltete Chunks führen
nicht mehr zum Hängenbleiben.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 13:58:35 +02:00

109 lines
2.9 KiB
TypeScript

import { useEffect, useRef } from 'react'
import { useRegisterSW } from 'virtual:pwa-register/react'
import { markReloadAttempt, recentlyAttemptedReload } from '../services/pwaStartup.js'
const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000
const UPDATE_SUPPRESS_KEY = 'pwa_update_suppress_until'
const UPDATE_SUPPRESS_MS = 30_000
const UPDATE_DISMISS_SUPPRESS_MS = 60 * 60 * 1000
const UPDATE_RELOAD_FALLBACK_MS = 2000
function isUpdateSuppressed(): boolean {
const suppressUntil = Number(sessionStorage.getItem(UPDATE_SUPPRESS_KEY) || '0')
return Date.now() < suppressUntil
}
function suppressUpdatePrompt(durationMs = UPDATE_SUPPRESS_MS): void {
sessionStorage.setItem(UPDATE_SUPPRESS_KEY, String(Date.now() + durationMs))
}
function clearUpdateSuppression(): void {
sessionStorage.removeItem(UPDATE_SUPPRESS_KEY)
}
function scheduleUpdateChecks(registration: ServiceWorkerRegistration): () => void {
const checkForUpdate = () => {
if (isUpdateSuppressed()) return
registration.update().catch(() => {})
}
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') {
checkForUpdate()
}
}
document.addEventListener('visibilitychange', onVisibilityChange)
const intervalId = window.setInterval(checkForUpdate, UPDATE_CHECK_INTERVAL_MS)
return () => {
document.removeEventListener('visibilitychange', onVisibilityChange)
window.clearInterval(intervalId)
}
}
function reloadForServiceWorkerTakeover(): void {
if (recentlyAttemptedReload()) return
markReloadAttempt()
clearUpdateSuppression()
window.location.reload()
}
export function usePwaUpdate() {
const cleanupRef = useRef<(() => void) | null>(null)
const {
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker
} = useRegisterSW({
immediate: !import.meta.env.DEV,
onNeedReload() {
reloadForServiceWorkerTakeover()
},
onNeedRefresh() {
if (isUpdateSuppressed()) return
setNeedRefresh(true)
},
onRegisteredSW(_swUrl: string, registration: ServiceWorkerRegistration | undefined) {
if (!registration) return
if (isUpdateSuppressed() || !registration.waiting) {
setNeedRefresh(false)
}
cleanupRef.current?.()
cleanupRef.current = scheduleUpdateChecks(registration)
}
})
useEffect(() => {
if (isUpdateSuppressed()) {
setNeedRefresh(false)
}
return () => {
cleanupRef.current?.()
cleanupRef.current = null
}
}, [setNeedRefresh])
const updateApp = async () => {
setNeedRefresh(false)
suppressUpdatePrompt()
await updateServiceWorker(true)
// vite-plugin-pwa reloads via the "controlling" event; fallback if that does not fire.
window.setTimeout(() => {
reloadForServiceWorkerTakeover()
}, UPDATE_RELOAD_FALLBACK_MS)
}
const dismissUpdate = () => {
setNeedRefresh(false)
suppressUpdatePrompt(UPDATE_DISMISS_SUPPRESS_MS)
}
return { needRefresh, updateApp, dismissUpdate }
}