/** * SERVICE WORKER FOR NAUE ROLL CALCULATOR * ======================================== * Caching strategy: * 1. CACHE_FIRST for static assets (CSS, JS, images) * 2. NETWORK_FIRST for API/data (with fallback to cache) * 3. STALE_WHILE_REVALIDATE for article data */ const CACHE_NAME = 'rollcalc-v14-' + new Date().getTime(); const STATIC_CACHE = 'rollcalc-static-v14'; const DATA_CACHE = 'rollcalc-data-v14'; // Assets to cache on install const STATIC_ASSETS = [ '/', '/index.html', '/static/article-data.json', '/static/config.json' ]; // =================================================================== // INSTALL EVENT - Cache static assets // =================================================================== 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); // Don't fail installation if some assets can't be cached return Promise.resolve(); }); }) .then(() => self.skipWaiting()) ); }); // =================================================================== // ACTIVATE EVENT - Clean up old caches // =================================================================== 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()) ); }); // =================================================================== // FETCH EVENT - Intelligent caching strategy // =================================================================== self.addEventListener('fetch', event => { const { request } = event; const url = new URL(request.url); // Skip cross-origin requests if (url.origin !== location.origin) { return; } // --------------------------------------------------------------- // 1. JSON DATA (article-data.json, config.json) // STALE_WHILE_REVALIDATE: Use cache first, update in background // --------------------------------------------------------------- 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 => { // Update cache with fresh version if (networkResponse && networkResponse.status === 200) { cache.put(request, networkResponse.clone()); } return networkResponse; }).catch(() => { // Network failed, return cached version return response || cacheNotFound('JSON data not found'); }); // Return cached response immediately, update in background return response || fetchPromise; }); }) ); return; } // --------------------------------------------------------------- // 2. HTML PAGES // NETWORK_FIRST: Try network, fall back to cache // --------------------------------------------------------------- if (request.method === 'GET' && (url.pathname === '/' || url.pathname.endsWith('.html'))) { event.respondWith( fetch(request) .then(response => { if (response && response.status === 200) { const cache_copy = response.clone(); caches.open(STATIC_CACHE).then(cache => { cache.put(request, cache_copy); }); } return response; }) .catch(() => { return caches.match(request) .then(response => response || cacheNotFound('Page not found')); }) ); return; } // --------------------------------------------------------------- // 3. STATIC ASSETS (CSS, JS, images) // CACHE_FIRST: Use cache if available, fall back to network // --------------------------------------------------------------- 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 => { // Only cache successful responses if (networkResponse && networkResponse.status === 200) { cache.put(request, networkResponse.clone()); } return networkResponse; }).catch(() => { // Return offline placeholder if (request.destination === 'image') { return createPlaceholderImage(); } return cacheNotFound('Asset not found'); }); }); }) ); return; } // --------------------------------------------------------------- // 4. POST/PUT/DELETE (API calls) // NETWORK_ONLY: Do not cache, but handle errors gracefully // --------------------------------------------------------------- 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: new Headers({ 'Content-Type': 'application/json' }) } ); }) ); return; } }); // =================================================================== // HELPER FUNCTIONS // =================================================================== function cacheNotFound(message) { return new Response( `

Offline

${message}

Roll Calculator is offline or the resource is not cached.

`, { status: 404, statusText: 'Not Found', headers: new Headers({ 'Content-Type': 'text/html' }) } ); } function createPlaceholderImage() { const svg = ` Image offline `; return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' } }); } // =================================================================== // LOG SERVICE WORKER STATUS // =================================================================== console.log('[ServiceWorker] Naue Roll Calculator Service Worker loaded');