/** * SERVICE WORKER FOR NAUE ROLL CALCULATOR * Offline support + intelligent caching */ const CACHE_NAME = 'rollcalc-v14-' + new Date().getTime(); const STATIC_CACHE = 'rollcalc-static-v14'; const DATA_CACHE = 'rollcalc-data-v14'; const STATIC_ASSETS = ['/', '/index.html', '/static/article-data.json', '/static/config.json']; self.addEventListener('install', event => { console.log('[ServiceWorker] Installing...'); event.waitUntil( caches.open(STATIC_CACHE) .then(cache => { console.log('[ServiceWorker] Caching static assets'); return cache.addAll(STATIC_ASSETS).catch(err => { console.warn('[ServiceWorker] Some assets failed to cache:', err); return Promise.resolve(); }); }) .then(() => self.skipWaiting()) ); }); self.addEventListener('activate', event => { console.log('[ServiceWorker] Activating...'); event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== STATIC_CACHE && cacheName !== DATA_CACHE && cacheName !== CACHE_NAME) { console.log('[ServiceWorker] Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }).then(() => self.clients.claim()) ); }); self.addEventListener('fetch', event => { const { request } = event; const url = new URL(request.url); if (url.origin !== location.origin) return; // JSON data - STALE_WHILE_REVALIDATE if (url.pathname.includes('.json')) { event.respondWith( caches.open(DATA_CACHE).then(cache => { return cache.match(request).then(response => { const fetchPromise = fetch(request).then(networkResponse => { if (networkResponse && networkResponse.status === 200) { cache.put(request, networkResponse.clone()); } return networkResponse; }).catch(() => response || cacheNotFound('JSON data')); return response || fetchPromise; }); }) ); return; } // HTML - NETWORK_FIRST if (request.method === 'GET' && (url.pathname === '/' || url.pathname.endsWith('.html'))) { event.respondWith( fetch(request) .then(response => { if (response && response.status === 200) { caches.open(STATIC_CACHE).then(cache => cache.put(request, response.clone())); } return response; }) .catch(() => caches.match(request).then(response => response || cacheNotFound('Page'))) ); return; } // Static assets - CACHE_FIRST if (request.method === 'GET') { event.respondWith( caches.open(STATIC_CACHE).then(cache => { return cache.match(request).then(response => { if (response) return response; return fetch(request).then(networkResponse => { if (networkResponse && networkResponse.status === 200) { cache.put(request, networkResponse.clone()); } return networkResponse; }).catch(() => { if (request.destination === 'image') return createPlaceholderImage(); return cacheNotFound('Asset'); }); }); }) ); return; } // POST/PUT/DELETE - NETWORK_ONLY if (['POST', 'PUT', 'DELETE'].includes(request.method)) { event.respondWith( fetch(request).catch(() => { return new Response( JSON.stringify({ error: 'You are offline. API calls are not available.', status: 'offline' }), { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'application/json' } } ); }) ); } }); function cacheNotFound(message) { return new Response( `
${message} not found or you are offline.
`, { status: 404, statusText: 'Not Found', headers: { 'Content-Type': 'text/html' } } ); } function createPlaceholderImage() { const svg = ``; return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' } }); } console.log('[ServiceWorker] RollCalc Service Worker loaded');