const CACHE_NAME = "epicure-v1"; const SHELL_ASSETS = ["/", "/recipes", "/offline"]; self.addEventListener("install", event => { event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(SHELL_ASSETS))); self.skipWaiting(); }); self.addEventListener("activate", event => { event.waitUntil( caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))) ) ); self.clients.claim(); }); self.addEventListener("fetch", event => { const { request } = event; if (request.method !== "GET") return; const url = new URL(request.url); // Skip API routes if (url.pathname.startsWith("/api/")) return; // Cache-first for cook mode pages if (url.pathname.includes("/cook")) { event.respondWith( caches.match(request).then(cached => cached ?? fetch(request).then(res => { const clone = res.clone(); caches.open(CACHE_NAME).then(c => c.put(request, clone)); return res; }) ) ); return; } // Network-first for other pages, but keep a copy of every successful // response — this is what actually makes "recently visited recipes are // available offline" (see /offline) true, and what a "Save for offline" // action (save-offline-button.tsx) piggybacks on by re-fetching a page // it wants pinned. event.respondWith( fetch(request) .then(res => { if (res.ok) { const clone = res.clone(); caches.open(CACHE_NAME).then(c => c.put(request, clone)); } return res; }) .catch(() => caches.match(request).then(cached => cached ?? caches.match("/offline"))) ); }); // --- Background Sync: replay mutations queued while offline --- // Mirrors lib/offline-queue.ts's flushOfflineQueue, duplicated here because // this file is registered as a classic (non-module) script and can't import // app code. Both read/write the same IndexedDB database — keep the store // names and shapes in sync with lib/offline-db.ts if either changes. const DB_NAME = "epicure-offline"; const PENDING_ACTIONS_STORE = "pendingActions"; const SYNC_TAG = "epicure-sync"; function openOfflineDb() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 1); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function getPendingActions(db) { return new Promise((resolve, reject) => { const tx = db.transaction(PENDING_ACTIONS_STORE, "readonly"); const req = tx.objectStore(PENDING_ACTIONS_STORE).getAll(); req.onsuccess = () => resolve(req.result.sort((a, b) => a.createdAt.localeCompare(b.createdAt))); req.onerror = () => reject(req.error); }); } function deletePendingAction(db, id) { return new Promise((resolve, reject) => { const tx = db.transaction(PENDING_ACTIONS_STORE, "readwrite"); tx.objectStore(PENDING_ACTIONS_STORE).delete(id); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } async function replayPendingActions() { const db = await openOfflineDb(); const pending = await getPendingActions(db); let synced = 0; for (const action of pending) { let ok = false; try { const res = await fetch(action.url, { method: action.method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(action.body), }); ok = res.ok; } catch { ok = false; } if (!ok) break; await deletePendingAction(db, action.id); synced += 1; } if (synced > 0) { const clients = await self.clients.matchAll(); for (const client of clients) client.postMessage({ type: "epicure:offline-synced", count: synced }); } } self.addEventListener("sync", event => { if (event.tag === SYNC_TAG) event.waitUntil(replayPendingActions()); });