The service worker was using cache-first for ALL non-API routes, including page routes like /queue, /capture, /browser. This meant the browser kept serving old cached HTML with old inline JS even after deploys, which is why the queue file list never updated (old JS was running, silently crashing on API mismatches). Now only /static/ assets use cache-first (they're versioned via SW cache bumps). All pages and API calls use network-first with cache as offline fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
5.4 KiB
JavaScript
153 lines
5.4 KiB
JavaScript
// NextSnap Service Worker
|
|
// Provides offline-first caching for the app shell
|
|
|
|
const CACHE_VERSION = 'nextsnap-v19';
|
|
const APP_SHELL_CACHE = 'nextsnap-shell-v15';
|
|
const RUNTIME_CACHE = 'nextsnap-runtime-v15';
|
|
|
|
// 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 - 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', { 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))
|
|
);
|
|
})
|
|
);
|
|
}
|
|
});
|