fix(pwa): Startup-Hänger nach Inaktivität stabilisieren
Verhindert Blank-Screens und Reload-Schleifen beim Wiederöffnen der PWA, indem Recovery nur bei bestätigter SW-Übernahme greift und Navigationen bevorzugt frisch aus dem Netzwerk geladen werden. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -54,6 +54,12 @@ describe('forcePwaRecovery stale counter reset', () => {
|
||||
expect(sessionStorage.getItem(STALE_RECOVERY_LAST_KEY)).toBeNull()
|
||||
expect(reload).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('returns false when hard recovery was just attempted', async () => {
|
||||
sessionStorage.setItem('pwa_hard_recovery_ts', String(Date.now()))
|
||||
const result = await forcePwaRecovery()
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reconcileServiceWorkerOnStartup', () => {
|
||||
@@ -85,6 +91,34 @@ describe('reconcileServiceWorkerOnStartup', () => {
|
||||
|
||||
await expect(reconcileServiceWorkerOnStartup()).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when waiting worker activation never takes over', async () => {
|
||||
vi.useFakeTimers()
|
||||
const postMessage = vi.fn()
|
||||
const addEventListener = vi.fn()
|
||||
vi.stubEnv('DEV', false)
|
||||
|
||||
Object.defineProperty(navigator, 'serviceWorker', {
|
||||
configurable: true,
|
||||
value: {
|
||||
controller: { scriptURL: '/sw.js?v=1' },
|
||||
getRegistration: vi.fn().mockResolvedValue({
|
||||
waiting: { postMessage },
|
||||
installing: null,
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
addEventListener: vi.fn()
|
||||
}),
|
||||
addEventListener
|
||||
}
|
||||
})
|
||||
|
||||
const reconcilePromise = reconcileServiceWorkerOnStartup()
|
||||
await vi.advanceTimersByTimeAsync(4_000)
|
||||
|
||||
await expect(reconcilePromise).resolves.toBe(false)
|
||||
expect(postMessage).toHaveBeenCalledWith({ type: 'SKIP_WAITING' })
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
describe('reconcileVersionOnStartup', () => {
|
||||
|
||||
@@ -90,14 +90,15 @@ export async function clearPwaCachesAndWorkers(): Promise<void> {
|
||||
* Last-resort recovery when soft reloads cannot escape a stale precache.
|
||||
* Equivalent to manually clearing site data / reinstalling the PWA.
|
||||
*/
|
||||
export async function forcePwaRecovery(): Promise<void> {
|
||||
if (recentlyAttemptedHardRecovery()) return
|
||||
export async function forcePwaRecovery(): Promise<boolean> {
|
||||
if (recentlyAttemptedHardRecovery()) return false
|
||||
|
||||
markHardRecoveryAttempt()
|
||||
markReloadAttempt()
|
||||
resetStaleRecoveryCount()
|
||||
await clearPwaCachesAndWorkers()
|
||||
window.location.reload()
|
||||
return true
|
||||
}
|
||||
|
||||
async function waitForWaitingWorker(
|
||||
@@ -163,21 +164,21 @@ export async function triggerServiceWorkerUpdate(timeoutMs = 5_000): Promise<boo
|
||||
}
|
||||
|
||||
async function activateWaitingWorker(waiting: ServiceWorker): Promise<boolean> {
|
||||
const currentController = navigator.serviceWorker.controller?.scriptURL ?? null
|
||||
waiting.postMessage({ type: 'SKIP_WAITING' })
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeoutId = window.setTimeout(resolve, 4_000)
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const timeoutId = window.setTimeout(() => resolve(false), 4_000)
|
||||
navigator.serviceWorker.addEventListener(
|
||||
'controllerchange',
|
||||
() => {
|
||||
window.clearTimeout(timeoutId)
|
||||
resolve()
|
||||
const nextController = navigator.serviceWorker.controller?.scriptURL ?? null
|
||||
resolve(nextController !== null && nextController !== currentController)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,8 +206,11 @@ export async function reconcileServiceWorkerOnStartup(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
markColdStartUpdateAttempt()
|
||||
return activateWaitingWorker(waiting)
|
||||
const activated = await activateWaitingWorker(waiting)
|
||||
if (activated) {
|
||||
markColdStartUpdateAttempt()
|
||||
}
|
||||
return activated
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,15 +237,19 @@ export async function reconcileVersionOnStartup(): Promise<'reload' | 'recovered
|
||||
const registration = await navigator.serviceWorker.getRegistration()
|
||||
const waiting = registration?.waiting
|
||||
if (waiting) {
|
||||
markColdStartUpdateAttempt()
|
||||
await activateWaitingWorker(waiting)
|
||||
return 'reload'
|
||||
const activated = await activateWaitingWorker(waiting)
|
||||
if (activated) {
|
||||
markColdStartUpdateAttempt()
|
||||
return 'reload'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!recentlyAttemptedHardRecovery()) {
|
||||
await forcePwaRecovery()
|
||||
return 'recovered'
|
||||
const recovered = await forcePwaRecovery()
|
||||
if (recovered) {
|
||||
return 'recovered'
|
||||
}
|
||||
}
|
||||
|
||||
return 'noop'
|
||||
|
||||
Reference in New Issue
Block a user