/// const CACHE_NAME = 'soulsync-shell-v2' const DB_NAME = 'soulsync-offline' const AUDIO_STORE = 'audio-blobs' // App shell files to cache for offline UI access const SHELL_FILES = [ '/', '/music', '/radio', '/offline', ] // Install: pre-cache app shell self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_FILES).catch(() => { // Non-critical if some pages fail to cache }) ) ) self.skipWaiting() }) // Activate: clean old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE_NAME && k.startsWith('soulsync-')).map((k) => caches.delete(k))) ) ) self.clients.claim() }) /** * Open IndexedDB from the service worker to serve cached audio */ function openDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 1) req.onupgradeneeded = () => { const db = req.result if (!db.objectStoreNames.contains(AUDIO_STORE)) { db.createObjectStore(AUDIO_STORE) } if (!db.objectStoreNames.contains('track-meta')) { db.createObjectStore('track-meta') } } req.onsuccess = () => resolve(req.result) req.onerror = () => reject(req.error) }) } function getFromDB(trackId) { return openDB().then( (db) => new Promise((resolve, reject) => { const tx = db.transaction(AUDIO_STORE, 'readonly') const req = tx.objectStore(AUDIO_STORE).get(trackId) req.onsuccess = () => { db.close() resolve(req.result) } req.onerror = () => { db.close() reject(req.error) } }) ) } // Fetch: intercept /api/music/stream/ requests to serve from IndexedDB self.addEventListener('fetch', (event) => { const url = new URL(event.request.url) // Intercept stream requests const streamMatch = url.pathname.match(/^\/api\/music\/stream\/(.+)$/) if (streamMatch) { const trackId = streamMatch[1] event.respondWith( getFromDB(trackId).then((blob) => { if (blob) { return new Response(blob, { headers: { 'Content-Type': blob.type || 'audio/mpeg', 'Content-Length': String(blob.size), }, }) } // Not cached, fetch from network return fetch(event.request) }).catch(() => fetch(event.request)) ) return } // For navigation requests: try network first, fall back to cache if (event.request.mode === 'navigate') { event.respondWith( fetch(event.request).catch(() => caches.match(event.request).then((cached) => cached || caches.match('/')) ) ) return } })