feat(pwa): Service-Worker-Gerüst mit Shell-Cache + Fetch-Dispatch
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m17s

src/service-worker.ts installiert die App-Shell-Assets (build +
files aus $service-worker) beim install-Event in kochwas-shell-
<version>, räumt alte Shell-Caches beim activate und dispatcht
jeden Fetch via resolveStrategy — shell/images cache-first, swr
stale-while-revalidate, network-only unangetastet. Pre-Cache-
Orchestrator kommt in Task 9.

Client-seitig registriert sw-register.ts den SW und verdrahtet
Messages vom SW in den sync-status-Store.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-18 16:38:09 +02:00
parent 7c8edb9b92
commit 582d902c62
3 changed files with 82 additions and 69 deletions

View File

@@ -2,88 +2,78 @@
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { build, files, version } from '$service-worker';
import { resolveStrategy } from '$lib/sw/cache-strategy';
const sw = self as unknown as ServiceWorkerGlobalScope;
declare const self: ServiceWorkerGlobalScope;
const APP_CACHE = `kochwas-app-${version}`;
const IMAGE_CACHE = `kochwas-images-v1`;
const APP_ASSETS = [...build, ...files];
const SHELL_CACHE = `kochwas-shell-${version}`;
const DATA_CACHE = 'kochwas-data-v1';
const IMAGES_CACHE = 'kochwas-images-v1';
sw.addEventListener('install', (event) => {
event.waitUntil(
caches.open(APP_CACHE).then((cache) => cache.addAll(APP_ASSETS))
);
// Activate new worker without waiting for old clients to close.
void sw.skipWaiting();
});
// App-Shell-Assets (Build-Output + statische Dateien, die SvelteKit kennt)
const SHELL_ASSETS = [...build, ...files];
sw.addEventListener('activate', (event) => {
self.addEventListener('install', (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(
keys
.filter((k) => k.startsWith('kochwas-app-') && k !== APP_CACHE)
.map((k) => caches.delete(k))
);
await sw.clients.claim();
const cache = await caches.open(SHELL_CACHE);
await cache.addAll(SHELL_ASSETS);
await self.skipWaiting();
})()
);
});
sw.addEventListener('fetch', (event) => {
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
// Alte Shell-Caches (vorherige Versionen) räumen
const keys = await caches.keys();
await Promise.all(
keys
.filter((k) => k.startsWith('kochwas-shell-') && k !== SHELL_CACHE)
.map((k) => caches.delete(k))
);
await self.clients.claim();
})()
);
});
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') return;
if (new URL(req.url).origin !== self.location.origin) return; // Cross-Origin unangetastet
const url = new URL(req.url);
if (url.origin !== location.origin) return;
const strategy = resolveStrategy({ url: req.url, method: req.method });
if (strategy === 'network-only') return;
// Images served from /images/* — cache-first with background update
if (url.pathname.startsWith('/images/')) {
event.respondWith(
(async () => {
const cache = await caches.open(IMAGE_CACHE);
const cached = await cache.match(req);
const network = fetch(req)
.then((res) => {
if (res.ok) void cache.put(req, res.clone());
return res;
})
.catch(() => undefined);
return cached ?? (await network) ?? new Response('Offline', { status: 503 });
})()
);
return;
}
// App shell assets (build/* and static files) — cache-first
if (APP_ASSETS.includes(url.pathname)) {
event.respondWith(
(async () => {
const cache = await caches.open(APP_CACHE);
const cached = await cache.match(req);
return cached ?? fetch(req);
})()
);
return;
}
// API and HTML pages — network-first, fall back to cache for HTML
if (req.destination === 'document') {
event.respondWith(
(async () => {
try {
const res = await fetch(req);
const cache = await caches.open(APP_CACHE);
if (res.ok) void cache.put(req, res.clone());
return res;
} catch {
const cached = await caches.match(req);
return cached ?? new Response('Offline', { status: 503 });
}
})()
);
if (strategy === 'shell') {
event.respondWith(cacheFirst(req, SHELL_CACHE));
} else if (strategy === 'images') {
event.respondWith(cacheFirst(req, IMAGES_CACHE));
} else if (strategy === 'swr') {
event.respondWith(staleWhileRevalidate(req, DATA_CACHE));
}
});
async function cacheFirst(req: Request, cacheName: string): Promise<Response> {
const cache = await caches.open(cacheName);
const hit = await cache.match(req);
if (hit) return hit;
const fresh = await fetch(req);
if (fresh.ok) cache.put(req, fresh.clone()).catch(() => {});
return fresh;
}
async function staleWhileRevalidate(req: Request, cacheName: string): Promise<Response> {
const cache = await caches.open(cacheName);
const hit = await cache.match(req);
const fetchPromise = fetch(req)
.then((res) => {
if (res.ok) cache.put(req, res.clone()).catch(() => {});
return res;
})
.catch(() => hit ?? Response.error());
return hit ?? fetchPromise;
}
export {};