feat(ui): installable offline PWA — service worker, manifest, icons (F5)
Tests · UI / test (push) Failing after 7m31s
Tests · UI / test (push) Failing after 7m31s
Native SvelteKit service worker (src/service-worker.ts): a version-keyed cache precaches the app shell + build artefacts (incl. core.wasm) + static files; activate purges old caches; the gateway is never intercepted; navigations fall back to the cached shell offline. Adds static/manifest.webmanifest, a generated placeholder icon set (scripts/gen-pwa-icons.mjs — dependency-free pure-Node PNG encoder), and manifest / theme-color / apple-touch tags in app.html. Gated by Playwright against a production preview (playwright.pwa.config.ts + tests/pwa/pwa.spec.ts via `pnpm test:pwa`, wired into ui-test): manifest + installable icons, SW registration + a single version-keyed cache, and offline shell load. Lighthouse is not used — its PWA category was removed in v12. Docs: ui/docs/pwa-strategy.md (+ index); F5 marked done. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
// Native SvelteKit service worker (no Workbox). It precaches the app
|
||||
// shell, the build artefacts (JS/CSS + core.wasm), and the static files
|
||||
// under a version-keyed cache, so the installed PWA loads offline and a
|
||||
// new deploy (new `version`) drops the stale cache instead of serving
|
||||
// old code. The gateway is never intercepted — it is always live network.
|
||||
//
|
||||
// SvelteKit registers this worker automatically in the production build.
|
||||
|
||||
import { build, files, version } from "$service-worker";
|
||||
|
||||
const sw = self as unknown as ServiceWorkerGlobalScope;
|
||||
|
||||
const CACHE = `galaxy-cache-${version}`;
|
||||
// "/" is the SPA shell (adapter-static fallback); precaching it makes the
|
||||
// start_url load offline.
|
||||
const PRECACHE = ["/", ...build, ...files];
|
||||
|
||||
sw.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(CACHE)
|
||||
.then((cache) => cache.addAll(PRECACHE))
|
||||
.then(() => sw.skipWaiting()),
|
||||
);
|
||||
});
|
||||
|
||||
sw.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
for (const key of await caches.keys()) {
|
||||
if (key !== CACHE) await caches.delete(key);
|
||||
}
|
||||
await sw.clients.claim();
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
sw.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
if (request.method !== "GET") return;
|
||||
const url = new URL(request.url);
|
||||
// Cross-origin (the gateway) is always live network — never cached.
|
||||
if (url.origin !== sw.location.origin) return;
|
||||
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
const cache = await caches.open(CACHE);
|
||||
// Version-keyed build/files: cache-first (content-hashed/immutable).
|
||||
if (PRECACHE.includes(url.pathname)) {
|
||||
const hit = await cache.match(url.pathname);
|
||||
if (hit) return hit;
|
||||
}
|
||||
// Everything else: network-first, falling back to the cache, and
|
||||
// for a navigation to the cached app shell when fully offline.
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok && response.type === "basic") {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch (err) {
|
||||
const cached = await cache.match(request);
|
||||
if (cached) return cached;
|
||||
if (request.mode === "navigate") {
|
||||
const shell = await cache.match("/");
|
||||
if (shell) return shell;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
})(),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user