feat: proper PWA update flow — clear SW cache + activate new worker

The "Update Now" banner now clears all service worker caches,
activates any waiting service worker via SKIP_WAITING message,
and reloads. SW registration also periodically checks for updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-10 17:37:42 -04:00
parent c5aa2f0e40
commit a192dcaa7f
3 changed files with 50 additions and 4 deletions

View File

@ -4,11 +4,25 @@ import { useEffect } from 'react'
export function ServiceWorkerRegister() {
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch((err) => {
if (!('serviceWorker' in navigator)) return
navigator.serviceWorker
.register('/sw.js')
.then((reg) => {
// Check for updates periodically (every 60s, matching version poll)
setInterval(() => reg.update().catch(() => {}), 60_000)
// Also check on visibility change
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
reg.update().catch(() => {})
}
})
})
.catch((err) => {
console.warn('SW registration failed:', err)
})
}
}, [])
return null
}

View File

@ -43,8 +43,33 @@ export function UpdateBanner() {
if (!updateAvailable) return null
const handleUpdate = () => {
const handleUpdate = async () => {
setUpdating(true)
try {
// 1. Clear all service worker caches
const keys = await caches.keys()
await Promise.all(keys.map((k) => caches.delete(k)))
// 2. If a new service worker is waiting, activate it
const reg = await navigator.serviceWorker?.getRegistration()
if (reg?.waiting) {
// Listen for the new SW to take control, then reload
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload()
}, { once: true })
reg.waiting.postMessage({ type: 'SKIP_WAITING' })
// Fallback reload if controllerchange doesn't fire within 3s
setTimeout(() => window.location.reload(), 3000)
return
}
// 3. If there's an active SW but no waiting one, unregister and reload
if (reg) {
await reg.unregister()
}
} catch {
// If anything fails, just reload
}
window.location.reload()
}

View File

@ -72,6 +72,13 @@ function getFromDB(trackId) {
)
}
// Listen for SKIP_WAITING message from the app to activate a waiting SW
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
}
})
// Fetch: intercept /api/music/stream/ requests to serve from IndexedDB
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)