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
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:
21
src/lib/client/sw-register.ts
Normal file
21
src/lib/client/sw-register.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Registriert den Service-Worker und verdrahtet ihn mit dem
|
||||
// Sync-Status-Store. Im Dev-Modus läuft Kochwas über HTTP; die
|
||||
// SW-API ist da nur auf localhost verfügbar. SvelteKit liefert den
|
||||
// SW unter /service-worker.js im Production-Build.
|
||||
import { syncStatus, type SWMessage } from '$lib/client/sync-status.svelte';
|
||||
|
||||
export async function registerServiceWorker(): Promise<void> {
|
||||
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return;
|
||||
try {
|
||||
await navigator.serviceWorker.register('/service-worker.js', { type: 'module' });
|
||||
} catch (e) {
|
||||
console.warn('SW-Registrierung fehlgeschlagen', e);
|
||||
return;
|
||||
}
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
const data = event.data as SWMessage | undefined;
|
||||
if (data && typeof data === 'object' && 'type' in data) {
|
||||
syncStatus.handle(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
import Toast from '$lib/components/Toast.svelte';
|
||||
import SyncIndicator from '$lib/components/SyncIndicator.svelte';
|
||||
import { network } from '$lib/client/network.svelte';
|
||||
import { registerServiceWorker } from '$lib/client/sw-register';
|
||||
import type { SearchHit } from '$lib/server/recipes/search-local';
|
||||
import type { WebHit } from '$lib/server/search/searxng';
|
||||
|
||||
@@ -206,6 +207,7 @@
|
||||
void searchFilterStore.load();
|
||||
void pwaStore.init();
|
||||
network.init();
|
||||
void registerServiceWorker();
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('keydown', handleKey);
|
||||
return () => {
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
Reference in New Issue
Block a user