// NOTE: This service worker is for the web/PWA build. // For development we aggressively unregister SWs in `src/player.js`. // // Bump this value whenever caching logic changes to guarantee clients don't // keep an old UI after updates. const CACHE_NAME = 'radioplayer-pwa-v4'; const CORE_ASSETS = [ './', 'index.html', 'stations.json', 'manifest.json', 'assets/radioplayer-logo-192.png', 'assets/radioplayer-logo-512.png', ]; const CORE_PATHS = new Set(CORE_ASSETS.map((p) => new URL(p, self.registration.scope).pathname)); self.addEventListener('install', (event) => { // Activate updated SW as soon as it's installed. self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME).then(async (cache) => { const reqs = CORE_ASSETS.map((p) => new Request(p, { cache: 'reload' })); await cache.addAll(reqs); // Vite fingerprints JS/CSS assets in production. Parse the built HTML so // the installed PWA can launch offline after its first install. try { const indexResp = await fetch(new Request('./', { cache: 'reload' })); const indexText = await indexResp.clone().text(); await cache.put('./', indexResp); const assetUrls = [...indexText.matchAll(/(?:src|href)="([^"]+)"/g)] .map((match) => match[1]) .filter((assetPath) => assetPath.startsWith('./assets/') || assetPath.startsWith('assets/')) .map((assetPath) => new URL(assetPath, self.registration.scope).href); await Promise.all(assetUrls.map((assetUrl) => { return cache.add(new Request(assetUrl, { cache: 'reload' })).catch(() => {}); })); } catch (e) { // If HTML parsing fails, runtime caching below still catches assets. } }) ); }); self.addEventListener('activate', (event) => { event.waitUntil( Promise.all([ self.clients.claim(), caches.keys().then((keys) => Promise.all( keys.map((k) => { if (k !== CACHE_NAME) return caches.delete(k); return null; }) )), ]) ); }); self.addEventListener('fetch', (event) => { // Only handle GET requests if (event.request.method !== 'GET') return; const url = new URL(event.request.url); // Don't cache cross-origin requests (station logos, APIs, etc.). if (url.origin !== self.location.origin) { return; } const isCore = CORE_PATHS.has(url.pathname); const isHtmlNavigation = event.request.mode === 'navigate' || (event.request.headers.get('accept') || '').includes('text/html'); // Network-first for navigations and core assets to prevent "old UI" issues. if (isHtmlNavigation || isCore) { event.respondWith( fetch(event.request) .then((networkResp) => { const respClone = networkResp.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(event.request, respClone)).catch(() => {}); return networkResp; }) .catch(() => caches.match(event.request).then((cached) => cached || caches.match('./') || caches.match('index.html'))) ); return; } event.respondWith( caches.match(event.request).then((cached) => { if (cached) return cached; return fetch(event.request).then((networkResp) => { // Optionally cache new resources (best-effort) try { const respClone = networkResp.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(event.request, respClone)).catch(()=>{}); } catch (e) {} return networkResp; }).catch(() => { // If offline and HTML navigation, return cached index.html if (event.request.mode === 'navigate') return caches.match('./') || caches.match('index.html'); return new Response('', { status: 503, statusText: 'Service Unavailable' }); }); }) ); });