/** * Tractatus Service Worker * - Version management and update notifications * - Cache management for offline support * - PWA functionality */ const CACHE_VERSION = '0.1.1'; const CACHE_NAME = `tractatus-v${CACHE_VERSION}`; const VERSION_CHECK_INTERVAL = 3600000; // 1 hour in milliseconds // Assets to cache immediately on install const CRITICAL_ASSETS = [ '/', '/index.html', '/css/tailwind.css', '/js/components/navbar.js', '/images/tractatus-icon.svg', '/favicon.svg' ]; // Install event - cache critical assets self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { console.log('[Service Worker] Caching critical assets'); return cache.addAll(CRITICAL_ASSETS); }).then(() => { // Force activation of new service worker return self.skipWaiting(); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME) .map((name) => { console.log('[Service Worker] Deleting old cache:', name); return caches.delete(name); }) ); }).then(() => { // Take control of all clients immediately return self.clients.claim(); }) ); }); // Fetch event - network-first strategy with cache fallback self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip chrome-extension and other non-http requests if (!url.protocol.startsWith('http')) { return; } // HTML files: Network-ONLY (never cache, always fetch fresh) // This ensures users always get the latest content without cache refresh if (request.destination === 'document' || url.pathname.endsWith('.html')) { event.respondWith( fetch(request) .catch(() => { // Only for offline fallback: serve cached index.html if (url.pathname === '/' || url.pathname === '/index.html') { return caches.match('/index.html'); } // All other HTML: network only, fail if offline throw new Error('Network required for HTML pages'); }) ); return; } // Static assets (CSS, JS, images): Network-first for versioned URLs, cache-first for others if ( request.destination === 'style' || request.destination === 'script' || request.destination === 'image' || request.destination === 'font' ) { // If URL has version parameter, always fetch fresh (network-first) const hasVersionParam = url.searchParams.has('v'); if (hasVersionParam) { // Network-first for versioned assets (ensures cache-busting works) event.respondWith( fetch(request).then((response) => { // Cache the response for offline use const responseClone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(request, responseClone); }); return response; }).catch(() => { // Fallback to cache if offline return caches.match(request); }) ); } else { // Cache-first for non-versioned assets event.respondWith( caches.match(request).then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } return fetch(request).then((response) => { const responseClone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(request, responseClone); }); return response; }); }) ); } return; } // API calls and other requests: Network-first event.respondWith( fetch(request) .then((response) => { return response; }) .catch(() => { return caches.match(request); }) ); }); // Message event - handle version checks from clients self.addEventListener('message', (event) => { if (event.data.type === 'CHECK_VERSION') { checkVersion().then((versionInfo) => { event.ports[0].postMessage({ type: 'VERSION_INFO', ...versionInfo }); }); } if (event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } }); // Check for version updates async function checkVersion() { try { const response = await fetch('/version.json', { cache: 'no-store' }); const serverVersion = await response.json(); return { currentVersion: CACHE_VERSION, serverVersion: serverVersion.version, updateAvailable: CACHE_VERSION !== serverVersion.version, forceUpdate: serverVersion.forceUpdate, changelog: serverVersion.changelog }; } catch (error) { console.error('[Service Worker] Version check failed:', error); return { currentVersion: CACHE_VERSION, serverVersion: null, updateAvailable: false, error: true }; } } // Periodic background sync for version checks (if supported) self.addEventListener('periodicsync', (event) => { if (event.tag === 'version-check') { event.waitUntil( checkVersion().then((versionInfo) => { if (versionInfo.updateAvailable) { // Notify all clients about update self.clients.matchAll().then((clients) => { clients.forEach((client) => { client.postMessage({ type: 'UPDATE_AVAILABLE', ...versionInfo }); }); }); } }) ); } });