// Service Worker for Media Downloader PWA const CACHE_NAME = 'media-downloader-v2'; const STATIC_CACHE_NAME = 'media-downloader-static-v2'; // Assets to cache immediately on install const PRECACHE_ASSETS = [ '/', '/index.html', '/manifest.json', '/apple-touch-icon.png', '/icon-192.png', '/icon-512.png' ]; // Install event - precache static assets self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( caches.open(STATIC_CACHE_NAME) .then((cache) => { console.log('[SW] Precaching static assets'); return cache.addAll(PRECACHE_ASSETS); }) .then(() => { console.log('[SW] Install complete'); return self.skipWaiting(); }) .catch((err) => { console.error('[SW] Precache failed:', err); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker...'); event.waitUntil( caches.keys() .then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME && name !== STATIC_CACHE_NAME) .map((name) => { console.log('[SW] Deleting old cache:', name); return caches.delete(name); }) ); }) .then(() => { console.log('[SW] Activation complete'); return self.clients.claim(); }) ); }); // Fetch event - serve from cache with network fallback self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip API requests - always go to network if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws')) { return; } // Skip external requests if (url.origin !== self.location.origin) { return; } // For hashed JS/CSS assets (Vite bundles like /assets/index-BYx1TwGc.js), // use cache-first since the hash guarantees uniqueness - no stale cache risk. // This prevents full reloads on iOS PWA when the app resumes from background. if (url.pathname.startsWith('/assets/') && url.pathname.match(/\.[a-f0-9]{8,}\.(js|css)$/)) { event.respondWith( caches.match(request) .then((cachedResponse) => { if (cachedResponse) return cachedResponse; return fetch(request).then((networkResponse) => { if (networkResponse.ok) { const responseClone = networkResponse.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone)); } return networkResponse; }); }) ); return; } // Skip non-hashed JS (sw.js, totp.js, etc) - always go to network if (url.pathname.endsWith('.js')) { return; } // For static assets (css, images, fonts), use cache-first strategy if (request.destination === 'style' || request.destination === 'image' || request.destination === 'font' || url.pathname.match(/\.(css|png|jpg|jpeg|gif|svg|woff|woff2|ico)$/)) { event.respondWith( caches.match(request) .then((cachedResponse) => { if (cachedResponse) { // Return cached version, but update cache in background event.waitUntil( fetch(request) .then((networkResponse) => { if (networkResponse.ok) { caches.open(CACHE_NAME) .then((cache) => cache.put(request, networkResponse)); } }) .catch(() => {}) ); return cachedResponse; } // Not in cache, fetch from network and cache return fetch(request) .then((networkResponse) => { if (networkResponse.ok) { const responseClone = networkResponse.clone(); caches.open(CACHE_NAME) .then((cache) => cache.put(request, responseClone)); } return networkResponse; }); }) ); return; } // For HTML/navigation requests, use network-first with cache fallback if (request.mode === 'navigate' || request.destination === 'document') { event.respondWith( fetch(request) .then((networkResponse) => { // Cache the latest version const responseClone = networkResponse.clone(); caches.open(CACHE_NAME) .then((cache) => cache.put(request, responseClone)); return networkResponse; }) .catch(() => { // Network failed, try cache return caches.match(request) .then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } // Fallback to cached index.html for SPA routing return caches.match('/index.html'); }); }) ); return; } }); // Handle messages from the app self.addEventListener('message', (event) => { if (event.data === 'skipWaiting') { self.skipWaiting(); } if (event.data === 'clearCache') { caches.keys().then((names) => { names.forEach((name) => caches.delete(name)); }); } });