tempest-react-sdk 0.48.0 → 0.49.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/dist/audio/sfx-pool.cjs +1 -1
- package/dist/audio/sfx-pool.cjs.map +1 -1
- package/dist/audio/sfx-pool.js +16 -7
- package/dist/audio/sfx-pool.js.map +1 -1
- package/dist/audio/use-sfx-pool.cjs +1 -1
- package/dist/audio/use-sfx-pool.cjs.map +1 -1
- package/dist/audio/use-sfx-pool.js +3 -8
- package/dist/audio/use-sfx-pool.js.map +1 -1
- package/dist/auth/passkey.cjs +1 -1
- package/dist/auth/passkey.cjs.map +1 -1
- package/dist/auth/passkey.js +79 -81
- package/dist/auth/passkey.js.map +1 -1
- package/dist/charts.cjs +1 -1
- package/dist/charts.js +4 -4
- package/dist/components/BarList/BarList.cjs +1 -1
- package/dist/components/BarList/BarList.cjs.map +1 -1
- package/dist/components/BarList/BarList.js +31 -24
- package/dist/components/BarList/BarList.js.map +1 -1
- package/dist/components/BarList/bar-list-model.cjs +1 -1
- package/dist/components/BarList/bar-list-model.cjs.map +1 -1
- package/dist/components/BarList/bar-list-model.js +5 -5
- package/dist/components/BarList/bar-list-model.js.map +1 -1
- package/dist/components/ClickOutside/ClickOutside.cjs +1 -1
- package/dist/components/ClickOutside/ClickOutside.cjs.map +1 -1
- package/dist/components/ClickOutside/ClickOutside.js +13 -14
- package/dist/components/ClickOutside/ClickOutside.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +124 -122
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/FilterBar/filter-apply.cjs +1 -1
- package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
- package/dist/components/FilterBar/filter-apply.js +35 -38
- package/dist/components/FilterBar/filter-apply.js.map +1 -1
- package/dist/components/FilterBar/filter-query.cjs +1 -1
- package/dist/components/FilterBar/filter-query.cjs.map +1 -1
- package/dist/components/FilterBar/filter-query.js +16 -13
- package/dist/components/FilterBar/filter-query.js.map +1 -1
- package/dist/geo/use-position-tracker.cjs +1 -1
- package/dist/geo/use-position-tracker.cjs.map +1 -1
- package/dist/geo/use-position-tracker.js +25 -26
- package/dist/geo/use-position-tracker.js.map +1 -1
- package/dist/hooks/use-async.cjs +1 -1
- package/dist/hooks/use-async.cjs.map +1 -1
- package/dist/hooks/use-async.js +27 -30
- package/dist/hooks/use-async.js.map +1 -1
- package/dist/hooks/use-click-outside.cjs +1 -1
- package/dist/hooks/use-click-outside.cjs.map +1 -1
- package/dist/hooks/use-click-outside.js +8 -9
- package/dist/hooks/use-click-outside.js.map +1 -1
- package/dist/hooks/use-countdown.cjs +1 -1
- package/dist/hooks/use-countdown.cjs.map +1 -1
- package/dist/hooks/use-countdown.js +8 -13
- package/dist/hooks/use-countdown.js.map +1 -1
- package/dist/hooks/use-event-listener.cjs +1 -1
- package/dist/hooks/use-event-listener.cjs.map +1 -1
- package/dist/hooks/use-event-listener.js +6 -6
- package/dist/hooks/use-event-listener.js.map +1 -1
- package/dist/hooks/use-interval.cjs +1 -1
- package/dist/hooks/use-interval.cjs.map +1 -1
- package/dist/hooks/use-interval.js +5 -6
- package/dist/hooks/use-interval.js.map +1 -1
- package/dist/hooks/use-latest-ref.cjs.map +1 -1
- package/dist/hooks/use-latest-ref.js.map +1 -1
- package/dist/hooks/use-long-press-handlers.cjs +1 -1
- package/dist/hooks/use-long-press-handlers.cjs.map +1 -1
- package/dist/hooks/use-long-press-handlers.js +8 -10
- package/dist/hooks/use-long-press-handlers.js.map +1 -1
- package/dist/hooks/use-long-press.cjs +1 -1
- package/dist/hooks/use-long-press.cjs.map +1 -1
- package/dist/hooks/use-long-press.js +6 -6
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-online.cjs +1 -1
- package/dist/hooks/use-online.cjs.map +1 -1
- package/dist/hooks/use-online.js +10 -10
- package/dist/hooks/use-online.js.map +1 -1
- package/dist/hooks/use-service-worker-update.cjs +1 -1
- package/dist/hooks/use-service-worker-update.cjs.map +1 -1
- package/dist/hooks/use-service-worker-update.js +24 -25
- package/dist/hooks/use-service-worker-update.js.map +1 -1
- package/dist/hooks/use-stable-callback.cjs +1 -1
- package/dist/hooks/use-stable-callback.cjs.map +1 -1
- package/dist/hooks/use-stable-callback.js +4 -3
- package/dist/hooks/use-stable-callback.js.map +1 -1
- package/dist/hooks/use-timeout.cjs +1 -1
- package/dist/hooks/use-timeout.cjs.map +1 -1
- package/dist/hooks/use-timeout.js +5 -6
- package/dist/hooks/use-timeout.js.map +1 -1
- package/dist/hooks/use-typewriter.cjs +1 -1
- package/dist/hooks/use-typewriter.cjs.map +1 -1
- package/dist/hooks/use-typewriter.js +9 -18
- package/dist/hooks/use-typewriter.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 +11 -16
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/describe-api-error.cjs +1 -1
- package/dist/http/describe-api-error.cjs.map +1 -1
- package/dist/http/describe-api-error.js +12 -15
- package/dist/http/describe-api-error.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +18 -6
- package/dist/http/errors.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +60 -61
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/retry.cjs +1 -1
- package/dist/http/retry.cjs.map +1 -1
- package/dist/http/retry.js +20 -16
- package/dist/http/retry.js.map +1 -1
- package/dist/http/use-describe-api-error.cjs +1 -1
- package/dist/http/use-describe-api-error.cjs.map +1 -1
- package/dist/http/use-describe-api-error.js +2 -2
- package/dist/http/use-describe-api-error.js.map +1 -1
- package/dist/i18n/create-i18n.cjs +1 -1
- package/dist/i18n/create-i18n.cjs.map +1 -1
- package/dist/i18n/create-i18n.js +6 -6
- package/dist/i18n/create-i18n.js.map +1 -1
- package/dist/icons/material-symbols.cjs +1 -1
- package/dist/icons/material-symbols.cjs.map +1 -1
- package/dist/icons/material-symbols.js +136 -5
- package/dist/icons/material-symbols.js.map +1 -1
- package/dist/icons.d.ts +17 -5
- package/dist/offline/create-offline-sync.cjs +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js +17 -9
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/offline/use-offline-sync.cjs +1 -1
- package/dist/offline/use-offline-sync.cjs.map +1 -1
- package/dist/offline/use-offline-sync.js +7 -8
- package/dist/offline/use-offline-sync.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 +13 -12
- package/dist/push/use-push-subscription.js.map +1 -1
- package/dist/query/retry-policy.cjs +1 -1
- package/dist/query/retry-policy.cjs.map +1 -1
- package/dist/query/retry-policy.js +3 -3
- package/dist/query/retry-policy.js.map +1 -1
- package/dist/sse/use-event-stream.cjs +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/sse/use-event-stream.js +19 -20
- package/dist/sse/use-event-stream.js.map +1 -1
- package/dist/tabular/use-tabular-predictor.cjs +1 -1
- package/dist/tabular/use-tabular-predictor.cjs.map +1 -1
- package/dist/tabular/use-tabular-predictor.js +20 -17
- package/dist/tabular/use-tabular-predictor.js.map +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +363 -149
- package/dist/tempest-react-sdk.js +143 -143
- package/dist/theme/ThemeProvider.cjs +1 -1
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.js +12 -17
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/utils/base64.cjs +2 -0
- package/dist/utils/base64.cjs.map +1 -0
- package/dist/utils/base64.js +16 -0
- package/dist/utils/base64.js.map +1 -0
- package/dist/utils/compare-values.cjs +1 -1
- package/dist/utils/compare-values.cjs.map +1 -1
- package/dist/utils/compare-values.js +4 -3
- package/dist/utils/compare-values.js.map +1 -1
- package/dist/utils/compressed-storage.cjs +1 -1
- package/dist/utils/compressed-storage.cjs.map +1 -1
- package/dist/utils/compressed-storage.js +16 -46
- package/dist/utils/compressed-storage.js.map +1 -1
- package/dist/utils/storage.cjs +1 -1
- package/dist/utils/storage.cjs.map +1 -1
- package/dist/utils/storage.js +37 -17
- package/dist/utils/storage.js.map +1 -1
- package/dist/vision/use-camera-stream.cjs +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-camera-stream.js +34 -33
- package/dist/vision/use-camera-stream.js.map +1 -1
- package/dist/vite/base-url.cjs +2 -0
- package/dist/vite/base-url.cjs.map +1 -0
- package/dist/vite/base-url.js +8 -0
- package/dist/vite/base-url.js.map +1 -0
- package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.js +54 -29
- package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
- package/dist/vite/tempest-pwa-manifest.cjs +2 -2
- package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
- package/dist/vite/tempest-pwa-manifest.js +15 -14
- package/dist/vite/tempest-pwa-manifest.js.map +1 -1
- package/dist/vite.d.ts +2 -2
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +40 -42
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-service-worker-update.cjs","names":[],"sources":["../../src/hooks/use-service-worker-update.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n registerServiceWorker,\n skipWaiting,\n type RegisterServiceWorkerOptions,\n} from \"../sw/register-service-worker\";\n\n/**\n * Options for {@link useServiceWorkerUpdate}. Mirrors the registration options\n * minus the callbacks the hook owns internally (`onUpdate`); `onReady` and\n * `onError` still pass through for host-side logging.\n *\n * Leave `autoUpdate` off (the default): this hook exists to hand the reload\n * decision to the user, so a silent auto-reload would defeat the prompt.\n */\nexport type UseServiceWorkerUpdateOptions = Omit<RegisterServiceWorkerOptions, \"onUpdate\">;\n\n/** Reactive service-worker update state plus the apply action. */\nexport interface UseServiceWorkerUpdateResult {\n /** `true` once a new worker has installed and is waiting to activate. */\n updateAvailable: boolean;\n /**\n * Activate the waiting worker and reload the page once it takes control.\n * No-op when no update is waiting.\n */\n applyUpdate: () => void;\n /** The active registration, or `null` before it resolves / when unsupported. */\n registration: ServiceWorkerRegistration | null;\n}\n\n/**\n * Register a service worker and expose a user-driven update flow.\n *\n * Wraps {@link registerServiceWorker}: when a fresh worker finishes installing\n * while the page is still controlled by the old one, `updateAvailable` flips to\n * `true`. Call `applyUpdate` — typically from an \"atualizar\" toast — to tell the\n * waiting worker to activate and reload the page once it takes control.\n *\n * Skips cleanly when the runtime has no service-worker support (`registration`\n * stays `null`, `updateAvailable` stays `false`).\n *\n * @param options - Registration options (`url` required); `autoUpdate` should\n * stay off so the reload is user-confirmed.\n * @returns The update state and `applyUpdate` action.\n *\n * @example\n * const { updateAvailable, applyUpdate } = useServiceWorkerUpdate({ url: \"/sw.js\" });\n * return (\n * <UpdatePrompt open={updateAvailable} onUpdate={applyUpdate} />\n * );\n */\nexport function useServiceWorkerUpdate(\n options: UseServiceWorkerUpdateOptions,\n): UseServiceWorkerUpdateResult {\n const [updateAvailable, setUpdateAvailable] = useState<boolean>(false);\n const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);\n const waitingRef = useRef<ServiceWorker | null>(null);\n\n const optionsRef =
|
|
1
|
+
{"version":3,"file":"use-service-worker-update.cjs","names":[],"sources":["../../src/hooks/use-service-worker-update.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useLatestRef } from \"./use-latest-ref\";\nimport {\n registerServiceWorker,\n skipWaiting,\n type RegisterServiceWorkerOptions,\n} from \"../sw/register-service-worker\";\n\n/**\n * Options for {@link useServiceWorkerUpdate}. Mirrors the registration options\n * minus the callbacks the hook owns internally (`onUpdate`); `onReady` and\n * `onError` still pass through for host-side logging.\n *\n * Leave `autoUpdate` off (the default): this hook exists to hand the reload\n * decision to the user, so a silent auto-reload would defeat the prompt.\n */\nexport type UseServiceWorkerUpdateOptions = Omit<RegisterServiceWorkerOptions, \"onUpdate\">;\n\n/** Reactive service-worker update state plus the apply action. */\nexport interface UseServiceWorkerUpdateResult {\n /** `true` once a new worker has installed and is waiting to activate. */\n updateAvailable: boolean;\n /**\n * Activate the waiting worker and reload the page once it takes control.\n * No-op when no update is waiting.\n */\n applyUpdate: () => void;\n /** The active registration, or `null` before it resolves / when unsupported. */\n registration: ServiceWorkerRegistration | null;\n}\n\n/**\n * Register a service worker and expose a user-driven update flow.\n *\n * Wraps {@link registerServiceWorker}: when a fresh worker finishes installing\n * while the page is still controlled by the old one, `updateAvailable` flips to\n * `true`. Call `applyUpdate` — typically from an \"atualizar\" toast — to tell the\n * waiting worker to activate and reload the page once it takes control.\n *\n * Skips cleanly when the runtime has no service-worker support (`registration`\n * stays `null`, `updateAvailable` stays `false`).\n *\n * @param options - Registration options (`url` required); `autoUpdate` should\n * stay off so the reload is user-confirmed.\n * @returns The update state and `applyUpdate` action.\n *\n * @example\n * const { updateAvailable, applyUpdate } = useServiceWorkerUpdate({ url: \"/sw.js\" });\n * return (\n * <UpdatePrompt open={updateAvailable} onUpdate={applyUpdate} />\n * );\n */\nexport function useServiceWorkerUpdate(\n options: UseServiceWorkerUpdateOptions,\n): UseServiceWorkerUpdateResult {\n const [updateAvailable, setUpdateAvailable] = useState<boolean>(false);\n const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);\n const waitingRef = useRef<ServiceWorker | null>(null);\n\n const optionsRef = useLatestRef(options);\n\n useEffect(() => {\n let cancelled = false;\n void registerServiceWorker({\n ...optionsRef.current,\n onUpdate: (waiting, reg) => {\n waitingRef.current = waiting;\n setRegistration(reg);\n setUpdateAvailable(true);\n },\n onReady: (reg) => {\n if (cancelled) return;\n setRegistration(reg);\n if (reg.waiting && navigator.serviceWorker.controller) {\n waitingRef.current = reg.waiting;\n setUpdateAvailable(true);\n }\n optionsRef.current.onReady?.(reg);\n },\n }).then((reg) => {\n if (!cancelled && reg) setRegistration(reg);\n });\n return () => {\n cancelled = true;\n };\n }, [options.url, optionsRef]);\n\n const applyUpdate = useCallback(() => {\n const waiting = waitingRef.current;\n if (!waiting || typeof navigator === \"undefined\") return;\n let reloading = false;\n navigator.serviceWorker.addEventListener(\"controllerchange\", () => {\n if (reloading) return;\n reloading = true;\n window.location.reload();\n });\n skipWaiting(waiting);\n }, []);\n\n return { updateAvailable, applyUpdate, registration };\n}\n"],"mappings":"8GAoDA,SAAgB,EACZ,EAC4B,CAC5B,GAAM,CAAC,EAAiB,IAAA,EAAsB,EAAA,SAAA,CAAkB,EAAK,EAC/D,CAAC,EAAc,IAAA,EAAmB,EAAA,SAAA,CAA2C,IAAI,EACjF,GAAA,EAAa,EAAA,OAAA,CAA6B,IAAI,EAE9C,EAAa,EAAA,aAAa,CAAO,EAwCvC,OAtCA,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAI,EAAY,GAoBhB,OAnBA,EAAK,sBAAsB,CACvB,GAAG,EAAW,QACd,UAAW,EAAS,IAAQ,CACxB,EAAW,QAAU,EACrB,EAAgB,CAAG,EACnB,EAAmB,EAAI,CAC3B,EACA,QAAU,GAAQ,CACV,IACJ,EAAgB,CAAG,EACf,EAAI,SAAW,UAAU,cAAc,aACvC,EAAW,QAAU,EAAI,QACzB,EAAmB,EAAI,GAE3B,EAAW,QAAQ,UAAU,CAAG,EACpC,CACJ,CAAC,CAAC,CAAC,KAAM,GAAQ,CACT,CAAC,GAAa,GAAK,EAAgB,CAAG,CAC9C,CAAC,MACY,CACT,EAAY,EAChB,CACJ,EAAG,CAAC,EAAQ,IAAK,CAAU,CAAC,EAcrB,CAAE,kBAAiB,aAAA,EAZN,EAAA,YAAA,KAAkB,CAClC,IAAM,EAAU,EAAW,QAC3B,GAAI,CAAC,GAAW,OAAO,UAAc,IAAa,OAClD,IAAI,EAAY,GAChB,UAAU,cAAc,iBAAiB,uBAA0B,CAC3D,IACJ,EAAY,GACZ,OAAO,SAAS,OAAO,EAC3B,CAAC,EACD,EAAA,YAAY,CAAO,CACvB,EAAG,CAAC,CAEsB,EAAa,cAAa,CACxD"}
|
|
@@ -1,39 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useLatestRef as e } from "./use-latest-ref.js";
|
|
2
|
+
import { registerServiceWorker as t, skipWaiting as n } from "../sw/register-service-worker.js";
|
|
3
|
+
import { useCallback as r, useEffect as i, useRef as a, useState as o } from "react";
|
|
3
4
|
//#region src/hooks/use-service-worker-update.ts
|
|
4
|
-
function
|
|
5
|
-
let [
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return e({
|
|
11
|
-
...f.current,
|
|
5
|
+
function s(s) {
|
|
6
|
+
let [c, l] = o(!1), [u, d] = o(null), f = a(null), p = e(s);
|
|
7
|
+
return i(() => {
|
|
8
|
+
let e = !1;
|
|
9
|
+
return t({
|
|
10
|
+
...p.current,
|
|
12
11
|
onUpdate: (e, t) => {
|
|
13
|
-
|
|
12
|
+
f.current = e, d(t), l(!0);
|
|
14
13
|
},
|
|
15
|
-
onReady: (
|
|
16
|
-
|
|
14
|
+
onReady: (t) => {
|
|
15
|
+
e || (d(t), t.waiting && navigator.serviceWorker.controller && (f.current = t.waiting, l(!0)), p.current.onReady?.(t));
|
|
17
16
|
}
|
|
18
|
-
}).then((
|
|
19
|
-
!
|
|
17
|
+
}).then((t) => {
|
|
18
|
+
!e && t && d(t);
|
|
20
19
|
}), () => {
|
|
21
|
-
|
|
20
|
+
e = !0;
|
|
22
21
|
};
|
|
23
|
-
}, [
|
|
24
|
-
updateAvailable:
|
|
25
|
-
applyUpdate:
|
|
26
|
-
let e =
|
|
22
|
+
}, [s.url, p]), {
|
|
23
|
+
updateAvailable: c,
|
|
24
|
+
applyUpdate: r(() => {
|
|
25
|
+
let e = f.current;
|
|
27
26
|
if (!e || typeof navigator > "u") return;
|
|
28
|
-
let
|
|
27
|
+
let t = !1;
|
|
29
28
|
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
|
30
|
-
|
|
31
|
-
}),
|
|
29
|
+
t || (t = !0, window.location.reload());
|
|
30
|
+
}), n(e);
|
|
32
31
|
}, []),
|
|
33
|
-
registration:
|
|
32
|
+
registration: u
|
|
34
33
|
};
|
|
35
34
|
}
|
|
36
35
|
//#endregion
|
|
37
|
-
export {
|
|
36
|
+
export { s as useServiceWorkerUpdate };
|
|
38
37
|
|
|
39
38
|
//# sourceMappingURL=use-service-worker-update.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-service-worker-update.js","names":[],"sources":["../../src/hooks/use-service-worker-update.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n registerServiceWorker,\n skipWaiting,\n type RegisterServiceWorkerOptions,\n} from \"../sw/register-service-worker\";\n\n/**\n * Options for {@link useServiceWorkerUpdate}. Mirrors the registration options\n * minus the callbacks the hook owns internally (`onUpdate`); `onReady` and\n * `onError` still pass through for host-side logging.\n *\n * Leave `autoUpdate` off (the default): this hook exists to hand the reload\n * decision to the user, so a silent auto-reload would defeat the prompt.\n */\nexport type UseServiceWorkerUpdateOptions = Omit<RegisterServiceWorkerOptions, \"onUpdate\">;\n\n/** Reactive service-worker update state plus the apply action. */\nexport interface UseServiceWorkerUpdateResult {\n /** `true` once a new worker has installed and is waiting to activate. */\n updateAvailable: boolean;\n /**\n * Activate the waiting worker and reload the page once it takes control.\n * No-op when no update is waiting.\n */\n applyUpdate: () => void;\n /** The active registration, or `null` before it resolves / when unsupported. */\n registration: ServiceWorkerRegistration | null;\n}\n\n/**\n * Register a service worker and expose a user-driven update flow.\n *\n * Wraps {@link registerServiceWorker}: when a fresh worker finishes installing\n * while the page is still controlled by the old one, `updateAvailable` flips to\n * `true`. Call `applyUpdate` — typically from an \"atualizar\" toast — to tell the\n * waiting worker to activate and reload the page once it takes control.\n *\n * Skips cleanly when the runtime has no service-worker support (`registration`\n * stays `null`, `updateAvailable` stays `false`).\n *\n * @param options - Registration options (`url` required); `autoUpdate` should\n * stay off so the reload is user-confirmed.\n * @returns The update state and `applyUpdate` action.\n *\n * @example\n * const { updateAvailable, applyUpdate } = useServiceWorkerUpdate({ url: \"/sw.js\" });\n * return (\n * <UpdatePrompt open={updateAvailable} onUpdate={applyUpdate} />\n * );\n */\nexport function useServiceWorkerUpdate(\n options: UseServiceWorkerUpdateOptions,\n): UseServiceWorkerUpdateResult {\n const [updateAvailable, setUpdateAvailable] = useState<boolean>(false);\n const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);\n const waitingRef = useRef<ServiceWorker | null>(null);\n\n const optionsRef =
|
|
1
|
+
{"version":3,"file":"use-service-worker-update.js","names":[],"sources":["../../src/hooks/use-service-worker-update.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useLatestRef } from \"./use-latest-ref\";\nimport {\n registerServiceWorker,\n skipWaiting,\n type RegisterServiceWorkerOptions,\n} from \"../sw/register-service-worker\";\n\n/**\n * Options for {@link useServiceWorkerUpdate}. Mirrors the registration options\n * minus the callbacks the hook owns internally (`onUpdate`); `onReady` and\n * `onError` still pass through for host-side logging.\n *\n * Leave `autoUpdate` off (the default): this hook exists to hand the reload\n * decision to the user, so a silent auto-reload would defeat the prompt.\n */\nexport type UseServiceWorkerUpdateOptions = Omit<RegisterServiceWorkerOptions, \"onUpdate\">;\n\n/** Reactive service-worker update state plus the apply action. */\nexport interface UseServiceWorkerUpdateResult {\n /** `true` once a new worker has installed and is waiting to activate. */\n updateAvailable: boolean;\n /**\n * Activate the waiting worker and reload the page once it takes control.\n * No-op when no update is waiting.\n */\n applyUpdate: () => void;\n /** The active registration, or `null` before it resolves / when unsupported. */\n registration: ServiceWorkerRegistration | null;\n}\n\n/**\n * Register a service worker and expose a user-driven update flow.\n *\n * Wraps {@link registerServiceWorker}: when a fresh worker finishes installing\n * while the page is still controlled by the old one, `updateAvailable` flips to\n * `true`. Call `applyUpdate` — typically from an \"atualizar\" toast — to tell the\n * waiting worker to activate and reload the page once it takes control.\n *\n * Skips cleanly when the runtime has no service-worker support (`registration`\n * stays `null`, `updateAvailable` stays `false`).\n *\n * @param options - Registration options (`url` required); `autoUpdate` should\n * stay off so the reload is user-confirmed.\n * @returns The update state and `applyUpdate` action.\n *\n * @example\n * const { updateAvailable, applyUpdate } = useServiceWorkerUpdate({ url: \"/sw.js\" });\n * return (\n * <UpdatePrompt open={updateAvailable} onUpdate={applyUpdate} />\n * );\n */\nexport function useServiceWorkerUpdate(\n options: UseServiceWorkerUpdateOptions,\n): UseServiceWorkerUpdateResult {\n const [updateAvailable, setUpdateAvailable] = useState<boolean>(false);\n const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);\n const waitingRef = useRef<ServiceWorker | null>(null);\n\n const optionsRef = useLatestRef(options);\n\n useEffect(() => {\n let cancelled = false;\n void registerServiceWorker({\n ...optionsRef.current,\n onUpdate: (waiting, reg) => {\n waitingRef.current = waiting;\n setRegistration(reg);\n setUpdateAvailable(true);\n },\n onReady: (reg) => {\n if (cancelled) return;\n setRegistration(reg);\n if (reg.waiting && navigator.serviceWorker.controller) {\n waitingRef.current = reg.waiting;\n setUpdateAvailable(true);\n }\n optionsRef.current.onReady?.(reg);\n },\n }).then((reg) => {\n if (!cancelled && reg) setRegistration(reg);\n });\n return () => {\n cancelled = true;\n };\n }, [options.url, optionsRef]);\n\n const applyUpdate = useCallback(() => {\n const waiting = waitingRef.current;\n if (!waiting || typeof navigator === \"undefined\") return;\n let reloading = false;\n navigator.serviceWorker.addEventListener(\"controllerchange\", () => {\n if (reloading) return;\n reloading = true;\n window.location.reload();\n });\n skipWaiting(waiting);\n }, []);\n\n return { updateAvailable, applyUpdate, registration };\n}\n"],"mappings":";;;;AAoDA,SAAgB,EACZ,GAC4B;CAC5B,IAAM,CAAC,GAAiB,KAAsB,EAAkB,EAAK,GAC/D,CAAC,GAAc,KAAmB,EAA2C,IAAI,GACjF,IAAa,EAA6B,IAAI,GAE9C,IAAa,EAAa,CAAO;CAwCvC,OAtCA,QAAgB;EACZ,IAAI,IAAY;EAoBhB,OAnBA,EAA2B;GACvB,GAAG,EAAW;GACd,WAAW,GAAS,MAAQ;IAGxB,AAFA,EAAW,UAAU,GACrB,EAAgB,CAAG,GACnB,EAAmB,EAAI;GAC3B;GACA,UAAU,MAAQ;IACV,MACJ,EAAgB,CAAG,GACf,EAAI,WAAW,UAAU,cAAc,eACvC,EAAW,UAAU,EAAI,SACzB,EAAmB,EAAI,IAE3B,EAAW,QAAQ,UAAU,CAAG;GACpC;EACJ,CAAC,CAAC,CAAC,MAAM,MAAQ;GACb,AAAI,CAAC,KAAa,KAAK,EAAgB,CAAG;EAC9C,CAAC,SACY;GACT,IAAY;EAChB;CACJ,GAAG,CAAC,EAAQ,KAAK,CAAU,CAAC,GAcrB;EAAE;EAAiB,aAZN,QAAkB;GAClC,IAAM,IAAU,EAAW;GAC3B,IAAI,CAAC,KAAW,OAAO,YAAc,KAAa;GAClD,IAAI,IAAY;GAMhB,AALA,UAAU,cAAc,iBAAiB,0BAA0B;IAC3D,MACJ,IAAY,IACZ,OAAO,SAAS,OAAO;GAC3B,CAAC,GACD,EAAY,CAAO;EACvB,GAAG,CAAC,CAEsB;EAAa;CAAa;AACxD"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
const e=require("./use-latest-ref.cjs");let t=require("react");function n(n){let r=e.useLatestRef(n);return(0,t.useCallback)((...e)=>r.current(...e),[r])}exports.useStableCallback=n;
|
|
2
2
|
//# sourceMappingURL=use-stable-callback.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-stable-callback.cjs","names":[],"sources":["../../src/hooks/use-stable-callback.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"use-stable-callback.cjs","names":[],"sources":["../../src/hooks/use-stable-callback.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useLatestRef } from \"./use-latest-ref\";\n\n/**\n * Returns a stable function reference that always invokes the latest\n * `callback` argument. Use to break dependency cycles in effects without\n * triggering re-runs when the callback identity changes.\n *\n * Built on {@link useLatestRef}, which owns the render-time assignment and the\n * reason it is not an effect: an effect would satisfy the React Compiler rules\n * but open a one-commit staleness window. What this adds is the callable with a\n * stable identity, so callers do not reach through `.current`. Prefer React's\n * `useEffectEvent` once it ships as stable.\n *\n * The ref is listed as a dependency because it is one — a stable object across\n * renders, so the returned identity never changes. Listing it is what lets the\n * exhaustive-deps rule verify that instead of taking an empty array on trust.\n */\nexport function useStableCallback<TArgs extends unknown[], TReturn>(\n callback: (...args: TArgs) => TReturn,\n): (...args: TArgs) => TReturn {\n const ref = useLatestRef(callback);\n return useCallback((...args: TArgs) => ref.current(...args), [ref]);\n}\n"],"mappings":"+DAkBA,SAAgB,EACZ,EAC2B,CAC3B,IAAM,EAAM,EAAA,aAAa,CAAQ,EACjC,OAAA,EAAO,EAAA,YAAA,EAAa,GAAG,IAAgB,EAAI,QAAQ,GAAG,CAAI,EAAG,CAAC,CAAG,CAAC,CACtE"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useLatestRef as e } from "./use-latest-ref.js";
|
|
2
|
+
import { useCallback as t } from "react";
|
|
2
3
|
//#region src/hooks/use-stable-callback.ts
|
|
3
4
|
function n(n) {
|
|
4
|
-
let r =
|
|
5
|
-
return
|
|
5
|
+
let r = e(n);
|
|
6
|
+
return t((...e) => r.current(...e), [r]);
|
|
6
7
|
}
|
|
7
8
|
//#endregion
|
|
8
9
|
export { n as useStableCallback };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-stable-callback.js","names":[],"sources":["../../src/hooks/use-stable-callback.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"use-stable-callback.js","names":[],"sources":["../../src/hooks/use-stable-callback.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useLatestRef } from \"./use-latest-ref\";\n\n/**\n * Returns a stable function reference that always invokes the latest\n * `callback` argument. Use to break dependency cycles in effects without\n * triggering re-runs when the callback identity changes.\n *\n * Built on {@link useLatestRef}, which owns the render-time assignment and the\n * reason it is not an effect: an effect would satisfy the React Compiler rules\n * but open a one-commit staleness window. What this adds is the callable with a\n * stable identity, so callers do not reach through `.current`. Prefer React's\n * `useEffectEvent` once it ships as stable.\n *\n * The ref is listed as a dependency because it is one — a stable object across\n * renders, so the returned identity never changes. Listing it is what lets the\n * exhaustive-deps rule verify that instead of taking an empty array on trust.\n */\nexport function useStableCallback<TArgs extends unknown[], TReturn>(\n callback: (...args: TArgs) => TReturn,\n): (...args: TArgs) => TReturn {\n const ref = useLatestRef(callback);\n return useCallback((...args: TArgs) => ref.current(...args), [ref]);\n}\n"],"mappings":";;;AAkBA,SAAgB,EACZ,GAC2B;CAC3B,IAAM,IAAM,EAAa,CAAQ;CACjC,OAAO,GAAa,GAAG,MAAgB,EAAI,QAAQ,GAAG,CAAI,GAAG,CAAC,CAAG,CAAC;AACtE"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
const e=require("./use-latest-ref.cjs");let t=require("react");function n(n,r){let i=e.useLatestRef(n);(0,t.useEffect)(()=>{if(r===null)return;let e=setTimeout(()=>i.current(),r);return()=>clearTimeout(e)},[r,i])}exports.useTimeout=n;
|
|
2
2
|
//# sourceMappingURL=use-timeout.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-timeout.cjs","names":[],"sources":["../../src/hooks/use-timeout.ts"],"sourcesContent":["import { useEffect
|
|
1
|
+
{"version":3,"file":"use-timeout.cjs","names":[],"sources":["../../src/hooks/use-timeout.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { useLatestRef } from \"./use-latest-ref\";\n\n/**\n * Run a callback after `delay` ms. Pass `null` to disable. Resets when\n * `delay` changes; the callback ref always points at the latest closure.\n *\n * @example\n * useTimeout(() => setShow(false), show ? 3000 : null);\n */\nexport function useTimeout(fn: () => void, delay: number | null): void {\n const fnRef = useLatestRef(fn);\n\n useEffect(() => {\n if (delay === null) return;\n const id = setTimeout(() => fnRef.current(), delay);\n return () => clearTimeout(id);\n }, [delay, fnRef]);\n}\n"],"mappings":"+DAUA,SAAgB,EAAW,EAAgB,EAA4B,CACnE,IAAM,EAAQ,EAAA,aAAa,CAAE,GAE7B,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,IAAU,KAAM,OACpB,IAAM,EAAK,eAAiB,EAAM,QAAQ,EAAG,CAAK,EAClD,UAAa,aAAa,CAAE,CAChC,EAAG,CAAC,EAAO,CAAK,CAAC,CACrB"}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useLatestRef as e } from "./use-latest-ref.js";
|
|
2
|
+
import { useEffect as t } from "react";
|
|
2
3
|
//#region src/hooks/use-timeout.ts
|
|
3
4
|
function n(n, r) {
|
|
4
|
-
let i =
|
|
5
|
-
|
|
6
|
-
i.current = n;
|
|
7
|
-
}, [n]), e(() => {
|
|
5
|
+
let i = e(n);
|
|
6
|
+
t(() => {
|
|
8
7
|
if (r === null) return;
|
|
9
8
|
let e = setTimeout(() => i.current(), r);
|
|
10
9
|
return () => clearTimeout(e);
|
|
11
|
-
}, [r]);
|
|
10
|
+
}, [r, i]);
|
|
12
11
|
}
|
|
13
12
|
//#endregion
|
|
14
13
|
export { n as useTimeout };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-timeout.js","names":[],"sources":["../../src/hooks/use-timeout.ts"],"sourcesContent":["import { useEffect
|
|
1
|
+
{"version":3,"file":"use-timeout.js","names":[],"sources":["../../src/hooks/use-timeout.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { useLatestRef } from \"./use-latest-ref\";\n\n/**\n * Run a callback after `delay` ms. Pass `null` to disable. Resets when\n * `delay` changes; the callback ref always points at the latest closure.\n *\n * @example\n * useTimeout(() => setShow(false), show ? 3000 : null);\n */\nexport function useTimeout(fn: () => void, delay: number | null): void {\n const fnRef = useLatestRef(fn);\n\n useEffect(() => {\n if (delay === null) return;\n const id = setTimeout(() => fnRef.current(), delay);\n return () => clearTimeout(id);\n }, [delay, fnRef]);\n}\n"],"mappings":";;;AAUA,SAAgB,EAAW,GAAgB,GAA4B;CACnE,IAAM,IAAQ,EAAa,CAAE;CAE7B,QAAgB;EACZ,IAAI,MAAU,MAAM;EACpB,IAAM,IAAK,iBAAiB,EAAM,QAAQ,GAAG,CAAK;EAClD,aAAa,aAAa,CAAE;CAChC,GAAG,CAAC,GAAO,CAAK,CAAC;AACrB"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
const e=require("./use-interval.cjs");let t=require("react");function n(n,r){let i=n??``,a=r>0?0:i.length,[o,s]=(0,t.useState)(i),[c,l]=(0,t.useState)(a);o!==i&&(s(i),l(a));let u=r>0?Math.min(c,i.length):i.length;e.useInterval(()=>l(e=>e+1),r>0&&u<i.length?r:null);let d=(0,t.useCallback)(()=>l(i.length),[i.length]);return{displayedText:i.slice(0,u),isComplete:u>=i.length,skip:d}}exports.useTypewriter=n;
|
|
2
2
|
//# sourceMappingURL=use-typewriter.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-typewriter.cjs","names":[],"sources":["../../src/hooks/use-typewriter.ts"],"sourcesContent":["import { useCallback,
|
|
1
|
+
{"version":3,"file":"use-typewriter.cjs","names":[],"sources":["../../src/hooks/use-typewriter.ts"],"sourcesContent":["import { useCallback, useState } from \"react\";\nimport { useInterval } from \"./use-interval\";\n\n/** State returned by {@link useTypewriter}. */\nexport interface UseTypewriterResult {\n /** The prefix of `text` revealed so far. */\n displayedText: string;\n /** `true` once the whole string is on screen. */\n isComplete: boolean;\n /** Reveal the rest immediately — wire it to a tap or a key press. */\n skip: () => void;\n}\n\n/**\n * Reveals a string one character at a time.\n *\n * Changing `text` restarts the reveal. The reset happens during render rather\n * than in an effect, so the new string never flashes in full for one frame\n * before the animation takes over.\n *\n * Always give the reader a way out: an animation that cannot be skipped is a\n * tax on anyone re-reading or moving fast, which is what `skip` is for.\n *\n * How far the reveal got is derived rather than stored: a `speedMs` of `0` reads\n * as \"all of it\" without a state write, and `useInterval` pausing on a `null`\n * delay is what stops the timer at the end. Both used to need an effect that\n * cleared its own interval from inside a state updater.\n *\n * @param text - The full string to reveal. Nullish is treated as empty.\n * @param speedMs - Delay between characters. `0` or less renders instantly,\n * which is the hook-safe way to honour `prefers-reduced-motion`.\n * @returns The revealed prefix, whether it finished, and a `skip` action.\n *\n * @example\n * const reduced = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n * const { displayedText, isComplete, skip } = useTypewriter(line, reduced ? 0 : 30);\n *\n * return <p onClick={skip}>{displayedText}{isComplete ? \"\" : \"▌\"}</p>;\n */\nexport function useTypewriter(text: string, speedMs: number): UseTypewriterResult {\n const safeText = text ?? \"\";\n const initialCount = speedMs > 0 ? 0 : safeText.length;\n\n const [renderedText, setRenderedText] = useState(safeText);\n const [count, setCount] = useState(initialCount);\n\n if (renderedText !== safeText) {\n setRenderedText(safeText);\n setCount(initialCount);\n }\n\n const revealed = speedMs > 0 ? Math.min(count, safeText.length) : safeText.length;\n\n useInterval(\n () => setCount((current) => current + 1),\n speedMs > 0 && revealed < safeText.length ? speedMs : null,\n );\n\n const skip = useCallback(() => setCount(safeText.length), [safeText.length]);\n\n return {\n displayedText: safeText.slice(0, revealed),\n isComplete: revealed >= safeText.length,\n skip,\n };\n}\n"],"mappings":"6DAuCA,SAAgB,EAAc,EAAc,EAAsC,CAC9E,IAAM,EAAW,GAAQ,GACnB,EAAe,EAAU,EAAI,EAAI,EAAS,OAE1C,CAAC,EAAc,IAAA,EAAmB,EAAA,SAAA,CAAS,CAAQ,EACnD,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAS,CAAY,EAE3C,IAAiB,IACjB,EAAgB,CAAQ,EACxB,EAAS,CAAY,GAGzB,IAAM,EAAW,EAAU,EAAI,KAAK,IAAI,EAAO,EAAS,MAAM,EAAI,EAAS,OAE3E,EAAA,gBACU,EAAU,GAAY,EAAU,CAAC,EACvC,EAAU,GAAK,EAAW,EAAS,OAAS,EAAU,IAC1D,EAEA,IAAM,GAAA,EAAO,EAAA,YAAA,KAAkB,EAAS,EAAS,MAAM,EAAG,CAAC,EAAS,MAAM,CAAC,EAE3E,MAAO,CACH,cAAe,EAAS,MAAM,EAAG,CAAQ,EACzC,WAAY,GAAY,EAAS,OACjC,MACJ,CACJ"}
|
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useInterval as e } from "./use-interval.js";
|
|
2
|
+
import { useCallback as t, useState as n } from "react";
|
|
2
3
|
//#region src/hooks/use-typewriter.ts
|
|
3
4
|
function r(r, i) {
|
|
4
5
|
let a = r ?? "", o = i > 0 ? 0 : a.length, [s, c] = n(a), [l, u] = n(o);
|
|
5
|
-
s !== a && (c(a), u(o))
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
let e = setInterval(() => {
|
|
11
|
-
u((t) => {
|
|
12
|
-
let n = t + 1;
|
|
13
|
-
return n >= a.length ? (clearInterval(e), a.length) : n;
|
|
14
|
-
});
|
|
15
|
-
}, i);
|
|
16
|
-
return () => clearInterval(e);
|
|
17
|
-
}, [a, i]);
|
|
18
|
-
let d = e(() => u(a.length), [a.length]);
|
|
6
|
+
s !== a && (c(a), u(o));
|
|
7
|
+
let d = i > 0 ? Math.min(l, a.length) : a.length;
|
|
8
|
+
e(() => u((e) => e + 1), i > 0 && d < a.length ? i : null);
|
|
9
|
+
let f = t(() => u(a.length), [a.length]);
|
|
19
10
|
return {
|
|
20
|
-
displayedText: a.slice(0,
|
|
21
|
-
isComplete:
|
|
22
|
-
skip:
|
|
11
|
+
displayedText: a.slice(0, d),
|
|
12
|
+
isComplete: d >= a.length,
|
|
13
|
+
skip: f
|
|
23
14
|
};
|
|
24
15
|
}
|
|
25
16
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-typewriter.js","names":[],"sources":["../../src/hooks/use-typewriter.ts"],"sourcesContent":["import { useCallback,
|
|
1
|
+
{"version":3,"file":"use-typewriter.js","names":[],"sources":["../../src/hooks/use-typewriter.ts"],"sourcesContent":["import { useCallback, useState } from \"react\";\nimport { useInterval } from \"./use-interval\";\n\n/** State returned by {@link useTypewriter}. */\nexport interface UseTypewriterResult {\n /** The prefix of `text` revealed so far. */\n displayedText: string;\n /** `true` once the whole string is on screen. */\n isComplete: boolean;\n /** Reveal the rest immediately — wire it to a tap or a key press. */\n skip: () => void;\n}\n\n/**\n * Reveals a string one character at a time.\n *\n * Changing `text` restarts the reveal. The reset happens during render rather\n * than in an effect, so the new string never flashes in full for one frame\n * before the animation takes over.\n *\n * Always give the reader a way out: an animation that cannot be skipped is a\n * tax on anyone re-reading or moving fast, which is what `skip` is for.\n *\n * How far the reveal got is derived rather than stored: a `speedMs` of `0` reads\n * as \"all of it\" without a state write, and `useInterval` pausing on a `null`\n * delay is what stops the timer at the end. Both used to need an effect that\n * cleared its own interval from inside a state updater.\n *\n * @param text - The full string to reveal. Nullish is treated as empty.\n * @param speedMs - Delay between characters. `0` or less renders instantly,\n * which is the hook-safe way to honour `prefers-reduced-motion`.\n * @returns The revealed prefix, whether it finished, and a `skip` action.\n *\n * @example\n * const reduced = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n * const { displayedText, isComplete, skip } = useTypewriter(line, reduced ? 0 : 30);\n *\n * return <p onClick={skip}>{displayedText}{isComplete ? \"\" : \"▌\"}</p>;\n */\nexport function useTypewriter(text: string, speedMs: number): UseTypewriterResult {\n const safeText = text ?? \"\";\n const initialCount = speedMs > 0 ? 0 : safeText.length;\n\n const [renderedText, setRenderedText] = useState(safeText);\n const [count, setCount] = useState(initialCount);\n\n if (renderedText !== safeText) {\n setRenderedText(safeText);\n setCount(initialCount);\n }\n\n const revealed = speedMs > 0 ? Math.min(count, safeText.length) : safeText.length;\n\n useInterval(\n () => setCount((current) => current + 1),\n speedMs > 0 && revealed < safeText.length ? speedMs : null,\n );\n\n const skip = useCallback(() => setCount(safeText.length), [safeText.length]);\n\n return {\n displayedText: safeText.slice(0, revealed),\n isComplete: revealed >= safeText.length,\n skip,\n };\n}\n"],"mappings":";;;AAuCA,SAAgB,EAAc,GAAc,GAAsC;CAC9E,IAAM,IAAW,KAAQ,IACnB,IAAe,IAAU,IAAI,IAAI,EAAS,QAE1C,CAAC,GAAc,KAAmB,EAAS,CAAQ,GACnD,CAAC,GAAO,KAAY,EAAS,CAAY;CAE/C,AAAI,MAAiB,MACjB,EAAgB,CAAQ,GACxB,EAAS,CAAY;CAGzB,IAAM,IAAW,IAAU,IAAI,KAAK,IAAI,GAAO,EAAS,MAAM,IAAI,EAAS;CAE3E,QACU,GAAU,MAAY,IAAU,CAAC,GACvC,IAAU,KAAK,IAAW,EAAS,SAAS,IAAU,IAC1D;CAEA,IAAM,IAAO,QAAkB,EAAS,EAAS,MAAM,GAAG,CAAC,EAAS,MAAM,CAAC;CAE3E,OAAO;EACH,eAAe,EAAS,MAAM,GAAG,CAAQ;EACzC,YAAY,KAAY,EAAS;EACjC;CACJ;AACJ"}
|
package/dist/http/api-client.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./errors.cjs"),r=require("./retry.cjs");var i=new Set([`GET`,`HEAD`,`OPTIONS`])
|
|
1
|
+
const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./errors.cjs"),r=require("./retry.cjs");var i=new Set([`GET`,`HEAD`,`OPTIONS`]);function a(e,t){return!i.has(t)||!(e instanceof n.TempestApiError)?!1:n.isRetriableStatus(e.status)}function o(e){return e?e===!0?{}:e:null}function s(e){return typeof FormData<`u`&&e instanceof FormData}function c(e){return Math.round(performance.now()-e)}async function l(e,t){let r;try{r=await e.clone().json()}catch{try{r=await e.text()}catch{r=null}}return new n.TempestApiError(n.buildApiError(e.status,r,e.headers,t))}function u(n){let i=n.fetcher??globalThis.fetch.bind(globalThis);function u(){let e=n.getToken?.();return e?{Authorization:`Bearer ${e}`}:{}}async function d(e,r,a){let{body:o,params:c,headers:l,...d}=r,f=s(o),p={...f?{}:{"Content-Type":`application/json`},...a?{"X-Request-ID":a}:{},...n.headers,...u(),...l},m={...d,headers:p,credentials:n.withCredentials?`include`:d.credentials,body:o==null?void 0:f?o:JSON.stringify(o)};return i(t.buildApiUrl(n.baseURL,e,{prefix:n.prefix,params:c}),m)}async function f(e,t,r,i){let a=n.logger;if(!a)return d(e,t,r);let o=performance.now();try{let n=await d(e,t,r),s={requestId:r,status:n.status,ms:c(o)},l=`${i} ${e} → ${n.status}`;return n.status>=400?a.warn(l,s):a.debug(l,s),n}catch(t){throw a.warn(`${i} ${e} → no response`,{requestId:r,ms:c(o),error:t}),t}}async function p(e,t){if(n.onUnauthorized){n.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await n.onUnauthorized(e)}catch(r){n.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:r})}}}async function m(t,r){let i=n.requestId?n.requestId():e.randomId(),a=(r.method??`GET`).toUpperCase(),o=await f(t,r,i,a);if(o.status===401){if(n.refresh){try{await n.refresh(),o=await f(t,r,i,a)}catch{throw await p(o,i),await l(o,i)}o.status===401&&await p(o,i)}else await p(o,i)}if(!o.ok)throw await l(o,i);if(o.status!==204)return(o.headers.get(`content-type`)??``).includes(`application/json`)?await o.json():await o.text()}async function h(e,t={}){let i=o(n.retry);if(!i)return m(e,t);let s=(t.method??`GET`).toUpperCase();return r.retry(()=>m(e,t),{...i,shouldRetry:i.shouldRetry??(e=>a(e,s))})}async function g(e,t,n=`POST`){return h(e,{method:n,body:t})}return{request:h,get:(e,t)=>h(e,{...t,method:`GET`}),post:(e,t)=>h(e,{...t,method:`POST`}),put:(e,t)=>h(e,{...t,method:`PUT`}),patch:(e,t)=>h(e,{...t,method:`PATCH`}),delete:(e,t)=>h(e,{...t,method:`DELETE`}),upload:g}}exports.createApiClient=u;
|
|
2
2
|
//# sourceMappingURL=api-client.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { buildApiError, TempestApiError } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return RETRIABLE_STATUSES.has(error.status) || error.status >= 500;\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, ...rest } = options;\n const isForm = isFormData(body);\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const init: RequestInit = {\n ...rest,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n return fetcher(buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }), init);\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n ): Promise<T> {\n return request<T>(path, { method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n upload,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, ...rest } = options;\n const isForm = isFormData(body);\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const init: RequestInit = {\n ...rest,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n return fetcher(buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }), init);\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n ): Promise<T> {\n return request<T>(path, { method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n upload,\n };\n}\n"],"mappings":"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"}
|
package/dist/http/api-client.js
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import { randomId as e } from "../utils/ids.js";
|
|
2
2
|
import { buildApiUrl as t } from "./build-url.js";
|
|
3
|
-
import { TempestApiError as n, buildApiError as r } from "./errors.js";
|
|
4
|
-
import { retry as
|
|
3
|
+
import { TempestApiError as n, buildApiError as r, isRetriableStatus as i } from "./errors.js";
|
|
4
|
+
import { retry as a } from "./retry.js";
|
|
5
5
|
//#region src/http/api-client.ts
|
|
6
|
-
var
|
|
6
|
+
var o = /* @__PURE__ */ new Set([
|
|
7
7
|
"GET",
|
|
8
8
|
"HEAD",
|
|
9
9
|
"OPTIONS"
|
|
10
|
-
]), o = /* @__PURE__ */ new Set([
|
|
11
|
-
0,
|
|
12
|
-
408,
|
|
13
|
-
425,
|
|
14
|
-
429
|
|
15
10
|
]);
|
|
16
11
|
function s(e, t) {
|
|
17
|
-
return !
|
|
12
|
+
return !o.has(t) || !(e instanceof n) ? !1 : i(e.status);
|
|
18
13
|
}
|
|
19
14
|
function c(e) {
|
|
20
15
|
return e ? e === !0 ? {} : e : null;
|
|
@@ -40,16 +35,16 @@ async function d(e, t) {
|
|
|
40
35
|
}
|
|
41
36
|
function f(n) {
|
|
42
37
|
let r = n.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
43
|
-
function
|
|
38
|
+
function i() {
|
|
44
39
|
let e = n.getToken?.();
|
|
45
40
|
return e ? { Authorization: `Bearer ${e}` } : {};
|
|
46
41
|
}
|
|
47
|
-
async function o(e,
|
|
48
|
-
let { body: s, params: c, headers: u, ...d } =
|
|
42
|
+
async function o(e, a, o) {
|
|
43
|
+
let { body: s, params: c, headers: u, ...d } = a, f = l(s), p = {
|
|
49
44
|
...f ? {} : { "Content-Type": "application/json" },
|
|
50
45
|
...o ? { "X-Request-ID": o } : {},
|
|
51
46
|
...n.headers,
|
|
52
|
-
...
|
|
47
|
+
...i(),
|
|
53
48
|
...u
|
|
54
49
|
}, m = {
|
|
55
50
|
...d,
|
|
@@ -116,10 +111,10 @@ function f(n) {
|
|
|
116
111
|
async function h(e, t = {}) {
|
|
117
112
|
let r = c(n.retry);
|
|
118
113
|
if (!r) return m(e, t);
|
|
119
|
-
let
|
|
120
|
-
return
|
|
114
|
+
let i = (t.method ?? "GET").toUpperCase();
|
|
115
|
+
return a(() => m(e, t), {
|
|
121
116
|
...r,
|
|
122
|
-
shouldRetry: r.shouldRetry ?? ((e) => s(e,
|
|
117
|
+
shouldRetry: r.shouldRetry ?? ((e) => s(e, i))
|
|
123
118
|
});
|
|
124
119
|
}
|
|
125
120
|
async function g(e, t, n = "POST") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.js","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { buildApiError, TempestApiError } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return RETRIABLE_STATUSES.has(error.status) || error.status >= 500;\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, ...rest } = options;\n const isForm = isFormData(body);\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const init: RequestInit = {\n ...rest,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n return fetcher(buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }), init);\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n ): Promise<T> {\n return request<T>(path, { method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n upload,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api-client.js","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, ...rest } = options;\n const isForm = isFormData(body);\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const init: RequestInit = {\n ...rest,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n return fetcher(buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }), init);\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n ): Promise<T> {\n return request<T>(path, { method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n upload,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./errors.cjs");var t={offline:`Sem conexão com o servidor. Verifique sua internet e tente de novo.`,validation:`Confira os campos destacados e tente de novo.`},n=`tempest.error.offline`,r=`tempest.error.validation`;function i(
|
|
1
|
+
const e=require("./errors.cjs");var t={offline:`Sem conexão com o servidor. Verifique sua internet e tente de novo.`,validation:`Confira os campos destacados e tente de novo.`},n=`tempest.error.offline`,r=`tempest.error.validation`;function i(){return typeof navigator<`u`&&navigator.onLine===!1}function a(n,r,a){let o=a?.offline??t.offline;if(e.isApiError(n)){if(n.status===0)return o;if(n.fields&&Object.keys(n.fields).length>0)return a?.validation??t.validation;let i=n.detail.trim();return i!==``&&i!==e.syntheticDetail(n.status)?i:`${r} (HTTP ${n.status})`}return i()?o:r}exports.API_ERROR_OFFLINE_KEY=n,exports.API_ERROR_VALIDATION_KEY=r,exports.DEFAULT_API_ERROR_STRINGS=t,exports.describeApiError=a;
|
|
2
2
|
//# sourceMappingURL=describe-api-error.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-api-error.cjs","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"describe-api-error.cjs","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a `422` the `detail` line is assembled from\n * the backend's field paths and the validator's own wording\n * (`\"items.0.price: Input should be greater than 0\"`), which is right for a\n * log and wrong for a person: it is half English in a pt-BR screen and it\n * names internals. The per-field messages stay on `fields`, where a form can\n * attach them to the inputs that failed.\n * 3. The backend's own `detail`, which is the most specific thing available and\n * is already written for a person.\n * 4. `fallback`, with `(HTTP <status>)` appended when a status is known, so the\n * screenshot in the support ticket carries the one fact a developer needs.\n *\n * Pure on purpose: it works in an interceptor, in a logger and anywhere outside\n * the React tree. {@link useDescribeApiError} is the same funnel with the\n * sentences resolved through `I18nProvider`.\n *\n * @example\n * catch (error) {\n * toast(describeApiError(error, \"Não foi possível salvar o pedido\"));\n * }\n *\n * @param error - The caught value, of any shape.\n * @param fallback - What to say when the error carries nothing better.\n * @param strings - Overrides for the fixed sentences.\n * @returns A sentence to show the user.\n */\nexport function describeApiError(\n error: unknown,\n fallback: string,\n strings?: Partial<ApiErrorStrings>,\n): string {\n const offline = strings?.offline ?? DEFAULT_API_ERROR_STRINGS.offline;\n\n if (isApiError(error)) {\n if (error.status === 0) return offline;\n if (error.fields && Object.keys(error.fields).length > 0) {\n return strings?.validation ?? DEFAULT_API_ERROR_STRINGS.validation;\n }\n const detail = error.detail.trim();\n if (detail !== \"\" && detail !== syntheticDetail(error.status)) return detail;\n return `${fallback} (HTTP ${error.status})`;\n }\n\n if (browserIsOffline()) return offline;\n\n return fallback;\n}\n"],"mappings":"gCA2BA,IAAa,EAA6C,CACtD,QAAS,sEACT,WAAY,+CAChB,EAUa,EAAwB,wBAMxB,EAA2B,2BAYxC,SAAS,GAA4B,CACjC,OAAO,OAAO,UAAc,KAAe,UAAU,SAAW,EACpE,CAqCA,SAAgB,EACZ,EACA,EACA,EACM,CACN,IAAM,EAAU,GAAS,SAAW,EAA0B,QAE9D,GAAI,EAAA,WAAW,CAAK,EAAG,CACnB,GAAI,EAAM,SAAW,EAAG,OAAO,EAC/B,GAAI,EAAM,QAAU,OAAO,KAAK,EAAM,MAAM,CAAC,CAAC,OAAS,EACnD,OAAO,GAAS,YAAc,EAA0B,WAE5D,IAAM,EAAS,EAAM,OAAO,KAAK,EAEjC,OADI,IAAW,IAAM,IAAW,EAAA,gBAAgB,EAAM,MAAM,EAAU,EAC/D,GAAG,EAAS,SAAS,EAAM,OAAO,EAC7C,CAIA,OAFI,EAAiB,EAAU,EAExB,CACX"}
|