tempest-react-sdk 0.82.0 → 0.83.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 +6 -6
- package/bin/lib/openapi/generate.hostile.test.mjs +91 -0
- package/bin/lib/openapi/generate.mjs +42 -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 +135 -132
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +124 -75
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js +100 -44
- package/dist/http/upload-with-progress.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 +57 -44
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +82 -32
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw.d.ts +21 -1
- package/dist/tempest-react-sdk.d.ts +90 -6
- package/dist/vite/base-url.cjs +1 -1
- package/dist/vite/base-url.cjs.map +1 -1
- package/dist/vite/base-url.js +5 -1
- package/dist/vite/base-url.js.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.js +96 -31
- package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
- package/dist/vite/tempest-pwa-manifest.cjs +2 -2
- package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
- package/dist/vite/tempest-pwa-manifest.js +32 -16
- package/dist/vite/tempest-pwa-manifest.js.map +1 -1
- package/dist/vite.cjs +1 -1
- package/dist/vite.d.ts +63 -12
- package/dist/vite.js +4 -4
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-with-progress.js","names":["parseErrorBody","raw","uploadWithProgress","options","signal","xhr","method","url","withCredentials","token","getToken","sentRequestId","requestId","randomId","finalHeaders","headers","scope","credentialOrigin","isTrustedCredentialTarget","trustedOrigins","reportSuppressedCredential","csrfHeaders","key","value","onProgress","event","handleAbort","isSuccess","contentType","errorBody","TempestApiError","buildApiError","name","parser","err","reject","body"],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — XMLHttpRequest is used instead of fetch precisely\n * because it reports upload progress, and its API is event-based: the body wires\n * load, error, abort, timeout and progress against one request object, plus the\n * AbortSignal bridge.\n */\nimport { randomId } from \"../utils\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport type { CsrfOptions } from \"./csrf\";\nimport { buildApiError, TempestApiError } from \"./errors\";\n\nexport interface UploadProgressEvent {\n /** Bytes already uploaded. */\n loaded: number;\n /** Total payload size in bytes — only meaningful when `lengthComputable` is true. */\n total: number;\n /** Fraction between 0 and 1, or null when total is unknown. */\n fraction: number | null;\n lengthComputable: boolean;\n}\n\nexport interface UploadWithProgressOptions {\n url: string;\n body: FormData | Blob | File;\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n headers?: Record<string, string>;\n /** Returns the current bearer token. */\n getToken?: () => string | null | undefined;\n /**\n * Origin the `getToken` credential belongs to, normally the API's.\n * Defaults to the page's own origin (`location.origin`).\n *\n * Unlike `createApiClient`, this helper has no base URL to infer a scope\n * from — the caller names the destination on every call, and the `url` can\n * come from somewhere other than your own code (a signed URL the API handed\n * back, a value from a response body). Until 0.82.0 an unset scope sent the\n * token wherever `url` pointed; it now starts closed, like the CSRF token\n * always did, and an API on another origin sets this to receive it (#530).\n *\n * Outside a browsing context there is no page origin, and the scope falls\n * back to `url` itself.\n */\n credentialOrigin?: string;\n /**\n * Extra origins allowed to receive the credential, on top of\n * {@link credentialOrigin} (or the page origin it defaults to).\n */\n trustedOrigins?: readonly string[];\n /** Send cookies. Defaults to false. */\n withCredentials?: boolean;\n /**\n * Send a CSRF token with the upload, as `createApiClient`'s `csrf` does.\n * Off by default; `true` reads the `csrf_token` cookie into `X-CSRF-Token`.\n *\n * Scoped exactly like the bearer token: to {@link credentialOrigin} (the\n * page's own origin when unset) plus {@link trustedOrigins}. An API on\n * another origin sets `credentialOrigin` to receive the token.\n */\n csrf?: boolean | CsrfOptions;\n /** Called on every `progress` event from the XHR upload channel. */\n onProgress?: (event: UploadProgressEvent) => void;\n /** Abort the request. */\n signal?: AbortSignal;\n /** Override the JSON parser. Defaults to `JSON.parse`. */\n parser?: (raw: string) => unknown;\n /**\n * Per-request correlation id sent as `X-Request-ID` (Tempest convention).\n * Defaults to a generated id. Return an empty string to disable.\n */\n requestId?: () => string;\n}\n\nfunction parseErrorBody(raw: string): unknown {\n if (!raw) return null;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Upload a file (or any payload) with byte-level progress reporting.\n *\n * `fetch` cannot report upload progress in browsers, so this helper falls\n * back to `XMLHttpRequest`. It mirrors the error contract used by\n * {@link createApiClient}: non-2xx responses reject with a `TempestApiError`.\n *\n * @returns The parsed JSON response, or the raw text when the response is not JSON.\n */\nexport function uploadWithProgress<T = unknown>(options: UploadWithProgressOptions): Promise<T> {\n const {\n url,\n body,\n method = \"POST\",\n headers = {},\n getToken,\n credentialOrigin,\n trustedOrigins,\n withCredentials = false,\n csrf,\n onProgress,\n signal,\n parser = JSON.parse,\n requestId,\n } = options;\n\n return new Promise<T>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.open(method, url);\n xhr.withCredentials = withCredentials;\n\n const token = getToken?.();\n const sentRequestId = requestId ? requestId() : randomId();\n const finalHeaders: Record<string, string> = { ...headers };\n const scope = credentialOrigin ?? globalThis.location?.origin ?? url;\n if (token && !(\"Authorization\" in finalHeaders)) {\n if (isTrustedCredentialTarget(url, scope, trustedOrigins)) {\n finalHeaders.Authorization = `Bearer ${token}`;\n } else {\n reportSuppressedCredential(url, scope);\n }\n }\n Object.assign(\n finalHeaders,\n csrfHeaders({\n method,\n url,\n reference: scope,\n csrf,\n trustedOrigins,\n headers,\n }),\n );\n if (sentRequestId && !(\"X-Request-ID\" in finalHeaders)) {\n finalHeaders[\"X-Request-ID\"] = sentRequestId;\n }\n for (const [key, value] of Object.entries(finalHeaders)) {\n xhr.setRequestHeader(key, value);\n }\n\n if (onProgress) {\n xhr.upload.onprogress = (event) => {\n onProgress({\n loaded: event.loaded,\n total: event.total,\n fraction: event.lengthComputable ? event.loaded / event.total : null,\n lengthComputable: event.lengthComputable,\n });\n };\n }\n\n function handleAbort(): void {\n xhr.abort();\n }\n signal?.addEventListener(\"abort\", handleAbort);\n\n xhr.onload = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n const isSuccess = xhr.status >= 200 && xhr.status < 300;\n const contentType = xhr.getResponseHeader(\"content-type\") ?? \"\";\n\n if (!isSuccess) {\n const errorBody = parseErrorBody(xhr.responseText);\n reject(\n new TempestApiError(\n buildApiError(\n xhr.status,\n errorBody,\n { get: (name) => xhr.getResponseHeader(name) },\n sentRequestId,\n ),\n ),\n );\n return;\n }\n\n if (xhr.status === 204 || !xhr.responseText) {\n resolve(undefined as T);\n return;\n }\n\n if (contentType.includes(\"application/json\")) {\n try {\n resolve(parser(xhr.responseText) as T);\n } catch (err) {\n reject(err);\n }\n } else {\n resolve(xhr.responseText as unknown as T);\n }\n };\n\n xhr.onerror = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: sentRequestId || undefined,\n }),\n );\n };\n\n xhr.onabort = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n\n xhr.send(body);\n });\n}\n"],"mappings":";;;;;AAyEA,SAASA,EAAe,GAAsB;CAC1C,IAAI,CAACC,GAAK,OAAO;CACjB,IAAI;EACA,OAAO,KAAK,MAAMA,CAAG;CACzB,QAAQ;EACJ,OAAOA;CACX;AACJ;AAWA,SAAgBC,EAAgC,GAAgD;CAC5F,IAAM,EACF,QACA,SACA,YAAS,QACT,aAAU,CAAC,GACX,aACA,qBACA,mBACA,qBAAkB,IAClB,SACA,eACA,WACA,YAAS,KAAK,OACd,iBACAC;CAEJ,OAAO,IAAI,SAAY,GAAS,MAAW;EACvC,IAAIC,GAAQ,SAAS;GACjB,EAAO,IAAI,aAAa,WAAW,YAAY,CAAC;GAChD;EACJ;EAEA,IAAMC,IAAM,IAAI,eAAe;EAE/B,AADAA,EAAI,KAAKC,GAAQC,CAAG,GACpB,EAAI,kBAAkBC;EAEtB,IAAMC,IAAQC,IAAW,GACnBC,IAAgBC,IAAYA,EAAU,IAAIC,EAAS,GACnDC,IAAuC,EAAE,GAAGC,EAAQ,GACpDC,IAAQC,KAAoB,WAAW,UAAU,UAAUV;EAmBjE,AAlBIE,KAAS,EAAE,mBAAmBK,OAC1BI,EAA0BX,GAAKS,GAAOG,CAAc,IACpD,EAAa,gBAAgB,UAAUV,MAEvCW,EAA2Bb,GAAKS,CAAK,IAG7C,OAAO,OACHF,GACAO,EAAY;GACR;GACA;GACA,WAAWL;GACX;GACA;GACA;EACJ,CAAC,CACL,GACIL,KAAiB,EAAE,kBAAkBG,OACrC,EAAa,kBAAkBH;EAEnC,KAAK,IAAM,CAACW,GAAKC,MAAU,OAAO,QAAQT,CAAY,GAClD,EAAI,iBAAiBQ,GAAKC,CAAK;EAGnC,AAAIC,MACA,EAAI,OAAO,cAAc,MAAU;GAC/B,EAAW;IACP,QAAQC,EAAM;IACd,OAAOA,EAAM;IACb,UAAUA,EAAM,mBAAmBA,EAAM,SAASA,EAAM,QAAQ;IAChE,kBAAkBA,EAAM;GAC5B,CAAC;EACL;EAGJ,SAASC,IAAoB;GACzB,EAAI,MAAM;EACd;EAuDA,AAtDAtB,GAAQ,iBAAiB,SAASsB,CAAW,GAE7C,EAAI,eAAe;GACf,GAAQ,oBAAoB,SAASA,CAAW;GAChD,IAAMC,IAAYtB,EAAI,UAAU,OAAOA,EAAI,SAAS,KAC9CuB,IAAcvB,EAAI,kBAAkB,cAAc,KAAK;GAE7D,IAAI,CAACsB,GAAW;IACZ,IAAME,IAAY7B,EAAeK,EAAI,YAAY;IACjD,EACI,IAAIyB,EACAC,EACI1B,EAAI,QACJwB,GACA,EAAE,MAAM,MAASxB,EAAI,kBAAkB2B,CAAI,EAAE,GAC7CrB,CACJ,CACJ,CACJ;GAEJ,OAEA,IAAIN,EAAI,WAAW,OAAO,CAACA,EAAI,cAC3B,EAAQ,KAAA,CAAc;QAI1B,IAAIuB,EAAY,SAAS,kBAAkB,GACvC,IAAI;IACA,EAAQK,EAAO5B,EAAI,YAAY,CAAM;GACzC,SAAS6B,GAAK;IACV,EAAOA,CAAG;GACd;QAEA,EAAQ7B,EAAI,YAA4B;EAEhD,GAEA,EAAI,gBAAgB;GAEhB,AADAD,GAAQ,oBAAoB,SAASsB,CAAW,GAChDS,EACI,IAAIL,EAAgB;IAChB,QAAQ;IACR,QAAQ;IACR,WAAWnB,KAAiB,KAAA;GAChC,CAAC,CACL;EACJ,GAEA,EAAI,gBAAgB;GAEhB,AADAP,GAAQ,oBAAoB,SAASsB,CAAW,GAChDS,EAAO,IAAI,aAAa,WAAW,YAAY,CAAC;EACpD,GAEA9B,EAAI,KAAK+B,CAAI;CACjB,CAAC;AACL"}
|
|
1
|
+
{"version":3,"file":"upload-with-progress.js","names":["UPLOAD_REDIRECT_REFUSED","parseErrorBody","raw","settle","answer","TempestApiError","buildApiError","requestId","parser","originOf","resolved","resolveRequestUrl","url","sendWithoutRedirects","response","init","name","uploadWithProgress","options","signal","token","getToken","sentRequestId","randomId","finalHeaders","headers","scope","credentialOrigin","isTrustedCredentialTarget","trustedOrigins","reportSuppressedCredential","csrfHeaders","redirect","sized","body","total","onProgress","xhr","method","withCredentials","key","value","event","handleAbort","landed","err","reject"],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — XMLHttpRequest is used instead of fetch precisely\n * because it reports upload progress, and its API is event-based: the body wires\n * load, error, abort, timeout and progress against one request object, plus the\n * AbortSignal bridge.\n */\nimport { randomId } from \"../utils\";\nimport {\n isTrustedCredentialTarget,\n reportSuppressedCredential,\n resolveRequestUrl,\n} from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport type { CsrfOptions } from \"./csrf\";\nimport { buildApiError, TempestApiError } from \"./errors\";\n\nexport interface UploadProgressEvent {\n /** Bytes already uploaded. */\n loaded: number;\n /** Total payload size in bytes — only meaningful when `lengthComputable` is true. */\n total: number;\n /** Fraction between 0 and 1, or null when total is unknown. */\n fraction: number | null;\n lengthComputable: boolean;\n}\n\nexport interface UploadWithProgressOptions {\n url: string;\n body: FormData | Blob | File;\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n headers?: Record<string, string>;\n /** Returns the current bearer token. */\n getToken?: () => string | null | undefined;\n /**\n * Origin the `getToken` credential belongs to, normally the API's.\n * Defaults to the page's own origin (`location.origin`).\n *\n * Unlike `createApiClient`, this helper has no base URL to infer a scope\n * from — the caller names the destination on every call, and the `url` can\n * come from somewhere other than your own code (a signed URL the API handed\n * back, a value from a response body). Until 0.82.0 an unset scope sent the\n * token wherever `url` pointed; it now starts closed, like the CSRF token\n * always did, and an API on another origin sets this to receive it (#530).\n *\n * Outside a browsing context there is no page origin, and the scope falls\n * back to `url` itself.\n */\n credentialOrigin?: string;\n /**\n * Extra origins allowed to receive the credential, on top of\n * {@link credentialOrigin} (or the page origin it defaults to).\n */\n trustedOrigins?: readonly string[];\n /** Send cookies. Defaults to false. */\n withCredentials?: boolean;\n /**\n * Send a CSRF token with the upload, as `createApiClient`'s `csrf` does.\n * Off by default; `true` reads the `csrf_token` cookie into `X-CSRF-Token`.\n *\n * Scoped exactly like the bearer token: to {@link credentialOrigin} (the\n * page's own origin when unset) plus {@link trustedOrigins}. An API on\n * another origin sets `credentialOrigin` to receive the token.\n */\n csrf?: boolean | CsrfOptions;\n /** Called on every `progress` event from the XHR upload channel. */\n onProgress?: (event: UploadProgressEvent) => void;\n /** Abort the request. */\n signal?: AbortSignal;\n /** Override the JSON parser. Defaults to `JSON.parse`. */\n parser?: (raw: string) => unknown;\n /**\n * Per-request correlation id sent as `X-Request-ID` (Tempest convention).\n * Defaults to a generated id. Return an empty string to disable.\n */\n requestId?: () => string;\n /**\n * What to do when the server answers with a redirect. Default `\"follow\"`.\n *\n * - `\"follow\"` — the `XMLHttpRequest` path, with byte-level progress. The\n * browser follows every redirect and offers no way to stop it: measured\n * in Chrome 150 (#570), a same-origin upload answered `301`/`302`/`303`/\n * `307`/`308` towards another origin delivered `X-Api-Key` from\n * {@link headers} and the SDK's own `X-CSRF-Token` there on all five, and\n * the body on `307`/`308`; only `Authorization` was stripped. What this\n * path does enforce (since 0.83.0): an answer from another origin than\n * the one the request was sent to is never the result — it rejects with\n * `code: \"REDIRECT_REFUSED\"`, so that host cannot hand the app a URL to\n * store. Until 0.82.0 it resolved with the other host's body.\n * - `\"error\"` — goes out over `fetch` with `redirect: \"manual\"`, and a\n * redirect rejects with `code: \"REDIRECT_REFUSED\"` before anything is\n * sent to the other host. The cost: `fetch` reports no upload progress,\n * so `onProgress` fires once, when the server confirms the whole body. A\n * streamed `fetch` body would report progress, but Chrome refuses one\n * over HTTP/1.1 (`TypeError: Failed to fetch`, measured) and Firefox and\n * Safari do not support it.\n *\n * Use `\"error\"` when {@link headers} carry a credential (an `X-Api-Key`) and\n * the server behind `url` could redirect; sending the bearer through\n * {@link getToken} keeps `\"follow\"` safe for it, because the browser strips\n * `Authorization` on a cross-origin redirect.\n */\n redirect?: \"follow\" | \"error\";\n}\n\n/**\n * The `code` of the {@link TempestApiError} an upload rejects with when a\n * redirect is refused, or when the answer came from another origin — the code\n * `createApiClient` and `createResumableUpload` use for the same refusal.\n */\nconst UPLOAD_REDIRECT_REFUSED = \"REDIRECT_REFUSED\";\n\n/** What a finished upload answered, whichever transport carried it. */\ninterface UploadAnswer {\n status: number;\n text: string;\n header: (name: string) => string | null;\n}\n\nfunction parseErrorBody(raw: string): unknown {\n if (!raw) return null;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Turn a finished upload's answer into the promised value, or the error the\n * contract promises. Shared by both transports, so `redirect: \"follow\"` and\n * `redirect: \"error\"` resolve and reject the same way.\n *\n * @param answer - Status, raw text and a header reader.\n * @param parser - The JSON parser.\n * @param requestId - The `X-Request-ID` sent, for the error.\n * @returns The parsed JSON, the raw text, or `undefined` for an empty answer.\n * @throws {TempestApiError} On a non-2xx status.\n */\nfunction settle<T>(answer: UploadAnswer, parser: (raw: string) => unknown, requestId: string): T {\n if (answer.status < 200 || answer.status >= 300) {\n throw new TempestApiError(\n buildApiError(\n answer.status,\n parseErrorBody(answer.text),\n { get: answer.header },\n requestId,\n ),\n );\n }\n if (answer.status === 204 || !answer.text) return undefined as T;\n if ((answer.header(\"content-type\") ?? \"\").includes(\"application/json\")) {\n return parser(answer.text) as T;\n }\n return answer.text as unknown as T;\n}\n\n/**\n * The origin a URL resolves to, or `null` when it does not resolve.\n *\n * @param url - Absolute or relative URL.\n * @returns The origin.\n */\nfunction originOf(url: string): string | null {\n const resolved = resolveRequestUrl(url);\n return resolved === null ? null : new URL(resolved).origin;\n}\n\n/** What {@link sendWithoutRedirects} sends. */\ninterface ManualUpload {\n url: string;\n method: string;\n headers: Record<string, string>;\n body: FormData | Blob;\n withCredentials: boolean;\n signal?: AbortSignal;\n requestId: string;\n}\n\n/**\n * Send the upload over `fetch` with `redirect: \"manual\"` — the\n * `redirect: \"error\"` path of {@link uploadWithProgress}.\n *\n * A browser hands the stopped redirect back as an `opaqueredirect` with\n * status `0`; a server runtime hands back the real `3xx`. Both are refused;\n * `304` is a cache answer, not a redirect.\n *\n * @param init - Where, how, and what to send.\n * @returns The answer.\n * @throws {TempestApiError} `code: \"REDIRECT_REFUSED\"` on a redirect, status\n * `0` on a network failure.\n */\nasync function sendWithoutRedirects(init: ManualUpload): Promise<UploadAnswer> {\n let response: Response;\n try {\n response = await fetch(init.url, {\n method: init.method,\n headers: init.headers,\n body: init.body,\n credentials: init.withCredentials ? \"include\" : \"same-origin\",\n redirect: \"manual\",\n signal: init.signal,\n });\n } catch {\n if (init.signal?.aborted) throw new DOMException(\"Aborted\", \"AbortError\");\n throw new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: init.requestId || undefined,\n });\n }\n if (\n response.type === \"opaqueredirect\" ||\n (response.status >= 300 && response.status < 400 && response.status !== 304)\n ) {\n throw new TempestApiError({\n status: response.status,\n code: UPLOAD_REDIRECT_REFUSED,\n detail: \"Redirect não seguido no upload.\",\n requestId: init.requestId || undefined,\n });\n }\n return {\n status: response.status,\n text: await response.text(),\n header: (name) => response.headers.get(name),\n };\n}\n\n/**\n * Upload a file (or any payload) with byte-level progress reporting.\n *\n * `fetch` cannot report upload progress in browsers, so this helper falls\n * back to `XMLHttpRequest`. It mirrors the error contract used by\n * {@link createApiClient}: non-2xx responses reject with a `TempestApiError`.\n *\n * An `XMLHttpRequest` cannot refuse a redirect; see\n * {@link UploadWithProgressOptions.redirect} for what follows one, what the\n * helper still refuses, and the `fetch` path that trades progress for never\n * following.\n *\n * @returns The parsed JSON response, or the raw text when the response is not JSON.\n */\nexport function uploadWithProgress<T = unknown>(options: UploadWithProgressOptions): Promise<T> {\n const {\n url,\n body,\n method = \"POST\",\n headers = {},\n getToken,\n credentialOrigin,\n trustedOrigins,\n withCredentials = false,\n csrf,\n onProgress,\n signal,\n parser = JSON.parse,\n requestId,\n redirect = \"follow\",\n } = options;\n\n if (signal?.aborted) return Promise.reject(new DOMException(\"Aborted\", \"AbortError\"));\n\n const token = getToken?.();\n const sentRequestId = requestId ? requestId() : randomId();\n const finalHeaders: Record<string, string> = { ...headers };\n const scope = credentialOrigin ?? globalThis.location?.origin ?? url;\n if (token && !(\"Authorization\" in finalHeaders)) {\n if (isTrustedCredentialTarget(url, scope, trustedOrigins)) {\n finalHeaders.Authorization = `Bearer ${token}`;\n } else {\n reportSuppressedCredential(url, scope);\n }\n }\n Object.assign(\n finalHeaders,\n csrfHeaders({\n method,\n url,\n reference: scope,\n csrf,\n trustedOrigins,\n headers,\n }),\n );\n if (sentRequestId && !(\"X-Request-ID\" in finalHeaders)) {\n finalHeaders[\"X-Request-ID\"] = sentRequestId;\n }\n\n if (redirect === \"error\") {\n return sendWithoutRedirects({\n url,\n method,\n headers: finalHeaders,\n body,\n withCredentials,\n signal,\n requestId: sentRequestId,\n }).then((answer) => {\n const sized = body instanceof Blob;\n const total = sized ? body.size : 0;\n onProgress?.({\n loaded: total,\n total,\n fraction: sized ? 1 : null,\n lengthComputable: sized,\n });\n return settle<T>(answer, parser, sentRequestId);\n });\n }\n\n return new Promise<T>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(method, url);\n xhr.withCredentials = withCredentials;\n for (const [key, value] of Object.entries(finalHeaders)) {\n xhr.setRequestHeader(key, value);\n }\n\n if (onProgress) {\n xhr.upload.onprogress = (event) => {\n onProgress({\n loaded: event.loaded,\n total: event.total,\n fraction: event.lengthComputable ? event.loaded / event.total : null,\n lengthComputable: event.lengthComputable,\n });\n };\n }\n\n function handleAbort(): void {\n xhr.abort();\n }\n signal?.addEventListener(\"abort\", handleAbort);\n\n xhr.onload = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n const landed = xhr.responseURL ? originOf(xhr.responseURL) : null;\n if (landed !== null && landed !== originOf(url)) {\n reject(\n new TempestApiError({\n status: xhr.status,\n code: UPLOAD_REDIRECT_REFUSED,\n detail: \"O upload foi redirecionado para outra origem.\",\n requestId: sentRequestId || undefined,\n }),\n );\n return;\n }\n try {\n resolve(\n settle<T>(\n {\n status: xhr.status,\n text: xhr.responseText,\n header: (name) => xhr.getResponseHeader(name),\n },\n parser,\n sentRequestId,\n ),\n );\n } catch (err) {\n reject(err);\n }\n };\n\n xhr.onerror = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: sentRequestId || undefined,\n }),\n );\n };\n\n xhr.onabort = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n\n xhr.send(body);\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../utils/dev-mode.cjs"),t=require("../utils/owner-fingerprint.cjs"),n=require("../offline/create-offline-store.cjs");let r=require("@tanstack/react-query");var i=new Map
|
|
1
|
+
const e=require("../utils/dev-mode.cjs"),t=require("../utils/owner-fingerprint.cjs"),n=require("../offline/create-offline-store.cjs");let r=require("@tanstack/react-query");var i=new Map,a=`p:anonymous`;function o(e){try{return t.ownerFingerprint(`p`,e())}catch{return null}}function s(t){let{queryClient:s,throttleMs:c=1e3,getPrincipal:l}=t,u=t.key??`tanstack-query`,d=t.databaseName??`TempestQueryCache`,f=n.createOfflineStore({databaseName:d,version:1,tableName:`cache`,indexes:`&id`,keyPath:`id`}),p=`${d}\u0000${u}`,m=!1,h=!1,g=!1,_=null,v=null,y=new Set;async function b(){let e=i.get(p);e&&await e}function x(){!h&&e.isDevBuild()&&(h=!0,console.warn(`[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); create a new one after login to keep persisting.`))}function S(){!g&&e.isDevBuild()&&(g=!0,console.warn(`[tempest-react-sdk] persistQueryClientOffline: another account signed in on a handle bound to the previous one; its snapshot is deleted instead of written. Create a new handle after login to keep persisting.`))}async function C(){if(m||(await b(),m))return;let e={id:u,state:(0,r.dehydrate)(s)},t=!1;if(l){let n=o(l);t=_!==null&&n!==_,t&&n!==null&&S(),n!==null&&(_??=n),e.owner=n??a}let n=t?f.delete(u):f.put(e);y.add(n);try{await n}finally{y.delete(n)}}function w(){v&&=(clearTimeout(v),null)}function T(){v||m||(v=setTimeout(()=>{v=null,C()},c))}function E(e){m?(e.type===`added`||e.type===`updated`)&&(D(),x()):T()}let D=s.getQueryCache().subscribe(E);async function O(e){try{await Promise.allSettled([...y]),await f.delete(u)}finally{e&&s.clear()}}return{async restore(){if(m){x();return}if(await b(),m)return;let e=await f.get(u);if(e&&!m){if(l){let t=o(l);if(e.owner!==(t??a))return;t!==null&&(_??=t)}(0,r.hydrate)(s,e.state)}},async flush(){m?x():(w(),await C())},unsubscribe(){w(),D()},async clear(e={}){m=!0,w();let t=i.get(p),n=O(e.resetClient??!1),r=Promise.allSettled([t,n]);i.set(p,r),r.then(()=>{i.get(p)===r&&i.delete(p)}),await n}}}exports.persistQueryClientOffline=s;
|
|
2
2
|
//# sourceMappingURL=offline-persistence.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-persistence.cjs","names":["ownerFingerprint","createOfflineStore","isDevBuild","dehydrate","require_dev_mode","require_owner_fingerprint","require_create_offline_store","_tanstack_react_query","pendingClears","currentOwner","getPrincipal","persistQueryClientOffline","options","key","databaseName","store","slot","ended","warnedEnded","timer","inFlight","afterPendingClear","pending","warnEnded","save","snapshot","queryClient","write","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\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 on every write, which stamps the\n * snapshot with a one-way fingerprint of it (never the id itself), and on\n * `restore()`, which hydrates only a snapshot stamped with the same\n * fingerprint.\n *\n * `clear()` on logout already deletes the snapshot; this covers the session\n * that ends without it — a token that expired, a crash, a tab closed while\n * signed in. Without it, the next account to boot on the browser is shown\n * the previous account's cached responses (#531). A snapshot written before\n * this option was set carries no stamp and is not restored either; it is a\n * cache, so the cost is one cold start.\n *\n * A foreign snapshot is skipped, not deleted: a cookie app that only learns\n * the principal after a `/me` round trip would otherwise wipe its own cache\n * on every cold start. The next write replaces it.\n *\n * Read synchronously. A non-string, non-number value (the `Promise` of an\n * `async` getter) or a getter that throws reads as nobody. Omit it to restore\n * every snapshot, as before 0.82.0.\n */\n getPrincipal?: () => string | number | null | undefined;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called, and — with\n * `getPrincipal` — for a snapshot written under another principal.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called.\n */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. Idempotent. */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n /** Owner fingerprint when `getPrincipal` was set at write time; absent otherwise. */\n owner?: string | null;\n}\n\n/**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param getPrincipal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a throw.\n */\nfunction currentOwner(getPrincipal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", getPrincipal());\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000, getPrincipal } = options;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let warnedEnded = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n async function save(): Promise<void> {\n if (ended) return;\n await afterPendingClear();\n if (ended) return;\n const snapshot: Snapshot = { id: key, state: dehydrate(queryClient) };\n if (getPrincipal) snapshot.owner = currentOwner(getPrincipal);\n const write = store.put(snapshot);\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) queryClient.clear();\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (!snapshot || ended) return;\n if (getPrincipal && snapshot.owner !== currentOwner(getPrincipal)) return;\n hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n cancelTimer();\n unsubscribeCache();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":"A,6KAmBA,IAAMQ,EAAgB,IAAI,IAsH1B,SAASC,EAAa,EAA4C,CAC9D,GAAI,CACA,OAAOT,EAAAA,iBAAiB,IAAKU,EAAa,CAAC,CAC/C,MAAQ,CACJ,OAAO,IACX,CACJ,CA4BA,SAAgBC,EACZ,EACuB,CACvB,GAAM,CAAE,cAAa,aAAa,IAAM,gBAAiBC,EACnDC,EAAMD,EAAQ,KAAO,iBACrBE,EAAeF,EAAQ,cAAgB,oBACvCG,EAAQd,EAAAA,mBAAqC,CAC/C,eACA,QAAS,EACT,UAAW,QACX,QAAS,MACT,QAAS,IACb,CAAC,EAEKe,EAAO,GAAGF,EAAa,QAAQD,IACjCI,EAAQ,GACRC,EAAc,GACdC,EAA8C,KAC5CC,EAAW,IAAI,IAErB,eAAeC,GAAmC,CAC9C,IAAMC,EAAUd,EAAc,IAAIQ,CAAI,EAClCM,GAAS,MAAMA,CACvB,CAEA,SAASC,GAAkB,CACnB,IAAgBrB,EAAAA,WAAW,IAC/B,EAAc,GACd,QAAQ,KACJ,0HAEJ,EACJ,CAEA,eAAesB,GAAsB,CAGjC,GAFIP,IACJ,MAAMI,EAAkB,EACpBJ,GAAO,OACX,IAAMQ,EAAqB,CAAE,GAAIZ,EAAK,OAAA,EAAOV,EAAAA,UAAAA,CAAUuB,CAAW,CAAE,EAChEhB,IAAc,EAAS,MAAQD,EAAaC,CAAY,GAC5D,IAAMiB,EAAQZ,EAAM,IAAIU,CAAQ,EAChCL,EAAS,IAAIO,CAAK,EAClB,GAAI,CACA,MAAMA,CACV,QAAU,CACNP,EAAS,OAAOO,CAAK,CACzB,CACJ,CAEA,SAASC,GAAoB,CACzB,AAEIT,KADA,aAAaA,CAAK,EACV,KAEhB,CAEA,SAASU,GAAiB,CAClBV,GAASF,IACb,EAAQ,eAAiB,CACrB,EAAQ,KACRO,EAAU,CACd,EAAGM,CAAU,EACjB,CAEA,SAASC,EAAa,EAAoC,CACjDd,GAIDe,EAAM,OAAS,SAAWA,EAAM,OAAS,aAC7CC,EAAiB,EACjBV,EAAU,GALNM,EAAS,CAMjB,CAEA,IAAMI,EAAmBP,EAAY,cAAc,CAAC,CAAC,UAAUK,CAAY,EAE3E,eAAeG,EAAW,EAAqC,CAC3D,GAAI,CACA,MAAM,QAAQ,WAAW,CAAC,GAAGd,CAAQ,CAAC,EACtC,MAAML,EAAM,OAAOF,CAAG,CAC1B,QAAU,CACFsB,GAAaT,EAAY,MAAM,CACvC,CACJ,CAEA,MAAO,CACH,MAAM,SAAU,CACZ,GAAIT,EAAO,CACPM,EAAU,EACV,MACJ,CAEA,GADA,MAAMF,EAAkB,EACpBJ,EAAO,OACX,IAAMQ,EAAW,MAAMV,EAAM,IAAIF,CAAG,EAC/BY,GAAY,KACbf,GAAgBe,EAAS,QAAUhB,EAAaC,CAAY,IAChE,EAAAH,EAAA,QAAA,CAAQmB,EAAaD,EAAS,KAAK,EACvC,EACA,MAAM,OAAQ,CACNR,EACAM,EAAU,GAGdK,EAAY,EACZ,MAAMJ,EAAK,EACf,EACA,aAAc,CACVI,EAAY,EACZK,EAAiB,CACrB,EACA,MAAM,MAAM,EAAe,CAAC,EAAG,CAC3B,EAAQ,GACRL,EAAY,EACZ,IAAMQ,EAAW5B,EAAc,IAAIQ,CAAI,EACjCqB,EAAMH,EAAWI,EAAa,aAAe,EAAK,EAClDC,EAAU,QAAQ,WAAW,CAACH,EAAUC,CAAG,CAAC,EAClD7B,EAAc,IAAIQ,EAAMuB,CAAO,EAC/BA,EAAa,SAAW,CAChB/B,EAAc,IAAIQ,CAAI,IAAMuB,GAAS/B,EAAc,OAAOQ,CAAI,CACtE,CAAC,EACD,MAAMqB,CACV,CACJ,CACJ,C"}
|
|
1
|
+
{"version":3,"file":"offline-persistence.cjs","names":["ownerFingerprint","createOfflineStore","isDevBuild","dehydrate","require_dev_mode","require_owner_fingerprint","require_create_offline_store","_tanstack_react_query","pendingClears","ANONYMOUS_SNAPSHOT_OWNER","currentOwner","getPrincipal","persistQueryClientOffline","options","key","databaseName","store","slot","ended","warnedEnded","warnedRebound","boundOwner","timer","inFlight","afterPendingClear","pending","warnEnded","warnRebound","save","snapshot","queryClient","foreign","owner","write","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\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 on every write, which stamps the\n * snapshot with a one-way fingerprint of it (never the id itself), and on\n * `restore()`, which hydrates only a snapshot stamped with the same\n * fingerprint.\n *\n * `clear()` on logout already deletes the snapshot; this covers the session\n * that ends without it — a token that expired, a crash, a tab closed while\n * signed in. Without it, the next account to boot on the browser is shown\n * the previous account's cached responses (#531). A snapshot written before\n * this option was set carries no stamp and is not restored either; it is a\n * cache, so the cost is one cold start.\n *\n * A foreign snapshot is skipped, not deleted: a cookie app that only learns\n * the principal after a `/me` round trip would otherwise wipe its own cache\n * on every cold start. The next write replaces it.\n *\n * The handle is bound to the first signed-in owner it writes or restores.\n * Once bound, a write under another owner — nobody, because the token\n * expired without `clear()`, or a second account on the same tab — deletes\n * the snapshot instead of stamping the bound owner's cached responses with\n * the new owner (#563); the same owner coming back writes again. Writes\n * before anyone signs in stay anonymous and are restored while nobody is,\n * so an app that persists logged-out data keeps doing so. An anonymous\n * snapshot written by 0.82.0 is not restored (it may hold an expired\n * session's cache): one cold start, once.\n *\n * Read synchronously. A non-string, non-number value (the `Promise` of an\n * `async` getter) or a getter that throws reads as nobody. Omit it to restore\n * every snapshot, as before 0.82.0.\n */\n getPrincipal?: () => string | number | null | undefined;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called, and — with\n * `getPrincipal` — for a snapshot written under another principal.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called.\n */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. Idempotent. */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\n/**\n * Owner stamp of a snapshot written while nobody was signed in.\n *\n * Not a fingerprint (`ownerFingerprint` only yields `p:` + 16 hex characters),\n * so it never matches a signed-in owner. It replaces the `null` stamp of 0.82.0\n * on purpose: a `null` row from 0.82.0 may hold the cache of a session that\n * expired before the write (#563), and nothing in it tells that row apart from\n * genuinely anonymous data, so `restore()` skips it — one cold start, once.\n */\nconst ANONYMOUS_SNAPSHOT_OWNER = \"p:anonymous\";\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n /**\n * Owner fingerprint, or {@link ANONYMOUS_SNAPSHOT_OWNER}, when `getPrincipal`\n * was set at write time; absent otherwise.\n */\n owner?: string | null;\n}\n\n/**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param getPrincipal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a throw.\n */\nfunction currentOwner(getPrincipal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", getPrincipal());\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000, getPrincipal } = options;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let warnedEnded = false;\n let warnedRebound = false;\n let boundOwner: string | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n function warnRebound(): void {\n if (warnedRebound || !isDevBuild()) return;\n warnedRebound = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: another account signed in on a \" +\n \"handle bound to the previous one; its snapshot is deleted instead of written. \" +\n \"Create a new handle after login to keep persisting.\",\n );\n }\n\n /**\n * Write the snapshot, or delete the row when the session it holds is over.\n *\n * With `getPrincipal`, the handle is bound to the first signed-in owner it\n * writes or restores. A write under any other owner — nobody, after a token\n * expired, or a second account signing in on the same tab — deletes the row\n * instead of stamping it: the in-memory cache still holds the bound owner's\n * responses, and stamping them with the new owner is what handed them to the\n * next account (#563). The same owner coming back writes again.\n *\n * A second account (not an expiry) means the app kept the handle across a\n * login, which stops persistence for that account in silence; a dev build\n * logs one `console.warn` saying so, as for a handle ended by `clear()`.\n */\n async function save(): Promise<void> {\n if (ended) return;\n await afterPendingClear();\n if (ended) return;\n const snapshot: Snapshot = { id: key, state: dehydrate(queryClient) };\n let foreign = false;\n if (getPrincipal) {\n const owner = currentOwner(getPrincipal);\n foreign = boundOwner !== null && owner !== boundOwner;\n if (foreign && owner !== null) warnRebound();\n if (owner !== null) boundOwner = boundOwner ?? owner;\n snapshot.owner = owner ?? ANONYMOUS_SNAPSHOT_OWNER;\n }\n const write = foreign ? store.delete(key) : store.put(snapshot);\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) queryClient.clear();\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (!snapshot || ended) return;\n if (getPrincipal) {\n const owner = currentOwner(getPrincipal);\n if (snapshot.owner !== (owner ?? ANONYMOUS_SNAPSHOT_OWNER)) return;\n if (owner !== null) boundOwner = boundOwner ?? owner;\n }\n hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n cancelTimer();\n unsubscribeCache();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -3,89 +3,102 @@ import { ownerFingerprint as t } from "../utils/owner-fingerprint.js";
|
|
|
3
3
|
import { createOfflineStore as n } from "../offline/create-offline-store.js";
|
|
4
4
|
import { dehydrate as r, hydrate as i } from "@tanstack/react-query";
|
|
5
5
|
//#region src/query/offline-persistence.ts
|
|
6
|
-
var a = /* @__PURE__ */ new Map();
|
|
7
|
-
function
|
|
6
|
+
var a = /* @__PURE__ */ new Map(), o = "p:anonymous";
|
|
7
|
+
function s(e) {
|
|
8
8
|
try {
|
|
9
9
|
return t("p", e());
|
|
10
10
|
} catch {
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
let { queryClient:
|
|
16
|
-
databaseName:
|
|
14
|
+
function c(t) {
|
|
15
|
+
let { queryClient: c, throttleMs: l = 1e3, getPrincipal: u } = t, d = t.key ?? "tanstack-query", f = t.databaseName ?? "TempestQueryCache", p = n({
|
|
16
|
+
databaseName: f,
|
|
17
17
|
version: 1,
|
|
18
18
|
tableName: "cache",
|
|
19
19
|
indexes: "&id",
|
|
20
20
|
keyPath: "id"
|
|
21
|
-
}),
|
|
22
|
-
async function
|
|
23
|
-
let e = a.get(
|
|
21
|
+
}), m = `${f}\u0000${d}`, h = !1, g = !1, _ = !1, v = null, y = null, b = /* @__PURE__ */ new Set();
|
|
22
|
+
async function x() {
|
|
23
|
+
let e = a.get(m);
|
|
24
24
|
e && await e;
|
|
25
25
|
}
|
|
26
|
-
function
|
|
27
|
-
!
|
|
26
|
+
function S() {
|
|
27
|
+
!g && e() && (g = !0, console.warn("[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); create a new one after login to keep persisting."));
|
|
28
|
+
}
|
|
29
|
+
function C() {
|
|
30
|
+
!_ && e() && (_ = !0, console.warn("[tempest-react-sdk] persistQueryClientOffline: another account signed in on a handle bound to the previous one; its snapshot is deleted instead of written. Create a new handle after login to keep persisting."));
|
|
28
31
|
}
|
|
29
|
-
async function
|
|
30
|
-
if (
|
|
32
|
+
async function w() {
|
|
33
|
+
if (h || (await x(), h)) return;
|
|
31
34
|
let e = {
|
|
32
|
-
id:
|
|
33
|
-
state: r(
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
id: d,
|
|
36
|
+
state: r(c)
|
|
37
|
+
}, t = !1;
|
|
38
|
+
if (u) {
|
|
39
|
+
let n = s(u);
|
|
40
|
+
t = v !== null && n !== v, t && n !== null && C(), n !== null && (v ??= n), e.owner = n ?? o;
|
|
41
|
+
}
|
|
42
|
+
let n = t ? p.delete(d) : p.put(e);
|
|
43
|
+
b.add(n);
|
|
38
44
|
try {
|
|
39
|
-
await
|
|
45
|
+
await n;
|
|
40
46
|
} finally {
|
|
41
|
-
|
|
47
|
+
b.delete(n);
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
50
|
+
function T() {
|
|
51
|
+
y &&= (clearTimeout(y), null);
|
|
46
52
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
53
|
+
function E() {
|
|
54
|
+
y || h || (y = setTimeout(() => {
|
|
55
|
+
y = null, w();
|
|
56
|
+
}, l));
|
|
51
57
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
58
|
+
function D(e) {
|
|
59
|
+
h ? (e.type === "added" || e.type === "updated") && (O(), S()) : E();
|
|
54
60
|
}
|
|
55
|
-
let
|
|
56
|
-
async function
|
|
61
|
+
let O = c.getQueryCache().subscribe(D);
|
|
62
|
+
async function k(e) {
|
|
57
63
|
try {
|
|
58
|
-
await Promise.allSettled([...
|
|
64
|
+
await Promise.allSettled([...b]), await p.delete(d);
|
|
59
65
|
} finally {
|
|
60
|
-
e &&
|
|
66
|
+
e && c.clear();
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
return {
|
|
64
70
|
async restore() {
|
|
65
|
-
if (
|
|
66
|
-
|
|
71
|
+
if (h) {
|
|
72
|
+
S();
|
|
67
73
|
return;
|
|
68
74
|
}
|
|
69
|
-
if (await
|
|
70
|
-
let e = await
|
|
71
|
-
e && !
|
|
75
|
+
if (await x(), h) return;
|
|
76
|
+
let e = await p.get(d);
|
|
77
|
+
if (e && !h) {
|
|
78
|
+
if (u) {
|
|
79
|
+
let t = s(u);
|
|
80
|
+
if (e.owner !== (t ?? o)) return;
|
|
81
|
+
t !== null && (v ??= t);
|
|
82
|
+
}
|
|
83
|
+
i(c, e.state);
|
|
84
|
+
}
|
|
72
85
|
},
|
|
73
86
|
async flush() {
|
|
74
|
-
|
|
87
|
+
h ? S() : (T(), await w());
|
|
75
88
|
},
|
|
76
89
|
unsubscribe() {
|
|
77
|
-
|
|
90
|
+
T(), O();
|
|
78
91
|
},
|
|
79
92
|
async clear(e = {}) {
|
|
80
|
-
|
|
81
|
-
let t = a.get(
|
|
82
|
-
a.set(
|
|
83
|
-
a.get(
|
|
93
|
+
h = !0, T();
|
|
94
|
+
let t = a.get(m), n = k(e.resetClient ?? !1), r = Promise.allSettled([t, n]);
|
|
95
|
+
a.set(m, r), r.then(() => {
|
|
96
|
+
a.get(m) === r && a.delete(m);
|
|
84
97
|
}), await n;
|
|
85
98
|
}
|
|
86
99
|
};
|
|
87
100
|
}
|
|
88
101
|
//#endregion
|
|
89
|
-
export {
|
|
102
|
+
export { c as persistQueryClientOffline };
|
|
90
103
|
|
|
91
104
|
//# sourceMappingURL=offline-persistence.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-persistence.js","names":["pendingClears","currentOwner","ownerFingerprint","getPrincipal","persistQueryClientOffline","options","key","databaseName","store","createOfflineStore","slot","ended","warnedEnded","timer","inFlight","afterPendingClear","pending","warnEnded","isDevBuild","save","snapshot","dehydrate","queryClient","write","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","hydrate","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\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 on every write, which stamps the\n * snapshot with a one-way fingerprint of it (never the id itself), and on\n * `restore()`, which hydrates only a snapshot stamped with the same\n * fingerprint.\n *\n * `clear()` on logout already deletes the snapshot; this covers the session\n * that ends without it — a token that expired, a crash, a tab closed while\n * signed in. Without it, the next account to boot on the browser is shown\n * the previous account's cached responses (#531). A snapshot written before\n * this option was set carries no stamp and is not restored either; it is a\n * cache, so the cost is one cold start.\n *\n * A foreign snapshot is skipped, not deleted: a cookie app that only learns\n * the principal after a `/me` round trip would otherwise wipe its own cache\n * on every cold start. The next write replaces it.\n *\n * Read synchronously. A non-string, non-number value (the `Promise` of an\n * `async` getter) or a getter that throws reads as nobody. Omit it to restore\n * every snapshot, as before 0.82.0.\n */\n getPrincipal?: () => string | number | null | undefined;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called, and — with\n * `getPrincipal` — for a snapshot written under another principal.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called.\n */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. Idempotent. */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n /** Owner fingerprint when `getPrincipal` was set at write time; absent otherwise. */\n owner?: string | null;\n}\n\n/**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param getPrincipal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a throw.\n */\nfunction currentOwner(getPrincipal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", getPrincipal());\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000, getPrincipal } = options;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let warnedEnded = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n async function save(): Promise<void> {\n if (ended) return;\n await afterPendingClear();\n if (ended) return;\n const snapshot: Snapshot = { id: key, state: dehydrate(queryClient) };\n if (getPrincipal) snapshot.owner = currentOwner(getPrincipal);\n const write = store.put(snapshot);\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) queryClient.clear();\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (!snapshot || ended) return;\n if (getPrincipal && snapshot.owner !== currentOwner(getPrincipal)) return;\n hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n cancelTimer();\n unsubscribeCache();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":";;;;;AAmBA,IAAMA,oBAAgB,IAAI,IAA8B;AAsHxD,SAASC,EAAa,GAA4C;CAC9D,IAAI;EACA,OAAOC,EAAiB,KAAKC,EAAa,CAAC;CAC/C,QAAQ;EACJ,OAAO;CACX;AACJ;AA4BA,SAAgBC,EACZ,GACuB;CACvB,IAAM,EAAE,gBAAa,gBAAa,KAAM,oBAAiBC,GACnDC,IAAMD,EAAQ,OAAO,kBACrBE,IAAeF,EAAQ,gBAAgB,qBACvCG,IAAQC,EAAqC;EAC/C;EACA,SAAS;EACT,WAAW;EACX,SAAS;EACT,SAAS;CACb,CAAC,GAEKC,IAAO,GAAGH,EAAa,QAAQD,KACjCK,IAAQ,IACRC,IAAc,IACdC,IAA8C,MAC5CC,oBAAW,IAAI,IAAsB;CAE3C,eAAeC,IAAmC;EAC9C,IAAMC,IAAUhB,EAAc,IAAIU,CAAI;EACtC,AAAIM,KAAS,MAAMA;CACvB;CAEA,SAASC,IAAkB;EACvB,AAAI,MAAgBC,EAAW,MAC/B,IAAc,IACd,QAAQ,KACJ,0HAEJ;CACJ;CAEA,eAAeC,IAAsB;EAGjC,IAFIR,MACJ,MAAMI,EAAkB,GACpBJ,IAAO;EACX,IAAMS,IAAqB;GAAE,IAAId;GAAK,OAAOe,EAAUC,CAAW;EAAE;EACpE,AAAInB,MAAc,EAAS,QAAQF,EAAaE,CAAY;EAC5D,IAAMoB,IAAQf,EAAM,IAAIY,CAAQ;EAChC,EAAS,IAAIG,CAAK;EAClB,IAAI;GACA,MAAMA;EACV,UAAU;GACN,EAAS,OAAOA,CAAK;EACzB;CACJ;CAEA,SAASC,IAAoB;EACzB,AAEIX,OADA,aAAaA,CAAK,GACV;CAEhB;CAEA,SAASY,IAAiB;EACtB,AAAIZ,KAASF,MACb,IAAQ,iBAAiB;GAErB,AADA,IAAQ,MACRQ,EAAU;EACd,GAAGO,CAAU;CACjB;CAEA,SAASC,EAAa,GAAoC;EACtD,AAAKhB,KAIDiB,EAAM,SAAS,WAAWA,EAAM,SAAS,eAC7CC,EAAiB,GACjBZ,EAAU,KALNQ,EAAS;CAMjB;CAEA,IAAMI,IAAmBP,EAAY,cAAc,CAAC,CAAC,UAAUK,CAAY;CAE3E,eAAeG,EAAW,GAAqC;EAC3D,IAAI;GAEA,AADA,MAAM,QAAQ,WAAW,CAAC,GAAGhB,CAAQ,CAAC,GACtC,MAAMN,EAAM,OAAOF,CAAG;EAC1B,UAAU;GACN,AAAIyB,KAAaT,EAAY,MAAM;EACvC;CACJ;CAEA,OAAO;EACH,MAAM,UAAU;GACZ,IAAIX,GAAO;IACP,EAAU;IACV;GACJ;GAEA,IADA,MAAMI,EAAkB,GACpBJ,GAAO;GACX,IAAMS,IAAW,MAAMZ,EAAM,IAAIF,CAAG;GACpC,AAAKc,KAAY,OACbjB,KAAgBiB,EAAS,UAAUnB,EAAaE,CAAY,KAChE6B,EAAQV,GAAaF,EAAS,KAAK;EACvC;EACA,MAAM,QAAQ;GACV,AAAIT,IACAM,EAAU,KAGdO,EAAY,GACZ,MAAML,EAAK;EACf;EACA,cAAc;GAEV,AADAK,EAAY,GACZK,EAAiB;EACrB;EACA,MAAM,MAAM,IAAe,CAAC,GAAG;GAE3B,AADA,IAAQ,IACRL,EAAY;GACZ,IAAMS,IAAWjC,EAAc,IAAIU,CAAI,GACjCwB,IAAMJ,EAAWK,EAAa,eAAe,EAAK,GAClDC,IAAU,QAAQ,WAAW,CAACH,GAAUC,CAAG,CAAC;GAKlD,AAJAlC,EAAc,IAAIU,GAAM0B,CAAO,GAC/BA,EAAa,WAAW;IACpB,AAAIpC,EAAc,IAAIU,CAAI,MAAM0B,KAASpC,EAAc,OAAOU,CAAI;GACtE,CAAC,GACD,MAAMwB;EACV;CACJ;AACJ"}
|
|
1
|
+
{"version":3,"file":"offline-persistence.js","names":["pendingClears","ANONYMOUS_SNAPSHOT_OWNER","currentOwner","ownerFingerprint","getPrincipal","persistQueryClientOffline","options","key","databaseName","store","createOfflineStore","slot","ended","warnedEnded","warnedRebound","boundOwner","timer","inFlight","afterPendingClear","pending","warnEnded","isDevBuild","warnRebound","save","snapshot","dehydrate","queryClient","foreign","owner","write","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\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 on every write, which stamps the\n * snapshot with a one-way fingerprint of it (never the id itself), and on\n * `restore()`, which hydrates only a snapshot stamped with the same\n * fingerprint.\n *\n * `clear()` on logout already deletes the snapshot; this covers the session\n * that ends without it — a token that expired, a crash, a tab closed while\n * signed in. Without it, the next account to boot on the browser is shown\n * the previous account's cached responses (#531). A snapshot written before\n * this option was set carries no stamp and is not restored either; it is a\n * cache, so the cost is one cold start.\n *\n * A foreign snapshot is skipped, not deleted: a cookie app that only learns\n * the principal after a `/me` round trip would otherwise wipe its own cache\n * on every cold start. The next write replaces it.\n *\n * The handle is bound to the first signed-in owner it writes or restores.\n * Once bound, a write under another owner — nobody, because the token\n * expired without `clear()`, or a second account on the same tab — deletes\n * the snapshot instead of stamping the bound owner's cached responses with\n * the new owner (#563); the same owner coming back writes again. Writes\n * before anyone signs in stay anonymous and are restored while nobody is,\n * so an app that persists logged-out data keeps doing so. An anonymous\n * snapshot written by 0.82.0 is not restored (it may hold an expired\n * session's cache): one cold start, once.\n *\n * Read synchronously. A non-string, non-number value (the `Promise` of an\n * `async` getter) or a getter that throws reads as nobody. Omit it to restore\n * every snapshot, as before 0.82.0.\n */\n getPrincipal?: () => string | number | null | undefined;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called, and — with\n * `getPrincipal` — for a snapshot written under another principal.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called.\n */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. Idempotent. */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\n/**\n * Owner stamp of a snapshot written while nobody was signed in.\n *\n * Not a fingerprint (`ownerFingerprint` only yields `p:` + 16 hex characters),\n * so it never matches a signed-in owner. It replaces the `null` stamp of 0.82.0\n * on purpose: a `null` row from 0.82.0 may hold the cache of a session that\n * expired before the write (#563), and nothing in it tells that row apart from\n * genuinely anonymous data, so `restore()` skips it — one cold start, once.\n */\nconst ANONYMOUS_SNAPSHOT_OWNER = \"p:anonymous\";\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n /**\n * Owner fingerprint, or {@link ANONYMOUS_SNAPSHOT_OWNER}, when `getPrincipal`\n * was set at write time; absent otherwise.\n */\n owner?: string | null;\n}\n\n/**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param getPrincipal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a throw.\n */\nfunction currentOwner(getPrincipal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", getPrincipal());\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000, getPrincipal } = options;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let warnedEnded = false;\n let warnedRebound = false;\n let boundOwner: string | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n function warnRebound(): void {\n if (warnedRebound || !isDevBuild()) return;\n warnedRebound = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: another account signed in on a \" +\n \"handle bound to the previous one; its snapshot is deleted instead of written. \" +\n \"Create a new handle after login to keep persisting.\",\n );\n }\n\n /**\n * Write the snapshot, or delete the row when the session it holds is over.\n *\n * With `getPrincipal`, the handle is bound to the first signed-in owner it\n * writes or restores. A write under any other owner — nobody, after a token\n * expired, or a second account signing in on the same tab — deletes the row\n * instead of stamping it: the in-memory cache still holds the bound owner's\n * responses, and stamping them with the new owner is what handed them to the\n * next account (#563). The same owner coming back writes again.\n *\n * A second account (not an expiry) means the app kept the handle across a\n * login, which stops persistence for that account in silence; a dev build\n * logs one `console.warn` saying so, as for a handle ended by `clear()`.\n */\n async function save(): Promise<void> {\n if (ended) return;\n await afterPendingClear();\n if (ended) return;\n const snapshot: Snapshot = { id: key, state: dehydrate(queryClient) };\n let foreign = false;\n if (getPrincipal) {\n const owner = currentOwner(getPrincipal);\n foreign = boundOwner !== null && owner !== boundOwner;\n if (foreign && owner !== null) warnRebound();\n if (owner !== null) boundOwner = boundOwner ?? owner;\n snapshot.owner = owner ?? ANONYMOUS_SNAPSHOT_OWNER;\n }\n const write = foreign ? store.delete(key) : store.put(snapshot);\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) queryClient.clear();\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (!snapshot || ended) return;\n if (getPrincipal) {\n const owner = currentOwner(getPrincipal);\n if (snapshot.owner !== (owner ?? ANONYMOUS_SNAPSHOT_OWNER)) return;\n if (owner !== null) boundOwner = boundOwner ?? owner;\n }\n hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n cancelTimer();\n unsubscribeCache();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/sw/cache.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./credential-headers.cjs");function t(){return globalThis}var n
|
|
1
|
+
const e=require("./credential-headers.cjs");function t(){return globalThis}var n=new WeakSet,r=`/__tempest-precache-state__`,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){return(e??``).toLowerCase().split(`,`).map(e=>e.trim())}function c(e,t){if(!e.ok||s(e.headers.get(`vary`)).includes(`*`))return!1;let n=o(e);return n.includes(`no-store`)?!1:t.cacheAuthenticated===!0||!n.includes(`private`)}function l(e,t){return e.mode===`cors`&&e.credentials===`include`||u(e,t).length>0}function u(t,n){return[...e.credentialHeaderSet(n.credentialHeaders)].filter(e=>t.headers.has(e))}async function d(e,t,n,r){if(!c(n,r))return;let a=new Headers(n.headers);if(a.set(i,String(Date.now())),r.cacheAuthenticated){let e=a.get(`vary`),n=s(e),i=u(t,r).filter(e=>!n.includes(e));i.length>0&&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 f(r.cacheName,r.maxEntries)}async function f(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 p(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 d(n,e,i,t),i}async function m(e,t,n){let r=await caches.open(t.cacheName),i=(async()=>{let n=await fetch(e);return await d(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 h(e,t,n){let r=await caches.open(t.cacheName),i=await r.match(e),o=fetch(e).then(async n=>(await d(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 g(e,t,n){switch(t.strategy){case`cache-first`:return p(e,t);case`network-first`:return m(e,t,n);case`stale-while-revalidate`:return h(e,t,n)}}function _(e,t,n){return typeof e.match==`function`?e.match(t,n):e.match.test(t.href)}async function v(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 y(e){t().addEventListener(`fetch`,t=>{let r=t.request;if(r.method!==`GET`)return;let i=new URL(r.url),a=e.find(e=>_(e,i,r));if(!a||!a.cacheAuthenticated&&l(r,a))return;n.add(t);let o=e=>t.waitUntil(e);if(a.rangeRequests&&r.headers.has(`range`)){let e=new Request(r.url,{headers:b(r.headers),credentials:r.credentials,mode:r.mode===`navigate`?`same-origin`:r.mode});t.respondWith(g(e,a,o).then(e=>v(r,e)))}else t.respondWith(g(r,a,o))})}function b(e){let t=new Headers(e);return t.delete(`range`),t}async function x(e){let t=await e.match(r);if(t)try{return await t.json()}catch{return}}async function S(e,t){await e.put(r,new Response(JSON.stringify(t),{headers:{"content-type":`application/json`}}))}async function C(e,t){let n;for(let r of await caches.keys()){if(!r.startsWith(`${e}-`))continue;let i=await x(await caches.open(r)),a=i?t(i):void 0;a!==void 0&&(!n||a>n.at)&&(n={name:r,at:a})}return n?.name}async function w(e){let t=await(await caches.open(e)).keys();return{name:e,paths:new Set(t.map(e=>new URL(e.url).pathname).filter(e=>e!==r))}}async function T(e,t){return await(await caches.open(e)).match(t)??fetch(t)}function E(e={}){let r=t(),{manifestUrl:i=`/precache-manifest.json`,cacheName:a=`tempest-precache`,navigateFallback:o=`/index.html`,navigateFallbackDenylist:s=[],skipWaiting:c=!0,navigationPreload:l=!0}=e,u,d,f=!1,p,m=()=>f?Promise.resolve(d):(p??=(async()=>{let e=await C(a,e=>e.activatedAt),t=e?await w(e):void 0;return f||=(d=t,!0),d})().finally(()=>{p=void 0}),p);r.addEventListener(`install`,e=>{e.waitUntil((async()=>{let e=await(await fetch(i,{cache:`no-cache`})).json(),t=`${a}-${e.version}`,n=await caches.open(t);await n.addAll(e.urls),await S(n,{...await x(n),installedAt:Date.now()}),u=t,c&&await r.skipWaiting()})())}),r.addEventListener(`activate`,e=>{e.waitUntil((async()=>{let e=u??await C(a,e=>e.installedAt>(e.activatedAt??-1/0)?e.installedAt:void 0);if(e){let t=await caches.open(e),n=await x(t);await S(t,{installedAt:Date.now(),...n,activatedAt:Date.now()}),d=await w(e),f=!0;let r=await caches.keys();await Promise.all(r.filter(t=>t.startsWith(`${a}-`)&&t!==e).map(e=>caches.delete(e)))}l&&r.registration?.navigationPreload&&await r.registration.navigationPreload.enable(),await r.clients.claim()})())}),r.addEventListener(`fetch`,e=>{let t=e.request;if(t.method!==`GET`||n.has(e))return;let i=new URL(t.url);if(i.origin===r.location.origin){if(t.mode===`navigate`){if(s.some(e=>e.test(i.pathname)))return;e.respondWith((async()=>{try{return await e.preloadResponse||await fetch(t)}catch{let e=await m(),t=e?await(await caches.open(e.name)).match(o):void 0;if(t)return t;throw Error(`offline and no cached app shell`)}})())}else f?d?.paths.has(i.pathname)&&e.respondWith(T(d.name,t)):e.respondWith(m().catch(()=>void 0).then(e=>e?.paths.has(i.pathname)?T(e.name,t):fetch(t)))}})}exports.createPartialResponse=v,exports.installPrecache=E,exports.installRuntimeCache=y;
|
|
2
2
|
//# sourceMappingURL=cache.cjs.map
|