Lumen/share/sw.js
Lumen Stage1 e83946c207
Some checks failed
ci / check (push) Has been cancelled
Checkpoint: current working state
2026-09-29 21:33:33 -05:00

140 lines
4.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Lumen — minimal hand-written service worker (Stage 3)
* Scope: application shell only. No IDB, no dataset, no sync.
* Trace: ARCHITECTURE-DESIGN.md §8, IMPLEMENTATION-CONTRACT.md §16–§18
*/
/* eslint-disable no-restricted-globals -- SW global scope is intentional */
/** Cache version — replaced at build time via Vite define/plugin. Deterministic per shell build. */
const CACHE_NAME = "lumen-shell-0.1.0-fcc18ddc";
/**
* Precache list — replaced at build time with versioned shell list
* (index.html, hashed JS/CSS, manifest, icons, fallback.html).
* Placeholder is replaced by Vite plugin after bundle.
*/
const PRECACHE_URLS = [
"/",
"/index.html",
"/fallback.html",
"/manifest.webmanifest",
"/sol_lunar_icon.svg",
"/icon.png",
"/icon-192.png",
"/icon-512.png",
"/assets/index-CaoEjGwW.js",
"/assets/index-CaoEjGwW.js.map",
"/assets/style-D9rb7TfV.css"
];
self.addEventListener("install", (event) => {
// Safe population: atomically cache all shell URLs; if any fails, install fails
// and the new cache is not treated as valid (existing shell remains usable).
event.waitUntil(
caches
.open(CACHE_NAME)
.then((cache) => cache.addAll(PRECACHE_URLS))
// Do NOT call skipWaiting here — wait for explicit message or next full start
// per ARCHITECTURE-DESIGN.md:255 (no mid-session clients.claim).
.catch((err) => {
// Ensure failed install does not leave partial cache as valid:
// delete the newly created cache on failure.
caches.delete(CACHE_NAME).finally(() => {
// re-throw to signal install failure to the browser
throw err;
});
}),
);
});
self.addEventListener("activate", (event) => {
// Safe replacement: delete old shell caches only after new shell is installed.
// Old shell remains usable until this runs (next full start).
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(
keys
.filter((k) => k.startsWith("lumen-shell-") && k !== CACHE_NAME)
.map((k) => caches.delete(k)),
),
),
);
});
self.addEventListener("fetch", (event) => {
const req = event.request;
const url = new URL(req.url);
// Only handle same-origin GET
if (req.method !== "GET" || url.origin !== self.location.origin) return;
// Package endpoints are NOT intercepted — app-layer sync controls staging (C-21)
if (url.pathname === "/latest.json" || url.pathname.startsWith("/editions/")) {
return; // let network handle it
}
// Navigation requests — cache-first against shell cache, fallback to network, then fallback.html
const isNavigation =
req.mode === "navigate" ||
req.destination === "document" ||
req.headers.get("accept")?.includes("text/html");
if (isNavigation) {
event.respondWith(
caches.match(req).then((cached) => {
if (cached) return cached;
return fetch(req)
.then((res) => {
// Optional: opportunistically re-fill cache with fresh index.html on navigation fetch
// but do not block response on cache put.
if (res.ok) {
const clone = res.clone();
caches.open(CACHE_NAME).then((c) => c.put(req, clone));
}
return res;
})
.catch(() =>
caches
.match("/fallback.html")
.then((fb) => fb ?? new Response("Offline", { status: 503 })),
);
}),
);
return;
}
// Hashed shell assets (/assets/*) — cache-first (immutable, 1 year per ARCH §24)
if (url.pathname.startsWith("/assets/")) {
event.respondWith(
caches.match(req).then((cached) => {
if (cached) return cached;
return fetch(req).then((res) => {
if (res.ok) {
const clone = res.clone();
caches.open(CACHE_NAME).then((c) => c.put(req, clone));
}
return res;
});
}),
);
return;
}
// Other same-origin GETs (manifest, icons) — cache-first with network fallback
event.respondWith(
caches.match(req).then((cached) => {
if (cached) return cached;
return fetch(req).catch(() => caches.match(req));
}),
);
});
self.addEventListener("message", (event) => {
const data = event.data;
if (typeof data === "object" && data !== null && "type" in data && data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});