const CACHE_NAME = 'know-foolery-v1.0.0' const STATIC_CACHE_NAME = 'know-foolery-static-v1.0.0' const DYNAMIC_CACHE_NAME = 'know-foolery-dynamic-v1.0.0' // Static assets to cache on install const STATIC_ASSETS = [ '/', '/static/js/bundle.js', '/static/css/main.css', '/manifest.json', '/icons/icon-192x192.png', '/icons/icon-512x512.png' ] // API endpoints that should be cached const API_CACHE_PATTERNS = [ /\/api\/themes/, /\/api\/questions/, /\/api\/leaderboard/ ] // Network-first patterns (always try network first) const NETWORK_FIRST_PATTERNS = [ /\/api\/game/, /\/api\/player/, /\/api\/admin/ ] // Install event - cache static assets self.addEventListener('install', (event) => { console.log('[SW] Install event') event.waitUntil( caches.open(STATIC_CACHE_NAME) .then((cache) => { console.log('[SW] Caching static assets') return cache.addAll(STATIC_ASSETS) }) .then(() => { console.log('[SW] Static assets cached successfully') return self.skipWaiting() }) .catch((error) => { console.error('[SW] Failed to cache static assets:', error) }) ) }) // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activate event') event.waitUntil( caches.keys() .then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== STATIC_CACHE_NAME && cacheName !== DYNAMIC_CACHE_NAME && cacheName.startsWith('know-foolery-')) { console.log('[SW] Deleting old cache:', cacheName) return caches.delete(cacheName) } }) ) }) .then(() => { console.log('[SW] Cache cleanup complete') 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-extension requests if (url.protocol === 'chrome-extension:') { return } // Network-first strategy for dynamic content if (NETWORK_FIRST_PATTERNS.some(pattern => pattern.test(url.pathname))) { event.respondWith(networkFirst(request)) return } // Cache-first strategy for API data that can be stale if (API_CACHE_PATTERNS.some(pattern => pattern.test(url.pathname))) { event.respondWith(cacheFirst(request)) return } // Stale-while-revalidate for static assets if (url.origin === location.origin) { event.respondWith(staleWhileRevalidate(request)) return } // Network-only for external resources event.respondWith(fetch(request)) }) // Network-first strategy async function networkFirst(request) { try { const networkResponse = await fetch(request) if (networkResponse.ok) { const cache = await caches.open(DYNAMIC_CACHE_NAME) cache.put(request, networkResponse.clone()) } return networkResponse } catch (error) { console.log('[SW] Network failed, trying cache:', request.url) const cachedResponse = await caches.match(request) if (cachedResponse) { return cachedResponse } // Return offline fallback for navigation requests if (request.mode === 'navigate') { return caches.match('/offline.html') || new Response('Offline', { status: 503 }) } throw error } } // Cache-first strategy async function cacheFirst(request) { const cachedResponse = await caches.match(request) if (cachedResponse) { // Update cache in background fetch(request).then(response => { if (response.ok) { caches.open(DYNAMIC_CACHE_NAME).then(cache => { cache.put(request, response) }) } }).catch(() => { // Ignore network errors when updating cache }) return cachedResponse } try { const networkResponse = await fetch(request) if (networkResponse.ok) { const cache = await caches.open(DYNAMIC_CACHE_NAME) cache.put(request, networkResponse.clone()) } return networkResponse } catch (error) { console.error('[SW] Cache-first failed:', error) throw error } } // Stale-while-revalidate strategy async function staleWhileRevalidate(request) { const cache = await caches.open(STATIC_CACHE_NAME) const cachedResponse = await cache.match(request) const networkResponsePromise = fetch(request).then(response => { if (response.ok) { cache.put(request, response.clone()) } return response }).catch(() => { // Ignore network errors return null }) return cachedResponse || await networkResponsePromise } // Background sync for offline actions self.addEventListener('sync', (event) => { console.log('[SW] Background sync event:', event.tag) if (event.tag === 'game-actions') { event.waitUntil(syncGameActions()) } }) async function syncGameActions() { try { // Get pending actions from IndexedDB const pendingActions = await getPendingActions() for (const action of pendingActions) { try { const response = await fetch(action.url, { method: action.method, headers: action.headers, body: action.body }) if (response.ok) { await removePendingAction(action.id) console.log('[SW] Synced action:', action.id) } } catch (error) { console.error('[SW] Failed to sync action:', action.id, error) } } } catch (error) { console.error('[SW] Background sync failed:', error) } } // Push notifications self.addEventListener('push', (event) => { console.log('[SW] Push event received:', event) if (!event.data) { return } const data = event.data.json() const options = { body: data.body || 'New update available!', icon: '/icons/icon-192x192.png', badge: '/icons/icon-96x96.png', data: data.data || {}, actions: [ { action: 'view', title: 'View', icon: '/icons/icon-96x96.png' }, { action: 'dismiss', title: 'Dismiss' } ] } event.waitUntil( self.registration.showNotification(data.title || 'Know Foolery', options) ) }) // Notification click handler self.addEventListener('notificationclick', (event) => { console.log('[SW] Notification click:', event.action) event.notification.close() if (event.action === 'view' || !event.action) { const urlToOpen = event.notification.data.url || '/' event.waitUntil( clients.matchAll({ type: 'window' }).then(clientsArr => { const hadWindowToFocus = clientsArr.some(windowClient => { if (windowClient.url === urlToOpen) { windowClient.focus() return true } }) if (!hadWindowToFocus) { clients.openWindow(urlToOpen) } }) ) } }) // Helper functions for IndexedDB operations async function getPendingActions() { return new Promise((resolve) => { // Mock implementation - in real app would use IndexedDB resolve([]) }) } async function removePendingAction(actionId) { return new Promise((resolve) => { // Mock implementation - in real app would use IndexedDB resolve() }) } // Message handler for communication with main thread self.addEventListener('message', (event) => { console.log('[SW] Message received:', event.data) if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting() } if (event.data && event.data.type === 'GET_VERSION') { event.ports[0].postMessage({ version: CACHE_NAME }) } }) console.log('[SW] Service Worker loaded successfully')