// NextSnap Service Worker // Provides offline-first caching for the app shell const CACHE_VERSION = 'nextsnap-v18'; const APP_SHELL_CACHE = 'nextsnap-shell-v14'; const RUNTIME_CACHE = 'nextsnap-runtime-v14'; // Assets to cache on install const APP_SHELL_ASSETS = [ '/', '/static/css/style.css', '/static/js/app.js', '/static/js/auth.js', '/static/js/camera.js', '/static/js/storage.js', '/static/js/sync.js', '/static/js/filebrowser.js', '/static/lib/dexie.min.js', '/static/manifest.json', '/static/icons/icon-192.png', '/static/icons/icon-512.png' ]; // Install event - precache app shell self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( caches.open(APP_SHELL_CACHE) .then((cache) => { console.log('[SW] Caching app shell'); return cache.addAll(APP_SHELL_ASSETS); }) .then(() => { console.log('[SW] App shell cached successfully'); return self.skipWaiting(); // Activate immediately }) .catch((error) => { console.error('[SW] Failed to cache app shell:', error); }) ); }); // 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.map((cacheName) => { if (cacheName !== APP_SHELL_CACHE && cacheName !== RUNTIME_CACHE) { console.log('[SW] Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { console.log('[SW] Service worker activated'); return self.clients.claim(); // Take control immediately }) ); }); // Fetch event - cache-first for static assets, network-first for API self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // API requests - network-first with offline fallback if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(request) .then((response) => { // Clone response for cache const responseClone = response.clone(); // Only cache successful responses (not errors or auth failures) if (response.status === 200) { caches.open(RUNTIME_CACHE).then((cache) => { // Don't cache file uploads or large responses if (!url.pathname.includes('/upload') && !url.pathname.includes('/thumbnail')) { cache.put(request, responseClone); } }); } return response; }) .catch(() => { // Network failed - try cache, then offline response return caches.match(request) .then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } // Return offline fallback for API return new Response( JSON.stringify({ error: 'offline', message: 'You are offline. This feature requires connectivity.' }), { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'application/json' } } ); }); }) ); return; } // Static assets - cache-first strategy event.respondWith( caches.match(request) .then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } // Not in cache - fetch from network and cache it return fetch(request) .then((response) => { // Don't cache non-successful responses if (!response || response.status !== 200 || response.type === 'error') { return response; } // Clone response for cache const responseClone = response.clone(); caches.open(RUNTIME_CACHE).then((cache) => { cache.put(request, responseClone); }); return response; }) .catch(() => { // Network failed and not in cache // For HTML pages, could return offline page here return new Response('Offline', { status: 503 }); }); }) ); }); // Listen for messages from clients self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CLEAR_CACHE') { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => caches.delete(cacheName)) ); }) ); } });