/** * Service worker for Watched Folder retry scheduling. * * Reads the earliest pending retry from IndexedDB and sets a setTimeout for it. * When the timer fires it posts PROCESS_DUE_RETRIES to all window clients so * the main thread can atomically claim and process the due entries. * * Limitations: * - Browsers may terminate idle service workers after ~30 s. The main thread * therefore also drains due retries on mount and on visibilitychange as a * fallback — no retries are lost, they may just fire slightly late. * - Multiple clients each post a SCHEDULE_RETRY message; the SW deduplicates * by resetting the timer each time, so only one notification is sent. */ const DB_NAME = "stirling-pdf-retry-schedule"; const STORE_NAME = "retries"; let retryTimer = null; self.addEventListener("install", () => self.skipWaiting()); self.addEventListener("activate", (event) => { event.waitUntil(self.clients.claim().then(scheduleNextTimer)); }); self.addEventListener("message", (event) => { if (event.data?.type === "SCHEDULE_RETRY") { scheduleNextTimer(); } }); function openDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 1); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(new Error("SW: failed to open retry DB")); // Create store if this SW activates before the main thread has opened the DB req.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { const store = db.createObjectStore(STORE_NAME, { keyPath: "id" }); store.createIndex("dueAt", "dueAt", { unique: false }); } }; }); } async function getEarliestDueAt() { try { const db = await openDB(); return new Promise((resolve) => { const tx = db.transaction([STORE_NAME], "readonly"); const req = tx.objectStore(STORE_NAME).index("dueAt").openCursor(); req.onsuccess = () => resolve(req.result ? req.result.value.dueAt : null); req.onerror = () => resolve(null); }); } catch { return null; } } async function notifyClients() { const clients = await self.clients.matchAll({ type: "window", includeUncontrolled: true, }); for (const client of clients) { client.postMessage({ type: "PROCESS_DUE_RETRIES" }); } } async function scheduleNextTimer() { if (retryTimer !== null) { clearTimeout(retryTimer); retryTimer = null; } const earliest = await getEarliestDueAt(); if (earliest === null) return; const delay = Math.max(0, earliest - Date.now()); retryTimer = setTimeout(async () => { retryTimer = null; await notifyClients(); // Re-schedule for any remaining entries that were not yet due await scheduleNextTimer(); }, delay); }