const CACHE_VERSION = 'v2'; const STATIC_CACHE = `mengya-static-${CACHE_VERSION}`; const RUNTIME_CACHE = `mengya-runtime-${CACHE_VERSION}`; const API_CACHE = `mengya-api-${CACHE_VERSION}`; const OFFLINE_FALLBACK = '/offline.html'; const APP_SHELL = [ '/', '/index.html', '/styles.css', '/app.js', '/manifest.webmanifest', '/logo.png', '/favicon.ico', OFFLINE_FALLBACK, ]; self.addEventListener('install', (event) => { event.waitUntil( caches.open(STATIC_CACHE).then((cache) => cache.addAll(APP_SHELL)) ); self.skipWaiting(); }); self.addEventListener('activate', (event) => { event.waitUntil( (async () => { const cacheNames = await caches.keys(); await Promise.all( cacheNames .filter((name) => ![STATIC_CACHE, RUNTIME_CACHE, API_CACHE].includes(name)) .map((name) => caches.delete(name)) ); if ('navigationPreload' in self.registration) { await self.registration.navigationPreload.enable(); } await self.clients.claim(); })() ); }); self.addEventListener('message', (event) => { if (event.data === 'SKIP_WAITING') { self.skipWaiting(); } }); self.addEventListener('fetch', (event) => { const { request } = event; if (request.method !== 'GET') { return; } const url = new URL(request.url); if (request.mode === 'navigate') { event.respondWith(handleNavigationRequest(event)); return; } if (url.origin !== self.location.origin) { return; } if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirst(request, API_CACHE)); return; } if (isStaticAssetRequest(request, url)) { event.respondWith(staleWhileRevalidate(request, RUNTIME_CACHE)); return; } event.respondWith(cacheFirst(request, RUNTIME_CACHE)); }); async function handleNavigationRequest(event) { try { const preloadResponse = await event.preloadResponse; if (preloadResponse) { return preloadResponse; } const networkResponse = await fetch(event.request); if (networkResponse && networkResponse.ok) { const cache = await caches.open(RUNTIME_CACHE); cache.put(event.request, networkResponse.clone()); } return networkResponse; } catch (error) { const cachedPage = await caches.match(event.request); if (cachedPage) { return cachedPage; } const offlineResponse = await caches.match(OFFLINE_FALLBACK); return offlineResponse || new Response('Offline', { status: 503 }); } } async function networkFirst(request, cacheName) { const cache = await caches.open(cacheName); try { const response = await fetch(request); if (shouldCacheResponse(response)) { cache.put(request, response.clone()); } return response; } catch (error) { const cached = await cache.match(request); if (cached) { return cached; } if (request.mode === 'navigate') { const offlineResponse = await caches.match(OFFLINE_FALLBACK); if (offlineResponse) { return offlineResponse; } } return new Response(JSON.stringify({ message: 'Network unavailable' }), { status: 503, headers: { 'Content-Type': 'application/json;charset=UTF-8' }, }); } } async function staleWhileRevalidate(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); const networkPromise = fetch(request) .then((response) => { if (shouldCacheResponse(response)) { cache.put(request, response.clone()); } return response; }) .catch(() => undefined); return cached || networkPromise || new Response('Not found', { status: 404 }); } async function cacheFirst(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); if (cached) { return cached; } const response = await fetch(request); if (shouldCacheResponse(response)) { cache.put(request, response.clone()); } return response; } function isStaticAssetRequest(request, url) { if (request.destination === 'script' || request.destination === 'style' || request.destination === 'image' || request.destination === 'font') { return true; } return ( url.pathname.endsWith('.css') || url.pathname.endsWith('.js') || url.pathname.endsWith('.png') || url.pathname.endsWith('.jpg') || url.pathname.endsWith('.jpeg') || url.pathname.endsWith('.svg') || url.pathname.endsWith('.ico') || url.pathname.endsWith('.webmanifest') ); } function shouldCacheResponse(response) { return Boolean(response && (response.status === 200 || response.type === 'opaque')); }