fix(sw): network-first + 3s timeout statt SWR fuer Daten
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 30s

SWR lieferte bei jedem Cache-Hit sofort die alte Antwort und
aktualisierte das Cache nur fuer den naechsten Request. Folge:
UI zeigte stale Daten, frische Daten erst nach Refresh.

Neu: network-first mit 3 s Timeout-Fallback. Netz gewinnt bei
frischer Antwort; Timeout oder Netzwerk-Fehler fallen auf Cache
zurueck. Pre-Cache-Logik (runSync) bleibt unveraendert, Shell
und Bilder bleiben cache-first.
This commit is contained in:
hsiegeln
2026-04-20 08:29:00 +02:00
parent b5c01b950e
commit 633e497bdc
5 changed files with 47 additions and 22 deletions

View File

@@ -1,4 +1,4 @@
export type CacheStrategy = 'shell' | 'swr' | 'images' | 'network-only';
export type CacheStrategy = 'shell' | 'network-first' | 'images' | 'network-only';
type RequestShape = { url: string; method: string };
@@ -37,6 +37,7 @@ export function resolveStrategy(req: RequestShape): CacheStrategy {
return 'shell';
}
// Everything else: recipe pages, API reads, lists — all SWR.
return 'swr';
// Everything else: recipe pages, API reads, lists — network-first with
// timeout fallback to cache (handled in service-worker.ts).
return 'network-first';
}

View File

@@ -56,11 +56,13 @@ self.addEventListener('fetch', (event) => {
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));
} else if (strategy === 'network-first') {
event.respondWith(networkFirstWithTimeout(req, DATA_CACHE, NETWORK_TIMEOUT_MS));
}
});
const NETWORK_TIMEOUT_MS = 3000;
async function cacheFirst(req: Request, cacheName: string): Promise<Response> {
const cache = await caches.open(cacheName);
const hit = await cache.match(req);
@@ -70,16 +72,36 @@ async function cacheFirst(req: Request, cacheName: string): Promise<Response> {
return fresh;
}
async function staleWhileRevalidate(req: Request, cacheName: string): Promise<Response> {
// Network-first mit Timeout-Fallback: frische Daten gewinnen, wenn das Netz
// innerhalb von NETWORK_TIMEOUT_MS antwortet. Sonst wird der Cache geliefert
// (falls vorhanden), während der Netz-Fetch noch im Hintergrund weiterläuft
// und den Cache für den nächsten Request aktualisiert. Ohne Cache wartet der
// Client trotzdem aufs Netz, weil ein Error-Response hier nichts nützt.
async function networkFirstWithTimeout(
req: Request,
cacheName: string,
timeoutMs: number
): Promise<Response> {
const cache = await caches.open(cacheName);
const hit = await cache.match(req);
const fetchPromise = fetch(req)
const networkPromise: Promise<Response | null> = fetch(req)
.then((res) => {
if (res.ok) cache.put(req, res.clone()).catch(() => {});
return res;
})
.catch(() => hit ?? Response.error());
return hit ?? fetchPromise;
.catch(() => null);
const timeoutPromise = new Promise<'timeout'>((resolve) =>
setTimeout(() => resolve('timeout'), timeoutMs)
);
const winner = await Promise.race([networkPromise, timeoutPromise]);
if (winner instanceof Response) return winner;
// Timeout oder Netzwerk-Fehler: Cache bevorzugen, sonst auf Netz warten.
const hit = await cache.match(req);
if (hit) return hit;
const late = await networkPromise;
return late ?? Response.error();
}
const META_CACHE = 'kochwas-meta';