const CACHE_NAME = 'media-player-cache-v2'; let cachedFiles = [ "/index.html", "/version.txt", "/eruda.js", "/f/sa.opus", "/f/sv.mp4", "/f/aa.opus", "/f/av.mp4", "/f/ta.opus", "/f/tv.mp4", "/fonts/MPLUSRounded1c-Bold.ttf", "/f/piano.mid", "/mei-ver.mei", "/midipane.js", "/musicxmlpane.js", "/tools-bg-sw-metronome.js", "/Midi.js", "/CustomEase.min.js", "/draggabilly.pkgd.min.js", "/gsap.js", "/verovio-toolkit-wasm.js", ]; // ====================================================== // install // ====================================================== self.addEventListener('install', (event) => { console.log('[SW] install'); self.skipWaiting(); event.waitUntil( (async () => { const cache = await caches.open(CACHE_NAME); console.log('[SW] precache start'); await Promise.allSettled( cachedFiles.map(async (file) => { try { console.group('[SW] PRECACHE'); console.log('file:', file); // Git LFS redirect対策 const response = await fetch(file, { mode: 'no-cors' }); console.log('response.type:', response.type); console.log('response.status:', response.status); console.log('response.url:', response.url); const request = new Request(file); await cache.put(request, response); console.log('cache.put success'); console.groupEnd(); } catch (err) { console.error('[SW] precache failed:', file, err); console.groupEnd(); } }) ); console.log('[SW] precache complete'); })() ); }); // ====================================================== // activate // ====================================================== self.addEventListener('activate', (event) => { console.log('[SW] activate'); event.waitUntil( (async () => { const keys = await caches.keys(); await Promise.all( keys.map((key) => { if (key !== CACHE_NAME) { console.log('[SW] delete old cache:', key); return caches.delete(key); } }) ); await self.clients.claim(); console.log('[SW] clients claimed'); })() ); }); // ====================================================== // fetch // ====================================================== self.addEventListener('fetch', (event) => { event.respondWith(handleRequest(event.request)); }); async function handleRequest(req) { const url = new URL(req.url); console.group('[SW FETCH]'); console.log('url:', url.href); console.log('method:', req.method); console.log('destination:', req.destination); const range = req.headers.get('range'); if (range) { console.warn('Range Request:', range); } const accept = req.headers.get('accept') || ''; const cache = await caches.open(CACHE_NAME); if (url.pathname === '/p1/version.txt') { const normalizedRequest = new Request('/p1/version.txt', { method: 'GET' }); const cacheResp = await cache.match(normalizedRequest); if (cacheResp) return cacheResp; try { const networkResp = await fetch(req); await cache.put(normalizedRequest, networkResp.clone()); return networkResp; } catch { return new Response('Offline and no cached version.txt', { status: 504, headers: { 'content-type': 'text/plain' } }); } } // ====================================================== // HTML // ====================================================== if (accept.includes('text/html')) { console.log('strategy: network-first'); try { const networkResponse = await fetch(req); console.log('network success'); console.log('network.type:', networkResponse.type); console.log('network.status:', networkResponse.status); try { await cache.put(req, networkResponse.clone()); console.log('cache updated'); } catch (cacheErr) { console.error('cache.put failed:', cacheErr); } console.groupEnd(); return networkResponse; } catch (networkErr) { console.error('network failed:', networkErr); const cacheResp = await cache.match(req.url); if (cacheResp) { console.log('fallback cache hit'); console.groupEnd(); return cacheResp; } const fallback = await cache.match('/p1/index.html'); if (fallback) { console.log('fallback index.html'); console.groupEnd(); return fallback; } console.error('no html fallback'); console.groupEnd(); return new Response( 'Offline and no cached HTML', { status: 504, headers: { 'content-type': 'text/plain' } } ); } } // ====================================================== // static/media // ====================================================== console.log('strategy: cache-first'); try { const normalizedUrl = new URL(req.url); normalizedUrl.search = ''; const cacheResp = await cache.match( normalizedUrl.pathname ); if (cacheResp) { console.log('CACHE HIT'); console.log('cache.type:', cacheResp.type); console.log('cache.status:', cacheResp.status); try { console.log( 'content-type:', cacheResp.headers.get('content-type') ); console.log( 'content-length:', cacheResp.headers.get('content-length') ); console.log( 'accept-ranges:', cacheResp.headers.get('accept-ranges') ); } catch (headerErr) { console.warn('header read failed:', headerErr); } // opaque + range warning if (range && cacheResp.type === 'opaque') { console.error( '[SW] opaque response cannot properly handle range requests offline' ); } console.groupEnd(); return cacheResp; } console.warn('CACHE MISS'); } catch (cacheErr) { console.error('cache.match failed:', cacheErr); } // ====================================================== // network fallback // ====================================================== try { const networkResp = await fetch(req); console.log('NETWORK OK'); console.log('network.type:', networkResp.type); console.log('network.status:', networkResp.status); try { await cache.put(req, networkResp.clone()); console.log('cache.put success'); } catch (cachePutErr) { console.error('cache.put failed:', cachePutErr); } console.groupEnd(); return networkResp; } catch (networkErr) { console.error('NETWORK FAILED'); console.error(networkErr); console.groupEnd(); return new Response( JSON.stringify({ error: 'offline', url: req.url, range: range, destination: req.destination, timestamp: Date.now() }, null, 2), { status: 504, headers: { 'content-type': 'application/json' } } ); } } // ====================================================== // message // ====================================================== self.addEventListener('message', (event) => { console.log('[SW] message:', event.data); // ------------------------------------------------------ // dynamic cache update // ------------------------------------------------------ if (event.data?.type === 'CACHE_FILES') { const files = event.data.files || []; cachedFiles = [ '/p1/index.html', ...files ]; event.waitUntil( (async () => { const cache = await caches.open(CACHE_NAME); console.log('[SW] updating cache list'); // add new files await Promise.allSettled( cachedFiles.map(async (file) => { try { console.group('[SW] CACHE_FILES'); console.log('file:', file); const response = await fetch(file, { mode: 'no-cors' }); console.log('response.type:', response.type); console.log('response.status:', response.status); const request = new Request(file); await cache.put(request, response); console.log('cache.put success'); console.groupEnd(); } catch (err) { console.error('cache add failed:', file, err); console.groupEnd(); } }) ); // remove old files const keys = await cache.keys(); const validPaths = new Set( cachedFiles.map((p) => { try { return ( new URL(p, self.location.origin).pathname + (p.includes('?') ? '?' + p.split('?')[1] : '') ); } catch { return p; } }) ); for (const request of keys) { const url = new URL(request.url); const pathWithQuery = url.pathname + (url.search ? url.search : ''); if ( !validPaths.has(pathWithQuery) && !validPaths.has(url.pathname) ) { console.log('[SW] deleting old cache:', pathWithQuery); await cache.delete(request); } } console.log('[SW] cache update complete'); })() ); } // ------------------------------------------------------ // skip waiting // ------------------------------------------------------ if (event.data?.type === 'SKIP_WAITING') { console.log('[SW] skipWaiting'); self.skipWaiting(); } });