raycer/frontend/public/sw.js

128 lines
3.1 KiB
JavaScript
Raw Normal View History

/* raycer service worker
* - app shell: cache-first
* - /api/*: network-first with no cache fallback (data lives in Dexie)
* - dexie CDN module: cache-first (so the app boots offline)
*/
const VERSION = 'raycer-v1';
const SHELL_CACHE = `${VERSION}-shell`;
const RUNTIME_CACHE = `${VERSION}-runtime`;
const SHELL = [
'/',
'/index.html',
'/manifest.webmanifest',
'/css/styles.css',
'/js/app.js',
'/js/api.js',
'/js/db.js',
'/js/theme.js',
'/js/stats.js',
'/js/calendar.js',
'/icons/favicon.svg',
'/icons/icon-192.png',
'/icons/icon-512.png',
];
self.addEventListener('install', (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(SHELL_CACHE);
await cache.addAll(SHELL);
self.skipWaiting();
})()
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(
keys
.filter((k) => !k.startsWith(VERSION))
.map((k) => caches.delete(k))
);
await self.clients.claim();
})()
);
});
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
// API: network-first, never cache
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(req).catch(
() => new Response(JSON.stringify({ offline: true }), {
status: 503,
headers: { 'content-type': 'application/json' },
})
)
);
return;
}
// Same-origin SPA navigations: serve cached shell, fall back to network
if (req.mode === 'navigate') {
event.respondWith(
(async () => {
try {
const fresh = await fetch(req);
const cache = await caches.open(SHELL_CACHE);
cache.put('/index.html', fresh.clone()).catch(() => {});
return fresh;
} catch {
const cached = await caches.match('/index.html');
if (cached) return cached;
return new Response('offline', { status: 503 });
}
})()
);
return;
}
// Same-origin assets: cache-first
if (url.origin === self.location.origin) {
event.respondWith(
(async () => {
const cached = await caches.match(req);
if (cached) return cached;
try {
const fresh = await fetch(req);
if (fresh.ok) {
const cache = await caches.open(RUNTIME_CACHE);
cache.put(req, fresh.clone()).catch(() => {});
}
return fresh;
} catch {
return new Response('offline', { status: 503 });
}
})()
);
return;
}
// Cross-origin (e.g. Dexie CDN): cache-first
event.respondWith(
(async () => {
const cached = await caches.match(req);
if (cached) return cached;
try {
const fresh = await fetch(req);
if (fresh.ok) {
const cache = await caches.open(RUNTIME_CACHE);
cache.put(req, fresh.clone()).catch(() => {});
}
return fresh;
} catch {
return cached || new Response('offline', { status: 503 });
}
})()
);
});