///
// Lives in src/, not static/, so SvelteKit builds it and injects the manifest
// below. That matters: `build` and `files` are generated from the actual output,
// so the precache list can't drift, and `version` changes every deploy, so cache
// names rotate automatically instead of needing a hand-edited version string.
import { build, files, version } from '$service-worker';
const CACHE = `atlas-of-skills-${version}`;
// Content-hashed build output + everything in static/. Hashed assets are
// immutable, so once cached they never need revalidating.
const PRECACHE = [...build, ...files];
self.addEventListener('install', (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE);
// Individually, not cache.addAll(): addAll is atomic, so a single 404
// failed the whole install and left the site with no offline support at
// all — silently.
const results = await Promise.allSettled(
PRECACHE.map((asset) => cache.add(new Request(asset, { cache: 'reload' })))
);
const failed = results.filter((r) => r.status === 'rejected').length;
if (failed) {
console.warn(`[sw] ${failed}/${PRECACHE.length} assets failed to precache`);
}
})()
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
// Drop every previous version's cache. Because the name embeds `version`,
// this now actually prunes on each deploy instead of accumulating stale
// hashed chunks forever.
const keys = await caches.keys();
await Promise.all(keys.filter((key) => key !== CACHE).map((key) => caches.delete(key)));
await self.clients.claim();
})()
);
});
/** Assets emitted by the build are content-hashed, so cache-first is safe. */
const PRECACHED = new Set(PRECACHE);
self.addEventListener('fetch', (event) => {
const { request } = event;
// Never touch non-GET (cache.put throws on them) or cross-origin.
if (request.method !== 'GET') return;
const url = new URL(request.url);
if (url.origin !== self.location.origin) return;
event.respondWith(handle(event, url));
});
async function handle(event, url) {
const { request } = event;
const cache = await caches.open(CACHE);
// Content-hashed build output: cache-first, it can never go stale.
if (PRECACHED.has(url.pathname) && build.includes(url.pathname)) {
const hit = await cache.match(url.pathname);
if (hit) return hit;
}
// Everything else — navigations and unhashed static files (icons, headshot,
// the résumé PDF) — is network-first. The old worker served these cache-first
// with no expiry, which meant replacing the résumé at the same URL never
// reached anyone who had already visited.
try {
const response = await fetch(request);
// Only cache real successes. A 404/500 body used to get written and then
// served as the offline fallback. Opaque/redirected responses throw on
// cache.put, so they're excluded too.
if (response.status === 200 && response.type === 'basic' && !response.redirected) {
// waitUntil so the write survives worker termination.
event.waitUntil(cache.put(request, response.clone()));
}
return response;
} catch {
const hit = await cache.match(request);
if (hit) return hit;
// Navigations fall back to the cached shell.
if (request.mode === 'navigate') {
const shell = await cache.match('/');
if (shell) return shell;
}
// Every other failure must still resolve to a Response. Returning
// undefined here (the previous behaviour for non-image assets) makes
// respondWith throw a TypeError and turns a soft failure into a hard one.
if (request.destination === 'image') {
return new Response(
'',
{ status: 503, headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'no-store' } }
);
}
return new Response('Offline', {
status: 503,
headers: { 'Content-Type': 'text/plain', 'Cache-Control': 'no-store' }
});
}
}
// The page posts this after the user accepts an update prompt. Deliberately no
// skipWaiting() in install: taking over a page whose JS came from the previous
// deploy is how you get version skew.
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') self.skipWaiting();
});