const CACHE_NAME = 'bitfielder-v2'; const STATIC_CACHE = 'bitfielder-static-v2'; const DYNAMIC_CACHE = 'bitfielder-dynamic-v2'; // Core app files that should always be cached const CORE_ASSETS = [ '/', '/index.html', '/manifest.json', '/icon-192.png', '/icon-512.png' ]; // Assets that can be cached dynamically const DYNAMIC_ASSETS_PATTERNS = [ /\/src\/.+\.(ts|tsx|js|jsx)$/, /\/src\/.+\.css$/, /fonts\.googleapis\.com/, /fonts\.gstatic\.com/ ]; // Install event - cache core resources self.addEventListener('install', event => { console.log('Service Worker installing...'); event.waitUntil( Promise.all([ caches.open(STATIC_CACHE).then(cache => { console.log('Caching core assets'); return cache.addAll(CORE_ASSETS); }), caches.open(DYNAMIC_CACHE).then(cache => { console.log('Dynamic cache initialized'); }) ]).then(() => { console.log('Service Worker installed successfully'); return self.skipWaiting(); }) ); }); // Activate event - clean up old caches and take control self.addEventListener('activate', event => { console.log('Service Worker activating...'); event.waitUntil( caches.keys().then(cacheNames => { return Promise.all([ ...cacheNames.map(cacheName => { if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) { console.log('Deleting old cache:', cacheName); return caches.delete(cacheName); } }), self.clients.claim() ]); }).then(() => { console.log('Service Worker activated successfully'); }) ); }); // Fetch event - sophisticated caching strategy self.addEventListener('fetch', event => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip external APIs and chrome-extension if (!url.origin.includes(self.location.origin) && !url.hostname.includes('googleapis') && !url.hostname.includes('gstatic')) { return; } event.respondWith(handleFetch(request)); }); async function handleFetch(request) { const url = new URL(request.url); try { // Strategy 1: Core assets - Cache First if (CORE_ASSETS.some(asset => url.pathname === asset || url.pathname.endsWith(asset))) { return await cacheFirst(request, STATIC_CACHE); } // Strategy 2: Dynamic assets - Stale While Revalidate if (DYNAMIC_ASSETS_PATTERNS.some(pattern => pattern.test(url.pathname))) { return await staleWhileRevalidate(request, DYNAMIC_CACHE); } // Strategy 3: Fonts - Cache First with longer TTL if (url.hostname.includes('googleapis') || url.hostname.includes('gstatic')) { return await cacheFirst(request, DYNAMIC_CACHE); } // Strategy 4: Everything else - Network First return await networkFirst(request, DYNAMIC_CACHE); } catch (error) { console.error('Fetch error:', error); // Fallback for navigation requests if (request.mode === 'navigate') { const cache = await caches.open(STATIC_CACHE); return await cache.match('/index.html') || new Response('App offline', { status: 503 }); } return new Response('Resource not available offline', { status: 503 }); } } // Cache First strategy 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 (response.status === 200) { cache.put(request, response.clone()); } return response; } // Network First strategy async function networkFirst(request, cacheName) { const cache = await caches.open(cacheName); try { const response = await fetch(request); if (response.status === 200) { cache.put(request, response.clone()); } return response; } catch (error) { const cached = await cache.match(request); if (cached) { return cached; } throw error; } } // Stale While Revalidate strategy async function staleWhileRevalidate(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); // Always try to fetch and update cache in background const fetchPromise = fetch(request).then(response => { if (response.status === 200) { cache.put(request, response.clone()); } return response; }).catch(() => null); // Return cached version immediately if available, otherwise wait for network return cached || await fetchPromise; } // Handle background sync for offline actions self.addEventListener('sync', event => { console.log('Background sync triggered:', event.tag); if (event.tag === 'shader-save') { event.waitUntil(syncShaderData()); } }); async function syncShaderData() { // This would handle syncing shader data when coming back online // For now, just log that sync is available console.log('Shader data sync available for future implementation'); } // Handle push notifications (for future features) self.addEventListener('push', event => { if (event.data) { const data = event.data.json(); console.log('Push notification received:', data); // Could be used for shader sharing notifications, etc. } }); // Provide offline status self.addEventListener('message', event => { if (event.data && event.data.type === 'GET_VERSION') { event.ports[0].postMessage({ version: CACHE_NAME }); } });