tempest-react-sdk 0.76.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/create-tempest-app.e2e.test.mjs +112 -0
- package/bin/create-tempest-app.mjs +69 -28
- package/bin/lib/component/generate.mjs +191 -0
- package/bin/lib/component/generate.test.mjs +336 -0
- package/bin/lib/openapi/escape.mjs +210 -0
- package/bin/lib/openapi/generate.hostile.test.mjs +592 -0
- package/bin/lib/openapi/generate.mjs +93 -17
- package/bin/lib/openapi/schema-to-zod.mjs +41 -4
- package/bin/tempest.mjs +184 -3
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +127 -55
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/refresh-queue.cjs +1 -1
- package/dist/auth/refresh-queue.cjs.map +1 -1
- package/dist/auth/refresh-queue.js +13 -6
- package/dist/auth/refresh-queue.js.map +1 -1
- package/dist/br/pix.cjs +1 -1
- package/dist/br/pix.cjs.map +1 -1
- package/dist/br/pix.js +72 -62
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +12 -4
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +46 -38
- package/dist/components/Chat/ChatAttachments.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs +7 -5
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js +151 -87
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Toast/Toast.module.cjs +1 -1
- package/dist/components/Toast/Toast.module.cjs.map +1 -1
- package/dist/components/Toast/Toast.module.js +7 -3
- package/dist/components/Toast/Toast.module.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +77 -51
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +32 -21
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +81 -73
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +39 -35
- package/dist/http/errors.js.map +1 -1
- package/dist/oauth/callback.cjs +2 -0
- package/dist/oauth/callback.cjs.map +1 -0
- package/dist/oauth/callback.js +42 -0
- package/dist/oauth/callback.js.map +1 -0
- package/dist/oauth/use-oauth-callback.cjs +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/oauth/use-oauth-callback.js +13 -20
- package/dist/oauth/use-oauth-callback.js.map +1 -1
- package/dist/push/use-push-subscription.cjs +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/push/use-push-subscription.js +42 -42
- package/dist/push/use-push-subscription.js.map +1 -1
- package/dist/push/web-push-client.cjs +1 -1
- package/dist/push/web-push-client.cjs.map +1 -1
- package/dist/push/web-push-client.js +23 -3
- package/dist/push/web-push-client.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +68 -23
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/styles/component/Toast.css +6 -1
- package/dist/styles/feedback.css +6 -1
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +271 -60
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache-inspect.cjs.map +1 -1
- package/dist/sw/cache-inspect.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +91 -64
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/credential-headers.cjs +2 -0
- package/dist/sw/credential-headers.cjs.map +1 -0
- package/dist/sw/credential-headers.js +17 -0
- package/dist/sw/credential-headers.js.map +1 -0
- package/dist/sw/session-data.cjs +2 -0
- package/dist/sw/session-data.cjs.map +1 -0
- package/dist/sw/session-data.js +17 -0
- package/dist/sw/session-data.js.map +1 -0
- package/dist/sw.cjs +1 -1
- package/dist/sw.d.ts +271 -23
- package/dist/sw.js +7 -5
- package/dist/telemetry/app-error-owner.cjs +2 -0
- package/dist/telemetry/app-error-owner.cjs.map +1 -0
- package/dist/telemetry/app-error-owner.js +18 -0
- package/dist/telemetry/app-error-owner.js.map +1 -0
- package/dist/telemetry/app-error-queue.cjs +1 -1
- package/dist/telemetry/app-error-queue.cjs.map +1 -1
- package/dist/telemetry/app-error-queue.js +19 -14
- package/dist/telemetry/app-error-queue.js.map +1 -1
- package/dist/telemetry/app-error-reporter.cjs +1 -1
- package/dist/telemetry/app-error-reporter.cjs.map +1 -1
- package/dist/telemetry/app-error-reporter.js +75 -34
- package/dist/telemetry/app-error-reporter.js.map +1 -1
- package/dist/telemetry/app-error-transport.cjs +1 -1
- package/dist/telemetry/app-error-transport.cjs.map +1 -1
- package/dist/telemetry/app-error-transport.js +16 -3
- package/dist/telemetry/app-error-transport.js.map +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +979 -62
- package/dist/tempest-react-sdk.js +160 -156
- package/dist/utils/csv.cjs +3 -3
- package/dist/utils/csv.cjs.map +1 -1
- package/dist/utils/csv.js +16 -13
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/safe-url.cjs +2 -0
- package/dist/utils/safe-url.cjs.map +1 -0
- package/dist/utils/safe-url.js +39 -0
- package/dist/utils/safe-url.js.map +1 -0
- package/dist/vite/tempest-csp.cjs +2 -2
- package/dist/vite/tempest-csp.cjs.map +1 -1
- package/dist/vite/tempest-csp.js +118 -63
- package/dist/vite/tempest-csp.js.map +1 -1
- package/dist/vite.d.ts +21 -5
- package/dist/webrtc/peer-link.cjs +1 -1
- package/dist/webrtc/peer-link.cjs.map +1 -1
- package/dist/webrtc/peer-link.js +2 -10
- package/dist/webrtc/peer-link.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +110 -72
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +22 -0
- package/template-pwa/src/sw.ts +27 -14
- package/dist/components/Markdown/markdown-url.cjs +0 -2
- package/dist/components/Markdown/markdown-url.cjs.map +0 -1
- package/dist/components/Markdown/markdown-url.js +0 -28
- package/dist/components/Markdown/markdown-url.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let e=require("react");function t(t){let
|
|
1
|
+
let e=require("react");function t(t){let[n,r]=(0,e.useState)(null),[i,a]=(0,e.useState)(null),[o,s]=(0,e.useState)(`pending`),c=(0,e.useRef)(t),l=(0,e.useRef)(!1),u=(0,e.useRef)(!1);return(0,e.useEffect)(()=>{c.current=t}),(0,e.useEffect)(()=>(u.current=!0,l.current||(l.current=!0,c.current.exchange().then(e=>{if(u.current)return r(e),s(`success`),c.current.onSuccess?.(e)}).catch(e=>{u.current&&(a(e),s(`error`),c.current.onError?.(e))})),()=>{u.current=!1}),[]),{loading:o===`pending`,data:n,error:i,status:o}}exports.useOAuthCallback=t;
|
|
2
2
|
//# sourceMappingURL=use-oauth-callback.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-oauth-callback.cjs","names":[],"sources":["../../src/oauth/use-oauth-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nexport interface UseOAuthCallbackOptions<T> {\n /** Function that exchanges the provider response for an app session. Called once on mount. */\n exchange: () => Promise<T>;\n /** Fired with the result on success. Receives the value resolved by `exchange`. */\n onSuccess?: (result: T) => void | Promise<void>;\n /** Fired when `exchange` throws or rejects. */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseOAuthCallbackResult<T> {\n /** `true` while the exchange promise is pending. */\n loading: boolean;\n /** Last resolved value, when `status === \"success\"`. */\n data: T | null;\n /** Last rejection reason, when `status === \"error\"`. */\n error: unknown;\n /** Aggregated state — `\"pending\"`, `\"success\"`, or `\"error\"`. */\n status: \"pending\" | \"success\" | \"error\";\n}\n\n/**\n * Run an OAuth-callback \"exchange\" exactly once on mount. Designed for\n * `/callback` routes that receive provider redirects and need to swap a\n * code/token for an app session via the backend.\n *\n *
|
|
1
|
+
{"version":3,"file":"use-oauth-callback.cjs","names":[],"sources":["../../src/oauth/use-oauth-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nexport interface UseOAuthCallbackOptions<T> {\n /** Function that exchanges the provider response for an app session. Called once on mount. */\n exchange: () => Promise<T>;\n /** Fired with the result on success. Receives the value resolved by `exchange`. */\n onSuccess?: (result: T) => void | Promise<void>;\n /** Fired when `exchange` throws or rejects. */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseOAuthCallbackResult<T> {\n /** `true` while the exchange promise is pending. */\n loading: boolean;\n /** Last resolved value, when `status === \"success\"`. */\n data: T | null;\n /** Last rejection reason, when `status === \"error\"`. */\n error: unknown;\n /** Aggregated state — `\"pending\"`, `\"success\"`, or `\"error\"`. */\n status: \"pending\" | \"success\" | \"error\";\n}\n\n/**\n * Run an OAuth-callback \"exchange\" exactly once on mount. Designed for\n * `/callback` routes that receive provider redirects and need to swap a\n * code/token for an app session via the backend.\n *\n * Runs `exchange` once per logical mount and keeps its result. StrictMode's\n * simulated unmount/remount in dev and a parent re-render with inline\n * callbacks used to cancel the only exchange and skip the second, leaving the\n * hook `\"pending\"` forever with the single-use code already spent. Now the\n * options live in a latest-value ref, the effect has no dependencies, and the\n * result is dropped only when the component is still unmounted when it lands.\n * The newest `onSuccess`/`onError` are the ones called.\n *\n * For a provider redirect, the exchange must carry the `state` along with the\n * `code`, or no backend can tell a forged callback from a real one (login\n * CSRF). `exchangeOAuthCallback` does that against a\n * `tempest-fastapi-sdk` backend; `readOAuthCallbackParams` returns both\n * values (or throws) for any other backend.\n *\n * @example\n * function OAuthCallback() {\n * const { loading, error } = useOAuthCallback<LoginResponse>({\n * exchange: () => exchangeOAuthCallback<LoginResponse>(api, \"google\"),\n * onSuccess: ({ user_id, access_token }) => {\n * useAuthStore.getState().setSession({ user: { id: user_id }, token: access_token });\n * navigate(\"/dashboard\", { replace: true });\n * },\n * onError: () => navigate(\"/login?error=oauth\", { replace: true }),\n * });\n *\n * if (loading) return <Spinner />;\n * if (error) return <ErrorState description=\"OAuth falhou\" />;\n * return null;\n * }\n */\nexport function useOAuthCallback<T>(\n options: UseOAuthCallbackOptions<T>,\n): UseOAuthCallbackResult<T> {\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [status, setStatus] = useState<\"pending\" | \"success\" | \"error\">(\"pending\");\n const optionsRef = useRef<UseOAuthCallbackOptions<T>>(options);\n const ranRef = useRef<boolean>(false);\n const mountedRef = useRef<boolean>(false);\n\n useEffect(() => {\n optionsRef.current = options;\n });\n\n useEffect(() => {\n mountedRef.current = true;\n if (!ranRef.current) {\n ranRef.current = true;\n optionsRef.current\n .exchange()\n .then((result) => {\n if (!mountedRef.current) return;\n setData(result);\n setStatus(\"success\");\n return optionsRef.current.onSuccess?.(result);\n })\n .catch((err: unknown) => {\n if (!mountedRef.current) return;\n setError(err);\n setStatus(\"error\");\n optionsRef.current.onError?.(err);\n });\n }\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n return { loading: status === \"pending\", data, error, status };\n}\n"],"mappings":"uBAyDA,SAAgB,EACZ,EACyB,CACzB,GAAM,CAAC,EAAM,IAAA,EAAW,EAAA,SAAA,CAAmB,IAAI,EACzC,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAkB,IAAI,EAC1C,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAA0C,SAAS,EACzE,GAAA,EAAa,EAAA,OAAA,CAAmC,CAAO,EACvD,GAAA,EAAS,EAAA,OAAA,CAAgB,EAAK,EAC9B,GAAA,EAAa,EAAA,OAAA,CAAgB,EAAK,EA8BxC,OA5BA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAW,QAAU,CACzB,CAAC,GAED,EAAA,EAAA,UAAA,MACI,EAAW,QAAU,GAChB,EAAO,UACR,EAAO,QAAU,GACjB,EAAW,QACN,SAAS,CAAC,CACV,KAAM,GAAW,CACT,KAAW,QAGhB,OAFA,EAAQ,CAAM,EACd,EAAU,SAAS,EACZ,EAAW,QAAQ,YAAY,CAAM,CAChD,CAAC,CAAC,CACD,MAAO,GAAiB,CAChB,EAAW,UAChB,EAAS,CAAG,EACZ,EAAU,OAAO,EACjB,EAAW,QAAQ,UAAU,CAAG,EACpC,CAAC,OAEI,CACT,EAAW,QAAU,EACzB,GACD,CAAC,CAAC,EAEE,CAAE,QAAS,IAAW,UAAW,OAAM,QAAO,QAAO,CAChE"}
|
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
import { useEffect as e, useRef as t, useState as n } from "react";
|
|
2
2
|
//#region src/oauth/use-oauth-callback.ts
|
|
3
3
|
function r(r) {
|
|
4
|
-
let
|
|
4
|
+
let [i, a] = n(null), [o, s] = n(null), [c, l] = n("pending"), u = t(r), d = t(!1), f = t(!1);
|
|
5
5
|
return e(() => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
a,
|
|
19
|
-
o
|
|
20
|
-
]), {
|
|
21
|
-
loading: d === "pending",
|
|
22
|
-
data: s,
|
|
23
|
-
error: l,
|
|
24
|
-
status: d
|
|
6
|
+
u.current = r;
|
|
7
|
+
}), e(() => (f.current = !0, d.current || (d.current = !0, u.current.exchange().then((e) => {
|
|
8
|
+
if (f.current) return a(e), l("success"), u.current.onSuccess?.(e);
|
|
9
|
+
}).catch((e) => {
|
|
10
|
+
f.current && (s(e), l("error"), u.current.onError?.(e));
|
|
11
|
+
})), () => {
|
|
12
|
+
f.current = !1;
|
|
13
|
+
}), []), {
|
|
14
|
+
loading: c === "pending",
|
|
15
|
+
data: i,
|
|
16
|
+
error: o,
|
|
17
|
+
status: c
|
|
25
18
|
};
|
|
26
19
|
}
|
|
27
20
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-oauth-callback.js","names":[],"sources":["../../src/oauth/use-oauth-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nexport interface UseOAuthCallbackOptions<T> {\n /** Function that exchanges the provider response for an app session. Called once on mount. */\n exchange: () => Promise<T>;\n /** Fired with the result on success. Receives the value resolved by `exchange`. */\n onSuccess?: (result: T) => void | Promise<void>;\n /** Fired when `exchange` throws or rejects. */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseOAuthCallbackResult<T> {\n /** `true` while the exchange promise is pending. */\n loading: boolean;\n /** Last resolved value, when `status === \"success\"`. */\n data: T | null;\n /** Last rejection reason, when `status === \"error\"`. */\n error: unknown;\n /** Aggregated state — `\"pending\"`, `\"success\"`, or `\"error\"`. */\n status: \"pending\" | \"success\" | \"error\";\n}\n\n/**\n * Run an OAuth-callback \"exchange\" exactly once on mount. Designed for\n * `/callback` routes that receive provider redirects and need to swap a\n * code/token for an app session via the backend.\n *\n *
|
|
1
|
+
{"version":3,"file":"use-oauth-callback.js","names":[],"sources":["../../src/oauth/use-oauth-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nexport interface UseOAuthCallbackOptions<T> {\n /** Function that exchanges the provider response for an app session. Called once on mount. */\n exchange: () => Promise<T>;\n /** Fired with the result on success. Receives the value resolved by `exchange`. */\n onSuccess?: (result: T) => void | Promise<void>;\n /** Fired when `exchange` throws or rejects. */\n onError?: (error: unknown) => void;\n}\n\nexport interface UseOAuthCallbackResult<T> {\n /** `true` while the exchange promise is pending. */\n loading: boolean;\n /** Last resolved value, when `status === \"success\"`. */\n data: T | null;\n /** Last rejection reason, when `status === \"error\"`. */\n error: unknown;\n /** Aggregated state — `\"pending\"`, `\"success\"`, or `\"error\"`. */\n status: \"pending\" | \"success\" | \"error\";\n}\n\n/**\n * Run an OAuth-callback \"exchange\" exactly once on mount. Designed for\n * `/callback` routes that receive provider redirects and need to swap a\n * code/token for an app session via the backend.\n *\n * Runs `exchange` once per logical mount and keeps its result. StrictMode's\n * simulated unmount/remount in dev and a parent re-render with inline\n * callbacks used to cancel the only exchange and skip the second, leaving the\n * hook `\"pending\"` forever with the single-use code already spent. Now the\n * options live in a latest-value ref, the effect has no dependencies, and the\n * result is dropped only when the component is still unmounted when it lands.\n * The newest `onSuccess`/`onError` are the ones called.\n *\n * For a provider redirect, the exchange must carry the `state` along with the\n * `code`, or no backend can tell a forged callback from a real one (login\n * CSRF). `exchangeOAuthCallback` does that against a\n * `tempest-fastapi-sdk` backend; `readOAuthCallbackParams` returns both\n * values (or throws) for any other backend.\n *\n * @example\n * function OAuthCallback() {\n * const { loading, error } = useOAuthCallback<LoginResponse>({\n * exchange: () => exchangeOAuthCallback<LoginResponse>(api, \"google\"),\n * onSuccess: ({ user_id, access_token }) => {\n * useAuthStore.getState().setSession({ user: { id: user_id }, token: access_token });\n * navigate(\"/dashboard\", { replace: true });\n * },\n * onError: () => navigate(\"/login?error=oauth\", { replace: true }),\n * });\n *\n * if (loading) return <Spinner />;\n * if (error) return <ErrorState description=\"OAuth falhou\" />;\n * return null;\n * }\n */\nexport function useOAuthCallback<T>(\n options: UseOAuthCallbackOptions<T>,\n): UseOAuthCallbackResult<T> {\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [status, setStatus] = useState<\"pending\" | \"success\" | \"error\">(\"pending\");\n const optionsRef = useRef<UseOAuthCallbackOptions<T>>(options);\n const ranRef = useRef<boolean>(false);\n const mountedRef = useRef<boolean>(false);\n\n useEffect(() => {\n optionsRef.current = options;\n });\n\n useEffect(() => {\n mountedRef.current = true;\n if (!ranRef.current) {\n ranRef.current = true;\n optionsRef.current\n .exchange()\n .then((result) => {\n if (!mountedRef.current) return;\n setData(result);\n setStatus(\"success\");\n return optionsRef.current.onSuccess?.(result);\n })\n .catch((err: unknown) => {\n if (!mountedRef.current) return;\n setError(err);\n setStatus(\"error\");\n optionsRef.current.onError?.(err);\n });\n }\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n return { loading: status === \"pending\", data, error, status };\n}\n"],"mappings":";;AAyDA,SAAgB,EACZ,GACyB;CACzB,IAAM,CAAC,GAAM,KAAW,EAAmB,IAAI,GACzC,CAAC,GAAO,KAAY,EAAkB,IAAI,GAC1C,CAAC,GAAQ,KAAa,EAA0C,SAAS,GACzE,IAAa,EAAmC,CAAO,GACvD,IAAS,EAAgB,EAAK,GAC9B,IAAa,EAAgB,EAAK;CA8BxC,OA5BA,QAAgB;EACZ,EAAW,UAAU;CACzB,CAAC,GAED,SACI,EAAW,UAAU,IAChB,EAAO,YACR,EAAO,UAAU,IACjB,EAAW,QACN,SAAS,CAAC,CACV,MAAM,MAAW;EACT,MAAW,SAGhB,OAFA,EAAQ,CAAM,GACd,EAAU,SAAS,GACZ,EAAW,QAAQ,YAAY,CAAM;CAChD,CAAC,CAAC,CACD,OAAO,MAAiB;EAChB,EAAW,YAChB,EAAS,CAAG,GACZ,EAAU,OAAO,GACjB,EAAW,QAAQ,UAAU,CAAG;CACpC,CAAC,UAEI;EACT,EAAW,UAAU;CACzB,IACD,CAAC,CAAC,GAEE;EAAE,SAAS,MAAW;EAAW;EAAM;EAAO;CAAO;AAChE"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../hooks/use-latest-ref.cjs"),t=require("./utils.cjs"),n=require("./web-push-client.cjs");let r=require("react");function i(i){let a=e.useLatestRef(i),o=(0,r.useMemo)(()=>new n.WebPushClient({vapidPublicKey:i.vapidPublicKey,onSubscribe:e=>a.current.onSubscribe(e),onUnsubscribe:e=>a.current.onUnsubscribe?.(e),getRegistration:i.getRegistration}),[i.vapidPublicKey,i.getRegistration,a]),[s]=(0,r.useState)(()=>t.isPushSupported()),[c,l]=(0,r.useState)(()=>o.permission()),[u,d]=(0,r.useState)(!1),[f,p]=(0,r.useState)(!1),[m,h]=(0,r.useState)(null),g=(0,r.useCallback)(async()=>{if(s){l(o.permission());try{let e=await o.isSubscribed();d(e)}catch(e){h(e instanceof Error?e:Error(String(e)))}}},[o,s]);return(0,r.useEffect)(()=>{g()},[g]),{supported:s,permission:c,subscribed:u,loading:f,error:m,subscribe:(0,r.useCallback)(async()=>{if(s){p(!0),h(null);try{await o.subscribe(),d(!0),l(o.permission())}catch(e){throw h(e instanceof Error?e:Error(String(e))),e}finally{p(!1)}}},[o,s]),unsubscribe:(0,r.useCallback)(async()=>{if(s){p(!0),h(null);try{await o.unsubscribe(),d(!1)}catch(e){throw h(e instanceof Error?e:Error(String(e))),e}finally{p(!1)}}},[o,s]),refresh:g}}exports.usePushSubscription=i;
|
|
1
|
+
const e=require("../hooks/use-latest-ref.cjs"),t=require("./utils.cjs"),n=require("./web-push-client.cjs");let r=require("react");function i(i){let a=e.useLatestRef(i),o=(0,r.useMemo)(()=>new n.WebPushClient({vapidPublicKey:i.vapidPublicKey,onSubscribe:e=>a.current.onSubscribe(e),onUnsubscribe:e=>a.current.onUnsubscribe?.(e),getRegistration:i.getRegistration}),[i.vapidPublicKey,i.getRegistration,a]),[s]=(0,r.useState)(()=>t.isPushSupported()),[c,l]=(0,r.useState)(()=>o.permission()),[u,d]=(0,r.useState)(!1),[f,p]=(0,r.useState)(!1),[m,h]=(0,r.useState)(null),g=(0,r.useCallback)(async()=>{if(s){l(o.permission());try{let e=await o.isSubscribed();d(e)}catch(e){h(e instanceof Error?e:Error(String(e)))}}},[o,s]);return(0,r.useEffect)(()=>{g()},[g]),{supported:s,permission:c,subscribed:u,loading:f,error:m,subscribe:(0,r.useCallback)(async()=>{if(s){p(!0),h(null);try{await o.subscribe(),d(!0),l(o.permission())}catch(e){throw h(e instanceof Error?e:Error(String(e))),e}finally{p(!1)}}},[o,s]),unsubscribe:(0,r.useCallback)(async()=>{if(s){p(!0),h(null);try{await o.unsubscribe(),d(!1)}catch(e){throw e instanceof n.WebPushUnsubscribeError&&d(!e.browserUnsubscribed),h(e instanceof Error?e:Error(String(e))),e}finally{p(!1)}}},[o,s]),refresh:g}}exports.usePushSubscription=i;
|
|
2
2
|
//# sourceMappingURL=use-push-subscription.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-push-subscription.cjs","names":[],"sources":["../../src/push/use-push-subscription.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the ref is read inside deferred callbacks, not during render */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport {
|
|
1
|
+
{"version":3,"file":"use-push-subscription.cjs","names":[],"sources":["../../src/push/use-push-subscription.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the ref is read inside deferred callbacks, not during render */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport {\n WebPushClient,\n WebPushUnsubscribeError,\n type WebPushClientConfig,\n} from \"./web-push-client\";\nimport { isPushSupported } from \"./utils\";\n\nexport interface UsePushSubscriptionResult {\n /** True when the browser exposes the Push + Notification APIs. */\n supported: boolean;\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission: NotificationPermission | \"unsupported\";\n /** True when a subscription is active on this device. */\n subscribed: boolean;\n /** True while a subscribe/unsubscribe call is in-flight. */\n loading: boolean;\n /** Last error thrown by subscribe/unsubscribe, if any. */\n error: Error | null;\n subscribe: () => Promise<void>;\n unsubscribe: () => Promise<void>;\n /** Re-read the current subscription/permission state from the browser. */\n refresh: () => Promise<void>;\n}\n\n/**\n * React hook around {@link WebPushClient}. Tracks subscription, permission and\n * loading state; exposes imperative `subscribe`/`unsubscribe` actions.\n *\n * The hook does NOT register the service worker — the host app must do that\n * (e.g. via `vite-plugin-pwa` or a manual `navigator.serviceWorker.register`).\n *\n * @example\n * const push = usePushSubscription({\n * vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY,\n * onSubscribe: (sub) => api.post(\"/webpush/subscribe\", { body: sub }),\n * onUnsubscribe: () => api.delete(\"/webpush/my\"),\n * });\n */\nexport function usePushSubscription(config: WebPushClientConfig): UsePushSubscriptionResult {\n const configRef = useLatestRef(config);\n\n const client = useMemo(\n () =>\n new WebPushClient({\n vapidPublicKey: config.vapidPublicKey,\n onSubscribe: (sub) => configRef.current.onSubscribe(sub),\n onUnsubscribe: (sub) => configRef.current.onUnsubscribe?.(sub),\n getRegistration: config.getRegistration,\n }),\n [config.vapidPublicKey, config.getRegistration, configRef],\n );\n\n const [supported] = useState<boolean>(() => isPushSupported());\n const [permission, setPermission] = useState<NotificationPermission | \"unsupported\">(() =>\n client.permission(),\n );\n const [subscribed, setSubscribed] = useState<boolean>(false);\n const [loading, setLoading] = useState<boolean>(false);\n const [error, setError] = useState<Error | null>(null);\n\n const refresh = useCallback(async () => {\n if (!supported) return;\n setPermission(client.permission());\n try {\n const active = await client.isSubscribed();\n setSubscribed(active);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [client, supported]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const subscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.subscribe();\n setSubscribed(true);\n setPermission(client.permission());\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n const unsubscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.unsubscribe();\n setSubscribed(false);\n } catch (err) {\n if (err instanceof WebPushUnsubscribeError) setSubscribed(!err.browserUnsubscribed);\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n return {\n supported,\n permission,\n subscribed,\n loading,\n error,\n subscribe,\n unsubscribe,\n refresh,\n };\n}\n"],"mappings":"kIAyCA,SAAgB,EAAoB,EAAwD,CACxF,IAAM,EAAY,EAAA,aAAa,CAAM,EAE/B,GAAA,EAAS,EAAA,QAAA,KAEP,IAAI,EAAA,cAAc,CACd,eAAgB,EAAO,eACvB,YAAc,GAAQ,EAAU,QAAQ,YAAY,CAAG,EACvD,cAAgB,GAAQ,EAAU,QAAQ,gBAAgB,CAAG,EAC7D,gBAAiB,EAAO,eAC5B,CAAC,EACL,CAAC,EAAO,eAAgB,EAAO,gBAAiB,CAAS,CAC7D,EAEM,CAAC,IAAA,EAAa,EAAA,SAAA,KAAwB,EAAA,gBAAgB,CAAC,EACvD,CAAC,EAAY,IAAA,EAAiB,EAAA,SAAA,KAChC,EAAO,WAAW,CACtB,EACM,CAAC,EAAY,IAAA,EAAiB,EAAA,SAAA,CAAkB,EAAK,EACrD,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAkB,EAAK,EAC/C,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAuB,IAAI,EAE/C,GAAA,EAAU,EAAA,YAAA,CAAY,SAAY,CAC/B,KACL,GAAc,EAAO,WAAW,CAAC,EACjC,GAAI,CACA,IAAM,EAAS,MAAM,EAAO,aAAa,EACzC,EAAc,CAAM,CACxB,OAAS,EAAK,CACV,EAAS,aAAe,MAAQ,EAAU,MAAM,OAAO,CAAG,CAAC,CAAC,CAChE,CANiC,CAOrC,EAAG,CAAC,EAAQ,CAAS,CAAC,EAsCtB,OApCA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAa,CACjB,EAAG,CAAC,CAAO,CAAC,EAkCL,CACH,YACA,aACA,aACA,UACA,QACA,WAAA,EAtCc,EAAA,YAAA,CAAY,SAAY,CACjC,KAEL,CADA,EAAW,EAAI,EACf,EAAS,IAAI,EACb,GAAI,CACA,MAAM,EAAO,UAAU,EACvB,EAAc,EAAI,EAClB,EAAc,EAAO,WAAW,CAAC,CACrC,OAAS,EAAK,CAEV,MADA,EAAS,aAAe,MAAQ,EAAU,MAAM,OAAO,CAAG,CAAC,CAAC,EACtD,CACV,QAAU,CACN,EAAW,EAAK,CACpB,CAVa,CAWjB,EAAG,CAAC,EAAQ,CAAS,CAwBjB,EACA,aAAA,EAvBgB,EAAA,YAAA,CAAY,SAAY,CACnC,KAEL,CADA,EAAW,EAAI,EACf,EAAS,IAAI,EACb,GAAI,CACA,MAAM,EAAO,YAAY,EACzB,EAAc,EAAK,CACvB,OAAS,EAAK,CAGV,MAFI,aAAe,EAAA,yBAAyB,EAAc,CAAC,EAAI,mBAAmB,EAClF,EAAS,aAAe,MAAQ,EAAU,MAAM,OAAO,CAAG,CAAC,CAAC,EACtD,CACV,QAAU,CACN,EAAW,EAAK,CACpB,CAVa,CAWjB,EAAG,CAAC,EAAQ,CAAS,CASjB,EACA,SACJ,CACJ"}
|
|
@@ -1,65 +1,65 @@
|
|
|
1
1
|
import { useLatestRef as e } from "../hooks/use-latest-ref.js";
|
|
2
2
|
import { isPushSupported as t } from "./utils.js";
|
|
3
|
-
import { WebPushClient as n } from "./web-push-client.js";
|
|
4
|
-
import { useCallback as
|
|
3
|
+
import { WebPushClient as n, WebPushUnsubscribeError as r } from "./web-push-client.js";
|
|
4
|
+
import { useCallback as i, useEffect as a, useMemo as o, useState as s } from "react";
|
|
5
5
|
//#region src/push/use-push-subscription.ts
|
|
6
|
-
function
|
|
7
|
-
let
|
|
8
|
-
vapidPublicKey:
|
|
9
|
-
onSubscribe: (e) =>
|
|
10
|
-
onUnsubscribe: (e) =>
|
|
11
|
-
getRegistration:
|
|
6
|
+
function c(c) {
|
|
7
|
+
let l = e(c), u = o(() => new n({
|
|
8
|
+
vapidPublicKey: c.vapidPublicKey,
|
|
9
|
+
onSubscribe: (e) => l.current.onSubscribe(e),
|
|
10
|
+
onUnsubscribe: (e) => l.current.onUnsubscribe?.(e),
|
|
11
|
+
getRegistration: c.getRegistration
|
|
12
12
|
}), [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
]), [
|
|
17
|
-
if (
|
|
18
|
-
|
|
13
|
+
c.vapidPublicKey,
|
|
14
|
+
c.getRegistration,
|
|
15
|
+
l
|
|
16
|
+
]), [d] = s(() => t()), [f, p] = s(() => u.permission()), [m, h] = s(!1), [g, _] = s(!1), [v, y] = s(null), b = i(async () => {
|
|
17
|
+
if (d) {
|
|
18
|
+
p(u.permission());
|
|
19
19
|
try {
|
|
20
|
-
let e = await
|
|
21
|
-
|
|
20
|
+
let e = await u.isSubscribed();
|
|
21
|
+
h(e);
|
|
22
22
|
} catch (e) {
|
|
23
|
-
|
|
23
|
+
y(e instanceof Error ? e : Error(String(e)));
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
}, [
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
}, [
|
|
30
|
-
supported:
|
|
31
|
-
permission:
|
|
32
|
-
subscribed:
|
|
33
|
-
loading:
|
|
34
|
-
error:
|
|
35
|
-
subscribe:
|
|
36
|
-
if (
|
|
37
|
-
|
|
26
|
+
}, [u, d]);
|
|
27
|
+
return a(() => {
|
|
28
|
+
b();
|
|
29
|
+
}, [b]), {
|
|
30
|
+
supported: d,
|
|
31
|
+
permission: f,
|
|
32
|
+
subscribed: m,
|
|
33
|
+
loading: g,
|
|
34
|
+
error: v,
|
|
35
|
+
subscribe: i(async () => {
|
|
36
|
+
if (d) {
|
|
37
|
+
_(!0), y(null);
|
|
38
38
|
try {
|
|
39
|
-
await
|
|
39
|
+
await u.subscribe(), h(!0), p(u.permission());
|
|
40
40
|
} catch (e) {
|
|
41
|
-
throw
|
|
41
|
+
throw y(e instanceof Error ? e : Error(String(e))), e;
|
|
42
42
|
} finally {
|
|
43
|
-
|
|
43
|
+
_(!1);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
}, [
|
|
47
|
-
unsubscribe:
|
|
48
|
-
if (
|
|
49
|
-
|
|
46
|
+
}, [u, d]),
|
|
47
|
+
unsubscribe: i(async () => {
|
|
48
|
+
if (d) {
|
|
49
|
+
_(!0), y(null);
|
|
50
50
|
try {
|
|
51
|
-
await
|
|
51
|
+
await u.unsubscribe(), h(!1);
|
|
52
52
|
} catch (e) {
|
|
53
|
-
throw
|
|
53
|
+
throw e instanceof r && h(!e.browserUnsubscribed), y(e instanceof Error ? e : Error(String(e))), e;
|
|
54
54
|
} finally {
|
|
55
|
-
|
|
55
|
+
_(!1);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
}, [
|
|
59
|
-
refresh:
|
|
58
|
+
}, [u, d]),
|
|
59
|
+
refresh: b
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
//#endregion
|
|
63
|
-
export {
|
|
63
|
+
export { c as usePushSubscription };
|
|
64
64
|
|
|
65
65
|
//# sourceMappingURL=use-push-subscription.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-push-subscription.js","names":[],"sources":["../../src/push/use-push-subscription.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the ref is read inside deferred callbacks, not during render */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport {
|
|
1
|
+
{"version":3,"file":"use-push-subscription.js","names":[],"sources":["../../src/push/use-push-subscription.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- the ref is read inside deferred callbacks, not during render */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport {\n WebPushClient,\n WebPushUnsubscribeError,\n type WebPushClientConfig,\n} from \"./web-push-client\";\nimport { isPushSupported } from \"./utils\";\n\nexport interface UsePushSubscriptionResult {\n /** True when the browser exposes the Push + Notification APIs. */\n supported: boolean;\n /** Current `Notification.permission` value, or `\"unsupported\"`. */\n permission: NotificationPermission | \"unsupported\";\n /** True when a subscription is active on this device. */\n subscribed: boolean;\n /** True while a subscribe/unsubscribe call is in-flight. */\n loading: boolean;\n /** Last error thrown by subscribe/unsubscribe, if any. */\n error: Error | null;\n subscribe: () => Promise<void>;\n unsubscribe: () => Promise<void>;\n /** Re-read the current subscription/permission state from the browser. */\n refresh: () => Promise<void>;\n}\n\n/**\n * React hook around {@link WebPushClient}. Tracks subscription, permission and\n * loading state; exposes imperative `subscribe`/`unsubscribe` actions.\n *\n * The hook does NOT register the service worker — the host app must do that\n * (e.g. via `vite-plugin-pwa` or a manual `navigator.serviceWorker.register`).\n *\n * @example\n * const push = usePushSubscription({\n * vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY,\n * onSubscribe: (sub) => api.post(\"/webpush/subscribe\", { body: sub }),\n * onUnsubscribe: () => api.delete(\"/webpush/my\"),\n * });\n */\nexport function usePushSubscription(config: WebPushClientConfig): UsePushSubscriptionResult {\n const configRef = useLatestRef(config);\n\n const client = useMemo(\n () =>\n new WebPushClient({\n vapidPublicKey: config.vapidPublicKey,\n onSubscribe: (sub) => configRef.current.onSubscribe(sub),\n onUnsubscribe: (sub) => configRef.current.onUnsubscribe?.(sub),\n getRegistration: config.getRegistration,\n }),\n [config.vapidPublicKey, config.getRegistration, configRef],\n );\n\n const [supported] = useState<boolean>(() => isPushSupported());\n const [permission, setPermission] = useState<NotificationPermission | \"unsupported\">(() =>\n client.permission(),\n );\n const [subscribed, setSubscribed] = useState<boolean>(false);\n const [loading, setLoading] = useState<boolean>(false);\n const [error, setError] = useState<Error | null>(null);\n\n const refresh = useCallback(async () => {\n if (!supported) return;\n setPermission(client.permission());\n try {\n const active = await client.isSubscribed();\n setSubscribed(active);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [client, supported]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const subscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.subscribe();\n setSubscribed(true);\n setPermission(client.permission());\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n const unsubscribe = useCallback(async () => {\n if (!supported) return;\n setLoading(true);\n setError(null);\n try {\n await client.unsubscribe();\n setSubscribed(false);\n } catch (err) {\n if (err instanceof WebPushUnsubscribeError) setSubscribed(!err.browserUnsubscribed);\n setError(err instanceof Error ? err : new Error(String(err)));\n throw err;\n } finally {\n setLoading(false);\n }\n }, [client, supported]);\n\n return {\n supported,\n permission,\n subscribed,\n loading,\n error,\n subscribe,\n unsubscribe,\n refresh,\n };\n}\n"],"mappings":";;;;;AAyCA,SAAgB,EAAoB,GAAwD;CACxF,IAAM,IAAY,EAAa,CAAM,GAE/B,IAAS,QAEP,IAAI,EAAc;EACd,gBAAgB,EAAO;EACvB,cAAc,MAAQ,EAAU,QAAQ,YAAY,CAAG;EACvD,gBAAgB,MAAQ,EAAU,QAAQ,gBAAgB,CAAG;EAC7D,iBAAiB,EAAO;CAC5B,CAAC,GACL;EAAC,EAAO;EAAgB,EAAO;EAAiB;CAAS,CAC7D,GAEM,CAAC,KAAa,QAAwB,EAAgB,CAAC,GACvD,CAAC,GAAY,KAAiB,QAChC,EAAO,WAAW,CACtB,GACM,CAAC,GAAY,KAAiB,EAAkB,EAAK,GACrD,CAAC,GAAS,KAAc,EAAkB,EAAK,GAC/C,CAAC,GAAO,KAAY,EAAuB,IAAI,GAE/C,IAAU,EAAY,YAAY;EAC/B,OACL;KAAc,EAAO,WAAW,CAAC;GACjC,IAAI;IACA,IAAM,IAAS,MAAM,EAAO,aAAa;IACzC,EAAc,CAAM;GACxB,SAAS,GAAK;IACV,EAAS,aAAe,QAAQ,IAAU,MAAM,OAAO,CAAG,CAAC,CAAC;GAChE;EANiC;CAOrC,GAAG,CAAC,GAAQ,CAAS,CAAC;CAsCtB,OApCA,QAAgB;EACZ,EAAa;CACjB,GAAG,CAAC,CAAO,CAAC,GAkCL;EACH;EACA;EACA;EACA;EACA;EACA,WAtCc,EAAY,YAAY;GACjC,OAEL;IADA,EAAW,EAAI,GACf,EAAS,IAAI;IACb,IAAI;KAGA,AAFA,MAAM,EAAO,UAAU,GACvB,EAAc,EAAI,GAClB,EAAc,EAAO,WAAW,CAAC;IACrC,SAAS,GAAK;KAEV,MADA,EAAS,aAAe,QAAQ,IAAU,MAAM,OAAO,CAAG,CAAC,CAAC,GACtD;IACV,UAAU;KACN,EAAW,EAAK;IACpB;GAVa;EAWjB,GAAG,CAAC,GAAQ,CAAS,CAwBjB;EACA,aAvBgB,EAAY,YAAY;GACnC,OAEL;IADA,EAAW,EAAI,GACf,EAAS,IAAI;IACb,IAAI;KAEA,AADA,MAAM,EAAO,YAAY,GACzB,EAAc,EAAK;IACvB,SAAS,GAAK;KAGV,MAFI,aAAe,KAAyB,EAAc,CAAC,EAAI,mBAAmB,GAClF,EAAS,aAAe,QAAQ,IAAU,MAAM,OAAO,CAAG,CAAC,CAAC,GACtD;IACV,UAAU;KACN,EAAW,EAAK;IACpB;GAVa;EAWjB,GAAG,CAAC,GAAQ,CAAS,CASjB;EACA;CACJ;AACJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./utils.cjs");var t=class extends Error{constructor(){super(`Web Push não suportado neste navegador.`),this.name=`WebPushUnsupportedError`}},n=class extends Error{constructor(){super(`Permissão de notificação negada pelo usuário.`),this.name=`WebPushPermissionDeniedError`}},r=class{config;constructor(e){this.config=e}static isSupported(){return e.isPushSupported()}async registration(){if(!e.isPushSupported())throw new t;return this.config.getRegistration?await this.config.getRegistration():await navigator.serviceWorker.ready}permission(){return typeof Notification>`u`?`unsupported`:Notification.permission}async requestPermission(){if(typeof Notification>`u`)throw new t;return Notification.requestPermission()}async getSubscription(){return(await this.registration()).pushManager.getSubscription()}async isSubscribed(){return e.isPushSupported()?await this.getSubscription()!==null:!1}async subscribe(){if(await this.requestPermission()!==`granted`)throw new n;let t=await this.registration(),r=await t.pushManager.getSubscription();if(r)return await this.config.onSubscribe(r.toJSON()),r;let i=await t.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:e.urlBase64ToUint8Array(this.config.vapidPublicKey)});return await this.config.onSubscribe(i.toJSON()),i}async unsubscribe(){let e=await this.getSubscription();return e
|
|
1
|
+
const e=require("./utils.cjs");var t=class extends Error{constructor(){super(`Web Push não suportado neste navegador.`),this.name=`WebPushUnsupportedError`}},n=class extends Error{constructor(){super(`Permissão de notificação negada pelo usuário.`),this.name=`WebPushPermissionDeniedError`}},r=class extends Error{subscription;browserUnsubscribed;constructor(e,t,n){super(`Falha ao remover a inscrição de push no backend.`,{cause:n}),this.name=`WebPushUnsubscribeError`,this.subscription=e,this.browserUnsubscribed=t}},i=class{config;constructor(e){this.config=e}static isSupported(){return e.isPushSupported()}async registration(){if(!e.isPushSupported())throw new t;return this.config.getRegistration?await this.config.getRegistration():await navigator.serviceWorker.ready}permission(){return typeof Notification>`u`?`unsupported`:Notification.permission}async requestPermission(){if(typeof Notification>`u`)throw new t;return Notification.requestPermission()}async getSubscription(){return(await this.registration()).pushManager.getSubscription()}async isSubscribed(){return e.isPushSupported()?await this.getSubscription()!==null:!1}async subscribe(){if(await this.requestPermission()!==`granted`)throw new n;let t=await this.registration(),r=await t.pushManager.getSubscription();if(r)return await this.config.onSubscribe(r.toJSON()),r;let i=await t.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:e.urlBase64ToUint8Array(this.config.vapidPublicKey)});return await this.config.onSubscribe(i.toJSON()),i}async unsubscribe(){let e=await this.getSubscription();if(!e)return!1;let t=e.toJSON(),n=null;if(this.config.onUnsubscribe)try{await this.config.onUnsubscribe(t)}catch(e){n={cause:e}}if(!n)return e.unsubscribe();let i;try{i=await e.unsubscribe()}catch(e){throw new r(t,!1,AggregateError([n.cause,e],`Backend delete and browser unsubscribe both failed.`))}throw new r(t,i,n.cause)}};exports.WebPushClient=i,exports.WebPushPermissionDeniedError=n,exports.WebPushUnsubscribeError=r,exports.WebPushUnsupportedError=t;
|
|
2
2
|
//# sourceMappingURL=web-push-client.cjs.map
|
|
@@ -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
|