/** * Service worker (docs/07 §11). Built to dist/sw.js by vite.config.ts, which fills in VERSION with a build id, * so every deploy with changed files installs a new worker. * - Page loads: network first, never cached (index.html is always fresh online); offline → /offline.html. * - /assets/* (hashed, immutable): cache first, at most MAX_ASSETS entries. * - /api, /ws, /healthz, /internal and other origins: not touched at all. */ const VERSION = '__BUILD__'; const SHELL = `bg-shell-${VERSION}`; const ASSETS = 'bg-assets-v1'; // hashed files never change, so they survive deploys const OFFLINE_URL = '/offline.html'; const MAX_ASSETS = 200; const BYPASS = /^\/(api|ws|healthz|internal)(\/|$)/; self.addEventListener('install', (event) => { event.waitUntil( caches .open(SHELL) .then((cache) => cache.add(new Request(OFFLINE_URL, { cache: 'reload' }))) .then(() => self.skipWaiting()), ); }); self.addEventListener('activate', (event) => { event.waitUntil( (async () => { for (const key of await caches.keys()) { if (key.startsWith('bg-') && key !== SHELL && key !== ASSETS) await caches.delete(key); } await self.clients.claim(); })(), ); }); self.addEventListener('fetch', (event) => { const req = event.request; if (req.method !== 'GET') return; const url = new URL(req.url); if (url.origin !== self.location.origin || BYPASS.test(url.pathname)) return; if (req.mode === 'navigate') event.respondWith(page(req)); else if (url.pathname.startsWith('/assets/')) event.respondWith(asset(event, req)); }); async function page(req) { try { return await fetch(req); } catch { const offline = await caches.match(OFFLINE_URL, { cacheName: SHELL }); return offline ?? new Response('인터넷 연결이 없어요.', { status: 503, headers: { 'Content-Type': 'text/plain; charset=utf-8' } }); } } async function asset(event, req) { const cache = await caches.open(ASSETS); const hit = await cache.match(req); if (hit) return hit; const res = await fetch(req); // A missing file falls back to index.html on the server; never keep that (or partial/error responses). if (res.status === 200 && !(res.headers.get('Content-Type') ?? '').startsWith('text/html')) { event.waitUntil(cache.put(req, res.clone()).then(() => trim(cache))); } return res; } async function trim(cache) { const keys = await cache.keys(); for (const key of keys.slice(0, Math.max(0, keys.length - MAX_ASSETS))) await cache.delete(key); }