tempest-react-sdk 0.76.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/create-tempest-app.e2e.test.mjs +112 -0
- package/bin/create-tempest-app.mjs +69 -28
- package/bin/lib/component/generate.mjs +191 -0
- package/bin/lib/component/generate.test.mjs +336 -0
- package/bin/lib/openapi/escape.mjs +210 -0
- package/bin/lib/openapi/generate.hostile.test.mjs +592 -0
- package/bin/lib/openapi/generate.mjs +93 -17
- package/bin/lib/openapi/schema-to-zod.mjs +41 -4
- package/bin/tempest.mjs +184 -3
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +127 -55
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/refresh-queue.cjs +1 -1
- package/dist/auth/refresh-queue.cjs.map +1 -1
- package/dist/auth/refresh-queue.js +13 -6
- package/dist/auth/refresh-queue.js.map +1 -1
- package/dist/br/pix.cjs +1 -1
- package/dist/br/pix.cjs.map +1 -1
- package/dist/br/pix.js +72 -62
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +12 -4
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +46 -38
- package/dist/components/Chat/ChatAttachments.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs +7 -5
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js +151 -87
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Toast/Toast.module.cjs +1 -1
- package/dist/components/Toast/Toast.module.cjs.map +1 -1
- package/dist/components/Toast/Toast.module.js +7 -3
- package/dist/components/Toast/Toast.module.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +77 -51
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +32 -21
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +81 -73
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +39 -35
- package/dist/http/errors.js.map +1 -1
- package/dist/oauth/callback.cjs +2 -0
- package/dist/oauth/callback.cjs.map +1 -0
- package/dist/oauth/callback.js +42 -0
- package/dist/oauth/callback.js.map +1 -0
- package/dist/oauth/use-oauth-callback.cjs +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/oauth/use-oauth-callback.js +13 -20
- package/dist/oauth/use-oauth-callback.js.map +1 -1
- package/dist/push/use-push-subscription.cjs +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/push/use-push-subscription.js +42 -42
- package/dist/push/use-push-subscription.js.map +1 -1
- package/dist/push/web-push-client.cjs +1 -1
- package/dist/push/web-push-client.cjs.map +1 -1
- package/dist/push/web-push-client.js +23 -3
- package/dist/push/web-push-client.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +68 -23
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/styles/component/Toast.css +6 -1
- package/dist/styles/feedback.css +6 -1
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +271 -60
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache-inspect.cjs.map +1 -1
- package/dist/sw/cache-inspect.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +91 -64
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/credential-headers.cjs +2 -0
- package/dist/sw/credential-headers.cjs.map +1 -0
- package/dist/sw/credential-headers.js +17 -0
- package/dist/sw/credential-headers.js.map +1 -0
- package/dist/sw/session-data.cjs +2 -0
- package/dist/sw/session-data.cjs.map +1 -0
- package/dist/sw/session-data.js +17 -0
- package/dist/sw/session-data.js.map +1 -0
- package/dist/sw.cjs +1 -1
- package/dist/sw.d.ts +271 -23
- package/dist/sw.js +7 -5
- package/dist/telemetry/app-error-owner.cjs +2 -0
- package/dist/telemetry/app-error-owner.cjs.map +1 -0
- package/dist/telemetry/app-error-owner.js +18 -0
- package/dist/telemetry/app-error-owner.js.map +1 -0
- package/dist/telemetry/app-error-queue.cjs +1 -1
- package/dist/telemetry/app-error-queue.cjs.map +1 -1
- package/dist/telemetry/app-error-queue.js +19 -14
- package/dist/telemetry/app-error-queue.js.map +1 -1
- package/dist/telemetry/app-error-reporter.cjs +1 -1
- package/dist/telemetry/app-error-reporter.cjs.map +1 -1
- package/dist/telemetry/app-error-reporter.js +75 -34
- package/dist/telemetry/app-error-reporter.js.map +1 -1
- package/dist/telemetry/app-error-transport.cjs +1 -1
- package/dist/telemetry/app-error-transport.cjs.map +1 -1
- package/dist/telemetry/app-error-transport.js +16 -3
- package/dist/telemetry/app-error-transport.js.map +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +979 -62
- package/dist/tempest-react-sdk.js +160 -156
- package/dist/utils/csv.cjs +3 -3
- package/dist/utils/csv.cjs.map +1 -1
- package/dist/utils/csv.js +16 -13
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/safe-url.cjs +2 -0
- package/dist/utils/safe-url.cjs.map +1 -0
- package/dist/utils/safe-url.js +39 -0
- package/dist/utils/safe-url.js.map +1 -0
- package/dist/vite/tempest-csp.cjs +2 -2
- package/dist/vite/tempest-csp.cjs.map +1 -1
- package/dist/vite/tempest-csp.js +118 -63
- package/dist/vite/tempest-csp.js.map +1 -1
- package/dist/vite.d.ts +21 -5
- package/dist/webrtc/peer-link.cjs +1 -1
- package/dist/webrtc/peer-link.cjs.map +1 -1
- package/dist/webrtc/peer-link.js +2 -10
- package/dist/webrtc/peer-link.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +110 -72
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +22 -0
- package/template-pwa/src/sw.ts +27 -14
- package/dist/components/Markdown/markdown-url.cjs +0 -2
- package/dist/components/Markdown/markdown-url.cjs.map +0 -1
- package/dist/components/Markdown/markdown-url.js +0 -28
- package/dist/components/Markdown/markdown-url.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"background-sync.js","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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"}
|
|
1
|
+
{"version":3,"file":"background-sync.js","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 * Credential headers (`Authorization` and the rest of\n * {@link DEFAULT_CREDENTIAL_HEADERS}) are never written to IndexedDB: the queue\n * outlives the session, and a stored bearer stayed readable after logout and was\n * replayed with the old identity (reproduced in Chromium 153, 11/10/2026). On\n * replay the worker asks the open pages for the *current* credentials — see\n * {@link provideBackgroundSyncCredentials} — or a custom `beforeReplay` hook.\n * Credentials carried in the URL or the body are out of reach and are stored\n * as sent.\n *\n * @example\n * import { installBackgroundSync } from \"tempest-react-sdk/sw\";\n *\n * installBackgroundSync({ match: (url) => url.pathname.startsWith(\"/api/\") });\n */\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { DEFAULT_CREDENTIAL_HEADERS, credentialHeaderSet } from \"./credential-headers\";\n\ninterface SyncEventLike {\n tag: string;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgFetchEventLike {\n request: Request;\n /** Id of the window client that issued the request, when there is one. */\n clientId?: string;\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\n/** Minimal shape of a window client the worker can message. */\ninterface WindowClientLike {\n postMessage(message: unknown, transfer: Transferable[]): void;\n}\n\ninterface BgSwGlobal {\n registration: { sync?: { register(tag: string): Promise<void> } };\n clients?: {\n matchAll(options: { type: \"window\" }): Promise<readonly WindowClientLike[]>;\n get(id: string): Promise<WindowClientLike | undefined>;\n };\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/**\n * How an entry is tied to whoever queued it:\n * - `\"bound\"`: the issuing page reported a principal (or `null` for nobody);\n * the entry only goes out while the same one is signed in.\n * - `\"unbound\"`: the issuing page answered without `getPrincipal`.\n * - `\"unknown\"`: nobody could say — no issuing tab, no answer, an error, or an\n * entry written before owners existed. Fails closed.\n *\n * While the worker is still asking, the stored state is `\"pending\"`, which a\n * replay never sees: it waits, then reads a stamp that never came as `\"unknown\"`.\n */\nexport type BackgroundSyncOwnerState = \"bound\" | \"unbound\" | \"unknown\";\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 /** The original `credentials` mode, so cookie-based APIs replay with cookies. */\n credentials?: RequestCredentials;\n /** Lower-cased names of the credential headers removed before persisting. */\n strippedHeaders?: string[];\n /** Absent on entries written before 0.77 — read as `\"unknown\"`. */\n ownerState?: BackgroundSyncOwnerState | \"pending\";\n /** Fingerprint of the principal under `\"bound\"`; `null` for \"nobody signed in\". */\n owner?: string | null;\n}\n\n/** Default IndexedDB database name (and sync tag) of the background-sync queue. */\nconst DEFAULT_QUEUE_NAME = \"tempest-bg-sync\";\n\n/**\n * Schema version of the queue database. Version 2 adds the `meta` store (the\n * clear epoch) and migrates version-1 entries, scrubbing their plaintext\n * credential headers on first open.\n */\nconst DB_VERSION = 2;\nconst STORE = \"requests\";\nconst META = \"meta\";\nconst EPOCH_KEY = \"epoch\";\n\n/**\n * `type` of the message the worker posts to window clients to ask about a queued\n * request. Answered by {@link provideBackgroundSyncCredentials}.\n */\nexport const BACKGROUND_SYNC_CREDENTIALS_MESSAGE = \"tempest:bg-sync-credentials\";\n\n/** How long the worker waits for the window clients to answer, in milliseconds. */\nconst CREDENTIALS_TIMEOUT_MS = 3000;\n\n/**\n * How long a replay waits on an entry whose owner is still being stamped. Past\n * it (the worker died mid-question), the owner is read as `\"unknown\"`.\n */\nconst OWNER_STAMP_GRACE_MS = CREDENTIALS_TIMEOUT_MS * 2;\n\n/** What a queued request looked like when it was stored, passed to `beforeReplay`. */\nexport interface BackgroundSyncReplayContext {\n /** Epoch milliseconds of the original failed attempt. */\n queuedAt: number;\n /** Lower-cased credential header names stripped before the entry was stored. */\n strippedHeaders: readonly string[];\n /** How the entry is tied to its owner. */\n ownerState: BackgroundSyncOwnerState;\n /** Owner fingerprint under `\"bound\"` (`null` for nobody); `undefined` otherwise. */\n owner: string | null | undefined;\n}\n\n/** The request a window client is asked about, as sent by the worker. */\nexport interface BackgroundSyncCredentialsRequest {\n /** Absolute URL of the queued request. */\n url: string;\n /** HTTP method of the queued request. */\n method: string;\n /**\n * `\"enqueue\"` when the worker only wants the owner of a request that just\n * failed; `\"replay\"` when it is about to send it and needs the credentials.\n */\n phase: \"enqueue\" | \"replay\";\n}\n\n/**\n * Resolve the credentials for a queued request: a header record to apply, or\n * `null` when there is no session (the entry is then dropped).\n */\nexport type BackgroundSyncCredentialsProvider = (\n request: BackgroundSyncCredentialsRequest,\n) => Record<string, string> | null | Promise<Record<string, string> | null>;\n\n/** Options for {@link provideBackgroundSyncCredentials}. */\nexport interface ProvideBackgroundSyncCredentialsOptions {\n /**\n * Who is signed in right now — a stable, non-secret id such as the user id;\n * `null`/`undefined` when nobody is. Read when a request is queued and again\n * when it is replayed: an entry is only sent while the same principal is\n * signed in, and dropped otherwise, so a mutation queued by one account is\n * never sent with another account's credentials even when the app forgets\n * `clearSessionData()` on logout. Only a SHA-256 fingerprint reaches the\n * queue, never the value itself. A throw is answered as an explicit error,\n * which leaves the owner unknown (fail closed).\n */\n getPrincipal?: () => string | null | undefined | Promise<string | null | undefined>;\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 * Header names stripped before a request is persisted — and stripped again on\n * replay, so entries written by an older version never go out with the\n * credential they stored. Default {@link DEFAULT_CREDENTIAL_HEADERS}; extend it\n * for a custom credential header (`[...DEFAULT_CREDENTIAL_HEADERS,\n * \"x-tenant-key\"]`). Passing `[]` restores the pre-0.77 behavior of\n * persisting and replaying every header, credentials included.\n */\n credentialHeaders?: readonly string[];\n /**\n * Prepare each queued request right before it is replayed: return the\n * request to send (typically with the current credentials applied) or\n * `null` to drop the entry for good. A thrown error keeps the entry pending.\n * The context carries the entry's owner, so a custom hook can apply the\n * same rule as the default.\n *\n * Default: an entry with no stripped credentials and no bound owner is\n * replayed as-is. The others ask every open window client in parallel (see\n * {@link provideBackgroundSyncCredentials}) and pick the answer that fits:\n * - `\"bound\"`: the answer whose owner matches; none among valid answers →\n * dropped, never sent anonymously or as somebody else;\n * - `\"unknown\"`: only an answer without a principal (an app that does not use\n * `getPrincipal`); every answer has one → dropped;\n * - `\"unbound\"`: any answer, preferring one without a principal.\n * Then `null` headers drop a credentialed entry, an answer that lacks one of\n * the stripped headers keeps it pending, and only the stripped names are\n * re-applied. No valid answer at all keeps it pending until\n * `maxRetentionMinutes` (and warns once per replay in development builds).\n */\n beforeReplay?: (\n request: Request,\n context: BackgroundSyncReplayContext,\n ) => Request | null | Promise<Request | null>;\n}\n\n/**\n * Move credential headers out of a header list.\n *\n * @returns The remaining headers and the lower-cased names removed.\n */\nfunction splitHeaders(\n headers: Iterable<[string, string]>,\n credentialHeaders: ReadonlySet<string>,\n): { kept: [string, string][]; stripped: string[] } {\n const kept: [string, string][] = [];\n const stripped: string[] = [];\n for (const [name, value] of headers) {\n if (credentialHeaders.has(name.toLowerCase())) stripped.push(name.toLowerCase());\n else kept.push([name, value]);\n }\n return { kept, stripped };\n}\n\n/**\n * Scrub version-1 entries during the upgrade: their credential headers were\n * stored in plaintext, so they are removed from disk on the first open, recorded\n * as stripped, and the owner is marked unknown.\n */\nfunction migrateLegacyEntries(store: IDBObjectStore, credentialHeaders: ReadonlySet<string>): void {\n const cursorRequest = store.openCursor();\n cursorRequest.onsuccess = () => {\n const cursor = cursorRequest.result;\n if (!cursor) return;\n const entry = cursor.value as QueuedRequest;\n const { kept, stripped } = splitHeaders(entry.headers, credentialHeaders);\n cursor.update({\n ...entry,\n headers: kept,\n strippedHeaders: [...new Set([...(entry.strippedHeaders ?? []), ...stripped])],\n ownerState: entry.ownerState ?? \"unknown\",\n });\n cursor.continue();\n };\n}\n\nfunction openQueueDb(\n name: string,\n credentialHeaders: ReadonlySet<string> = credentialHeaderSet(),\n): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(name, DB_VERSION);\n request.onupgradeneeded = (event) => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE)) {\n db.createObjectStore(STORE, { keyPath: \"id\", autoIncrement: true });\n } else if (event.oldVersion < 2 && request.transaction) {\n migrateLegacyEntries(request.transaction.objectStore(STORE), credentialHeaders);\n }\n if (!db.objectStoreNames.contains(META)) db.createObjectStore(META);\n };\n request.onsuccess = () => {\n const db = request.result;\n db.onversionchange = () => db.close();\n resolve(db);\n };\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\n/** The current clear epoch: bumped by every {@link clearBackgroundSyncQueue}. */\nasync function readEpoch(name: string, credentialHeaders: ReadonlySet<string>): Promise<number> {\n const db = await openQueueDb(name, credentialHeaders);\n const tx = db.transaction(META, \"readonly\");\n const get = tx.objectStore(META).get(EPOCH_KEY);\n await txDone(tx);\n db.close();\n return (get.result as number | undefined) ?? 0;\n}\n\n/**\n * Add an entry unless the queue was cleared since `epoch` was read — checked in\n * the same transaction as the write, so a logout that lands while the request\n * is still failing never lets it into the next session's queue.\n *\n * @returns The new entry's key, or `undefined` when the clear won.\n */\nasync function enqueue(\n name: string,\n entry: QueuedRequest,\n epoch: number,\n credentialHeaders: ReadonlySet<string>,\n): Promise<number | undefined> {\n const db = await openQueueDb(name, credentialHeaders);\n const tx = db.transaction([STORE, META], \"readwrite\");\n let id: number | undefined;\n const current = tx.objectStore(META).get(EPOCH_KEY);\n current.onsuccess = () => {\n if (((current.result as number | undefined) ?? 0) !== epoch) return;\n const add = tx.objectStore(STORE).add(entry);\n add.onsuccess = () => {\n id = add.result as number;\n };\n };\n await txDone(tx);\n db.close();\n return id;\n}\n\n/**\n * Record how an entry is owned. A no-op when the entry is already gone\n * (replayed, or the queue was cleared).\n */\nasync function stampOwner(\n name: string,\n id: number,\n ownerState: BackgroundSyncOwnerState,\n owner: string | null | undefined,\n): Promise<void> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readwrite\");\n const store = tx.objectStore(STORE);\n const get = store.get(id);\n get.onsuccess = () => {\n const entry = get.result as QueuedRequest | undefined;\n if (!entry) return;\n entry.ownerState = ownerState;\n if (ownerState === \"bound\") entry.owner = owner ?? null;\n store.put(entry);\n };\n await txDone(tx);\n db.close();\n}\n\nasync function readAll(\n name: string,\n credentialHeaders: ReadonlySet<string>,\n): Promise<QueuedRequest[]> {\n const db = await openQueueDb(name, credentialHeaders);\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\n/** Whether an entry is still in the queue — `false` once it was cleared. */\nasync function stillQueued(name: string, id: number): Promise<boolean> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readonly\");\n const count = tx.objectStore(STORE).count(id);\n await txDone(tx);\n db.close();\n return count.result > 0;\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\n/**\n * Serialize a request for the queue, leaving out every credential header and\n * recording which ones were present.\n */\nasync function serializeRequest(\n request: Request,\n timestamp: number,\n credentialHeaders: ReadonlySet<string>,\n ownerState: QueuedRequest[\"ownerState\"],\n): Promise<QueuedRequest> {\n const buffer = await request.clone().arrayBuffer();\n const { kept, stripped } = splitHeaders(request.headers.entries(), credentialHeaders);\n return {\n url: request.url,\n method: request.method,\n headers: kept,\n body: buffer.byteLength > 0 ? buffer : null,\n timestamp,\n credentials: request.credentials,\n strippedHeaders: stripped,\n ownerState,\n };\n}\n\n/**\n * Rebuild a stored request, stripping credential headers once more so an entry\n * that still holds one (a custom `credentialHeaders` the migration did not know)\n * never replays it.\n */\nfunction deserializeRequest(\n entry: QueuedRequest,\n credentialHeaders: ReadonlySet<string>,\n): { request: Request; strippedHeaders: string[] } {\n const { kept, stripped } = splitHeaders(entry.headers, credentialHeaders);\n return {\n request: new Request(entry.url, {\n method: entry.method,\n headers: kept,\n body: entry.body ?? undefined,\n credentials: entry.credentials,\n }),\n strippedHeaders: [...new Set([...(entry.strippedHeaders ?? []), ...stripped])],\n };\n}\n\n/** A window client's parsed answer. */\ninterface ClientAnswer {\n headers: Record<string, string> | null;\n /** Whether the page has `getPrincipal`. */\n hasPrincipal: boolean;\n owner: string | null;\n}\n\n/**\n * Ask one window client about a queued request over a `MessageChannel`.\n *\n * @returns The parsed answer, or `undefined` when the client stayed silent until\n * `deadline`, answered `{ error: true }`, or answered something unrecognizable.\n */\nasync function askClient(\n client: WindowClientLike,\n request: BackgroundSyncCredentialsRequest,\n deadline: Promise<void>,\n): Promise<ClientAnswer | undefined> {\n const channel = new MessageChannel();\n const answer = await Promise.race([\n new Promise<unknown>((resolve) => {\n channel.port1.onmessage = (event: MessageEvent) => resolve(event.data);\n client.postMessage({ type: BACKGROUND_SYNC_CREDENTIALS_MESSAGE, ...request }, [\n channel.port2,\n ]);\n }),\n deadline.then(() => undefined),\n ]);\n channel.port1.close();\n if (answer === null || typeof answer !== \"object\" || !(\"headers\" in answer)) return undefined;\n const { headers, owner } = answer as { headers: unknown; owner?: unknown };\n const hasPrincipal = \"owner\" in answer;\n if (hasPrincipal && typeof owner !== \"string\" && owner !== null) return undefined;\n return {\n headers:\n headers !== null && typeof headers === \"object\"\n ? (headers as Record<string, string>)\n : null,\n hasPrincipal,\n owner: hasPrincipal ? (owner as string | null) : null,\n };\n}\n\n/** A deadline shared by every client asked in one round. */\nfunction startDeadline(): { promise: Promise<void>; cancel: () => void } {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const promise = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, CREDENTIALS_TIMEOUT_MS);\n });\n return { promise, cancel: () => clearTimeout(timer) };\n}\n\n/**\n * Ask every open window client in parallel, under one shared deadline.\n *\n * @returns Every valid answer; empty when nobody answered.\n */\nasync function askAllClients(\n sw: BgSwGlobal,\n request: BackgroundSyncCredentialsRequest,\n): Promise<ClientAnswer[]> {\n const clients = (await sw.clients?.matchAll({ type: \"window\" })) ?? [];\n if (clients.length === 0) return [];\n const deadline = startDeadline();\n const answers = await Promise.all(\n clients.map((client) => askClient(client, request, deadline.promise)),\n );\n deadline.cancel();\n return answers.filter((answer): answer is ClientAnswer => answer !== undefined);\n}\n\n/** State of one replay pass: answers cached per URL + method, and the warning flag. */\ninterface ReplayRun {\n ask(request: Request): Promise<ClientAnswer[]>;\n warnNoAnswer(): void;\n}\n\nfunction createReplayRun(sw: BgSwGlobal): ReplayRun {\n const answers = new Map<string, Promise<ClientAnswer[]>>();\n let warned = false;\n return {\n ask(request) {\n const key = `${request.method} ${request.url}`;\n let pending = answers.get(key);\n if (!pending) {\n pending = askAllClients(sw, {\n url: request.url,\n method: request.method,\n phase: \"replay\",\n });\n answers.set(key, pending);\n }\n return pending;\n },\n warnNoAnswer() {\n if (warned || !isDevBuild()) return;\n warned = true;\n console.warn(\n \"[tempest-react-sdk/sw] background-sync: a queued request needs the current \" +\n \"credentials, and no open page answered. Call \" +\n \"provideBackgroundSyncCredentials() in the page; the entry stays pending.\",\n );\n },\n };\n}\n\n/** Pick the answer an entry may be replayed with, per its owner state. */\nfunction chooseAnswer(\n answers: readonly ClientAnswer[],\n context: BackgroundSyncReplayContext,\n): ClientAnswer | undefined {\n switch (context.ownerState) {\n case \"bound\":\n return answers.find((answer) => answer.hasPrincipal && answer.owner === context.owner);\n case \"unknown\":\n return answers.find((answer) => !answer.hasPrincipal);\n case \"unbound\":\n return answers.find((answer) => !answer.hasPrincipal) ?? answers[0];\n }\n}\n\n/**\n * The default `beforeReplay` (rules on {@link InstallBackgroundSyncOptions}).\n *\n * @throws {Error} When no page answered, or the chosen answer lacks a stripped\n * header — the entry stays pending.\n */\nasync function defaultBeforeReplay(\n run: ReplayRun,\n request: Request,\n context: BackgroundSyncReplayContext,\n): Promise<Request | null> {\n const credentialed = context.strippedHeaders.length > 0;\n if (!credentialed && context.ownerState !== \"bound\") return request;\n const answers = await run.ask(request);\n if (answers.length === 0) {\n run.warnNoAnswer();\n throw new Error(\"background-sync: no client answered\");\n }\n const chosen = chooseAnswer(answers, context);\n if (!chosen) return null;\n if (!credentialed) return request;\n if (chosen.headers === null) return null;\n const provided = new Map(\n Object.entries(chosen.headers).map(([name, value]) => [name.toLowerCase(), value]),\n );\n if (context.strippedHeaders.some((name) => !provided.has(name))) {\n throw new Error(\"background-sync: the answer lacks a stripped credential header\");\n }\n const headers = new Headers(request.headers);\n for (const name of context.strippedHeaders) headers.set(name, provided.get(name) as string);\n return new Request(request, { headers });\n}\n\n/** Everything {@link replayQueue} needs besides the clock. */\ninterface ReplayConfig {\n sw: BgSwGlobal;\n name: string;\n maxRetentionMs: number;\n credentialHeaders: ReadonlySet<string>;\n beforeReplay?: InstallBackgroundSyncOptions[\"beforeReplay\"];\n}\n\n/**\n * Replay every queued request once. Entries that succeed, are stale, get a 4xx\n * (a client error that will not fix itself) or that the preparation rejects with\n * `null` are removed; entries that fail again — network error, 5xx, or a\n * preparation that throws — are kept, and so is an entry whose owner is still\n * being stamped (for up to {@link OWNER_STAMP_GRACE_MS}). Each entry is checked\n * against the store right before it is sent, so a queue cleared mid-replay stops\n * the replay. Throws if any entry is still pending, so a `sync` handler keeps\n * the sync registered.\n */\nasync function replayQueue(config: ReplayConfig, now: number): Promise<void> {\n const { sw, name, maxRetentionMs, credentialHeaders, beforeReplay } = config;\n const run = createReplayRun(sw);\n const prepare =\n beforeReplay ??\n ((request: Request, context: BackgroundSyncReplayContext) =>\n defaultBeforeReplay(run, request, context));\n const entries = await readAll(name, credentialHeaders);\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 const state = entry.ownerState ?? \"unknown\";\n if (state === \"pending\" && now - entry.timestamp < OWNER_STAMP_GRACE_MS) {\n pending += 1;\n continue;\n }\n const ownerState: BackgroundSyncOwnerState = state === \"pending\" ? \"unknown\" : state;\n try {\n const { request: stored, strippedHeaders } = deserializeRequest(\n entry,\n credentialHeaders,\n );\n const request = await prepare(stored, {\n queuedAt: entry.timestamp,\n strippedHeaders,\n ownerState,\n owner: ownerState === \"bound\" ? (entry.owner ?? null) : undefined,\n });\n if (!(await stillQueued(name, entry.id))) continue;\n if (request === null) {\n await remove(name, entry.id);\n continue;\n }\n const response = await fetch(request);\n if (response.ok || (response.status >= 400 && response.status < 500)) {\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\n/**\n * The replay in flight per queue. `sync`, `periodicsync` and the Safari fallback\n * can fire together; sharing one pass keeps a mutation from being sent twice.\n */\nconst replaysInFlight = new Map<string, Promise<void>>();\n\nfunction drainQueue(config: ReplayConfig): Promise<void> {\n const running = replaysInFlight.get(config.name);\n if (running) return running;\n const pass = replayQueue(config, Date.now()).finally(() => {\n replaysInFlight.delete(config.name);\n });\n replaysInFlight.set(config.name, pass);\n return pass;\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 * Ask the issuing page who owns a just-queued entry and record it.\n *\n * @returns Resolves once the owner is stamped; never rejects.\n */\nasync function stampFromIssuer(\n name: string,\n id: number,\n issuer: WindowClientLike,\n request: Request,\n): Promise<void> {\n const deadline = startDeadline();\n const answer = await askClient(\n issuer,\n { url: request.url, method: request.method, phase: \"enqueue\" },\n deadline.promise,\n );\n deadline.cancel();\n const state: BackgroundSyncOwnerState = !answer\n ? \"unknown\"\n : answer.hasPrincipal\n ? \"bound\"\n : \"unbound\";\n await stampOwner(name, id, state, answer?.owner);\n}\n\n/**\n * Install the background-sync queue: on a failed mutating request, the request\n * is serialized to IndexedDB (without its credential headers) and a sync is\n * registered; the original fetch still rejects (so your app can show an offline\n * state), and the request is replayed later when the network returns.\n *\n * Right after queueing, the worker asks the page that issued the request who is\n * signed in (`getPrincipal` on {@link provideBackgroundSyncCredentials}) and\n * stamps the entry with that owner, without delaying the rejection. The work is\n * handed to `event.waitUntil` synchronously, so the worker stays alive for it\n * in every engine. Without the Background Sync API (every non-Chromium\n * browser), the queue is drained opportunistically whenever a `GET` goes\n * through the worker.\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 = DEFAULT_QUEUE_NAME,\n maxRetentionMinutes = 1440,\n periodicSyncTag = `${options.queueName ?? DEFAULT_QUEUE_NAME}-periodic`,\n credentialHeaders = DEFAULT_CREDENTIAL_HEADERS,\n beforeReplay,\n } = options;\n const stripped = credentialHeaderSet(credentialHeaders);\n const replay: ReplayConfig = {\n sw,\n name: queueName,\n maxRetentionMs: maxRetentionMinutes * 60 * 1000,\n credentialHeaders: stripped,\n beforeReplay,\n };\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 const epoch = readEpoch(queueName, stripped);\n epoch.catch(() => undefined);\n const attempt = fetch(request.clone());\n const queued = attempt.then(\n () => undefined,\n async () => {\n const issuer = event.clientId ? await sw.clients?.get(event.clientId) : undefined;\n const entry = await serializeRequest(\n request,\n Date.now(),\n stripped,\n issuer ? \"pending\" : \"unknown\",\n );\n const id = await enqueue(queueName, entry, await epoch, stripped);\n if (id === undefined) return undefined;\n try {\n await sw.registration.sync?.register(queueName);\n } catch {\n /* empty */\n }\n return issuer ? { id, issuer } : undefined;\n },\n );\n event.respondWith(\n attempt.catch(async (error: unknown) => {\n await queued;\n throw error;\n }),\n );\n event.waitUntil(\n queued\n .then((stamp) =>\n stamp ? stampFromIssuer(queueName, stamp.id, stamp.issuer, request) : undefined,\n )\n .catch(() => undefined),\n );\n });\n\n sw.addEventListener(\"sync\", (event) => {\n if (event.tag !== queueName) return;\n event.waitUntil(drainQueue(replay));\n });\n\n sw.addEventListener(\"periodicsync\", (event) => {\n if (event.tag !== periodicSyncTag) return;\n event.waitUntil(drainQueue(replay).catch(() => undefined));\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n if (sw.registration.sync) return;\n if (event.request.method !== \"GET\") return;\n event.waitUntil(drainQueue(replay).catch(() => undefined));\n });\n}\n\n/**\n * Empty the background-sync queue — every request still waiting to be replayed —\n * and bump its clear epoch. Call it on logout (or use `clearSessionData()`, which\n * also clears the runtime caches) so mutations made under one session are never\n * replayed under the next.\n *\n * Works from the page and from the worker, and reaches work already in flight\n * in the worker: a replay checks each entry right before sending it, and a\n * request still failing when the queue is cleared is not written afterwards.\n *\n * @param queueName - The `queueName` given to {@link installBackgroundSync}.\n * Default `tempest-bg-sync`.\n * @returns `true` once the queue is empty; `false` when IndexedDB is\n * unavailable (SSR, old browsers).\n *\n * @example\n * import { clearBackgroundSyncQueue } from \"tempest-react-sdk/sw\";\n *\n * await clearBackgroundSyncQueue();\n */\nexport async function clearBackgroundSyncQueue(\n queueName: string = DEFAULT_QUEUE_NAME,\n): Promise<boolean> {\n if (typeof indexedDB === \"undefined\") return false;\n const db = await openQueueDb(queueName);\n const tx = db.transaction([STORE, META], \"readwrite\");\n tx.objectStore(STORE).clear();\n const meta = tx.objectStore(META);\n const current = meta.get(EPOCH_KEY);\n current.onsuccess = () => {\n meta.put(((current.result as number | undefined) ?? 0) + 1, EPOCH_KEY);\n };\n await txDone(tx);\n db.close();\n return true;\n}\n\n/**\n * One-way fingerprint of a principal: hex SHA-256, so the queue can compare\n * owners without ever holding the id itself.\n *\n * @returns The fingerprint, or `null` when nobody is signed in.\n */\nasync function ownerFingerprint(principal: string | null | undefined): Promise<string | null> {\n if (principal === null || principal === undefined || principal === \"\") return null;\n const digest = await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(`tempest-bg-sync:${principal}`),\n );\n return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/**\n * Answer the worker's questions about queued requests (see `beforeReplay` on\n * {@link installBackgroundSync}). Install it once in every page, next to your\n * auth setup; it reads the token at replay time, so the queue never has to store\n * it. Without it, a queued request that carried a credential header is never\n * replayed.\n *\n * `getHeaders` must return **every** credential header the app sends (the worker\n * keeps an entry pending when one it stripped is missing), or `null` when there\n * is no session — the worker then drops the entry instead of replaying it\n * anonymously. A throw from `getHeaders` or `getPrincipal` is answered as an\n * explicit `{ error: true }`. The worker only applies headers whose names the\n * original request carried, so returning `Authorization` for every URL never\n * leaks it to a URL that did not receive it in the first place.\n *\n * Pass `getPrincipal` to bind each entry to whoever queued it: the worker asks\n * the issuing tab right after queueing, asks every tab again before replaying,\n * and drops the entry when no signed-in tab is its owner.\n *\n * @param getHeaders - Resolves the headers for one queued request at replay.\n * @param options - `getPrincipal` binds entries to their owner.\n * @returns A function that removes the listener. A no-op outside the browser.\n *\n * @example\n * import { provideBackgroundSyncCredentials } from \"tempest-react-sdk/sw\";\n *\n * const stop = provideBackgroundSyncCredentials(\n * () => {\n * const token = localStorage.getItem(\"token\");\n * return token ? { Authorization: `Bearer ${token}` } : null;\n * },\n * { getPrincipal: () => localStorage.getItem(\"userId\") },\n * );\n */\nexport function provideBackgroundSyncCredentials(\n getHeaders: BackgroundSyncCredentialsProvider,\n options: ProvideBackgroundSyncCredentialsOptions = {},\n): () => void {\n if (typeof navigator === \"undefined\" || !navigator.serviceWorker) return () => undefined;\n const container = navigator.serviceWorker;\n const { getPrincipal } = options;\n const onMessage = (event: MessageEvent): void => {\n const data = event.data as Partial<BackgroundSyncCredentialsRequest> & { type?: unknown };\n const port = event.ports[0];\n if (data?.type !== BACKGROUND_SYNC_CREDENTIALS_MESSAGE || !port) return;\n const request: BackgroundSyncCredentialsRequest = {\n url: String(data.url),\n method: String(data.method),\n phase: data.phase === \"enqueue\" ? \"enqueue\" : \"replay\",\n };\n void (async () => {\n try {\n const headers = request.phase === \"enqueue\" ? null : await getHeaders(request);\n const answer: { headers: Record<string, string> | null; owner?: string | null } = {\n headers,\n };\n if (getPrincipal) answer.owner = await ownerFingerprint(await getPrincipal());\n port.postMessage(answer);\n } catch {\n port.postMessage({ error: true });\n }\n })();\n };\n container.addEventListener(\"message\", onMessage);\n return () => container.removeEventListener(\"message\", onMessage);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-inspect.cjs","names":[],"sources":["../../src/sw/cache-inspect.ts"],"sourcesContent":["/**\n * Main-thread helpers to inspect and clear the Cache Storage buckets a service\n * worker fills — for a \"X MB cacheado\" readout and a \"limpar cache\" action\n *
|
|
1
|
+
{"version":3,"file":"cache-inspect.cjs","names":[],"sources":["../../src/sw/cache-inspect.ts"],"sourcesContent":["/**\n * Main-thread helpers to inspect and clear the Cache Storage buckets a service\n * worker fills — for a \"X MB cacheado\" readout and a \"limpar cache\" action.\n * On logout prefer `clearSessionData`, which also drops the background-sync\n * queue and spares the app-shell precache. All guard `caches` so they no-op\n * safely under SSR / older browsers.\n */\n\n/** Per-cache usage summary returned by {@link inspectCaches}. */\nexport interface CacheReport {\n /** The Cache Storage bucket name. */\n name: string;\n /** Number of cached responses. */\n entries: number;\n /** Approximate total bytes, or `null` when byte measurement was skipped. */\n bytes: number | null;\n}\n\n/** A cache-name matcher: a prefix string, a `RegExp`, or a predicate. */\nexport type CacheFilter = string | RegExp | ((name: string) => boolean);\n\nfunction nameMatches(filter: CacheFilter | undefined, name: string): boolean {\n if (filter === undefined) return true;\n if (typeof filter === \"string\") return name.startsWith(filter);\n if (filter instanceof RegExp) return filter.test(name);\n return filter(name);\n}\n\nfunction cacheStorageAvailable(): boolean {\n return typeof caches !== \"undefined\";\n}\n\n/** Best-effort byte size of one cached response (Content-Length, else the blob). */\nasync function responseBytes(response: Response): Promise<number> {\n const header = response.headers.get(\"content-length\");\n if (header) {\n const parsed = Number(header);\n if (!Number.isNaN(parsed)) return parsed;\n }\n try {\n const blob = await response.clone().blob();\n return blob.size;\n } catch {\n return 0;\n }\n}\n\n/**\n * Report entry counts (and optionally byte sizes) for the Cache Storage\n * buckets whose name passes `filter`.\n *\n * @param options - `filter` narrows which caches to include; `measureBytes`\n * (default `true`) reads each response to sum sizes — set `false` for a fast,\n * count-only report.\n * @returns One {@link CacheReport} per matching cache. Empty when unsupported.\n *\n * @example\n * const reports = await inspectCaches({ filter: \"tempest-\" });\n * const totalMb = reports.reduce((n, r) => n + (r.bytes ?? 0), 0) / 1e6;\n */\nexport async function inspectCaches(\n options: { filter?: CacheFilter; measureBytes?: boolean } = {},\n): Promise<CacheReport[]> {\n if (!cacheStorageAvailable()) return [];\n const { filter, measureBytes = true } = options;\n\n const names = (await caches.keys()).filter((name) => nameMatches(filter, name));\n const reports: CacheReport[] = [];\n for (const name of names) {\n const cache = await caches.open(name);\n const requests = await cache.keys();\n let bytes: number | null = null;\n if (measureBytes) {\n bytes = 0;\n for (const request of requests) {\n const response = await cache.match(request);\n if (response) bytes += await responseBytes(response);\n }\n }\n reports.push({ name, entries: requests.length, bytes });\n }\n return reports;\n}\n\n/**\n * Delete Cache Storage buckets whose name passes `filter` (all of them when no\n * filter is given).\n *\n * @param filter - Which caches to delete.\n * @returns The names of the caches that were deleted.\n *\n * @example\n * await clearCaches(\"tempest-api\"); // drop one runtime bucket\n */\nexport async function clearCaches(filter?: CacheFilter): Promise<string[]> {\n if (!cacheStorageAvailable()) return [];\n const names = (await caches.keys()).filter((name) => nameMatches(filter, name));\n const deleted: string[] = [];\n for (const name of names) {\n if (await caches.delete(name)) deleted.push(name);\n }\n return deleted;\n}\n"],"mappings":"AAqBA,SAAS,EAAY,EAAiC,EAAuB,CAIzE,OAHI,IAAW,IAAA,GAAkB,GAC7B,OAAO,GAAW,SAAiB,EAAK,WAAW,CAAM,EACzD,aAAkB,OAAe,EAAO,KAAK,CAAI,EAC9C,EAAO,CAAI,CACtB,CAEA,SAAS,GAAiC,CACtC,OAAO,OAAO,OAAW,GAC7B,CAGA,eAAe,EAAc,EAAqC,CAC9D,IAAM,EAAS,EAAS,QAAQ,IAAI,gBAAgB,EACpD,GAAI,EAAQ,CACR,IAAM,EAAS,OAAO,CAAM,EAC5B,GAAI,CAAC,OAAO,MAAM,CAAM,EAAG,OAAO,CACtC,CACA,GAAI,CAEA,OAAO,MADY,EAAS,MAAM,CAAC,CAAC,KAAK,EAAA,CAC7B,IAChB,MAAQ,CACJ,MAAO,EACX,CACJ,CAeA,eAAsB,EAClB,EAA4D,CAAC,EACvC,CACtB,GAAI,CAAC,EAAsB,EAAG,MAAO,CAAC,EACtC,GAAM,CAAE,SAAQ,eAAe,IAAS,EAElC,GAAS,MAAM,OAAO,KAAK,EAAA,CAAG,OAAQ,GAAS,EAAY,EAAQ,CAAI,CAAC,EACxE,EAAyB,CAAC,EAChC,IAAK,IAAM,KAAQ,EAAO,CACtB,IAAM,EAAQ,MAAM,OAAO,KAAK,CAAI,EAC9B,EAAW,MAAM,EAAM,KAAK,EAC9B,EAAuB,KAC3B,GAAI,EAAc,CACd,EAAQ,EACR,IAAK,IAAM,KAAW,EAAU,CAC5B,IAAM,EAAW,MAAM,EAAM,MAAM,CAAO,EACtC,IAAU,GAAS,MAAM,EAAc,CAAQ,EACvD,CACJ,CACA,EAAQ,KAAK,CAAE,OAAM,QAAS,EAAS,OAAQ,OAAM,CAAC,CAC1D,CACA,OAAO,CACX,CAYA,eAAsB,EAAY,EAAyC,CACvE,GAAI,CAAC,EAAsB,EAAG,MAAO,CAAC,EACtC,IAAM,GAAS,MAAM,OAAO,KAAK,EAAA,CAAG,OAAQ,GAAS,EAAY,EAAQ,CAAI,CAAC,EACxE,EAAoB,CAAC,EAC3B,IAAK,IAAM,KAAQ,EACX,MAAM,OAAO,OAAO,CAAI,GAAG,EAAQ,KAAK,CAAI,EAEpD,OAAO,CACX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-inspect.js","names":[],"sources":["../../src/sw/cache-inspect.ts"],"sourcesContent":["/**\n * Main-thread helpers to inspect and clear the Cache Storage buckets a service\n * worker fills — for a \"X MB cacheado\" readout and a \"limpar cache\" action\n *
|
|
1
|
+
{"version":3,"file":"cache-inspect.js","names":[],"sources":["../../src/sw/cache-inspect.ts"],"sourcesContent":["/**\n * Main-thread helpers to inspect and clear the Cache Storage buckets a service\n * worker fills — for a \"X MB cacheado\" readout and a \"limpar cache\" action.\n * On logout prefer `clearSessionData`, which also drops the background-sync\n * queue and spares the app-shell precache. All guard `caches` so they no-op\n * safely under SSR / older browsers.\n */\n\n/** Per-cache usage summary returned by {@link inspectCaches}. */\nexport interface CacheReport {\n /** The Cache Storage bucket name. */\n name: string;\n /** Number of cached responses. */\n entries: number;\n /** Approximate total bytes, or `null` when byte measurement was skipped. */\n bytes: number | null;\n}\n\n/** A cache-name matcher: a prefix string, a `RegExp`, or a predicate. */\nexport type CacheFilter = string | RegExp | ((name: string) => boolean);\n\nfunction nameMatches(filter: CacheFilter | undefined, name: string): boolean {\n if (filter === undefined) return true;\n if (typeof filter === \"string\") return name.startsWith(filter);\n if (filter instanceof RegExp) return filter.test(name);\n return filter(name);\n}\n\nfunction cacheStorageAvailable(): boolean {\n return typeof caches !== \"undefined\";\n}\n\n/** Best-effort byte size of one cached response (Content-Length, else the blob). */\nasync function responseBytes(response: Response): Promise<number> {\n const header = response.headers.get(\"content-length\");\n if (header) {\n const parsed = Number(header);\n if (!Number.isNaN(parsed)) return parsed;\n }\n try {\n const blob = await response.clone().blob();\n return blob.size;\n } catch {\n return 0;\n }\n}\n\n/**\n * Report entry counts (and optionally byte sizes) for the Cache Storage\n * buckets whose name passes `filter`.\n *\n * @param options - `filter` narrows which caches to include; `measureBytes`\n * (default `true`) reads each response to sum sizes — set `false` for a fast,\n * count-only report.\n * @returns One {@link CacheReport} per matching cache. Empty when unsupported.\n *\n * @example\n * const reports = await inspectCaches({ filter: \"tempest-\" });\n * const totalMb = reports.reduce((n, r) => n + (r.bytes ?? 0), 0) / 1e6;\n */\nexport async function inspectCaches(\n options: { filter?: CacheFilter; measureBytes?: boolean } = {},\n): Promise<CacheReport[]> {\n if (!cacheStorageAvailable()) return [];\n const { filter, measureBytes = true } = options;\n\n const names = (await caches.keys()).filter((name) => nameMatches(filter, name));\n const reports: CacheReport[] = [];\n for (const name of names) {\n const cache = await caches.open(name);\n const requests = await cache.keys();\n let bytes: number | null = null;\n if (measureBytes) {\n bytes = 0;\n for (const request of requests) {\n const response = await cache.match(request);\n if (response) bytes += await responseBytes(response);\n }\n }\n reports.push({ name, entries: requests.length, bytes });\n }\n return reports;\n}\n\n/**\n * Delete Cache Storage buckets whose name passes `filter` (all of them when no\n * filter is given).\n *\n * @param filter - Which caches to delete.\n * @returns The names of the caches that were deleted.\n *\n * @example\n * await clearCaches(\"tempest-api\"); // drop one runtime bucket\n */\nexport async function clearCaches(filter?: CacheFilter): Promise<string[]> {\n if (!cacheStorageAvailable()) return [];\n const names = (await caches.keys()).filter((name) => nameMatches(filter, name));\n const deleted: string[] = [];\n for (const name of names) {\n if (await caches.delete(name)) deleted.push(name);\n }\n return deleted;\n}\n"],"mappings":";AAqBA,SAAS,EAAY,GAAiC,GAAuB;CAIzE,OAHI,MAAW,KAAA,IAAkB,KAC7B,OAAO,KAAW,WAAiB,EAAK,WAAW,CAAM,IACzD,aAAkB,SAAe,EAAO,KAAK,CAAI,IAC9C,EAAO,CAAI;AACtB;AAEA,SAAS,IAAiC;CACtC,OAAO,OAAO,SAAW;AAC7B;AAGA,eAAe,EAAc,GAAqC;CAC9D,IAAM,IAAS,EAAS,QAAQ,IAAI,gBAAgB;CACpD,IAAI,GAAQ;EACR,IAAM,IAAS,OAAO,CAAM;EAC5B,IAAI,CAAC,OAAO,MAAM,CAAM,GAAG,OAAO;CACtC;CACA,IAAI;EAEA,QAAO,MADY,EAAS,MAAM,CAAC,CAAC,KAAK,EAAA,CAC7B;CAChB,QAAQ;EACJ,OAAO;CACX;AACJ;AAeA,eAAsB,EAClB,IAA4D,CAAC,GACvC;CACtB,IAAI,CAAC,EAAsB,GAAG,OAAO,CAAC;CACtC,IAAM,EAAE,WAAQ,kBAAe,OAAS,GAElC,KAAS,MAAM,OAAO,KAAK,EAAA,CAAG,QAAQ,MAAS,EAAY,GAAQ,CAAI,CAAC,GACxE,IAAyB,CAAC;CAChC,KAAK,IAAM,KAAQ,GAAO;EACtB,IAAM,IAAQ,MAAM,OAAO,KAAK,CAAI,GAC9B,IAAW,MAAM,EAAM,KAAK,GAC9B,IAAuB;EAC3B,IAAI,GAAc;GACd,IAAQ;GACR,KAAK,IAAM,KAAW,GAAU;IAC5B,IAAM,IAAW,MAAM,EAAM,MAAM,CAAO;IAC1C,AAAI,MAAU,KAAS,MAAM,EAAc,CAAQ;GACvD;EACJ;EACA,EAAQ,KAAK;GAAE;GAAM,SAAS,EAAS;GAAQ;EAAM,CAAC;CAC1D;CACA,OAAO;AACX;AAYA,eAAsB,EAAY,GAAyC;CACvE,IAAI,CAAC,EAAsB,GAAG,OAAO,CAAC;CACtC,IAAM,KAAS,MAAM,OAAO,KAAK,EAAA,CAAG,QAAQ,MAAS,EAAY,GAAQ,CAAI,CAAC,GACxE,IAAoB,CAAC;CAC3B,KAAK,IAAM,KAAQ,GACf,AAAI,MAAM,OAAO,OAAO,CAAI,KAAG,EAAQ,KAAK,CAAI;CAEpD,OAAO;AACX"}
|
package/dist/sw/cache.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
const e=require("./credential-headers.cjs");function t(){return globalThis}var n=``,r=new Set,i=`x-tempest-cached-at`;function a(e,t){if(!t)return!1;let n=Number(e.headers.get(i)),r=e.headers.get(`date`),a=n>0?n:r?new Date(r).getTime():NaN;return!Number.isNaN(a)&&(Date.now()-a)/1e3>t}function o(e){return(e.headers.get(`cache-control`)??``).toLowerCase().split(`,`).map(e=>e.trim().split(`=`)[0])}function s(e,t){if(!e.ok)return!1;let n=o(e);return n.includes(`no-store`)?!1:t.cacheAuthenticated===!0||!n.includes(`private`)}function c(t,n){return[...e.credentialHeaderSet(n.credentialHeaders)].filter(e=>t.headers.has(e))}async function l(e,t,n,r){if(!s(n,r))return;let a=new Headers(n.headers);if(a.set(i,String(Date.now())),r.cacheAuthenticated){let e=a.get(`vary`),n=(e??``).toLowerCase().split(`,`).map(e=>e.trim()),i=c(t,r).filter(e=>!n.includes(e));i.length>0&&!n.includes(`*`)&&a.set(`vary`,[e,...i].filter(Boolean).join(`, `))}let o=n.clone();await e.put(t,new Response(o.body,{status:o.status,statusText:o.statusText,headers:a})),await u(r.cacheName,r.maxEntries)}async function u(e,t){if(!t)return;let n=await caches.open(e),r=await n.keys();if(!(r.length<=t))for(let e of r.slice(0,r.length-t))await n.delete(e)}async function d(e,t){let n=await caches.open(t.cacheName),r=await n.match(e);if(r&&!a(r,t.maxAgeSeconds))return r;let i=await fetch(e);return await l(n,e,i,t),i}async function f(e,t,n){let r=await caches.open(t.cacheName),i=(async()=>{let n=await fetch(e);return await l(r,e,n,t),n})(),o=(t.networkTimeoutSeconds??0)*1e3;try{if(o>0){let e=new Promise((e,t)=>setTimeout(()=>{n(i.catch(()=>void 0)),t(Error(`network-timeout`))},o));return await Promise.race([i,e])}return await i}catch{let n=await r.match(e);if(n&&!a(n,t.maxAgeSeconds))return n;throw Error(`network-first: no network and no cache`)}}async function p(e,t,n){let r=await caches.open(t.cacheName),i=await r.match(e),o=fetch(e).then(async n=>(await l(r,e,n,t),n)).catch(()=>void 0),s=i&&!a(i,t.maxAgeSeconds)?i:void 0;if(s)return n(o),s;let c=await o;if(c)return c;throw Error(`stale-while-revalidate: no network and no cache`)}function m(e,t,n){switch(t.strategy){case`cache-first`:return d(e,t);case`network-first`:return f(e,t,n);case`stale-while-revalidate`:return p(e,t,n)}}function h(e,t,n){return typeof e.match==`function`?e.match(t,n):e.match.test(t.href)}async function g(e,t){let n=e.headers.get(`range`);if(!n)return t;let r=/^bytes=(\d*)-(\d*)$/.exec(n.trim());if(!r||r[1]===``&&r[2]===``)return new Response(null,{status:416,statusText:`Range Not Satisfiable`});let i=await t.clone().arrayBuffer(),a=i.byteLength,o,s;if(r[1]===``){let e=Number(r[2]);o=Math.max(0,a-e),s=a-1}else o=Number(r[1]),s=r[2]===``?a-1:Math.min(Number(r[2]),a-1);if(o>s||o>=a)return new Response(null,{status:416,statusText:`Range Not Satisfiable`,headers:{"Content-Range":`bytes */${a}`}});let c=i.slice(o,s+1),l=new Headers(t.headers);return l.set(`Content-Range`,`bytes ${o}-${s}/${a}`),l.set(`Content-Length`,String(c.byteLength)),l.set(`Accept-Ranges`,`bytes`),new Response(c,{status:206,statusText:`Partial Content`,headers:l})}function _(e){t().addEventListener(`fetch`,t=>{let n=t.request;if(n.method!==`GET`)return;let r=new URL(n.url),i=e.find(e=>h(e,r,n));if(!i||!i.cacheAuthenticated&&c(n,i).length>0)return;let a=e=>t.waitUntil(e);if(i.rangeRequests&&n.headers.has(`range`)){let e=new Request(n.url,{headers:v(n.headers),credentials:n.credentials,mode:n.mode===`navigate`?`same-origin`:n.mode});t.respondWith(m(e,i,a).then(e=>g(n,e)));return}t.respondWith(m(n,i,a))})}function v(e){let t=new Headers(e);return t.delete(`range`),t}function y(e={}){let i=t(),{manifestUrl:a=`/precache-manifest.json`,cacheName:o=`tempest-precache`,navigateFallback:s=`/index.html`,navigateFallbackDenylist:c=[],skipWaiting:l=!0,navigationPreload:u=!0}=e;i.addEventListener(`install`,e=>{e.waitUntil((async()=>{let e=await(await fetch(a,{cache:`no-cache`})).json();n=`${o}-${e.version}`,await(await caches.open(n)).addAll(e.urls);for(let t of e.urls)r.add(new URL(t,i.location.origin).pathname);l&&await i.skipWaiting()})())}),i.addEventListener(`activate`,e=>{e.waitUntil((async()=>{let e=await caches.keys();await Promise.all(e.filter(e=>e.startsWith(`${o}-`)&&e!==n).map(e=>caches.delete(e))),u&&i.registration?.navigationPreload&&await i.registration.navigationPreload.enable(),await i.clients.claim()})())}),i.addEventListener(`fetch`,e=>{let t=e.request;if(t.method!==`GET`)return;let a=new URL(t.url);if(a.origin===i.location.origin){if(t.mode===`navigate`){if(c.some(e=>e.test(a.pathname)))return;e.respondWith((async()=>{try{return await e.preloadResponse||await fetch(t)}catch{let e=await(await caches.open(n)).match(s);if(e)return e;throw Error(`offline and no cached app shell`)}})());return}r.has(a.pathname)&&e.respondWith((async()=>await(await caches.open(n)).match(t)??fetch(t))())}})}exports.createPartialResponse=g,exports.installPrecache=y,exports.installRuntimeCache=_;
|
|
2
2
|
//# sourceMappingURL=cache.cjs.map
|
package/dist/sw/cache.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.cjs","names":[],"sources":["../../src/sw/cache.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — precache, runtime strategies (cache-first, network-\n * first, stale-while-revalidate), expiry and the inspect/clear helpers all address\n * the same Cache Storage namespace, and the naming scheme that keeps them from\n * evicting each other is the file's whole contract.\n */\n/**\n * Service-worker caching helpers — a small, dependency-free subset of what\n * Workbox provides: precaching of the build's app shell (so the app launches\n * offline) plus runtime caching strategies for fonts, APIs and images.\n *\n * Import these inside your own `sw.ts`. They run in the service-worker global\n * scope, not the main thread. Pair `installPrecache` with the\n * `tempestPwaManifest()` Vite plugin (from `tempest-react-sdk/vite`), which\n * emits the `precache-manifest.json` this reads at install time.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installRuntimeCache, installPrecache } from \"tempest-react-sdk/sw\";\n *\n * // Register specific routes FIRST so they win over the precache catch-all.\n * installRuntimeCache([\n * { match: /\\/api\\//, strategy: \"network-first\", cacheName: \"api\", maxAgeSeconds: 300 },\n * ]);\n * installPrecache();\n */\n\n/** Minimal shape of the events we touch, to avoid pulling in `lib.webworker`. */\ninterface ExtendableEventLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface FetchEventLike extends ExtendableEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n /** Resolves to the navigation-preload response when it is enabled, else `undefined`. */\n preloadResponse?: Promise<Response | undefined>;\n}\n\n/** Minimal shape of `registration.navigationPreload` (Navigation Preload API). */\ninterface NavigationPreloadManagerLike {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\ninterface CacheSwGlobal {\n addEventListener(type: \"install\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"activate\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchEventLike) => void): void;\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n location: { origin: string };\n registration?: { navigationPreload?: NavigationPreloadManagerLike };\n}\n\nfunction getSwScope(): CacheSwGlobal {\n return globalThis as unknown as CacheSwGlobal;\n}\n\n/** Caching strategy for a runtime route. Mirrors the common Workbox trio. */\nexport type RuntimeStrategy = \"cache-first\" | \"network-first\" | \"stale-while-revalidate\";\n\n/** A single runtime-caching rule, matched against each `GET` request. */\nexport interface RuntimeRoute {\n /** A `RegExp` tested against the full URL, or a predicate over the parsed URL. */\n match: RegExp | ((url: URL, request: Request) => boolean);\n /** How to resolve a match. */\n strategy: RuntimeStrategy;\n /** Cache bucket name for this route. */\n cacheName: string;\n /** Trim the cache to at most this many entries (FIFO) after each write. */\n maxEntries?: number;\n /** Treat a cached response older than this (seconds) as a miss. */\n maxAgeSeconds?: number;\n /** For `network-first`: fall back to cache after this timeout (seconds). */\n networkTimeoutSeconds?: number;\n /**\n * Serve HTTP `Range` requests (206 Partial Content) by slicing the cached\n * full response. Enable for audio/video so seeking works offline. The full\n * resource is cached once (the `Range` header is stripped before caching).\n */\n rangeRequests?: boolean;\n}\n\n/** Options for {@link installPrecache}. */\nexport interface InstallPrecacheOptions {\n /** URL of the manifest emitted by `tempestPwaManifest()`. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Cache name prefix; the manifest `version` is appended. Default `tempest-precache`. */\n cacheName?: string;\n /** App-shell document served for navigation requests offline. Default `/index.html`. */\n navigateFallback?: string;\n /** Navigation paths that should NOT use the fallback (e.g. `[/^\\/api\\//]`). */\n navigateFallbackDenylist?: RegExp[];\n /** Activate the new worker immediately after precaching. Default `true`. */\n skipWaiting?: boolean;\n /**\n * Enable the Navigation Preload API on `activate`, so the browser fetches\n * the navigation request in parallel with the worker boot and the handler\n * serves `event.preloadResponse` — cutting first-navigation latency after\n * the worker starts. Silently ignored where unsupported. Default `true`.\n */\n navigationPreload?: boolean;\n}\n\ninterface PrecacheManifest {\n version: string;\n urls: string[];\n}\n\nlet precacheName = \"\";\nconst precachedPaths = new Set<string>();\n\n/** Whether a cached response has aged past `maxAgeSeconds` (best-effort, via `Date`). */\nfunction isExpired(response: Response, maxAgeSeconds?: number): boolean {\n if (!maxAgeSeconds) return false;\n const dateHeader = response.headers.get(\"date\");\n if (!dateHeader) return false;\n const age = (Date.now() - new Date(dateHeader).getTime()) / 1000;\n return age > maxAgeSeconds;\n}\n\n/** Drop oldest entries until the cache holds at most `maxEntries`. */\nasync function trimCache(cacheName: string, maxEntries?: number): Promise<void> {\n if (!maxEntries) return;\n const cache = await caches.open(cacheName);\n const keys = await cache.keys();\n if (keys.length <= maxEntries) return;\n for (const key of keys.slice(0, keys.length - maxEntries)) {\n await cache.delete(key);\n }\n}\n\nasync function cacheFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n}\n\nasync function networkFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n\n const network = (async (): Promise<Response> => {\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })();\n\n const timeoutMs = (route.networkTimeoutSeconds ?? 0) * 1000;\n try {\n if (timeoutMs > 0) {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(\"network-timeout\")), timeoutMs),\n );\n return await Promise.race([network, timeout]);\n }\n return await network;\n } catch {\n const cached = await cache.match(request);\n if (cached) return cached;\n throw new Error(\"network-first: no network and no cache\");\n }\n}\n\nasync function staleWhileRevalidate(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n\n const network = fetch(request)\n .then(async (response) => {\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })\n .catch(() => undefined);\n\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n const response = await network;\n if (response) return response;\n if (cached) return cached;\n throw new Error(\"stale-while-revalidate: no network and no cache\");\n}\n\nfunction runRoute(request: Request, route: RuntimeRoute): Promise<Response> {\n switch (route.strategy) {\n case \"cache-first\":\n return cacheFirst(request, route);\n case \"network-first\":\n return networkFirst(request, route);\n case \"stale-while-revalidate\":\n return staleWhileRevalidate(request, route);\n }\n}\n\nfunction routeMatches(route: RuntimeRoute, url: URL, request: Request): boolean {\n return typeof route.match === \"function\"\n ? route.match(url, request)\n : route.match.test(url.href);\n}\n\n/**\n * Build a `206 Partial Content` response from a full one for an HTTP `Range`\n * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix\n * `bytes=-suffixLength`. Returns the original response when there is no usable\n * `Range` header, or a `416` when the range is unsatisfiable.\n *\n * @param request The incoming request (its `Range` header drives the slice).\n * @param response The full (200) response to slice.\n */\nexport async function createPartialResponse(\n request: Request,\n response: Response,\n): Promise<Response> {\n const rangeHeader = request.headers.get(\"range\");\n if (!rangeHeader) return response;\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match || (match[1] === \"\" && match[2] === \"\")) {\n return new Response(null, { status: 416, statusText: \"Range Not Satisfiable\" });\n }\n\n const buffer = await response.clone().arrayBuffer();\n const total = buffer.byteLength;\n\n let start: number;\n let end: number;\n if (match[1] === \"\") {\n // Suffix range: last N bytes.\n const suffix = Number(match[2]);\n start = Math.max(0, total - suffix);\n end = total - 1;\n } else {\n start = Number(match[1]);\n end = match[2] === \"\" ? total - 1 : Math.min(Number(match[2]), total - 1);\n }\n\n if (start > end || start >= total) {\n return new Response(null, {\n status: 416,\n statusText: \"Range Not Satisfiable\",\n headers: { \"Content-Range\": `bytes */${total}` },\n });\n }\n\n const slice = buffer.slice(start, end + 1);\n const headers = new Headers(response.headers);\n headers.set(\"Content-Range\", `bytes ${start}-${end}/${total}`);\n headers.set(\"Content-Length\", String(slice.byteLength));\n headers.set(\"Accept-Ranges\", \"bytes\");\n\n return new Response(slice, {\n status: 206,\n statusText: \"Partial Content\",\n headers,\n });\n}\n\n/**\n * Install a `fetch` handler that resolves matching `GET` requests with the\n * given runtime strategies. Non-matching requests are left untouched (no\n * `respondWith`), so a later {@link installPrecache} can handle them.\n *\n * Register this BEFORE `installPrecache` so specific routes win over the\n * precache catch-all.\n *\n * @param routes Ordered rules; the first whose `match` passes handles the request.\n */\nexport function installRuntimeCache(routes: RuntimeRoute[]): void {\n const sw = getSwScope();\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n const route = routes.find((candidate) => routeMatches(candidate, url, request));\n if (!route) return;\n\n if (route.rangeRequests && request.headers.has(\"range\")) {\n // Cache/serve the FULL resource once (no Range), then slice it.\n const full = new Request(request.url, {\n headers: stripRange(request.headers),\n credentials: request.credentials,\n mode: request.mode === \"navigate\" ? \"same-origin\" : request.mode,\n });\n event.respondWith(\n runRoute(full, route).then((response) => createPartialResponse(request, response)),\n );\n return;\n }\n\n event.respondWith(runRoute(request, route));\n });\n}\n\n/** Copy headers without the `Range` header (so the cached entry is the full file). */\nfunction stripRange(headers: Headers): Headers {\n const out = new Headers(headers);\n out.delete(\"range\");\n return out;\n}\n\n/**\n * Precache the app shell at `install` and serve it offline:\n * - reads `precache-manifest.json` (emitted by `tempestPwaManifest()`),\n * - caches every listed URL under a versioned cache,\n * - on `activate`, deletes stale precache versions and claims open clients,\n * - on `fetch`, serves precached assets cache-first and falls back to the\n * `navigateFallback` document for offline navigations (SPA routing).\n *\n * Same-origin only. Register this LAST, after any {@link installRuntimeCache}.\n */\nexport function installPrecache(options: InstallPrecacheOptions = {}): void {\n const sw = getSwScope();\n const {\n manifestUrl = \"/precache-manifest.json\",\n cacheName = \"tempest-precache\",\n navigateFallback = \"/index.html\",\n navigateFallbackDenylist = [],\n skipWaiting = true,\n navigationPreload = true,\n } = options;\n\n sw.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const response = await fetch(manifestUrl, { cache: \"no-cache\" });\n const manifest = (await response.json()) as PrecacheManifest;\n precacheName = `${cacheName}-${manifest.version}`;\n\n const cache = await caches.open(precacheName);\n await cache.addAll(manifest.urls);\n for (const url of manifest.urls) {\n precachedPaths.add(new URL(url, sw.location.origin).pathname);\n }\n if (skipWaiting) await sw.skipWaiting();\n })(),\n );\n });\n\n sw.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(`${cacheName}-`) && key !== precacheName)\n .map((key) => caches.delete(key)),\n );\n if (navigationPreload && sw.registration?.navigationPreload) {\n await sw.registration.navigationPreload.enable();\n }\n await sw.clients.claim();\n })(),\n );\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== sw.location.origin) return;\n\n // SPA navigations: serve the cached app shell when offline.\n if (request.mode === \"navigate\") {\n if (navigateFallbackDenylist.some((re) => re.test(url.pathname))) return;\n event.respondWith(\n (async () => {\n try {\n const preloaded = await event.preloadResponse;\n if (preloaded) return preloaded;\n return await fetch(request);\n } catch {\n const cache = await caches.open(precacheName);\n const shell = await cache.match(navigateFallback);\n if (shell) return shell;\n throw new Error(\"offline and no cached app shell\");\n }\n })(),\n );\n return;\n }\n\n // Precached static assets: cache-first.\n if (precachedPaths.has(url.pathname)) {\n event.respondWith(\n (async () => {\n const cache = await caches.open(precacheName);\n const cached = await cache.match(request);\n return cached ?? fetch(request);\n })(),\n );\n }\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cache.cjs","names":[],"sources":["../../src/sw/cache.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — precache, runtime strategies (cache-first, network-\n * first, stale-while-revalidate), expiry and the inspect/clear helpers all address\n * the same Cache Storage namespace, and the naming scheme that keeps them from\n * evicting each other is the file's whole contract.\n */\n/**\n * Service-worker caching helpers — a small, dependency-free subset of what\n * Workbox provides: precaching of the build's app shell (so the app launches\n * offline) plus runtime caching strategies for fonts, APIs and images.\n *\n * Import these inside your own `sw.ts`. They run in the service-worker global\n * scope, not the main thread. Pair `installPrecache` with the\n * `tempestPwaManifest()` Vite plugin (from `tempest-react-sdk/vite`), which\n * emits the `precache-manifest.json` this reads at install time.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installRuntimeCache, installPrecache } from \"tempest-react-sdk/sw\";\n *\n * // Register specific routes FIRST so they win over the precache catch-all.\n * installRuntimeCache([\n * { match: /\\/api\\//, strategy: \"network-first\", cacheName: \"tempest-api\", maxAgeSeconds: 300 },\n * ]);\n * installPrecache();\n */\n\nimport { credentialHeaderSet } from \"./credential-headers\";\n\n/** Minimal shape of the events we touch, to avoid pulling in `lib.webworker`. */\ninterface ExtendableEventLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface FetchEventLike extends ExtendableEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n /** Resolves to the navigation-preload response when it is enabled, else `undefined`. */\n preloadResponse?: Promise<Response | undefined>;\n}\n\n/** Minimal shape of `registration.navigationPreload` (Navigation Preload API). */\ninterface NavigationPreloadManagerLike {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\ninterface CacheSwGlobal {\n addEventListener(type: \"install\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"activate\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchEventLike) => void): void;\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n location: { origin: string };\n registration?: { navigationPreload?: NavigationPreloadManagerLike };\n}\n\nfunction getSwScope(): CacheSwGlobal {\n return globalThis as unknown as CacheSwGlobal;\n}\n\n/** Caching strategy for a runtime route. Mirrors the common Workbox trio. */\nexport type RuntimeStrategy = \"cache-first\" | \"network-first\" | \"stale-while-revalidate\";\n\n/** A single runtime-caching rule, matched against each `GET` request. */\nexport interface RuntimeRoute {\n /** A `RegExp` tested against the full URL, or a predicate over the parsed URL. */\n match: RegExp | ((url: URL, request: Request) => boolean);\n /** How to resolve a match. */\n strategy: RuntimeStrategy;\n /** Cache bucket name for this route. */\n cacheName: string;\n /** Trim the cache to at most this many entries (FIFO) after each write. */\n maxEntries?: number;\n /** Treat a cached response older than this (seconds) as a miss. */\n maxAgeSeconds?: number;\n /** For `network-first`: fall back to cache after this timeout (seconds). */\n networkTimeoutSeconds?: number;\n /**\n * Serve HTTP `Range` requests (206 Partial Content) by slicing the cached\n * full response. Enable for audio/video so seeking works offline. The full\n * resource is cached once (the `Range` header is stripped before caching).\n */\n rangeRequests?: boolean;\n /**\n * Opt in to caching requests that carry a credential header (see\n * `credentialHeaders`), and responses marked `Cache-Control: private`.\n * Default `false`.\n *\n * Cache Storage is keyed by URL and outlives the session: without this\n * gate, a `GET /api/me` stored for one account was served to the next\n * account on the same browser whenever the network failed or exceeded\n * `networkTimeoutSeconds` (reproduced in Chromium 153, 11/10/2026). With\n * the default, credentialed requests bypass the route entirely and reach\n * the network as if no service worker were installed.\n *\n * When enabled, every stored entry gets `Vary` on each credential header the\n * request carried, so the Cache API only matches it for the same credential —\n * another account (or no account) misses instead of reading it. A token\n * rotated while online simply repopulates the entry. Pair it with\n * `clearSessionData()` on logout, which removes the entries themselves.\n * `Cache-Control: no-store` is never cached, opt-in or not.\n *\n * A cookie session is invisible here: the worker cannot see `Cookie`, and\n * gating on `request.credentials` would switch off caching for every\n * same-origin image and font. A cookie-authenticated API stays private only\n * through `Cache-Control: private`/`no-store` on its responses and\n * `clearSessionData()` on logout.\n */\n cacheAuthenticated?: boolean;\n /**\n * Header names that mark a request as credentialed. Default\n * `DEFAULT_CREDENTIAL_HEADERS` (`authorization`, `x-api-key`,\n * `x-auth-token`, …) — the same list the background-sync queue strips.\n */\n credentialHeaders?: readonly string[];\n}\n\n/** Options for {@link installPrecache}. */\nexport interface InstallPrecacheOptions {\n /** URL of the manifest emitted by `tempestPwaManifest()`. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Cache name prefix; the manifest `version` is appended. Default `tempest-precache`. */\n cacheName?: string;\n /** App-shell document served for navigation requests offline. Default `/index.html`. */\n navigateFallback?: string;\n /** Navigation paths that should NOT use the fallback (e.g. `[/^\\/api\\//]`). */\n navigateFallbackDenylist?: RegExp[];\n /** Activate the new worker immediately after precaching. Default `true`. */\n skipWaiting?: boolean;\n /**\n * Enable the Navigation Preload API on `activate`, so the browser fetches\n * the navigation request in parallel with the worker boot and the handler\n * serves `event.preloadResponse` — cutting first-navigation latency after\n * the worker starts. Silently ignored where unsupported. Default `true`.\n */\n navigationPreload?: boolean;\n}\n\ninterface PrecacheManifest {\n version: string;\n urls: string[];\n}\n\nlet precacheName = \"\";\nconst precachedPaths = new Set<string>();\n\n/**\n * Header stamped on every runtime-cache entry with the epoch milliseconds of the\n * write. `Date` alone is not enough: it is not a CORS-safelisted response\n * header, so for a cross-origin API the worker reads `null` and `maxAgeSeconds`\n * would never expire anything.\n */\nconst CACHED_AT_HEADER = \"x-tempest-cached-at\";\n\n/**\n * Whether a cached response has aged past `maxAgeSeconds`. Reads the\n * {@link CACHED_AT_HEADER} stamp written by the runtime cache, and falls back to\n * `Date` for entries written before the stamp existed.\n */\nfunction isExpired(response: Response, maxAgeSeconds?: number): boolean {\n if (!maxAgeSeconds) return false;\n const stamp = Number(response.headers.get(CACHED_AT_HEADER));\n const dateHeader = response.headers.get(\"date\");\n const storedAt = stamp > 0 ? stamp : dateHeader ? new Date(dateHeader).getTime() : NaN;\n if (Number.isNaN(storedAt)) return false;\n return (Date.now() - storedAt) / 1000 > maxAgeSeconds;\n}\n\n/** The `Cache-Control` directives of a response, lower-cased. */\nfunction cacheControlDirectives(response: Response): string[] {\n return (response.headers.get(\"cache-control\") ?? \"\")\n .toLowerCase()\n .split(\",\")\n .map((directive) => directive.trim().split(\"=\")[0]);\n}\n\n/**\n * Whether a network response may be written to the route's cache: it must be\n * `ok`, never `no-store`, and `private` only under `cacheAuthenticated`.\n */\nfunction isStorable(response: Response, route: RuntimeRoute): boolean {\n if (!response.ok) return false;\n const directives = cacheControlDirectives(response);\n if (directives.includes(\"no-store\")) return false;\n return route.cacheAuthenticated === true || !directives.includes(\"private\");\n}\n\n/** The credential headers, lower-cased, that a request carries for its route. */\nfunction credentialsOn(request: Request, route: RuntimeRoute): string[] {\n return [...credentialHeaderSet(route.credentialHeaders)].filter((name) =>\n request.headers.has(name),\n );\n}\n\n/**\n * Write a response to the route's cache with the {@link CACHED_AT_HEADER} stamp\n * and, under `cacheAuthenticated`, `Vary` on each credential header the request\n * carried, so the entry only matches the credential that produced it. Skips\n * responses that {@link isStorable} rejects, then trims the cache to\n * `maxEntries`.\n */\nasync function store(\n cache: Cache,\n request: Request,\n response: Response,\n route: RuntimeRoute,\n): Promise<void> {\n if (!isStorable(response, route)) return;\n const headers = new Headers(response.headers);\n headers.set(CACHED_AT_HEADER, String(Date.now()));\n if (route.cacheAuthenticated) {\n const vary = headers.get(\"vary\");\n const varies = (vary ?? \"\")\n .toLowerCase()\n .split(\",\")\n .map((name) => name.trim());\n const missing = credentialsOn(request, route).filter((name) => !varies.includes(name));\n if (missing.length > 0 && !varies.includes(\"*\")) {\n headers.set(\"vary\", [vary, ...missing].filter(Boolean).join(\", \"));\n }\n }\n const copy = response.clone();\n await cache.put(\n request,\n new Response(copy.body, { status: copy.status, statusText: copy.statusText, headers }),\n );\n await trimCache(route.cacheName, route.maxEntries);\n}\n\n/** Drop oldest entries until the cache holds at most `maxEntries`. */\nasync function trimCache(cacheName: string, maxEntries?: number): Promise<void> {\n if (!maxEntries) return;\n const cache = await caches.open(cacheName);\n const keys = await cache.keys();\n if (keys.length <= maxEntries) return;\n for (const key of keys.slice(0, keys.length - maxEntries)) {\n await cache.delete(key);\n }\n}\n\nasync function cacheFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n\n const response = await fetch(request);\n await store(cache, request, response, route);\n return response;\n}\n\n/**\n * Network-first. When the timeout wins, the network request keeps running to\n * refresh the cache, and is handed to `extend` (`event.waitUntil`) so the\n * worker is not stopped mid-write.\n */\nasync function networkFirst(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n\n const network = (async (): Promise<Response> => {\n const response = await fetch(request);\n await store(cache, request, response, route);\n return response;\n })();\n\n const timeoutMs = (route.networkTimeoutSeconds ?? 0) * 1000;\n try {\n if (timeoutMs > 0) {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => {\n extend(network.catch(() => undefined));\n reject(new Error(\"network-timeout\"));\n }, timeoutMs),\n );\n return await Promise.race([network, timeout]);\n }\n return await network;\n } catch {\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n throw new Error(\"network-first: no network and no cache\");\n }\n}\n\n/**\n * Stale-while-revalidate. A fresh cached response is served at once, and the\n * revalidation is handed to `extend` (`event.waitUntil`): without it the\n * browser may stop the worker before the refreshed response is written.\n */\nasync function staleWhileRevalidate(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n\n const network = fetch(request)\n .then(async (response) => {\n await store(cache, request, response, route);\n return response;\n })\n .catch(() => undefined);\n\n const fresh = cached && !isExpired(cached, route.maxAgeSeconds) ? cached : undefined;\n if (fresh) {\n extend(network);\n return fresh;\n }\n const response = await network;\n if (response) return response;\n throw new Error(\"stale-while-revalidate: no network and no cache\");\n}\n\nfunction runRoute(\n request: Request,\n route: RuntimeRoute,\n extend: (promise: Promise<unknown>) => void,\n): Promise<Response> {\n switch (route.strategy) {\n case \"cache-first\":\n return cacheFirst(request, route);\n case \"network-first\":\n return networkFirst(request, route, extend);\n case \"stale-while-revalidate\":\n return staleWhileRevalidate(request, route, extend);\n }\n}\n\nfunction routeMatches(route: RuntimeRoute, url: URL, request: Request): boolean {\n return typeof route.match === \"function\"\n ? route.match(url, request)\n : route.match.test(url.href);\n}\n\n/**\n * Build a `206 Partial Content` response from a full one for an HTTP `Range`\n * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix\n * `bytes=-suffixLength` (the last N bytes). Returns the original response when there is no usable\n * `Range` header, or a `416` when the range is unsatisfiable.\n *\n * @param request The incoming request (its `Range` header drives the slice).\n * @param response The full (200) response to slice.\n */\nexport async function createPartialResponse(\n request: Request,\n response: Response,\n): Promise<Response> {\n const rangeHeader = request.headers.get(\"range\");\n if (!rangeHeader) return response;\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match || (match[1] === \"\" && match[2] === \"\")) {\n return new Response(null, { status: 416, statusText: \"Range Not Satisfiable\" });\n }\n\n const buffer = await response.clone().arrayBuffer();\n const total = buffer.byteLength;\n\n let start: number;\n let end: number;\n if (match[1] === \"\") {\n const suffix = Number(match[2]);\n start = Math.max(0, total - suffix);\n end = total - 1;\n } else {\n start = Number(match[1]);\n end = match[2] === \"\" ? total - 1 : Math.min(Number(match[2]), total - 1);\n }\n\n if (start > end || start >= total) {\n return new Response(null, {\n status: 416,\n statusText: \"Range Not Satisfiable\",\n headers: { \"Content-Range\": `bytes */${total}` },\n });\n }\n\n const slice = buffer.slice(start, end + 1);\n const headers = new Headers(response.headers);\n headers.set(\"Content-Range\", `bytes ${start}-${end}/${total}`);\n headers.set(\"Content-Length\", String(slice.byteLength));\n headers.set(\"Accept-Ranges\", \"bytes\");\n\n return new Response(slice, {\n status: 206,\n statusText: \"Partial Content\",\n headers,\n });\n}\n\n/**\n * Install a `fetch` handler that resolves matching `GET` requests with the\n * given runtime strategies. Non-matching requests are left untouched (no\n * `respondWith`), so a later {@link installPrecache} can handle them.\n *\n * A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the\n * route's `credentialHeaders`) is also left untouched unless its route sets\n * `cacheAuthenticated`, and a response with `Cache-Control: no-store`\n * (or `private`, without that opt-in) is served but never stored. `maxAgeSeconds`\n * applies to every read, including the offline fallback: an expired entry is a\n * miss, never a last resort. Under `rangeRequests`, the full resource is cached\n * once (the `Range` header stripped) and each range is sliced from it.\n *\n * Register this BEFORE `installPrecache` so specific routes win over the\n * precache catch-all.\n *\n * @param routes Ordered rules; the first whose `match` passes handles the request.\n */\nexport function installRuntimeCache(routes: RuntimeRoute[]): void {\n const sw = getSwScope();\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n const route = routes.find((candidate) => routeMatches(candidate, url, request));\n if (!route) return;\n if (!route.cacheAuthenticated && credentialsOn(request, route).length > 0) return;\n const extend = (promise: Promise<unknown>): void => event.waitUntil(promise);\n\n if (route.rangeRequests && request.headers.has(\"range\")) {\n const full = new Request(request.url, {\n headers: stripRange(request.headers),\n credentials: request.credentials,\n mode: request.mode === \"navigate\" ? \"same-origin\" : request.mode,\n });\n event.respondWith(\n runRoute(full, route, extend).then((response) =>\n createPartialResponse(request, response),\n ),\n );\n return;\n }\n\n event.respondWith(runRoute(request, route, extend));\n });\n}\n\n/** Copy headers without the `Range` header (so the cached entry is the full file). */\nfunction stripRange(headers: Headers): Headers {\n const out = new Headers(headers);\n out.delete(\"range\");\n return out;\n}\n\n/**\n * Precache the app shell at `install` and serve it offline:\n * - reads `precache-manifest.json` (emitted by `tempestPwaManifest()`),\n * - caches every listed URL under a versioned cache,\n * - on `activate`, deletes stale precache versions and claims open clients,\n * - on `fetch`, serves precached assets cache-first and falls back to the\n * `navigateFallback` document for offline navigations (SPA routing).\n *\n * Same-origin only. Register this LAST, after any {@link installRuntimeCache}.\n */\nexport function installPrecache(options: InstallPrecacheOptions = {}): void {\n const sw = getSwScope();\n const {\n manifestUrl = \"/precache-manifest.json\",\n cacheName = \"tempest-precache\",\n navigateFallback = \"/index.html\",\n navigateFallbackDenylist = [],\n skipWaiting = true,\n navigationPreload = true,\n } = options;\n\n sw.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const response = await fetch(manifestUrl, { cache: \"no-cache\" });\n const manifest = (await response.json()) as PrecacheManifest;\n precacheName = `${cacheName}-${manifest.version}`;\n\n const cache = await caches.open(precacheName);\n await cache.addAll(manifest.urls);\n for (const url of manifest.urls) {\n precachedPaths.add(new URL(url, sw.location.origin).pathname);\n }\n if (skipWaiting) await sw.skipWaiting();\n })(),\n );\n });\n\n sw.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(`${cacheName}-`) && key !== precacheName)\n .map((key) => caches.delete(key)),\n );\n if (navigationPreload && sw.registration?.navigationPreload) {\n await sw.registration.navigationPreload.enable();\n }\n await sw.clients.claim();\n })(),\n );\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== sw.location.origin) return;\n\n // SPA navigations: serve the cached app shell when offline.\n if (request.mode === \"navigate\") {\n if (navigateFallbackDenylist.some((re) => re.test(url.pathname))) return;\n event.respondWith(\n (async () => {\n try {\n const preloaded = await event.preloadResponse;\n if (preloaded) return preloaded;\n return await fetch(request);\n } catch {\n const cache = await caches.open(precacheName);\n const shell = await cache.match(navigateFallback);\n if (shell) return shell;\n throw new Error(\"offline and no cached app shell\");\n }\n })(),\n );\n return;\n }\n\n // Precached static assets: cache-first.\n if (precachedPaths.has(url.pathname)) {\n event.respondWith(\n (async () => {\n const cache = await caches.open(precacheName);\n const cached = await cache.match(request);\n return cached ?? fetch(request);\n })(),\n );\n }\n });\n}\n"],"mappings":"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"}
|
package/dist/sw/cache.js
CHANGED
|
@@ -1,60 +1,87 @@
|
|
|
1
|
+
import { credentialHeaderSet as e } from "./credential-headers.js";
|
|
1
2
|
//#region src/sw/cache.ts
|
|
2
|
-
function
|
|
3
|
+
function t() {
|
|
3
4
|
return globalThis;
|
|
4
5
|
}
|
|
5
|
-
var
|
|
6
|
-
function
|
|
6
|
+
var n = "", r = /* @__PURE__ */ new Set(), i = "x-tempest-cached-at";
|
|
7
|
+
function a(e, t) {
|
|
7
8
|
if (!t) return !1;
|
|
8
|
-
let n = e.headers.get("date");
|
|
9
|
-
return
|
|
9
|
+
let n = Number(e.headers.get(i)), r = e.headers.get("date"), a = n > 0 ? n : r ? new Date(r).getTime() : NaN;
|
|
10
|
+
return !Number.isNaN(a) && (Date.now() - a) / 1e3 > t;
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
+
function o(e) {
|
|
13
|
+
return (e.headers.get("cache-control") ?? "").toLowerCase().split(",").map((e) => e.trim().split("=")[0]);
|
|
14
|
+
}
|
|
15
|
+
function s(e, t) {
|
|
16
|
+
if (!e.ok) return !1;
|
|
17
|
+
let n = o(e);
|
|
18
|
+
return n.includes("no-store") ? !1 : t.cacheAuthenticated === !0 || !n.includes("private");
|
|
19
|
+
}
|
|
20
|
+
function c(t, n) {
|
|
21
|
+
return [...e(n.credentialHeaders)].filter((e) => t.headers.has(e));
|
|
22
|
+
}
|
|
23
|
+
async function l(e, t, n, r) {
|
|
24
|
+
if (!s(n, r)) return;
|
|
25
|
+
let a = new Headers(n.headers);
|
|
26
|
+
if (a.set(i, String(Date.now())), r.cacheAuthenticated) {
|
|
27
|
+
let e = a.get("vary"), n = (e ?? "").toLowerCase().split(",").map((e) => e.trim()), i = c(t, r).filter((e) => !n.includes(e));
|
|
28
|
+
i.length > 0 && !n.includes("*") && a.set("vary", [e, ...i].filter(Boolean).join(", "));
|
|
29
|
+
}
|
|
30
|
+
let o = n.clone();
|
|
31
|
+
await e.put(t, new Response(o.body, {
|
|
32
|
+
status: o.status,
|
|
33
|
+
statusText: o.statusText,
|
|
34
|
+
headers: a
|
|
35
|
+
})), await u(r.cacheName, r.maxEntries);
|
|
36
|
+
}
|
|
37
|
+
async function u(e, t) {
|
|
12
38
|
if (!t) return;
|
|
13
39
|
let n = await caches.open(e), r = await n.keys();
|
|
14
40
|
if (!(r.length <= t)) for (let e of r.slice(0, r.length - t)) await n.delete(e);
|
|
15
41
|
}
|
|
16
|
-
async function
|
|
17
|
-
let n = await caches.open(t.cacheName),
|
|
18
|
-
if (
|
|
19
|
-
let
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
async function
|
|
23
|
-
let
|
|
24
|
-
let
|
|
25
|
-
return
|
|
26
|
-
})(),
|
|
42
|
+
async function d(e, t) {
|
|
43
|
+
let n = await caches.open(t.cacheName), r = await n.match(e);
|
|
44
|
+
if (r && !a(r, t.maxAgeSeconds)) return r;
|
|
45
|
+
let i = await fetch(e);
|
|
46
|
+
return await l(n, e, i, t), i;
|
|
47
|
+
}
|
|
48
|
+
async function f(e, t, n) {
|
|
49
|
+
let r = await caches.open(t.cacheName), i = (async () => {
|
|
50
|
+
let n = await fetch(e);
|
|
51
|
+
return await l(r, e, n, t), n;
|
|
52
|
+
})(), o = (t.networkTimeoutSeconds ?? 0) * 1e3;
|
|
27
53
|
try {
|
|
28
|
-
if (
|
|
29
|
-
let e = new Promise((e, t) => setTimeout(() =>
|
|
30
|
-
|
|
54
|
+
if (o > 0) {
|
|
55
|
+
let e = new Promise((e, t) => setTimeout(() => {
|
|
56
|
+
n(i.catch(() => void 0)), t(/* @__PURE__ */ Error("network-timeout"));
|
|
57
|
+
}, o));
|
|
58
|
+
return await Promise.race([i, e]);
|
|
31
59
|
}
|
|
32
|
-
return await
|
|
60
|
+
return await i;
|
|
33
61
|
} catch {
|
|
34
|
-
let
|
|
35
|
-
if (t) return
|
|
62
|
+
let n = await r.match(e);
|
|
63
|
+
if (n && !a(n, t.maxAgeSeconds)) return n;
|
|
36
64
|
throw Error("network-first: no network and no cache");
|
|
37
65
|
}
|
|
38
66
|
}
|
|
39
|
-
async function
|
|
40
|
-
let
|
|
41
|
-
if (
|
|
42
|
-
let
|
|
43
|
-
if (
|
|
44
|
-
if (a) return a;
|
|
67
|
+
async function p(e, t, n) {
|
|
68
|
+
let r = await caches.open(t.cacheName), i = await r.match(e), o = fetch(e).then(async (n) => (await l(r, e, n, t), n)).catch(() => void 0), s = i && !a(i, t.maxAgeSeconds) ? i : void 0;
|
|
69
|
+
if (s) return n(o), s;
|
|
70
|
+
let c = await o;
|
|
71
|
+
if (c) return c;
|
|
45
72
|
throw Error("stale-while-revalidate: no network and no cache");
|
|
46
73
|
}
|
|
47
|
-
function
|
|
74
|
+
function m(e, t, n) {
|
|
48
75
|
switch (t.strategy) {
|
|
49
|
-
case "cache-first": return
|
|
50
|
-
case "network-first": return
|
|
51
|
-
case "stale-while-revalidate": return
|
|
76
|
+
case "cache-first": return d(e, t);
|
|
77
|
+
case "network-first": return f(e, t, n);
|
|
78
|
+
case "stale-while-revalidate": return p(e, t, n);
|
|
52
79
|
}
|
|
53
80
|
}
|
|
54
|
-
function
|
|
81
|
+
function h(e, t, n) {
|
|
55
82
|
return typeof e.match == "function" ? e.match(t, n) : e.match.test(t.href);
|
|
56
83
|
}
|
|
57
|
-
async function
|
|
84
|
+
async function g(e, t) {
|
|
58
85
|
let n = e.headers.get("range");
|
|
59
86
|
if (!n) return t;
|
|
60
87
|
let r = /^bytes=(\d*)-(\d*)$/.exec(n.trim());
|
|
@@ -79,66 +106,66 @@ async function u(e, t) {
|
|
|
79
106
|
headers: l
|
|
80
107
|
});
|
|
81
108
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
let n =
|
|
109
|
+
function _(e) {
|
|
110
|
+
t().addEventListener("fetch", (t) => {
|
|
111
|
+
let n = t.request;
|
|
85
112
|
if (n.method !== "GET") return;
|
|
86
|
-
let r = new URL(n.url), i =
|
|
87
|
-
if (i)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
e.respondWith(c(n, i));
|
|
113
|
+
let r = new URL(n.url), i = e.find((e) => h(e, r, n));
|
|
114
|
+
if (!i || !i.cacheAuthenticated && c(n, i).length > 0) return;
|
|
115
|
+
let a = (e) => t.waitUntil(e);
|
|
116
|
+
if (i.rangeRequests && n.headers.has("range")) {
|
|
117
|
+
let e = new Request(n.url, {
|
|
118
|
+
headers: v(n.headers),
|
|
119
|
+
credentials: n.credentials,
|
|
120
|
+
mode: n.mode === "navigate" ? "same-origin" : n.mode
|
|
121
|
+
});
|
|
122
|
+
t.respondWith(m(e, i, a).then((e) => g(n, e)));
|
|
123
|
+
return;
|
|
98
124
|
}
|
|
125
|
+
t.respondWith(m(n, i, a));
|
|
99
126
|
});
|
|
100
127
|
}
|
|
101
|
-
function
|
|
128
|
+
function v(e) {
|
|
102
129
|
let t = new Headers(e);
|
|
103
130
|
return t.delete("range"), t;
|
|
104
131
|
}
|
|
105
|
-
function
|
|
106
|
-
let i =
|
|
132
|
+
function y(e = {}) {
|
|
133
|
+
let i = t(), { manifestUrl: a = "/precache-manifest.json", cacheName: o = "tempest-precache", navigateFallback: s = "/index.html", navigateFallbackDenylist: c = [], skipWaiting: l = !0, navigationPreload: u = !0 } = e;
|
|
107
134
|
i.addEventListener("install", (e) => {
|
|
108
135
|
e.waitUntil((async () => {
|
|
109
136
|
let e = await (await fetch(a, { cache: "no-cache" })).json();
|
|
110
|
-
|
|
111
|
-
for (let t of e.urls)
|
|
137
|
+
n = `${o}-${e.version}`, await (await caches.open(n)).addAll(e.urls);
|
|
138
|
+
for (let t of e.urls) r.add(new URL(t, i.location.origin).pathname);
|
|
112
139
|
l && await i.skipWaiting();
|
|
113
140
|
})());
|
|
114
141
|
}), i.addEventListener("activate", (e) => {
|
|
115
142
|
e.waitUntil((async () => {
|
|
116
143
|
let e = await caches.keys();
|
|
117
|
-
await Promise.all(e.filter((e) => e.startsWith(`${o}-`) && e !==
|
|
144
|
+
await Promise.all(e.filter((e) => e.startsWith(`${o}-`) && e !== n).map((e) => caches.delete(e))), u && i.registration?.navigationPreload && await i.registration.navigationPreload.enable(), await i.clients.claim();
|
|
118
145
|
})());
|
|
119
146
|
}), i.addEventListener("fetch", (e) => {
|
|
120
|
-
let
|
|
121
|
-
if (
|
|
122
|
-
let a = new URL(
|
|
147
|
+
let t = e.request;
|
|
148
|
+
if (t.method !== "GET") return;
|
|
149
|
+
let a = new URL(t.url);
|
|
123
150
|
if (a.origin === i.location.origin) {
|
|
124
|
-
if (
|
|
151
|
+
if (t.mode === "navigate") {
|
|
125
152
|
if (c.some((e) => e.test(a.pathname))) return;
|
|
126
153
|
e.respondWith((async () => {
|
|
127
154
|
try {
|
|
128
|
-
return await e.preloadResponse || await fetch(
|
|
155
|
+
return await e.preloadResponse || await fetch(t);
|
|
129
156
|
} catch {
|
|
130
|
-
let e = await (await caches.open(
|
|
157
|
+
let e = await (await caches.open(n)).match(s);
|
|
131
158
|
if (e) return e;
|
|
132
159
|
throw Error("offline and no cached app shell");
|
|
133
160
|
}
|
|
134
161
|
})());
|
|
135
162
|
return;
|
|
136
163
|
}
|
|
137
|
-
|
|
164
|
+
r.has(a.pathname) && e.respondWith((async () => await (await caches.open(n)).match(t) ?? fetch(t))());
|
|
138
165
|
}
|
|
139
166
|
});
|
|
140
167
|
}
|
|
141
168
|
//#endregion
|
|
142
|
-
export {
|
|
169
|
+
export { g as createPartialResponse, y as installPrecache, _ as installRuntimeCache };
|
|
143
170
|
|
|
144
171
|
//# sourceMappingURL=cache.js.map
|