tempest-react-sdk 0.75.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/bin/create-tempest-app.e2e.test.mjs +112 -0
- package/bin/create-tempest-app.mjs +69 -28
- package/bin/lib/component/generate.mjs +191 -0
- package/bin/lib/component/generate.test.mjs +336 -0
- package/bin/lib/openapi/escape.mjs +210 -0
- package/bin/lib/openapi/generate.hostile.test.mjs +592 -0
- package/bin/lib/openapi/generate.mjs +93 -17
- package/bin/lib/openapi/schema-to-zod.mjs +41 -4
- package/bin/tempest.mjs +184 -3
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +127 -55
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/refresh-queue.cjs +1 -1
- package/dist/auth/refresh-queue.cjs.map +1 -1
- package/dist/auth/refresh-queue.js +13 -6
- package/dist/auth/refresh-queue.js.map +1 -1
- package/dist/br/pix.cjs +1 -1
- package/dist/br/pix.cjs.map +1 -1
- package/dist/br/pix.js +72 -62
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +12 -4
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +46 -38
- package/dist/components/Chat/ChatAttachments.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs +7 -5
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js +151 -87
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Toast/Toast.module.cjs +1 -1
- package/dist/components/Toast/Toast.module.cjs.map +1 -1
- package/dist/components/Toast/Toast.module.js +7 -3
- package/dist/components/Toast/Toast.module.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +77 -51
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +32 -21
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +81 -73
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +39 -35
- package/dist/http/errors.js.map +1 -1
- package/dist/neural.cjs +2 -0
- package/dist/neural.cjs.map +1 -0
- package/dist/neural.d.ts +110 -0
- package/dist/neural.js +48 -0
- package/dist/neural.js.map +1 -0
- package/dist/oauth/callback.cjs +2 -0
- package/dist/oauth/callback.cjs.map +1 -0
- package/dist/oauth/callback.js +42 -0
- package/dist/oauth/callback.js.map +1 -0
- package/dist/oauth/use-oauth-callback.cjs +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/oauth/use-oauth-callback.js +13 -20
- package/dist/oauth/use-oauth-callback.js.map +1 -1
- package/dist/push/use-push-subscription.cjs +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/push/use-push-subscription.js +42 -42
- package/dist/push/use-push-subscription.js.map +1 -1
- package/dist/push/web-push-client.cjs +1 -1
- package/dist/push/web-push-client.cjs.map +1 -1
- package/dist/push/web-push-client.js +23 -3
- package/dist/push/web-push-client.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +68 -23
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/styles/component/Toast.css +6 -1
- package/dist/styles/feedback.css +6 -1
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +271 -60
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache-inspect.cjs.map +1 -1
- package/dist/sw/cache-inspect.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +91 -64
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/credential-headers.cjs +2 -0
- package/dist/sw/credential-headers.cjs.map +1 -0
- package/dist/sw/credential-headers.js +17 -0
- package/dist/sw/credential-headers.js.map +1 -0
- package/dist/sw/session-data.cjs +2 -0
- package/dist/sw/session-data.cjs.map +1 -0
- package/dist/sw/session-data.js +17 -0
- package/dist/sw/session-data.js.map +1 -0
- package/dist/sw.cjs +1 -1
- package/dist/sw.d.ts +271 -23
- package/dist/sw.js +7 -5
- package/dist/telemetry/app-error-owner.cjs +2 -0
- package/dist/telemetry/app-error-owner.cjs.map +1 -0
- package/dist/telemetry/app-error-owner.js +18 -0
- package/dist/telemetry/app-error-owner.js.map +1 -0
- package/dist/telemetry/app-error-queue.cjs +1 -1
- package/dist/telemetry/app-error-queue.cjs.map +1 -1
- package/dist/telemetry/app-error-queue.js +19 -14
- package/dist/telemetry/app-error-queue.js.map +1 -1
- package/dist/telemetry/app-error-reporter.cjs +1 -1
- package/dist/telemetry/app-error-reporter.cjs.map +1 -1
- package/dist/telemetry/app-error-reporter.js +75 -34
- package/dist/telemetry/app-error-reporter.js.map +1 -1
- package/dist/telemetry/app-error-transport.cjs +1 -1
- package/dist/telemetry/app-error-transport.cjs.map +1 -1
- package/dist/telemetry/app-error-transport.js +16 -3
- package/dist/telemetry/app-error-transport.js.map +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +979 -62
- package/dist/tempest-react-sdk.js +160 -156
- package/dist/utils/csv.cjs +3 -3
- package/dist/utils/csv.cjs.map +1 -1
- package/dist/utils/csv.js +16 -13
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/safe-url.cjs +2 -0
- package/dist/utils/safe-url.cjs.map +1 -0
- package/dist/utils/safe-url.js +39 -0
- package/dist/utils/safe-url.js.map +1 -0
- package/dist/vite/tempest-csp.cjs +2 -2
- package/dist/vite/tempest-csp.cjs.map +1 -1
- package/dist/vite/tempest-csp.js +118 -63
- package/dist/vite/tempest-csp.js.map +1 -1
- package/dist/vite.d.ts +21 -5
- package/dist/webrtc/peer-link.cjs +1 -1
- package/dist/webrtc/peer-link.cjs.map +1 -1
- package/dist/webrtc/peer-link.js +2 -10
- package/dist/webrtc/peer-link.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +110 -72
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/package.json +11 -1
- package/template-pwa/src/pwa.tsx +22 -0
- package/template-pwa/src/sw.ts +27 -14
- package/dist/components/Markdown/markdown-url.cjs +0 -2
- package/dist/components/Markdown/markdown-url.cjs.map +0 -1
- package/dist/components/Markdown/markdown-url.js +0 -28
- package/dist/components/Markdown/markdown-url.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-push-client.cjs","names":[],"sources":["../../src/push/web-push-client.ts"],"sourcesContent":["import { isPushSupported, urlBase64ToUint8Array } from \"./utils\";\n\nexport interface WebPushClientConfig {\n /** VAPID public key (URL-safe base64). */\n vapidPublicKey: string;\n /**\n * Persist the subscription on the backend. The SDK does not own this call —\n * the host app decides the endpoint and how the payload is serialized.\n */\n onSubscribe: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /** Remove the subscription from the backend. Called before `subscription.unsubscribe()`. */\n onUnsubscribe?: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Override how the service worker registration is obtained.\n * Defaults to `navigator.serviceWorker.ready` — provide this only when you\n * register the SW yourself and want to reuse the registration.\n */\n getRegistration?: () => Promise<ServiceWorkerRegistration>;\n}\n\nexport class WebPushUnsupportedError extends Error {\n constructor() {\n super(\"Web Push não suportado neste navegador.\");\n this.name = \"WebPushUnsupportedError\";\n }\n}\n\nexport class WebPushPermissionDeniedError extends Error {\n constructor() {\n super(\"Permissão de notificação negada pelo usuário.\");\n this.name = \"WebPushPermissionDeniedError\";\n }\n}\n\n/**\n * Browser-side Web Push helper. Wraps `Notification.requestPermission`,\n * `pushManager.subscribe`, and the corresponding teardown. Transport is up to\n * the caller via `onSubscribe` / `onUnsubscribe` callbacks.\n */\nexport class WebPushClient {\n private readonly config: WebPushClientConfig;\n\n constructor(config: WebPushClientConfig) {\n this.config = config;\n }\n\n /** Whether the runtime supports the Push API. */\n static isSupported(): boolean {\n return isPushSupported();\n }\n\n private async registration(): Promise<ServiceWorkerRegistration> {\n if (!isPushSupported()) throw new WebPushUnsupportedError();\n return this.config.getRegistration\n ? await this.config.getRegistration()\n : await navigator.serviceWorker.ready;\n }\n\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission(): NotificationPermission | \"unsupported\" {\n if (typeof Notification === \"undefined\") return \"unsupported\";\n return Notification.permission;\n }\n\n /**\n * Ask the user for notification permission.\n *\n * @throws {WebPushUnsupportedError} If the runtime lacks the Notification API.\n * @returns The resulting `NotificationPermission` value.\n */\n async requestPermission(): Promise<NotificationPermission> {\n if (typeof Notification === \"undefined\") throw new WebPushUnsupportedError();\n return Notification.requestPermission();\n }\n\n /** Return the active push subscription, if any. */\n async getSubscription(): Promise<PushSubscription | null> {\n const registration = await this.registration();\n return registration.pushManager.getSubscription();\n }\n\n /** True when a subscription already exists for this browser. */\n async isSubscribed(): Promise<boolean> {\n if (!isPushSupported()) return false;\n const existing = await this.getSubscription();\n return existing !== null;\n }\n\n /**\n * Subscribe the current device. Requests permission, creates a push\n * subscription if none exists, and forwards the JSON payload to\n * `onSubscribe` for backend persistence.\n *\n * @returns The active `PushSubscription`.\n * @throws {WebPushUnsupportedError} If Web Push is unavailable.\n * @throws {WebPushPermissionDeniedError} If the user denies the prompt.\n */\n async subscribe(): Promise<PushSubscription> {\n const permission = await this.requestPermission();\n if (permission !== \"granted\") throw new WebPushPermissionDeniedError();\n\n const registration = await this.registration();\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n await this.config.onSubscribe(existing.toJSON());\n return existing;\n }\n\n const subscription = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(this.config.vapidPublicKey),\n });\n await this.config.onSubscribe(subscription.toJSON());\n return subscription;\n }\n\n /**\n * Cancel the current subscription. Calls `onUnsubscribe` first (so the\n * backend can purge the record) and then `subscription.unsubscribe()`.\n *\n * @returns True when an active subscription was removed, false otherwise.\n */\n async unsubscribe(): Promise<boolean> {\n const subscription = await this.getSubscription();\n if (!subscription) return false;\n if (this.config.onUnsubscribe) {\n await this.config.onUnsubscribe(subscription.toJSON());\n }\n return subscription.unsubscribe();\n }\n}\n"],"mappings":"+BAoBA,IAAa,EAAb,cAA6C,KAAM,CAC/C,aAAc,CACV,MAAM,yCAAyC,EAC/C,KAAK,KAAO,yBAChB,CACJ,EAEa,EAAb,cAAkD,KAAM,CACpD,aAAc,CACV,MAAM,+CAA+C,EACrD,KAAK,KAAO,8BAChB,CACJ,EAOa,EAAb,KAA2B,CACvB,OAEA,YAAY,EAA6B,CACrC,KAAK,OAAS,CAClB,CAGA,OAAO,aAAuB,CAC1B,OAAO,EAAA,gBAAgB,CAC3B,CAEA,MAAc,cAAmD,CAC7D,GAAI,CAAC,EAAA,gBAAgB,EAAG,MAAM,IAAI,EAClC,OAAO,KAAK,OAAO,gBACb,MAAM,KAAK,OAAO,gBAAgB,EAClC,MAAM,UAAU,cAAc,KACxC,CAGA,YAAqD,CAEjD,OADI,OAAO,aAAiB,IAAoB,cACzC,aAAa,UACxB,CAQA,MAAM,mBAAqD,CACvD,GAAI,OAAO,aAAiB,IAAa,MAAM,IAAI,EACnD,OAAO,aAAa,kBAAkB,CAC1C,CAGA,MAAM,iBAAoD,CAEtD,OAAO,MADoB,KAAK,aAAa,EAAA,CACzB,YAAY,gBAAgB,CACpD,CAGA,MAAM,cAAiC,CAGnC,OAFK,EAAA,gBAAgB,EAEd,MADgB,KAAK,gBAAgB,IACxB,KAFW,EAGnC,CAWA,MAAM,WAAuC,CAEzC,GAAI,MADqB,KAAK,kBAAkB,IAC7B,UAAW,MAAM,IAAI,EAExC,IAAM,EAAe,MAAM,KAAK,aAAa,EACvC,EAAW,MAAM,EAAa,YAAY,gBAAgB,EAChE,GAAI,EAEA,OADA,MAAM,KAAK,OAAO,YAAY,EAAS,OAAO,CAAC,EACxC,EAGX,IAAM,EAAe,MAAM,EAAa,YAAY,UAAU,CAC1D,gBAAiB,GACjB,qBAAsB,EAAA,sBAAsB,KAAK,OAAO,cAAc,CAC1E,CAAC,EAED,OADA,MAAM,KAAK,OAAO,YAAY,EAAa,OAAO,CAAC,EAC5C,CACX,CAQA,MAAM,aAAgC,CAClC,IAAM,EAAe,MAAM,KAAK,gBAAgB,EAKhD,OAJK,GACD,KAAK,OAAO,eACZ,MAAM,KAAK,OAAO,cAAc,EAAa,OAAO,CAAC,EAElD,EAAa,YAAY,GAJN,EAK9B,CACJ"}
|
|
1
|
+
{"version":3,"file":"web-push-client.cjs","names":[],"sources":["../../src/push/web-push-client.ts"],"sourcesContent":["import { isPushSupported, urlBase64ToUint8Array } from \"./utils\";\n\nexport interface WebPushClientConfig {\n /** VAPID public key (URL-safe base64). */\n vapidPublicKey: string;\n /**\n * Persist the subscription on the backend. The SDK does not own this call —\n * the host app decides the endpoint and how the payload is serialized.\n */\n onSubscribe: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Remove the subscription from the backend. Called before\n * `subscription.unsubscribe()`, with the subscription's JSON — key the\n * delete by its `endpoint`. A rejection no longer keeps the browser\n * subscribed: see {@link WebPushClient.unsubscribe}.\n */\n onUnsubscribe?: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Override how the service worker registration is obtained.\n * Defaults to `navigator.serviceWorker.ready` — provide this only when you\n * register the SW yourself and want to reuse the registration.\n */\n getRegistration?: () => Promise<ServiceWorkerRegistration>;\n}\n\nexport class WebPushUnsupportedError extends Error {\n constructor() {\n super(\"Web Push não suportado neste navegador.\");\n this.name = \"WebPushUnsupportedError\";\n }\n}\n\nexport class WebPushPermissionDeniedError extends Error {\n constructor() {\n super(\"Permissão de notificação negada pelo usuário.\");\n this.name = \"WebPushPermissionDeniedError\";\n }\n}\n\n/**\n * Thrown by {@link WebPushClient.unsubscribe} when `onUnsubscribe` failed. The\n * browser subscription was still released — `browserUnsubscribed` says whether\n * the browser confirmed it — so the only thing left behind is the backend row.\n *\n * Carries the `subscription` JSON so the app can retry the delete by\n * `endpoint` later; if it never does, the push service answers the next send\n * to that endpoint with `404`/`410`, which is the backend's cue to drop the row.\n */\nexport class WebPushUnsubscribeError extends Error {\n /** The subscription whose backend delete failed. */\n readonly subscription: PushSubscriptionJSON;\n /** Whether `PushSubscription.unsubscribe()` reported success. */\n readonly browserUnsubscribed: boolean;\n\n /**\n * @param subscription - The subscription whose backend delete failed.\n * @param browserUnsubscribed - What `PushSubscription.unsubscribe()` returned.\n * @param cause - What `onUnsubscribe` threw.\n */\n constructor(subscription: PushSubscriptionJSON, browserUnsubscribed: boolean, cause: unknown) {\n super(\"Falha ao remover a inscrição de push no backend.\", { cause });\n this.name = \"WebPushUnsubscribeError\";\n this.subscription = subscription;\n this.browserUnsubscribed = browserUnsubscribed;\n }\n}\n\n/**\n * Browser-side Web Push helper. Wraps `Notification.requestPermission`,\n * `pushManager.subscribe`, and the corresponding teardown. Transport is up to\n * the caller via `onSubscribe` / `onUnsubscribe` callbacks.\n */\nexport class WebPushClient {\n private readonly config: WebPushClientConfig;\n\n constructor(config: WebPushClientConfig) {\n this.config = config;\n }\n\n /** Whether the runtime supports the Push API. */\n static isSupported(): boolean {\n return isPushSupported();\n }\n\n private async registration(): Promise<ServiceWorkerRegistration> {\n if (!isPushSupported()) throw new WebPushUnsupportedError();\n return this.config.getRegistration\n ? await this.config.getRegistration()\n : await navigator.serviceWorker.ready;\n }\n\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission(): NotificationPermission | \"unsupported\" {\n if (typeof Notification === \"undefined\") return \"unsupported\";\n return Notification.permission;\n }\n\n /**\n * Ask the user for notification permission.\n *\n * @throws {WebPushUnsupportedError} If the runtime lacks the Notification API.\n * @returns The resulting `NotificationPermission` value.\n */\n async requestPermission(): Promise<NotificationPermission> {\n if (typeof Notification === \"undefined\") throw new WebPushUnsupportedError();\n return Notification.requestPermission();\n }\n\n /** Return the active push subscription, if any. */\n async getSubscription(): Promise<PushSubscription | null> {\n const registration = await this.registration();\n return registration.pushManager.getSubscription();\n }\n\n /** True when a subscription already exists for this browser. */\n async isSubscribed(): Promise<boolean> {\n if (!isPushSupported()) return false;\n const existing = await this.getSubscription();\n return existing !== null;\n }\n\n /**\n * Subscribe the current device. Requests permission, creates a push\n * subscription if none exists, and forwards the JSON payload to\n * `onSubscribe` for backend persistence.\n *\n * @returns The active `PushSubscription`.\n * @throws {WebPushUnsupportedError} If Web Push is unavailable.\n * @throws {WebPushPermissionDeniedError} If the user denies the prompt.\n */\n async subscribe(): Promise<PushSubscription> {\n const permission = await this.requestPermission();\n if (permission !== \"granted\") throw new WebPushPermissionDeniedError();\n\n const registration = await this.registration();\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n await this.config.onSubscribe(existing.toJSON());\n return existing;\n }\n\n const subscription = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(this.config.vapidPublicKey),\n });\n await this.config.onSubscribe(subscription.toJSON());\n return subscription;\n }\n\n /**\n * Cancel the current subscription: `onUnsubscribe` first, so the backend\n * can purge the record while the endpoint is known, then\n * `subscription.unsubscribe()` — **always**, even when `onUnsubscribe`\n * failed.\n *\n * Why the browser is released anyway: the two failure modes are not\n * symmetric. A backend row left behind costs one send to a dead endpoint,\n * which the push service answers with `404`/`410` and the backend deletes.\n * A browser subscription left behind keeps delivering this account's\n * notifications to whoever uses the browser next — and the backend call\n * fails precisely when that matters, at the end of a session, when the\n * token that authenticates it is already gone. Before 0.77.0 a rejected\n * `onUnsubscribe` aborted here and the browser stayed subscribed (#445).\n * The endpoint is not lost either way: it is read before both calls and\n * travels in the error.\n *\n * @returns True when an active subscription was removed, false when there\n * was none or the browser refused.\n * @throws {WebPushUnsubscribeError} When `onUnsubscribe` failed — thrown\n * after the browser subscription was released. When the browser call also\n * threw, `browserUnsubscribed` is `false` and `cause` is an\n * `AggregateError` holding both errors, backend first, so the endpoint the\n * error carries is never traded for a bare `DOMException`.\n */\n async unsubscribe(): Promise<boolean> {\n const subscription = await this.getSubscription();\n if (!subscription) return false;\n const json = subscription.toJSON();\n let backendError: { cause: unknown } | null = null;\n if (this.config.onUnsubscribe) {\n try {\n await this.config.onUnsubscribe(json);\n } catch (cause) {\n backendError = { cause };\n }\n }\n if (!backendError) return subscription.unsubscribe();\n let released: boolean;\n try {\n released = await subscription.unsubscribe();\n } catch (browserError) {\n throw new WebPushUnsubscribeError(\n json,\n false,\n new AggregateError(\n [backendError.cause, browserError],\n \"Backend delete and browser unsubscribe both failed.\",\n ),\n );\n }\n throw new WebPushUnsubscribeError(json, released, backendError.cause);\n }\n}\n"],"mappings":"+BAyBA,IAAa,EAAb,cAA6C,KAAM,CAC/C,aAAc,CACV,MAAM,yCAAyC,EAC/C,KAAK,KAAO,yBAChB,CACJ,EAEa,EAAb,cAAkD,KAAM,CACpD,aAAc,CACV,MAAM,+CAA+C,EACrD,KAAK,KAAO,8BAChB,CACJ,EAWa,EAAb,cAA6C,KAAM,CAE/C,aAEA,oBAOA,YAAY,EAAoC,EAA8B,EAAgB,CAC1F,MAAM,mDAAoD,CAAE,OAAM,CAAC,EACnE,KAAK,KAAO,0BACZ,KAAK,aAAe,EACpB,KAAK,oBAAsB,CAC/B,CACJ,EAOa,EAAb,KAA2B,CACvB,OAEA,YAAY,EAA6B,CACrC,KAAK,OAAS,CAClB,CAGA,OAAO,aAAuB,CAC1B,OAAO,EAAA,gBAAgB,CAC3B,CAEA,MAAc,cAAmD,CAC7D,GAAI,CAAC,EAAA,gBAAgB,EAAG,MAAM,IAAI,EAClC,OAAO,KAAK,OAAO,gBACb,MAAM,KAAK,OAAO,gBAAgB,EAClC,MAAM,UAAU,cAAc,KACxC,CAGA,YAAqD,CAEjD,OADI,OAAO,aAAiB,IAAoB,cACzC,aAAa,UACxB,CAQA,MAAM,mBAAqD,CACvD,GAAI,OAAO,aAAiB,IAAa,MAAM,IAAI,EACnD,OAAO,aAAa,kBAAkB,CAC1C,CAGA,MAAM,iBAAoD,CAEtD,OAAO,MADoB,KAAK,aAAa,EAAA,CACzB,YAAY,gBAAgB,CACpD,CAGA,MAAM,cAAiC,CAGnC,OAFK,EAAA,gBAAgB,EAEd,MADgB,KAAK,gBAAgB,IACxB,KAFW,EAGnC,CAWA,MAAM,WAAuC,CAEzC,GAAI,MADqB,KAAK,kBAAkB,IAC7B,UAAW,MAAM,IAAI,EAExC,IAAM,EAAe,MAAM,KAAK,aAAa,EACvC,EAAW,MAAM,EAAa,YAAY,gBAAgB,EAChE,GAAI,EAEA,OADA,MAAM,KAAK,OAAO,YAAY,EAAS,OAAO,CAAC,EACxC,EAGX,IAAM,EAAe,MAAM,EAAa,YAAY,UAAU,CAC1D,gBAAiB,GACjB,qBAAsB,EAAA,sBAAsB,KAAK,OAAO,cAAc,CAC1E,CAAC,EAED,OADA,MAAM,KAAK,OAAO,YAAY,EAAa,OAAO,CAAC,EAC5C,CACX,CA2BA,MAAM,aAAgC,CAClC,IAAM,EAAe,MAAM,KAAK,gBAAgB,EAChD,GAAI,CAAC,EAAc,MAAO,GAC1B,IAAM,EAAO,EAAa,OAAO,EAC7B,EAA0C,KAC9C,GAAI,KAAK,OAAO,cACZ,GAAI,CACA,MAAM,KAAK,OAAO,cAAc,CAAI,CACxC,OAAS,EAAO,CACZ,EAAe,CAAE,OAAM,CAC3B,CAEJ,GAAI,CAAC,EAAc,OAAO,EAAa,YAAY,EACnD,IAAI,EACJ,GAAI,CACA,EAAW,MAAM,EAAa,YAAY,CAC9C,OAAS,EAAc,CACnB,MAAM,IAAI,EACN,EACA,GACI,eACA,CAAC,EAAa,MAAO,CAAY,EACjC,qDACJ,CACJ,CACJ,CACA,MAAM,IAAI,EAAwB,EAAM,EAAU,EAAa,KAAK,CACxE,CACJ"}
|
|
@@ -8,7 +8,13 @@ var n = class extends Error {
|
|
|
8
8
|
constructor() {
|
|
9
9
|
super("Permissão de notificação negada pelo usuário."), this.name = "WebPushPermissionDeniedError";
|
|
10
10
|
}
|
|
11
|
-
}, i = class {
|
|
11
|
+
}, i = class extends Error {
|
|
12
|
+
subscription;
|
|
13
|
+
browserUnsubscribed;
|
|
14
|
+
constructor(e, t, n) {
|
|
15
|
+
super("Falha ao remover a inscrição de push no backend.", { cause: n }), this.name = "WebPushUnsubscribeError", this.subscription = e, this.browserUnsubscribed = t;
|
|
16
|
+
}
|
|
17
|
+
}, a = class {
|
|
12
18
|
config;
|
|
13
19
|
constructor(e) {
|
|
14
20
|
this.config = e;
|
|
@@ -45,10 +51,24 @@ var n = class extends Error {
|
|
|
45
51
|
}
|
|
46
52
|
async unsubscribe() {
|
|
47
53
|
let e = await this.getSubscription();
|
|
48
|
-
|
|
54
|
+
if (!e) return !1;
|
|
55
|
+
let t = e.toJSON(), n = null;
|
|
56
|
+
if (this.config.onUnsubscribe) try {
|
|
57
|
+
await this.config.onUnsubscribe(t);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
n = { cause: e };
|
|
60
|
+
}
|
|
61
|
+
if (!n) return e.unsubscribe();
|
|
62
|
+
let r;
|
|
63
|
+
try {
|
|
64
|
+
r = await e.unsubscribe();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new i(t, !1, AggregateError([n.cause, e], "Backend delete and browser unsubscribe both failed."));
|
|
67
|
+
}
|
|
68
|
+
throw new i(t, r, n.cause);
|
|
49
69
|
}
|
|
50
70
|
};
|
|
51
71
|
//#endregion
|
|
52
|
-
export {
|
|
72
|
+
export { a as WebPushClient, r as WebPushPermissionDeniedError, i as WebPushUnsubscribeError, n as WebPushUnsupportedError };
|
|
53
73
|
|
|
54
74
|
//# sourceMappingURL=web-push-client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-push-client.js","names":[],"sources":["../../src/push/web-push-client.ts"],"sourcesContent":["import { isPushSupported, urlBase64ToUint8Array } from \"./utils\";\n\nexport interface WebPushClientConfig {\n /** VAPID public key (URL-safe base64). */\n vapidPublicKey: string;\n /**\n * Persist the subscription on the backend. The SDK does not own this call —\n * the host app decides the endpoint and how the payload is serialized.\n */\n onSubscribe: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /** Remove the subscription from the backend. Called before `subscription.unsubscribe()`. */\n onUnsubscribe?: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Override how the service worker registration is obtained.\n * Defaults to `navigator.serviceWorker.ready` — provide this only when you\n * register the SW yourself and want to reuse the registration.\n */\n getRegistration?: () => Promise<ServiceWorkerRegistration>;\n}\n\nexport class WebPushUnsupportedError extends Error {\n constructor() {\n super(\"Web Push não suportado neste navegador.\");\n this.name = \"WebPushUnsupportedError\";\n }\n}\n\nexport class WebPushPermissionDeniedError extends Error {\n constructor() {\n super(\"Permissão de notificação negada pelo usuário.\");\n this.name = \"WebPushPermissionDeniedError\";\n }\n}\n\n/**\n * Browser-side Web Push helper. Wraps `Notification.requestPermission`,\n * `pushManager.subscribe`, and the corresponding teardown. Transport is up to\n * the caller via `onSubscribe` / `onUnsubscribe` callbacks.\n */\nexport class WebPushClient {\n private readonly config: WebPushClientConfig;\n\n constructor(config: WebPushClientConfig) {\n this.config = config;\n }\n\n /** Whether the runtime supports the Push API. */\n static isSupported(): boolean {\n return isPushSupported();\n }\n\n private async registration(): Promise<ServiceWorkerRegistration> {\n if (!isPushSupported()) throw new WebPushUnsupportedError();\n return this.config.getRegistration\n ? await this.config.getRegistration()\n : await navigator.serviceWorker.ready;\n }\n\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission(): NotificationPermission | \"unsupported\" {\n if (typeof Notification === \"undefined\") return \"unsupported\";\n return Notification.permission;\n }\n\n /**\n * Ask the user for notification permission.\n *\n * @throws {WebPushUnsupportedError} If the runtime lacks the Notification API.\n * @returns The resulting `NotificationPermission` value.\n */\n async requestPermission(): Promise<NotificationPermission> {\n if (typeof Notification === \"undefined\") throw new WebPushUnsupportedError();\n return Notification.requestPermission();\n }\n\n /** Return the active push subscription, if any. */\n async getSubscription(): Promise<PushSubscription | null> {\n const registration = await this.registration();\n return registration.pushManager.getSubscription();\n }\n\n /** True when a subscription already exists for this browser. */\n async isSubscribed(): Promise<boolean> {\n if (!isPushSupported()) return false;\n const existing = await this.getSubscription();\n return existing !== null;\n }\n\n /**\n * Subscribe the current device. Requests permission, creates a push\n * subscription if none exists, and forwards the JSON payload to\n * `onSubscribe` for backend persistence.\n *\n * @returns The active `PushSubscription`.\n * @throws {WebPushUnsupportedError} If Web Push is unavailable.\n * @throws {WebPushPermissionDeniedError} If the user denies the prompt.\n */\n async subscribe(): Promise<PushSubscription> {\n const permission = await this.requestPermission();\n if (permission !== \"granted\") throw new WebPushPermissionDeniedError();\n\n const registration = await this.registration();\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n await this.config.onSubscribe(existing.toJSON());\n return existing;\n }\n\n const subscription = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(this.config.vapidPublicKey),\n });\n await this.config.onSubscribe(subscription.toJSON());\n return subscription;\n }\n\n /**\n * Cancel the current subscription. Calls `onUnsubscribe` first (so the\n * backend can purge the record) and then `subscription.unsubscribe()`.\n *\n * @returns True when an active subscription was removed, false otherwise.\n */\n async unsubscribe(): Promise<boolean> {\n const subscription = await this.getSubscription();\n if (!subscription) return false;\n if (this.config.onUnsubscribe) {\n await this.config.onUnsubscribe(subscription.toJSON());\n }\n return subscription.unsubscribe();\n }\n}\n"],"mappings":";;AAoBA,IAAa,IAAb,cAA6C,MAAM;CAC/C,cAAc;EAEV,AADA,MAAM,yCAAyC,GAC/C,KAAK,OAAO;CAChB;AACJ,GAEa,IAAb,cAAkD,MAAM;CACpD,cAAc;EAEV,AADA,MAAM,+CAA+C,GACrD,KAAK,OAAO;CAChB;AACJ,GAOa,IAAb,MAA2B;CACvB;CAEA,YAAY,GAA6B;EACrC,KAAK,SAAS;CAClB;CAGA,OAAO,cAAuB;EAC1B,OAAO,EAAgB;CAC3B;CAEA,MAAc,eAAmD;EAC7D,IAAI,CAAC,EAAgB,GAAG,MAAM,IAAI,EAAwB;EAC1D,OAAO,KAAK,OAAO,kBACb,MAAM,KAAK,OAAO,gBAAgB,IAClC,MAAM,UAAU,cAAc;CACxC;CAGA,aAAqD;EAEjD,OADI,OAAO,eAAiB,MAAoB,gBACzC,aAAa;CACxB;CAQA,MAAM,oBAAqD;EACvD,IAAI,OAAO,eAAiB,KAAa,MAAM,IAAI,EAAwB;EAC3E,OAAO,aAAa,kBAAkB;CAC1C;CAGA,MAAM,kBAAoD;EAEtD,QAAO,MADoB,KAAK,aAAa,EAAA,CACzB,YAAY,gBAAgB;CACpD;CAGA,MAAM,eAAiC;EAGnC,OAFK,EAAgB,IAEd,MADgB,KAAK,gBAAgB,MACxB,OAFW;CAGnC;CAWA,MAAM,YAAuC;EAEzC,IAAI,MADqB,KAAK,kBAAkB,MAC7B,WAAW,MAAM,IAAI,EAA6B;EAErE,IAAM,IAAe,MAAM,KAAK,aAAa,GACvC,IAAW,MAAM,EAAa,YAAY,gBAAgB;EAChE,IAAI,GAEA,OADA,MAAM,KAAK,OAAO,YAAY,EAAS,OAAO,CAAC,GACxC;EAGX,IAAM,IAAe,MAAM,EAAa,YAAY,UAAU;GAC1D,iBAAiB;GACjB,sBAAsB,EAAsB,KAAK,OAAO,cAAc;EAC1E,CAAC;EAED,OADA,MAAM,KAAK,OAAO,YAAY,EAAa,OAAO,CAAC,GAC5C;CACX;CAQA,MAAM,cAAgC;EAClC,IAAM,IAAe,MAAM,KAAK,gBAAgB;EAKhD,OAJK,KACD,KAAK,OAAO,iBACZ,MAAM,KAAK,OAAO,cAAc,EAAa,OAAO,CAAC,GAElD,EAAa,YAAY,KAJN;CAK9B;AACJ"}
|
|
1
|
+
{"version":3,"file":"web-push-client.js","names":[],"sources":["../../src/push/web-push-client.ts"],"sourcesContent":["import { isPushSupported, urlBase64ToUint8Array } from \"./utils\";\n\nexport interface WebPushClientConfig {\n /** VAPID public key (URL-safe base64). */\n vapidPublicKey: string;\n /**\n * Persist the subscription on the backend. The SDK does not own this call —\n * the host app decides the endpoint and how the payload is serialized.\n */\n onSubscribe: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Remove the subscription from the backend. Called before\n * `subscription.unsubscribe()`, with the subscription's JSON — key the\n * delete by its `endpoint`. A rejection no longer keeps the browser\n * subscribed: see {@link WebPushClient.unsubscribe}.\n */\n onUnsubscribe?: (subscription: PushSubscriptionJSON) => Promise<void> | void;\n /**\n * Override how the service worker registration is obtained.\n * Defaults to `navigator.serviceWorker.ready` — provide this only when you\n * register the SW yourself and want to reuse the registration.\n */\n getRegistration?: () => Promise<ServiceWorkerRegistration>;\n}\n\nexport class WebPushUnsupportedError extends Error {\n constructor() {\n super(\"Web Push não suportado neste navegador.\");\n this.name = \"WebPushUnsupportedError\";\n }\n}\n\nexport class WebPushPermissionDeniedError extends Error {\n constructor() {\n super(\"Permissão de notificação negada pelo usuário.\");\n this.name = \"WebPushPermissionDeniedError\";\n }\n}\n\n/**\n * Thrown by {@link WebPushClient.unsubscribe} when `onUnsubscribe` failed. The\n * browser subscription was still released — `browserUnsubscribed` says whether\n * the browser confirmed it — so the only thing left behind is the backend row.\n *\n * Carries the `subscription` JSON so the app can retry the delete by\n * `endpoint` later; if it never does, the push service answers the next send\n * to that endpoint with `404`/`410`, which is the backend's cue to drop the row.\n */\nexport class WebPushUnsubscribeError extends Error {\n /** The subscription whose backend delete failed. */\n readonly subscription: PushSubscriptionJSON;\n /** Whether `PushSubscription.unsubscribe()` reported success. */\n readonly browserUnsubscribed: boolean;\n\n /**\n * @param subscription - The subscription whose backend delete failed.\n * @param browserUnsubscribed - What `PushSubscription.unsubscribe()` returned.\n * @param cause - What `onUnsubscribe` threw.\n */\n constructor(subscription: PushSubscriptionJSON, browserUnsubscribed: boolean, cause: unknown) {\n super(\"Falha ao remover a inscrição de push no backend.\", { cause });\n this.name = \"WebPushUnsubscribeError\";\n this.subscription = subscription;\n this.browserUnsubscribed = browserUnsubscribed;\n }\n}\n\n/**\n * Browser-side Web Push helper. Wraps `Notification.requestPermission`,\n * `pushManager.subscribe`, and the corresponding teardown. Transport is up to\n * the caller via `onSubscribe` / `onUnsubscribe` callbacks.\n */\nexport class WebPushClient {\n private readonly config: WebPushClientConfig;\n\n constructor(config: WebPushClientConfig) {\n this.config = config;\n }\n\n /** Whether the runtime supports the Push API. */\n static isSupported(): boolean {\n return isPushSupported();\n }\n\n private async registration(): Promise<ServiceWorkerRegistration> {\n if (!isPushSupported()) throw new WebPushUnsupportedError();\n return this.config.getRegistration\n ? await this.config.getRegistration()\n : await navigator.serviceWorker.ready;\n }\n\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission(): NotificationPermission | \"unsupported\" {\n if (typeof Notification === \"undefined\") return \"unsupported\";\n return Notification.permission;\n }\n\n /**\n * Ask the user for notification permission.\n *\n * @throws {WebPushUnsupportedError} If the runtime lacks the Notification API.\n * @returns The resulting `NotificationPermission` value.\n */\n async requestPermission(): Promise<NotificationPermission> {\n if (typeof Notification === \"undefined\") throw new WebPushUnsupportedError();\n return Notification.requestPermission();\n }\n\n /** Return the active push subscription, if any. */\n async getSubscription(): Promise<PushSubscription | null> {\n const registration = await this.registration();\n return registration.pushManager.getSubscription();\n }\n\n /** True when a subscription already exists for this browser. */\n async isSubscribed(): Promise<boolean> {\n if (!isPushSupported()) return false;\n const existing = await this.getSubscription();\n return existing !== null;\n }\n\n /**\n * Subscribe the current device. Requests permission, creates a push\n * subscription if none exists, and forwards the JSON payload to\n * `onSubscribe` for backend persistence.\n *\n * @returns The active `PushSubscription`.\n * @throws {WebPushUnsupportedError} If Web Push is unavailable.\n * @throws {WebPushPermissionDeniedError} If the user denies the prompt.\n */\n async subscribe(): Promise<PushSubscription> {\n const permission = await this.requestPermission();\n if (permission !== \"granted\") throw new WebPushPermissionDeniedError();\n\n const registration = await this.registration();\n const existing = await registration.pushManager.getSubscription();\n if (existing) {\n await this.config.onSubscribe(existing.toJSON());\n return existing;\n }\n\n const subscription = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: urlBase64ToUint8Array(this.config.vapidPublicKey),\n });\n await this.config.onSubscribe(subscription.toJSON());\n return subscription;\n }\n\n /**\n * Cancel the current subscription: `onUnsubscribe` first, so the backend\n * can purge the record while the endpoint is known, then\n * `subscription.unsubscribe()` — **always**, even when `onUnsubscribe`\n * failed.\n *\n * Why the browser is released anyway: the two failure modes are not\n * symmetric. A backend row left behind costs one send to a dead endpoint,\n * which the push service answers with `404`/`410` and the backend deletes.\n * A browser subscription left behind keeps delivering this account's\n * notifications to whoever uses the browser next — and the backend call\n * fails precisely when that matters, at the end of a session, when the\n * token that authenticates it is already gone. Before 0.77.0 a rejected\n * `onUnsubscribe` aborted here and the browser stayed subscribed (#445).\n * The endpoint is not lost either way: it is read before both calls and\n * travels in the error.\n *\n * @returns True when an active subscription was removed, false when there\n * was none or the browser refused.\n * @throws {WebPushUnsubscribeError} When `onUnsubscribe` failed — thrown\n * after the browser subscription was released. When the browser call also\n * threw, `browserUnsubscribed` is `false` and `cause` is an\n * `AggregateError` holding both errors, backend first, so the endpoint the\n * error carries is never traded for a bare `DOMException`.\n */\n async unsubscribe(): Promise<boolean> {\n const subscription = await this.getSubscription();\n if (!subscription) return false;\n const json = subscription.toJSON();\n let backendError: { cause: unknown } | null = null;\n if (this.config.onUnsubscribe) {\n try {\n await this.config.onUnsubscribe(json);\n } catch (cause) {\n backendError = { cause };\n }\n }\n if (!backendError) return subscription.unsubscribe();\n let released: boolean;\n try {\n released = await subscription.unsubscribe();\n } catch (browserError) {\n throw new WebPushUnsubscribeError(\n json,\n false,\n new AggregateError(\n [backendError.cause, browserError],\n \"Backend delete and browser unsubscribe both failed.\",\n ),\n );\n }\n throw new WebPushUnsubscribeError(json, released, backendError.cause);\n }\n}\n"],"mappings":";;AAyBA,IAAa,IAAb,cAA6C,MAAM;CAC/C,cAAc;EAEV,AADA,MAAM,yCAAyC,GAC/C,KAAK,OAAO;CAChB;AACJ,GAEa,IAAb,cAAkD,MAAM;CACpD,cAAc;EAEV,AADA,MAAM,+CAA+C,GACrD,KAAK,OAAO;CAChB;AACJ,GAWa,IAAb,cAA6C,MAAM;CAE/C;CAEA;CAOA,YAAY,GAAoC,GAA8B,GAAgB;EAI1F,AAHA,MAAM,oDAAoD,EAAE,SAAM,CAAC,GACnE,KAAK,OAAO,2BACZ,KAAK,eAAe,GACpB,KAAK,sBAAsB;CAC/B;AACJ,GAOa,IAAb,MAA2B;CACvB;CAEA,YAAY,GAA6B;EACrC,KAAK,SAAS;CAClB;CAGA,OAAO,cAAuB;EAC1B,OAAO,EAAgB;CAC3B;CAEA,MAAc,eAAmD;EAC7D,IAAI,CAAC,EAAgB,GAAG,MAAM,IAAI,EAAwB;EAC1D,OAAO,KAAK,OAAO,kBACb,MAAM,KAAK,OAAO,gBAAgB,IAClC,MAAM,UAAU,cAAc;CACxC;CAGA,aAAqD;EAEjD,OADI,OAAO,eAAiB,MAAoB,gBACzC,aAAa;CACxB;CAQA,MAAM,oBAAqD;EACvD,IAAI,OAAO,eAAiB,KAAa,MAAM,IAAI,EAAwB;EAC3E,OAAO,aAAa,kBAAkB;CAC1C;CAGA,MAAM,kBAAoD;EAEtD,QAAO,MADoB,KAAK,aAAa,EAAA,CACzB,YAAY,gBAAgB;CACpD;CAGA,MAAM,eAAiC;EAGnC,OAFK,EAAgB,IAEd,MADgB,KAAK,gBAAgB,MACxB,OAFW;CAGnC;CAWA,MAAM,YAAuC;EAEzC,IAAI,MADqB,KAAK,kBAAkB,MAC7B,WAAW,MAAM,IAAI,EAA6B;EAErE,IAAM,IAAe,MAAM,KAAK,aAAa,GACvC,IAAW,MAAM,EAAa,YAAY,gBAAgB;EAChE,IAAI,GAEA,OADA,MAAM,KAAK,OAAO,YAAY,EAAS,OAAO,CAAC,GACxC;EAGX,IAAM,IAAe,MAAM,EAAa,YAAY,UAAU;GAC1D,iBAAiB;GACjB,sBAAsB,EAAsB,KAAK,OAAO,cAAc;EAC1E,CAAC;EAED,OADA,MAAM,KAAK,OAAO,YAAY,EAAa,OAAO,CAAC,GAC5C;CACX;CA2BA,MAAM,cAAgC;EAClC,IAAM,IAAe,MAAM,KAAK,gBAAgB;EAChD,IAAI,CAAC,GAAc,OAAO;EAC1B,IAAM,IAAO,EAAa,OAAO,GAC7B,IAA0C;EAC9C,IAAI,KAAK,OAAO,eACZ,IAAI;GACA,MAAM,KAAK,OAAO,cAAc,CAAI;EACxC,SAAS,GAAO;GACZ,IAAe,EAAE,SAAM;EAC3B;EAEJ,IAAI,CAAC,GAAc,OAAO,EAAa,YAAY;EACnD,IAAI;EACJ,IAAI;GACA,IAAW,MAAM,EAAa,YAAY;EAC9C,SAAS,GAAc;GACnB,MAAM,IAAI,EACN,GACA,IACI,eACA,CAAC,EAAa,OAAO,CAAY,GACjC,qDACJ,CACJ;EACJ;EACA,MAAM,IAAI,EAAwB,GAAM,GAAU,EAAa,KAAK;CACxE;AACJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../offline/create-offline-store.cjs");let
|
|
1
|
+
const e=require("../utils/dev-mode.cjs"),t=require("../offline/create-offline-store.cjs");let n=require("@tanstack/react-query");var r=new Map;function i(i){let{queryClient:a,throttleMs:o=1e3}=i,s=i.key??`tanstack-query`,c=i.databaseName??`TempestQueryCache`,l=t.createOfflineStore({databaseName:c,version:1,tableName:`cache`,indexes:`&id`,keyPath:`id`}),u=`${c}\u0000${s}`,d=!1,f=!1,p=null,m=new Set;async function h(){let e=r.get(u);e&&await e}function g(){!f&&e.isDevBuild()&&(f=!0,console.warn(`[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); create a new one after login to keep persisting.`))}async function _(){if(d||(await h(),d))return;let e=l.put({id:s,state:(0,n.dehydrate)(a)});m.add(e);try{await e}finally{m.delete(e)}}function v(){p&&=(clearTimeout(p),null)}function y(){p||d||(p=setTimeout(()=>{p=null,_()},o))}function b(e){if(!d){y();return}(e.type===`added`||e.type===`updated`)&&(x(),g())}let x=a.getQueryCache().subscribe(b);async function S(e){try{await Promise.allSettled([...m]),await l.delete(s)}finally{e&&a.clear()}}return{async restore(){if(d){g();return}if(await h(),d)return;let e=await l.get(s);e&&!d&&(0,n.hydrate)(a,e.state)},async flush(){if(d){g();return}v(),await _()},unsubscribe(){v(),x()},async clear(e={}){d=!0,v();let t=r.get(u),n=S(e.resetClient??!1),i=Promise.allSettled([t,n]);r.set(u,i),i.then(()=>{r.get(u)===i&&r.delete(u)}),await n}}}exports.persistQueryClientOffline=i;
|
|
2
2
|
//# sourceMappingURL=offline-persistence.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-persistence.cjs","names":[],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import { dehydrate, hydrate, type QueryClient } from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\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\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.\n */\n restore: () => Promise<void>;\n /** Write the current cache snapshot immediately (bypassing the throttle). */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. */\n unsubscribe: () => void;\n /** Delete the persisted snapshot (e.g. on logout). */\n clear: () => Promise<void>;\n}\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\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 * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore(); // before first render\n * // ... later, on logout:\n * await persistence.clear();\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000 } = options;\n const key = options.key ?? \"tanstack-query\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName: options.databaseName ?? \"TempestQueryCache\",\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n async function save(): Promise<void> {\n await store.put({ id: key, state: dehydrate(queryClient) });\n }\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n function schedule(): void {\n if (timer) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(schedule);\n\n return {\n async restore() {\n const snapshot = await store.get(key);\n if (snapshot) hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n await save();\n },\n unsubscribe() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n unsubscribeCache();\n },\n async clear() {\n await store.delete(key);\n },\n };\n}\n"],"mappings":"8FAyDA,SAAgB,EACZ,EACuB,CACvB,GAAM,CAAE,cAAa,aAAa,KAAS,EACrC,EAAM,EAAQ,KAAO,iBACrB,EAAQ,EAAA,mBAAqC,CAC/C,aAAc,EAAQ,cAAgB,oBACtC,QAAS,EACT,UAAW,QACX,QAAS,MACT,QAAS,IACb,CAAC,EAED,eAAe,GAAsB,CACjC,MAAM,EAAM,IAAI,CAAE,GAAI,EAAK,OAAA,EAAO,EAAA,UAAA,CAAU,CAAW,CAAE,CAAC,CAC9D,CAEA,IAAI,EAA8C,KAClD,SAAS,GAAiB,CAClB,AACJ,IAAQ,eAAiB,CACrB,EAAQ,KACR,EAAU,CACd,EAAG,CAAU,CACjB,CAEA,IAAM,EAAmB,EAAY,cAAc,CAAC,CAAC,UAAU,CAAQ,EAEvE,MAAO,CACH,MAAM,SAAU,CACZ,IAAM,EAAW,MAAM,EAAM,IAAI,CAAG,EAChC,IAAU,EAAA,EAAA,QAAA,CAAQ,EAAa,EAAS,KAAK,CACrD,EACA,MAAM,OAAQ,CACV,AAEI,KADA,aAAa,CAAK,EACV,MAEZ,MAAM,EAAK,CACf,EACA,aAAc,CACV,AAEI,KADA,aAAa,CAAK,EACV,MAEZ,EAAiB,CACrB,EACA,MAAM,OAAQ,CACV,MAAM,EAAM,OAAO,CAAG,CAC1B,CACJ,CACJ"}
|
|
1
|
+
{"version":3,"file":"offline-persistence.cjs","names":[],"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\";\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\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.\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}\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 } = 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 write = store.put({ id: key, state: dehydrate(queryClient) });\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) 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":"iIAkBA,IAAM,EAAgB,IAAI,IAgH1B,SAAgB,EACZ,EACuB,CACvB,GAAM,CAAE,cAAa,aAAa,KAAS,EACrC,EAAM,EAAQ,KAAO,iBACrB,EAAe,EAAQ,cAAgB,oBACvC,EAAQ,EAAA,mBAAqC,CAC/C,eACA,QAAS,EACT,UAAW,QACX,QAAS,MACT,QAAS,IACb,CAAC,EAEK,EAAO,GAAG,EAAa,QAAQ,IACjC,EAAQ,GACR,EAAc,GACd,EAA8C,KAC5C,EAAW,IAAI,IAErB,eAAe,GAAmC,CAC9C,IAAM,EAAU,EAAc,IAAI,CAAI,EAClC,GAAS,MAAM,CACvB,CAEA,SAAS,GAAkB,CACnB,IAAgB,EAAA,WAAW,IAC/B,EAAc,GACd,QAAQ,KACJ,0HAEJ,EACJ,CAEA,eAAe,GAAsB,CAGjC,GAFI,IACJ,MAAM,EAAkB,EACpB,GAAO,OACX,IAAM,EAAQ,EAAM,IAAI,CAAE,GAAI,EAAK,OAAA,EAAO,EAAA,UAAA,CAAU,CAAW,CAAE,CAAC,EAClE,EAAS,IAAI,CAAK,EAClB,GAAI,CACA,MAAM,CACV,QAAU,CACN,EAAS,OAAO,CAAK,CACzB,CACJ,CAEA,SAAS,GAAoB,CACzB,AAEI,KADA,aAAa,CAAK,EACV,KAEhB,CAEA,SAAS,GAAiB,CAClB,GAAS,IACb,EAAQ,eAAiB,CACrB,EAAQ,KACR,EAAU,CACd,EAAG,CAAU,EACjB,CAEA,SAAS,EAAa,EAAoC,CACtD,GAAI,CAAC,EAAO,CACR,EAAS,EACT,MACJ,EACI,EAAM,OAAS,SAAW,EAAM,OAAS,aAC7C,EAAiB,EACjB,EAAU,EACd,CAEA,IAAM,EAAmB,EAAY,cAAc,CAAC,CAAC,UAAU,CAAY,EAE3E,eAAe,EAAW,EAAqC,CAC3D,GAAI,CACA,MAAM,QAAQ,WAAW,CAAC,GAAG,CAAQ,CAAC,EACtC,MAAM,EAAM,OAAO,CAAG,CAC1B,QAAU,CACF,GAAa,EAAY,MAAM,CACvC,CACJ,CAEA,MAAO,CACH,MAAM,SAAU,CACZ,GAAI,EAAO,CACP,EAAU,EACV,MACJ,CAEA,GADA,MAAM,EAAkB,EACpB,EAAO,OACX,IAAM,EAAW,MAAM,EAAM,IAAI,CAAG,EAChC,GAAY,CAAC,IAAO,EAAA,EAAA,QAAA,CAAQ,EAAa,EAAS,KAAK,CAC/D,EACA,MAAM,OAAQ,CACV,GAAI,EAAO,CACP,EAAU,EACV,MACJ,CACA,EAAY,EACZ,MAAM,EAAK,CACf,EACA,aAAc,CACV,EAAY,EACZ,EAAiB,CACrB,EACA,MAAM,MAAM,EAAe,CAAC,EAAG,CAC3B,EAAQ,GACR,EAAY,EACZ,IAAM,EAAW,EAAc,IAAI,CAAI,EACjC,EAAM,EAAW,EAAa,aAAe,EAAK,EAClD,EAAU,QAAQ,WAAW,CAAC,EAAU,CAAG,CAAC,EAClD,EAAc,IAAI,EAAM,CAAO,EAC/B,EAAa,SAAW,CAChB,EAAc,IAAI,CAAI,IAAM,GAAS,EAAc,OAAO,CAAI,CACtE,CAAC,EACD,MAAM,CACV,CACJ,CACJ"}
|
|
@@ -1,44 +1,89 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isDevBuild as e } from "../utils/dev-mode.js";
|
|
2
|
+
import { createOfflineStore as t } from "../offline/create-offline-store.js";
|
|
3
|
+
import { dehydrate as n, hydrate as r } from "@tanstack/react-query";
|
|
3
4
|
//#region src/query/offline-persistence.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
var i = /* @__PURE__ */ new Map();
|
|
6
|
+
function a(a) {
|
|
7
|
+
let { queryClient: o, throttleMs: s = 1e3 } = a, c = a.key ?? "tanstack-query", l = a.databaseName ?? "TempestQueryCache", u = t({
|
|
8
|
+
databaseName: l,
|
|
7
9
|
version: 1,
|
|
8
10
|
tableName: "cache",
|
|
9
11
|
indexes: "&id",
|
|
10
12
|
keyPath: "id"
|
|
11
|
-
});
|
|
12
|
-
async function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
}), d = `${l}\u0000${c}`, f = !1, p = !1, m = null, h = /* @__PURE__ */ new Set();
|
|
14
|
+
async function g() {
|
|
15
|
+
let e = i.get(d);
|
|
16
|
+
e && await e;
|
|
17
|
+
}
|
|
18
|
+
function _() {
|
|
19
|
+
!p && e() && (p = !0, console.warn("[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); create a new one after login to keep persisting."));
|
|
20
|
+
}
|
|
21
|
+
async function v() {
|
|
22
|
+
if (f || (await g(), f)) return;
|
|
23
|
+
let e = u.put({
|
|
24
|
+
id: c,
|
|
25
|
+
state: n(o)
|
|
16
26
|
});
|
|
27
|
+
h.add(e);
|
|
28
|
+
try {
|
|
29
|
+
await e;
|
|
30
|
+
} finally {
|
|
31
|
+
h.delete(e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function y() {
|
|
35
|
+
m &&= (clearTimeout(m), null);
|
|
17
36
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
function b() {
|
|
38
|
+
m || f || (m = setTimeout(() => {
|
|
39
|
+
m = null, v();
|
|
40
|
+
}, s));
|
|
41
|
+
}
|
|
42
|
+
function x(e) {
|
|
43
|
+
if (!f) {
|
|
44
|
+
b();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
(e.type === "added" || e.type === "updated") && (S(), _());
|
|
48
|
+
}
|
|
49
|
+
let S = o.getQueryCache().subscribe(x);
|
|
50
|
+
async function C(e) {
|
|
51
|
+
try {
|
|
52
|
+
await Promise.allSettled([...h]), await u.delete(c);
|
|
53
|
+
} finally {
|
|
54
|
+
e && o.clear();
|
|
55
|
+
}
|
|
23
56
|
}
|
|
24
|
-
let d = i.getQueryCache().subscribe(u);
|
|
25
57
|
return {
|
|
26
58
|
async restore() {
|
|
27
|
-
|
|
28
|
-
|
|
59
|
+
if (f) {
|
|
60
|
+
_();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (await g(), f) return;
|
|
64
|
+
let e = await u.get(c);
|
|
65
|
+
e && !f && r(o, e.state);
|
|
29
66
|
},
|
|
30
67
|
async flush() {
|
|
31
|
-
|
|
68
|
+
if (f) {
|
|
69
|
+
_();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
y(), await v();
|
|
32
73
|
},
|
|
33
74
|
unsubscribe() {
|
|
34
|
-
|
|
75
|
+
y(), S();
|
|
35
76
|
},
|
|
36
|
-
async clear() {
|
|
37
|
-
|
|
77
|
+
async clear(e = {}) {
|
|
78
|
+
f = !0, y();
|
|
79
|
+
let t = i.get(d), n = C(e.resetClient ?? !1), r = Promise.allSettled([t, n]);
|
|
80
|
+
i.set(d, r), r.then(() => {
|
|
81
|
+
i.get(d) === r && i.delete(d);
|
|
82
|
+
}), await n;
|
|
38
83
|
}
|
|
39
84
|
};
|
|
40
85
|
}
|
|
41
86
|
//#endregion
|
|
42
|
-
export {
|
|
87
|
+
export { a as persistQueryClientOffline };
|
|
43
88
|
|
|
44
89
|
//# sourceMappingURL=offline-persistence.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-persistence.js","names":[],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import { dehydrate, hydrate, type QueryClient } from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\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\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.\n */\n restore: () => Promise<void>;\n /** Write the current cache snapshot immediately (bypassing the throttle). */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. */\n unsubscribe: () => void;\n /** Delete the persisted snapshot (e.g. on logout). */\n clear: () => Promise<void>;\n}\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\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 * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore(); // before first render\n * // ... later, on logout:\n * await persistence.clear();\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000 } = options;\n const key = options.key ?? \"tanstack-query\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName: options.databaseName ?? \"TempestQueryCache\",\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n async function save(): Promise<void> {\n await store.put({ id: key, state: dehydrate(queryClient) });\n }\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n function schedule(): void {\n if (timer) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(schedule);\n\n return {\n async restore() {\n const snapshot = await store.get(key);\n if (snapshot) hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n await save();\n },\n unsubscribe() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n unsubscribeCache();\n },\n async clear() {\n await store.delete(key);\n },\n };\n}\n"],"mappings":";;;AAyDA,SAAgB,EACZ,GACuB;CACvB,IAAM,EAAE,gBAAa,gBAAa,QAAS,GACrC,IAAM,EAAQ,OAAO,kBACrB,IAAQ,EAAqC;EAC/C,cAAc,EAAQ,gBAAgB;EACtC,SAAS;EACT,WAAW;EACX,SAAS;EACT,SAAS;CACb,CAAC;CAED,eAAe,IAAsB;EACjC,MAAM,EAAM,IAAI;GAAE,IAAI;GAAK,OAAO,EAAU,CAAW;EAAE,CAAC;CAC9D;CAEA,IAAI,IAA8C;CAClD,SAAS,IAAiB;EAClB,AACJ,MAAQ,iBAAiB;GAErB,AADA,IAAQ,MACR,EAAU;EACd,GAAG,CAAU;CACjB;CAEA,IAAM,IAAmB,EAAY,cAAc,CAAC,CAAC,UAAU,CAAQ;CAEvE,OAAO;EACH,MAAM,UAAU;GACZ,IAAM,IAAW,MAAM,EAAM,IAAI,CAAG;GACpC,AAAI,KAAU,EAAQ,GAAa,EAAS,KAAK;EACrD;EACA,MAAM,QAAQ;GAKV,AAJA,AAEI,OADA,aAAa,CAAK,GACV,OAEZ,MAAM,EAAK;EACf;EACA,cAAc;GAKV,AAJA,AAEI,OADA,aAAa,CAAK,GACV,OAEZ,EAAiB;EACrB;EACA,MAAM,QAAQ;GACV,MAAM,EAAM,OAAO,CAAG;EAC1B;CACJ;AACJ"}
|
|
1
|
+
{"version":3,"file":"offline-persistence.js","names":[],"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\";\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\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.\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}\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 } = 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 write = store.put({ id: key, state: dehydrate(queryClient) });\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) 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":";;;;AAkBA,IAAM,oBAAgB,IAAI,IAA8B;AAgHxD,SAAgB,EACZ,GACuB;CACvB,IAAM,EAAE,gBAAa,gBAAa,QAAS,GACrC,IAAM,EAAQ,OAAO,kBACrB,IAAe,EAAQ,gBAAgB,qBACvC,IAAQ,EAAqC;EAC/C;EACA,SAAS;EACT,WAAW;EACX,SAAS;EACT,SAAS;CACb,CAAC,GAEK,IAAO,GAAG,EAAa,QAAQ,KACjC,IAAQ,IACR,IAAc,IACd,IAA8C,MAC5C,oBAAW,IAAI,IAAsB;CAE3C,eAAe,IAAmC;EAC9C,IAAM,IAAU,EAAc,IAAI,CAAI;EACtC,AAAI,KAAS,MAAM;CACvB;CAEA,SAAS,IAAkB;EACnB,MAAgB,EAAW,MAC/B,IAAc,IACd,QAAQ,KACJ,0HAEJ;CACJ;CAEA,eAAe,IAAsB;EAGjC,IAFI,MACJ,MAAM,EAAkB,GACpB,IAAO;EACX,IAAM,IAAQ,EAAM,IAAI;GAAE,IAAI;GAAK,OAAO,EAAU,CAAW;EAAE,CAAC;EAClE,EAAS,IAAI,CAAK;EAClB,IAAI;GACA,MAAM;EACV,UAAU;GACN,EAAS,OAAO,CAAK;EACzB;CACJ;CAEA,SAAS,IAAoB;EACzB,AAEI,OADA,aAAa,CAAK,GACV;CAEhB;CAEA,SAAS,IAAiB;EAClB,KAAS,MACb,IAAQ,iBAAiB;GAErB,AADA,IAAQ,MACR,EAAU;EACd,GAAG,CAAU;CACjB;CAEA,SAAS,EAAa,GAAoC;EACtD,IAAI,CAAC,GAAO;GACR,EAAS;GACT;EACJ;EACI,GAAM,SAAS,WAAW,EAAM,SAAS,eAC7C,EAAiB,GACjB,EAAU;CACd;CAEA,IAAM,IAAmB,EAAY,cAAc,CAAC,CAAC,UAAU,CAAY;CAE3E,eAAe,EAAW,GAAqC;EAC3D,IAAI;GAEA,AADA,MAAM,QAAQ,WAAW,CAAC,GAAG,CAAQ,CAAC,GACtC,MAAM,EAAM,OAAO,CAAG;EAC1B,UAAU;GACN,AAAI,KAAa,EAAY,MAAM;EACvC;CACJ;CAEA,OAAO;EACH,MAAM,UAAU;GACZ,IAAI,GAAO;IACP,EAAU;IACV;GACJ;GAEA,IADA,MAAM,EAAkB,GACpB,GAAO;GACX,IAAM,IAAW,MAAM,EAAM,IAAI,CAAG;GACpC,AAAI,KAAY,CAAC,KAAO,EAAQ,GAAa,EAAS,KAAK;EAC/D;EACA,MAAM,QAAQ;GACV,IAAI,GAAO;IACP,EAAU;IACV;GACJ;GAEA,AADA,EAAY,GACZ,MAAM,EAAK;EACf;EACA,cAAc;GAEV,AADA,EAAY,GACZ,EAAiB;EACrB;EACA,MAAM,MAAM,IAAe,CAAC,GAAG;GAE3B,AADA,IAAQ,IACR,EAAY;GACZ,IAAM,IAAW,EAAc,IAAI,CAAI,GACjC,IAAM,EAAW,EAAa,eAAe,EAAK,GAClD,IAAU,QAAQ,WAAW,CAAC,GAAU,CAAG,CAAC;GAKlD,AAJA,EAAc,IAAI,GAAM,CAAO,GAC/B,EAAa,WAAW;IACpB,AAAI,EAAc,IAAI,CAAI,MAAM,KAAS,EAAc,OAAO,CAAI;GACtE,CAAC,GACD,MAAM;EACV;CACJ;AACJ"}
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
.tempest_info_eq5bQ{border-left:4px solid var(--tempest-info-solid)}
|
|
15
15
|
.tempest_title_-H6R2{font-family:var(--tempest-font-sans);font-weight:var(--tempest-weight-semibold);font-size:var(--tempest-text-base);color:var(--tempest-text);line-height:var(--tempest-leading-snug);margin:0}
|
|
16
16
|
.tempest_description_-QwfC{font-size:var(--tempest-text-sm);color:var(--tempest-text-muted);line-height:var(--tempest-leading-snug);margin:2px 0 0}
|
|
17
|
-
.
|
|
17
|
+
.tempest_content_U6Xwq{overflow-wrap:anywhere;flex:auto;min-width:0}
|
|
18
|
+
.tempest_withIcon_2Z8Sz .tempest_content_U6Xwq{align-self:center}
|
|
19
|
+
.tempest_alignCenter_GpnzD .tempest_content_U6Xwq{text-align:center}
|
|
20
|
+
.tempest_leading_L3de1{color:var(--tempest-text);flex:none;justify-content:center;align-self:center;align-items:center;display:flex}
|
|
21
|
+
.tempest_leading_L3de1 img,.tempest_leading_L3de1 svg{max-width:var(--tempest-space-8);max-height:var(--tempest-space-8);object-fit:contain;display:block}
|
|
22
|
+
.tempest_close_i10-s{color:var(--tempest-text-muted);border-radius:var(--tempest-radius-sm);transition:var(--tempest-transition-color);background:0 0;border:none;flex:none;align-items:center;margin-left:auto;padding:4px;display:flex}
|
|
18
23
|
@media (hover:hover) and (pointer:fine){.tempest_close_i10-s:hover{background-color:var(--tempest-surface);color:var(--tempest-text)}}
|
|
19
24
|
.tempest_close_i10-s:focus-visible{outline:var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);outline-offset:var(--tempest-focus-ring-offset)}
|
|
20
25
|
@keyframes tempest_tempest-toast-in_E2d-A{0%{opacity:0;transform:translate(24px)}to{opacity:1;transform:translate(0)}}
|
package/dist/styles/feedback.css
CHANGED
|
@@ -185,7 +185,12 @@
|
|
|
185
185
|
.tempest_info_eq5bQ{border-left:4px solid var(--tempest-info-solid)}
|
|
186
186
|
.tempest_title_-H6R2{font-family:var(--tempest-font-sans);font-weight:var(--tempest-weight-semibold);font-size:var(--tempest-text-base);color:var(--tempest-text);line-height:var(--tempest-leading-snug);margin:0}
|
|
187
187
|
.tempest_description_-QwfC{font-size:var(--tempest-text-sm);color:var(--tempest-text-muted);line-height:var(--tempest-leading-snug);margin:2px 0 0}
|
|
188
|
-
.
|
|
188
|
+
.tempest_content_U6Xwq{overflow-wrap:anywhere;flex:auto;min-width:0}
|
|
189
|
+
.tempest_withIcon_2Z8Sz .tempest_content_U6Xwq{align-self:center}
|
|
190
|
+
.tempest_alignCenter_GpnzD .tempest_content_U6Xwq{text-align:center}
|
|
191
|
+
.tempest_leading_L3de1{color:var(--tempest-text);flex:none;justify-content:center;align-self:center;align-items:center;display:flex}
|
|
192
|
+
.tempest_leading_L3de1 img,.tempest_leading_L3de1 svg{max-width:var(--tempest-space-8);max-height:var(--tempest-space-8);object-fit:contain;display:block}
|
|
193
|
+
.tempest_close_i10-s{color:var(--tempest-text-muted);border-radius:var(--tempest-radius-sm);transition:var(--tempest-transition-color);background:0 0;border:none;flex:none;align-items:center;margin-left:auto;padding:4px;display:flex}
|
|
189
194
|
@media (hover:hover) and (pointer:fine){.tempest_close_i10-s:hover{background-color:var(--tempest-surface);color:var(--tempest-text)}}
|
|
190
195
|
.tempest_close_i10-s:focus-visible{outline:var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);outline-offset:var(--tempest-focus-ring-offset)}
|
|
191
196
|
@keyframes tempest_tempest-toast-in_E2d-A{0%{opacity:0;transform:translate(24px)}to{opacity:1;transform:translate(0)}}
|