Vanilla HTML/JS/CSS PWA with Dexie offline-first sync, Hono+SQLite backend, served via nginx reverse-proxy. Two seed goals (no-sugar, no-social-media) for users ray and cer. Local dev runs at https://raycer.test via the shared Traefik proxy. Production deploys to https://raycer.altweb.me on cool2026/personal via docker-compose.coolify.yaml — see deploy/COOLIFY.md.
127 lines
3.1 KiB
JavaScript
127 lines
3.1 KiB
JavaScript
/* 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 });
|
|
}
|
|
})()
|
|
);
|
|
});
|