/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Hikari */ const CACHE_VERSION = 'library-v1'; const STATIC_CACHE = `${CACHE_VERSION}-static`; const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`; const IMAGE_CACHE = `${CACHE_VERSION}-images`; // Static assets to cache on install const STATIC_ASSETS = [ '/offline.html' ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('[Service Worker] Installing...'); event.waitUntil( caches.open(STATIC_CACHE).then((cache) => { console.log('[Service Worker] Caching static assets'); return cache.addAll(STATIC_ASSETS).catch((err) => { console.error('[Service Worker] Failed to cache static assets:', err); // Don't fail installation if caching fails return Promise.resolve(); }); }).then(() => { return self.skipWaiting(); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[Service Worker] Activating...'); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((cacheName) => { return cacheName.startsWith('library-') && cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE && cacheName !== IMAGE_CACHE; }) .map((cacheName) => { console.log('[Service Worker] Deleting old cache:', cacheName); return caches.delete(cacheName); }) ); }).then(() => { return self.clients.claim(); }) ); }); // Fetch event - implement caching strategies self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip Chrome extensions and other protocols if (!url.protocol.startsWith('http')) { return; } // API requests - Network first, cache fallback if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(request) .then((response) => { // Clone the response before caching const responseClone = response.clone(); caches.open(DYNAMIC_CACHE).then((cache) => { cache.put(request, responseClone); }); return response; }) .catch(() => { return caches.match(request).then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } // Return offline page for failed API requests return caches.match('/offline.html'); }); }) ); return; } // Images - Cache first, network fallback if (request.destination === 'image' || url.pathname.match(/\.(jpg|jpeg|png|gif|webp|svg)$/)) { event.respondWith( caches.match(request).then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } return fetch(request).then((response) => { const responseClone = response.clone(); caches.open(IMAGE_CACHE).then((cache) => { cache.put(request, responseClone); }); return response; }); }) ); return; } // Static assets - Cache first, network fallback if (url.pathname.match(/\.(js|css|woff|woff2|ttf|eot)$/)) { event.respondWith( caches.match(request).then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } return fetch(request).then((response) => { const responseClone = response.clone(); caches.open(STATIC_CACHE).then((cache) => { cache.put(request, responseClone); }); return response; }); }) ); return; } // HTML pages - Network first, cache fallback event.respondWith( fetch(request) .then((response) => { // Only cache successful responses if (response.ok) { const responseClone = response.clone(); caches.open(DYNAMIC_CACHE).then((cache) => { cache.put(request, responseClone); }); } return response; }) .catch(() => { return caches.match(request).then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } // Return offline page as fallback return caches.match('/offline.html'); }); }) ); }); // Message event - handle cache clearing requests self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CLEAR_CACHE') { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => caches.delete(cacheName)) ); }) ); } });