{"version":3,"file":"background-sync.cjs","names":[],"sources":["../../src/sw/background-sync.ts"],"sourcesContent":["/**\n * Background-sync helper: queue failed mutating requests (POST/PUT/PATCH/DELETE)\n * while offline and replay them when connectivity returns. A dependency-free\n * take on Workbox's `BackgroundSyncPlugin`, backed by a tiny IndexedDB queue.\n *\n * Import inside your `sw.ts`. Uses the Background Sync API (`registration.sync`)\n * when available, and also replays opportunistically on the next request as a\n * fallback for browsers without it (e.g. Safari).\n *\n * @example\n *   import { installBackgroundSync } from \"tempest-react-sdk/sw\";\n *\n *   installBackgroundSync({ match: (url) => url.pathname.startsWith(\"/api/\") });\n */\n\ninterface SyncEventLike {\n    tag: string;\n    waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgFetchEventLike {\n    request: Request;\n    respondWith(response: Response | Promise<Response>): void;\n    waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface PeriodicSyncEventLike {\n    tag: string;\n    waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgSwGlobal {\n    registration: { sync?: { register(tag: string): Promise<void> } };\n    addEventListener(type: \"sync\", listener: (event: SyncEventLike) => void): void;\n    addEventListener(type: \"fetch\", listener: (event: BgFetchEventLike) => void): void;\n    addEventListener(type: \"periodicsync\", listener: (event: PeriodicSyncEventLike) => void): void;\n}\n\nfunction getSwScope(): BgSwGlobal {\n    return globalThis as unknown as BgSwGlobal;\n}\n\n/** A serialized request stored in the queue. */\ninterface QueuedRequest {\n    id?: number;\n    url: string;\n    method: string;\n    headers: [string, string][];\n    body: ArrayBuffer | null;\n    timestamp: number;\n}\n\n/** Options for {@link installBackgroundSync}. */\nexport interface InstallBackgroundSyncOptions {\n    /**\n     * Which requests to queue on failure. A `RegExp` against the URL or a\n     * predicate. Only non-`GET` requests are ever considered. Default: all\n     * non-`GET` requests.\n     */\n    match?: RegExp | ((url: URL, request: Request) => boolean);\n    /** IndexedDB database name, also used as the sync tag. Default `tempest-bg-sync`. */\n    queueName?: string;\n    /** Drop queued requests older than this (minutes) on replay. Default `1440` (24h). */\n    maxRetentionMinutes?: number;\n    /**\n     * Also drain the queue on the `periodicsync` event carrying this tag,\n     * complementing the one-off `sync` replay so long-pending mutations retry\n     * even without a fresh navigation. Register the periodic sync from the main\n     * thread with {@link registerPeriodicSync}. Default `${queueName}-periodic`.\n     */\n    periodicSyncTag?: string;\n}\n\nconst STORE = \"requests\";\n\nfunction openQueueDb(name: string): Promise<IDBDatabase> {\n    return new Promise((resolve, reject) => {\n        const request = indexedDB.open(name, 1);\n        request.onupgradeneeded = () => {\n            request.result.createObjectStore(STORE, { keyPath: \"id\", autoIncrement: true });\n        };\n        request.onsuccess = () => resolve(request.result);\n        request.onerror = () => reject(request.error);\n    });\n}\n\nfunction txDone(tx: IDBTransaction): Promise<void> {\n    return new Promise((resolve, reject) => {\n        tx.oncomplete = () => resolve();\n        tx.onerror = () => reject(tx.error);\n        tx.onabort = () => reject(tx.error);\n    });\n}\n\nasync function enqueue(name: string, entry: QueuedRequest): Promise<void> {\n    const db = await openQueueDb(name);\n    const tx = db.transaction(STORE, \"readwrite\");\n    tx.objectStore(STORE).add(entry);\n    await txDone(tx);\n    db.close();\n}\n\nasync function readAll(name: string): Promise<QueuedRequest[]> {\n    const db = await openQueueDb(name);\n    const tx = db.transaction(STORE, \"readonly\");\n    const all = await new Promise<QueuedRequest[]>((resolve, reject) => {\n        const req = tx.objectStore(STORE).getAll();\n        req.onsuccess = () => resolve(req.result as QueuedRequest[]);\n        req.onerror = () => reject(req.error);\n    });\n    db.close();\n    return all;\n}\n\nasync function remove(name: string, id: number): Promise<void> {\n    const db = await openQueueDb(name);\n    const tx = db.transaction(STORE, \"readwrite\");\n    tx.objectStore(STORE).delete(id);\n    await txDone(tx);\n    db.close();\n}\n\nasync function serializeRequest(request: Request, timestamp: number): Promise<QueuedRequest> {\n    const buffer = await request.clone().arrayBuffer();\n    return {\n        url: request.url,\n        method: request.method,\n        headers: [...request.headers.entries()],\n        body: buffer.byteLength > 0 ? buffer : null,\n        timestamp,\n    };\n}\n\nfunction deserializeRequest(entry: QueuedRequest): Request {\n    return new Request(entry.url, {\n        method: entry.method,\n        headers: entry.headers,\n        body: entry.body ?? undefined,\n    });\n}\n\n/**\n * Replay every queued request once. Successful (or stale) entries are removed;\n * entries that fail again are kept for the next attempt. Throws if any entry\n * still fails, so a `sync` handler keeps the sync pending.\n */\nasync function replayQueue(name: string, maxRetentionMs: number, now: number): Promise<void> {\n    const entries = await readAll(name);\n    let pending = 0;\n\n    for (const entry of entries) {\n        if (entry.id === undefined) continue;\n        if (now - entry.timestamp > maxRetentionMs) {\n            await remove(name, entry.id);\n            continue;\n        }\n        try {\n            const response = await fetch(deserializeRequest(entry));\n            if (response.ok || (response.status >= 400 && response.status < 500)) {\n                // 2xx = done; 4xx = client error that won't fix itself → drop it.\n                await remove(name, entry.id);\n            } else {\n                pending += 1;\n            }\n        } catch {\n            pending += 1;\n        }\n    }\n\n    if (pending > 0) throw new Error(`background-sync: ${pending} request(s) still pending`);\n}\n\nfunction matches(\n    match: InstallBackgroundSyncOptions[\"match\"],\n    url: URL,\n    request: Request,\n): boolean {\n    if (!match) return true;\n    return typeof match === \"function\" ? match(url, request) : match.test(url.href);\n}\n\n/**\n * Install the background-sync queue: on a failed mutating request, the request\n * is serialized to IndexedDB and a sync is registered; the original fetch still\n * rejects (so your app can show an offline state), and the request is replayed\n * later when the network returns.\n *\n * @tempest-limits empty-catch — `registration.sync.register` is unavailable on\n * every non-Chromium browser and fails on a revoked background-sync permission.\n * The entry is already durable in IndexedDB at that point, and the queue is also\n * drained opportunistically on the next successful request, so losing the OS-level\n * wakeup degrades *when* the replay happens, not *whether* it happens.\n */\nexport function installBackgroundSync(options: InstallBackgroundSyncOptions = {}): void {\n    const sw = getSwScope();\n    const {\n        match,\n        queueName = \"tempest-bg-sync\",\n        maxRetentionMinutes = 1440,\n        periodicSyncTag = `${options.queueName ?? \"tempest-bg-sync\"}-periodic`,\n    } = options;\n    const maxRetentionMs = maxRetentionMinutes * 60 * 1000;\n\n    sw.addEventListener(\"fetch\", (event) => {\n        const request = event.request;\n        if (request.method === \"GET\" || request.method === \"HEAD\") return;\n\n        const url = new URL(request.url);\n        if (!matches(match, url, request)) return;\n\n        event.respondWith(\n            fetch(request.clone()).catch(async (error) => {\n                const entry = await serializeRequest(request, Date.now());\n                await enqueue(queueName, entry);\n                try {\n                    await sw.registration.sync?.register(queueName);\n                } catch {\n                    /* empty */\n                }\n                throw error;\n            }),\n        );\n    });\n\n    sw.addEventListener(\"sync\", (event) => {\n        if (event.tag !== queueName) return;\n        event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()));\n    });\n\n    sw.addEventListener(\"periodicsync\", (event) => {\n        if (event.tag !== periodicSyncTag) return;\n        event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()).catch(() => undefined));\n    });\n\n    // Fallback for browsers without the Background Sync API: whenever a GET\n    // succeeds (a sign we're online), drain the queue opportunistically.\n    sw.addEventListener(\"fetch\", (event) => {\n        if (sw.registration.sync) return; // native sync will handle it\n        if (event.request.method !== \"GET\") return;\n        event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()).catch(() => undefined));\n    });\n}\n"],"mappings":"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"}