const APP_BASE = '/', SW_VERSION = 'v1.2.0', // Cache names SHELL_CACHE = `shell-${SW_VERSION}`, RUNTIME_CACHE = `runtime-${SW_VERSION}`, IMAGE_CACHE = `images-${SW_VERSION}`, AUDIO_CACHE = 'domsonic-cache-v1', // Limits MAX_IMAGE_ENTRIES = 10000, // App shell APP_SHELL = [ `${APP_BASE}`, `${APP_BASE}index.html`, `${APP_BASE}manifest.webmanifest`, `${APP_BASE}icon.png`, ], // Helpers isStaticAsset = (path) => (/\.(js|css|woff2?|ttf|png|jpg|svg|webp)$/i.test(path)), trimCache = async (cacheName, maxEntries) => { const cache = await caches.open(cacheName), keys = await cache.keys() if (keys.length <= maxEntries) return const deleteCount = keys.length - maxEntries for (let i = 0; i < deleteCount; i++) { await cache.delete(keys[i]) } } /* -------------------------------------------------- */ /* INSTALL */ /* -------------------------------------------------- */ self.addEventListener('install', event => { self.skipWaiting() event.waitUntil( caches.open(SHELL_CACHE) .then( cache => cache.addAll(APP_SHELL) ) ) }) /* -------------------------------------------------- */ /* ACTIVATE */ /* -------------------------------------------------- */ self.addEventListener( 'activate', event => event.waitUntil( (async () => { const keys = await caches.keys(), allowed = [ SHELL_CACHE, RUNTIME_CACHE, IMAGE_CACHE, AUDIO_CACHE ] await Promise.all( keys .filter(key => !allowed.includes(key)) .map(key => caches.delete(key)) ) await self.clients.claim() })() ) ) /* -------------------------------------------------- */ /* FETCH */ /* -------------------------------------------------- */ self.addEventListener( 'fetch', event => { const request = event.request, url = new URL(request.url) if (request.method !== 'GET') return /* SPA navigation */ if (request.mode === 'navigate') { event.respondWith(networkFirst(request, SHELL_CACHE)) return } /* AUDIO STREAMS */ if ( url.pathname.includes('/rest/stream') || url.pathname.includes('/rest/download') ) { event.respondWith(audioStrategy(request)) return } /* IMAGES */ if ( request.destination === 'image' || /\.(png|jpg|jpeg|webp|gif|svg)$/i.test(url.pathname) ) { event.respondWith(imageStrategy(request)) return } /* STATIC ASSETS */ if (isStaticAsset(url.pathname)) { event.respondWith(cacheFirst(request, SHELL_CACHE)) return } /* DEFAULT */ event.respondWith(networkFirst(request, RUNTIME_CACHE)) } ) /* -------------------------------------------------- */ /* MESSAGES */ /* -------------------------------------------------- */ self.addEventListener( 'message', event => { switch (event.data || {}) { case 'CLEAR_RUNTIME_CACHE': event.waitUntil(caches.delete(RUNTIME_CACHE)) break; case 'CLEAR_IMAGE_CACHE': event.waitUntil(caches.delete(IMAGE_CACHE)) break; case 'CLEAR_AUDIO_CACHE': event.waitUntil(caches.delete(AUDIO_CACHE)) break; } } ) /* -------------------------------------------------- */ /* AUDIO STRATEGY */ /* Network first / Cache fallback */ /* -------------------------------------------------- */ const audioStrategy = async (request) => { // Do not cache range requests if (request.headers.has('range')) return fetch(request) const cache = await caches.open(AUDIO_CACHE) try { const response = await fetch(request) if (response && response.status === 200) { const cached = await cache.match(request) // Avoid overwriting entries managed by the app if (!cached) cache.put( request, response.clone() ).catch(() => {}) } return response } catch (err) { const cached = await cache.match(request) if (cached) return cached throw err } } /* -------------------------------------------------- */ /* IMAGE STRATEGY */ /* Cache first + background update */ /* -------------------------------------------------- */ const imageStrategy = async (request) => { const cache = await caches.open(IMAGE_CACHE), cached = await cache.match(request), networkFetch = fetch(request) .then(async response => { if (response && response.status === 200) { await cache.put(request, response.clone()) await trimCache(IMAGE_CACHE, MAX_IMAGE_ENTRIES) } return response }) .catch(() => null) if (cached) return cached const network = await networkFetch if (network) return network return new Response('', { status: 504 }) } /* -------------------------------------------------- */ /* NETWORK FIRST */ /* -------------------------------------------------- */ const networkFirst = async (request, cacheName) => { const cache = await caches.open(cacheName) try { const response = await fetch(request) if (response && response.status === 200) cache.put( request, response.clone() ).catch(() => {}) return response } catch (err) { const cached = await cache.match(request) if (cached) return cached throw err } } /* -------------------------------------------------- */ /* CACHE FIRST */ /* -------------------------------------------------- */ const cacheFirst = async (request, cacheName) => { const cache = await caches.open(cacheName), cached = await cache.match(request) if (cached) return cached const response = await fetch(request) if (response && response.status === 200) cache.put( request, response.clone() ).catch(() => {}) return response }