From a192dcaa7fa4ccc7b51869a18f9883db26de8628 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 10 Apr 2026 17:37:42 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20proper=20PWA=20update=20flow=20?= =?UTF-8?q?=E2=80=94=20clear=20SW=20cache=20+=20activate=20new=20worker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- components/sw-register.tsx | 20 +++++++++++++++++--- components/update-banner.tsx | 27 ++++++++++++++++++++++++++- public/sw.js | 7 +++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/components/sw-register.tsx b/components/sw-register.tsx index a847761..3ca1f6a 100644 --- a/components/sw-register.tsx +++ b/components/sw-register.tsx @@ -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 } diff --git a/components/update-banner.tsx b/components/update-banner.tsx index 10a45da..6218398 100644 --- a/components/update-banner.tsx +++ b/components/update-banner.tsx @@ -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() } diff --git a/public/sw.js b/public/sw.js index 9fc1a63..6a3d60c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -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)