// EasyStream Service Worker - Enhanced PWA v2.0 const CACHE_VERSION = 'es-v2'; const STATIC_CACHE = `${CACHE_VERSION}-static`; const IMAGE_CACHE = `${CACHE_VERSION}-images`; const FONT_CACHE = `${CACHE_VERSION}-fonts`; const VIDEO_CACHE = `${CACHE_VERSION}-video`; // Assets to precache const PRECACHE = [ '/index.js', '/manifest.json', '/f_scripts/shared/design-system.css', '/f_scripts/shared/accessibility.css', '/f_scripts/shared/responsive.css', '/f_scripts/shared/themes.css' ]; // Cache size limits const CACHE_LIMITS = { images: 50, fonts: 20, video: 5 }; // Install event - precache critical assets self.addEventListener('install', (event) => { console.log('[SW] Installing service worker v2.0...'); event.waitUntil((async () => { try { const cache = await caches.open(STATIC_CACHE); await cache.addAll(PRECACHE); console.log('[SW] Precached static assets'); self.skipWaiting(); } catch (error) { console.error('[SW] Install failed:', error); } })()); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker v2.0...'); event.waitUntil((async () => { const keys = await caches.keys(); await Promise.all( keys .filter(k => !k.startsWith(CACHE_VERSION)) .map(k => { console.log('[SW] Deleting old cache:', k); return caches.delete(k); }) ); self.clients.claim(); console.log('[SW] Activated and claimed clients'); })()); }); // Fetch event - implement caching strategies self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip caching for: // - Uploads // - HLS video segments // - API calls that should always be fresh // - POST/PUT/DELETE requests if ( url.pathname.includes('/upload') || url.pathname.includes('/uploader') || url.pathname.includes('.m3u8') || url.pathname.includes('.ts') || url.pathname.includes('/api/') || request.method !== 'GET' ) { return; // Network only } // NAVIGATION REQUESTS: Network-first with offline fallback if (request.mode === 'navigate') { event.respondWith(networkFirstStrategy(request, STATIC_CACHE)); return; } // IMAGES: Cache-first with network fallback if (request.destination === 'image') { event.respondWith(cacheFirstStrategy(request, IMAGE_CACHE, CACHE_LIMITS.images)); return; } // FONTS: Cache-first (fonts rarely change) if (request.destination === 'font' || url.pathname.match(/\.(woff2?|ttf|eot)$/)) { event.respondWith(cacheFirstStrategy(request, FONT_CACHE, CACHE_LIMITS.fonts)); return; } // CSS/JS: Stale-while-revalidate if (request.destination === 'style' || request.destination === 'script') { event.respondWith(staleWhileRevalidate(request, STATIC_CACHE)); return; } // VIDEO THUMBNAILS: Cache-first if (url.pathname.match(/thumb|thumbnail|preview/i) && url.pathname.match(/\.(jpg|jpeg|png|webp)$/)) { event.respondWith(cacheFirstStrategy(request, IMAGE_CACHE, CACHE_LIMITS.images)); return; } // DEFAULT: Network-first event.respondWith(networkFirstStrategy(request, STATIC_CACHE)); }); // STRATEGY: Network-first with cache fallback async function networkFirstStrategy(request, cacheName) { try { const networkResponse = await fetch(request); if (networkResponse && networkResponse.ok) { const cache = await caches.open(cacheName); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return offline page for navigation requests if (request.mode === 'navigate') { return new Response(getOfflineHTML(), { headers: { 'Content-Type': 'text/html' } }); } return Response.error(); } } // STRATEGY: Cache-first with network fallback async function cacheFirstStrategy(request, cacheName, limit) { const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } try { const networkResponse = await fetch(request); if (networkResponse && networkResponse.ok) { const cache = await caches.open(cacheName); cache.put(request, networkResponse.clone()); // Limit cache size if (limit) { limitCacheSize(cacheName, limit); } } return networkResponse; } catch (error) { return Response.error(); } } // STRATEGY: Stale-while-revalidate async function staleWhileRevalidate(request, cacheName) { const cachedResponse = await caches.match(request); const fetchPromise = fetch(request).then((networkResponse) => { if (networkResponse && networkResponse.ok) { const cache = caches.open(cacheName); cache.then(c => c.put(request, networkResponse.clone())); } return networkResponse; }).catch(() => cachedResponse); return cachedResponse || fetchPromise; } // Limit cache size to prevent unlimited growth async function limitCacheSize(cacheName, maxItems) { const cache = await caches.open(cacheName); const keys = await cache.keys(); if (keys.length > maxItems) { // Delete oldest entries (FIFO) const deleteCount = keys.length - maxItems; for (let i = 0; i < deleteCount; i++) { await cache.delete(keys[i]); } } } // Offline page HTML function getOfflineHTML() { return `
It looks like you've lost your internet connection. Please check your network and try again.
Retry