diff --git a/.gitignore b/.gitignore index 978ea79..88fae03 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ dist/ build/ *.tsbuildinfo +# Generated at build time by next.config.ts — do not commit +public/sw.js + # Auto-generated by Next.js — never edit, never commit next-env.d.ts diff --git a/public/sw.js b/public/sw.js deleted file mode 100644 index 964bbfc..0000000 --- a/public/sw.js +++ /dev/null @@ -1,162 +0,0 @@ -// famapp service worker — v1778106925247 -// Generated at build time. Do not edit directly. -const CACHE_VERSION = "1778106925247"; -const SHELL = "famapp-shell-" + CACHE_VERSION; -const API = "famapp-api-" + CACHE_VERSION; -const OFFLINE = "/offline.html"; - -// --- install: precache the offline fallback --- - -self.addEventListener("install", (e) => { - e.waitUntil(caches.open(SHELL).then((c) => c.add(OFFLINE))); - self.skipWaiting(); -}); - -// --- activate: evict old-version caches, claim clients --- - -self.addEventListener("activate", (e) => { - e.waitUntil( - caches - .keys() - .then((keys) => - Promise.all( - keys - .filter( - (k) => - k.startsWith("famapp-") && !k.endsWith("-" + CACHE_VERSION) - ) - .map((k) => caches.delete(k)) - ) - ) - .then(() => self.clients.claim()) - ); -}); - -// --- fetch: strategy dispatch --- - -self.addEventListener("fetch", (e) => { - const { request } = e; - const url = new URL(request.url); - - // Mutations — network-only; notify clients if the network is unreachable. - if (request.method !== "GET") { - e.respondWith( - fetch(request).catch(() => { - self.clients - .matchAll({ includeUncontrolled: true }) - .then((cs) => - cs.forEach((c) => c.postMessage({ type: "OFFLINE_MUTATION" })) - ); - return new Response(JSON.stringify({ error: "offline" }), { - status: 503, - headers: { "Content-Type": "application/json" }, - }); - }) - ); - return; - } - - // Auth endpoints — always network-only. - if (url.pathname.startsWith("/api/auth/")) return; - - // Next.js immutable static chunks — stale-while-revalidate. - if (url.pathname.startsWith("/_next/static/")) { - e.respondWith(staleWhileRevalidate(request, SHELL)); - return; - } - - // API GETs — network-first with 2-second timeout, fall back to cache. - if (url.pathname.startsWith("/api/")) { - e.respondWith(networkFirstWithTimeout(request, API, 2000)); - return; - } - - // Navigation — stale-while-revalidate; fall back to offline page. - if (request.mode === "navigate") { - e.respondWith(navigateWithFallback(request)); - return; - } -}); - -// --- push notifications --- - -self.addEventListener("push", (e) => { - if (!e.data) return; - const data = e.data.json(); - e.waitUntil( - self.registration.showNotification(data.title || "famapp", { - body: data.body || "", - data: { url: data.url || "/" }, - icon: "/icon-192.png", - badge: "/icon-192.png", - }) - ); -}); - -self.addEventListener("notificationclick", (e) => { - e.notification.close(); - const url = e.notification.data?.url || "/"; - e.waitUntil( - self.clients - .matchAll({ type: "window", includeUncontrolled: true }) - .then((cs) => { - const match = cs.find((c) => c.url.includes(self.location.origin)); - if (match) { - match.focus(); - return match.navigate(url); - } - return self.clients.openWindow(url); - }) - ); -}); - -// --- strategy helpers --- - -async function staleWhileRevalidate(request, cacheName) { - const cache = await caches.open(cacheName); - const cached = await cache.match(request); - const update = fetch(request) - .then((res) => { - if (res.ok) cache.put(request, res.clone()); - return res; - }) - .catch(() => null); - // Serve cached immediately; let update happen in the background. - return cached ?? (await update); -} - -async function networkFirstWithTimeout(request, cacheName, ms) { - const cache = await caches.open(cacheName); - const ac = new AbortController(); - const timer = setTimeout(() => ac.abort(), ms); - try { - const res = await fetch(request, { signal: ac.signal }); - clearTimeout(timer); - if (res.ok) cache.put(request, res.clone()); - return res; - } catch { - clearTimeout(timer); - return (await cache.match(request)) ?? new Response(null, { status: 503 }); - } -} - -async function navigateWithFallback(request) { - const cache = await caches.open(SHELL); - const cached = await cache.match(request); - const networkFetch = fetch(request) - .then((res) => { - if (res.ok) cache.put(request, res.clone()); - return res; - }) - .catch(() => null); - if (cached) { - // Return cached immediately; revalidate in the background. - networkFetch; - return cached; - } - const net = await networkFetch; - if (net) return net; - return ( - (await cache.match(OFFLINE)) ?? new Response("Offline", { status: 503 }) - ); -}