// NextSnap Service Worker // Provides offline-first caching for the app shell const CACHE_VERSION = 'nextsnap-v21'; const APP_SHELL_CACHE = 'nextsnap-shell-v17'; const RUNTIME_CACHE = 'nextsnap-runtime-v17'; // Offline fallback page (served when network fails and no cached version exists) const OFFLINE_PAGE = ` Offline - NextSnap

You're Offline

Check your internet connection and try again.

`; // Assets to cache on install const APP_SHELL_ASSETS = [ '/', '/capture', '/queue', '/browser', '/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 - route requests to appropriate caching strategy self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests (uploads, form posts, etc.) if (request.method !== 'GET') { return; } // Static assets (/static/) - cache-first (versioned via SW cache bump) if (url.pathname.startsWith('/static/')) { event.respondWith( caches.match(request) .then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } return fetch(request) .then((response) => { if (!response || response.status !== 200) { return response; } const responseClone = response.clone(); caches.open(APP_SHELL_CACHE).then((cache) => { cache.put(request, responseClone); }); return response; }) .catch(() => new Response('Offline', { status: 503 })); }) ); return; } // Everything else (pages, API) - network-first with cache fallback event.respondWith( fetch(request) .then((response) => { // Cache successful GET responses for offline fallback if (response.status === 200) { const responseClone = response.clone(); caches.open(RUNTIME_CACHE).then((cache) => { // Don't cache file uploads or large binary responses if (!url.pathname.includes('/upload') && !url.pathname.includes('/thumbnail')) { cache.put(request, responseClone); } }); } return response; }) .catch(() => { // Network failed - try cache return caches.match(request) .then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } // No cache - return offline response if (url.pathname.startsWith('/api/')) { return new Response( JSON.stringify({ error: 'offline', message: 'You are offline.' }), { status: 503, headers: { 'Content-Type': 'application/json' } } ); } return new Response(OFFLINE_PAGE, { status: 200, headers: { 'Content-Type': 'text/html' } }); }); }) ); }); // 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)) ); }) ); } });