/** * Flimmer — Service Worker * * Bewusst handgeschrieben und knapp gehalten. Drei Strategien: * * Bilder und statische Dateien → zuerst aus dem Zwischenspeicher. * Poster ändern sich unter einem TMDB-Pfad nie, das spart jede Anfrage. * Seitenaufrufe → zuerst das Netz, sonst der Speicher, * sonst die Offline-Seite. Ein Tracker, der veraltete Stände zeigt, wäre * schlimmer als einer, der ehrlich sagt: gerade keine Verbindung. * Alles Übrige (API, Aktionen) → gar kein Zwischenspeicher. * * Schreibende Anfragen laufen nie über den Service Worker. */ const VERSION = "flimmer-v1"; const SHELL_CACHE = `${VERSION}-shell`; const IMAGE_CACHE = `${VERSION}-images`; const PAGE_CACHE = `${VERSION}-pages`; const OFFLINE_URL = "/offline"; const SHELL = [OFFLINE_URL, "/favicon.svg", "/icons/icon-192.png", "/manifest.webmanifest"]; self.addEventListener("install", (event) => { event.waitUntil( caches .open(SHELL_CACHE) .then((cache) => cache.addAll(SHELL)) .then(() => self.skipWaiting()), ); }); self.addEventListener("activate", (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((key) => !key.startsWith(VERSION)).map((key) => caches.delete(key))), ) .then(() => self.clients.claim()), ); }); /** Zwischenspeicher begrenzen, damit Poster ihn nicht endlos füllen. */ async function trim(cacheName, maxEntries) { const cache = await caches.open(cacheName); const keys = await cache.keys(); if (keys.length <= maxEntries) return; await Promise.all(keys.slice(0, keys.length - maxEntries).map((key) => cache.delete(key))); } async function cacheFirst(request, cacheName, maxEntries) { const cache = await caches.open(cacheName); const hit = await cache.match(request); if (hit) return hit; const response = await fetch(request); if (response.ok) { await cache.put(request, response.clone()); void trim(cacheName, maxEntries); } return response; } async function networkFirst(request) { const cache = await caches.open(PAGE_CACHE); try { const response = await fetch(request); if (response.ok) { await cache.put(request, response.clone()); void trim(PAGE_CACHE, 40); } return response; } catch (error) { const hit = await cache.match(request); if (hit) return hit; const offline = await caches.match(OFFLINE_URL); if (offline) return offline; throw error; } } self.addEventListener("fetch", (event) => { const { request } = event; if (request.method !== "GET") return; const url = new URL(request.url); if (url.origin !== self.location.origin) return; // Bilder aus dem eigenen Cache-Endpunkt und statische Next-Dateien if (url.pathname.startsWith("/api/image")) { event.respondWith(cacheFirst(request, IMAGE_CACHE, 400)); return; } if (url.pathname.startsWith("/_next/static") || url.pathname.startsWith("/icons/")) { event.respondWith(cacheFirst(request, SHELL_CACHE, 200)); return; } // Übrige API-Aufrufe nie zwischenspeichern if (url.pathname.startsWith("/api/")) return; if (request.mode === "navigate") { event.respondWith(networkFirst(request)); } });