tempest-react-sdk 0.81.0 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/bin/create-tempest-app.e2e.test.mjs +31 -0
- package/bin/create-tempest-app.mjs +11 -1
- package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
- package/bin/lib/doctor/vitest.mjs +120 -0
- package/bin/lib/doctor/vitest.test.mjs +139 -0
- package/bin/lib/fs/contain.mjs +7 -0
- package/bin/lib/openapi/escape.mjs +68 -4
- package/bin/lib/openapi/generate.hostile.test.mjs +137 -0
- package/bin/lib/openapi/generate.mjs +78 -13
- package/bin/lib/openapi/pattern-risk.mjs +134 -0
- package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
- package/bin/tempest-gen.test.mjs +100 -0
- package/bin/tempest.mjs +24 -8
- package/dist/access/use-can.cjs +1 -1
- package/dist/access/use-can.cjs.map +1 -1
- package/dist/access/use-can.js +25 -28
- package/dist/access/use-can.js.map +1 -1
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +192 -149
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/session-identity.cjs +2 -0
- package/dist/auth/session-identity.cjs.map +1 -0
- package/dist/auth/session-identity.js +12 -0
- package/dist/auth/session-identity.js.map +1 -0
- package/dist/br/boleto.cjs.map +1 -1
- package/dist/br/boleto.js.map +1 -1
- package/dist/components/AuthForm/AuthLinks.cjs +1 -1
- package/dist/components/AuthForm/AuthLinks.cjs.map +1 -1
- package/dist/components/AuthForm/AuthLinks.js +18 -15
- package/dist/components/AuthForm/AuthLinks.js.map +1 -1
- package/dist/components/AuthPage/AuthPage.cjs +1 -1
- package/dist/components/AuthPage/AuthPage.cjs.map +1 -1
- package/dist/components/AuthPage/AuthPage.js +58 -52
- package/dist/components/AuthPage/AuthPage.js.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
- package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/dist/components/Button/Button.cjs +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js +41 -40
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +38 -38
- package/dist/components/Form/Form.cjs +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js +30 -32
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/Layout/Layout.cjs +1 -1
- package/dist/components/Layout/Layout.cjs.map +1 -1
- package/dist/components/Layout/Layout.js +25 -24
- package/dist/components/Layout/Layout.js.map +1 -1
- package/dist/components/Layout/gap.cjs +2 -0
- package/dist/components/Layout/gap.cjs.map +1 -0
- package/dist/components/Layout/gap.js +15 -0
- package/dist/components/Layout/gap.js.map +1 -0
- package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.js +30 -29
- package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +1 -2
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/forms/validate-form.cjs +1 -1
- package/dist/forms/validate-form.cjs.map +1 -1
- package/dist/forms/validate-form.js +6 -1
- package/dist/forms/validate-form.js.map +1 -1
- package/dist/forms/zod-resolver.cjs +1 -1
- package/dist/forms/zod-resolver.cjs.map +1 -1
- package/dist/forms/zod-resolver.js +8 -3
- package/dist/forms/zod-resolver.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 +90 -72
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/credential-scope.cjs +1 -1
- package/dist/http/credential-scope.cjs.map +1 -1
- package/dist/http/credential-scope.js +33 -9
- package/dist/http/credential-scope.js.map +1 -1
- package/dist/http/presigned-post.cjs.map +1 -1
- package/dist/http/presigned-post.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 +90 -84
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js +8 -8
- package/dist/http/upload-with-progress.js.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js +5 -4
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- 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 +87 -69
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +54 -44
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/sse/create-event-stream.cjs +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js +48 -41
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/sse/ticket-event-stream.cjs +1 -1
- package/dist/sse/ticket-event-stream.cjs.map +1 -1
- package/dist/sse/ticket-event-stream.js +48 -19
- package/dist/sse/ticket-event-stream.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 -14
- package/dist/sse/use-event-stream.js.map +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +10 -8
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +36 -30
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/create-push-handler.cjs +1 -1
- package/dist/sw/create-push-handler.cjs.map +1 -1
- package/dist/sw/create-push-handler.js +20 -10
- package/dist/sw/create-push-handler.js.map +1 -1
- package/dist/sw.d.ts +44 -13
- package/dist/tabular/compact.cjs +1 -1
- package/dist/tabular/compact.cjs.map +1 -1
- package/dist/tabular/compact.js +35 -16
- package/dist/tabular/compact.js.map +1 -1
- package/dist/tabular/manifest.cjs +1 -1
- package/dist/tabular/manifest.cjs.map +1 -1
- package/dist/tabular/manifest.js +33 -15
- package/dist/tabular/manifest.js.map +1 -1
- package/dist/tabular.d.ts +11 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +457 -68
- package/dist/tempest-react-sdk.js +224 -224
- package/dist/theme/initial-theme.cjs +4 -4
- package/dist/theme/initial-theme.cjs.map +1 -1
- package/dist/theme/initial-theme.js +8 -5
- package/dist/theme/initial-theme.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 +13 -7
- package/dist/utils/compressed-storage.js.map +1 -1
- package/dist/utils/objects.cjs +1 -1
- package/dist/utils/objects.cjs.map +1 -1
- package/dist/utils/objects.js +13 -7
- package/dist/utils/objects.js.map +1 -1
- package/dist/utils/safe-url.cjs +1 -1
- package/dist/utils/safe-url.cjs.map +1 -1
- package/dist/utils/safe-url.js +4 -1
- package/dist/utils/safe-url.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +7 -1
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +77 -61
- package/dist/ws/create-web-socket.js.map +1 -1
- 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 +4 -3
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +42 -0
- package/template-pwa/src/sw.ts +5 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-persistence.cjs","names":["createOfflineStore","isDevBuild","dehydrate","require_dev_mode","require_create_offline_store","_tanstack_react_query","pendingClears","persistQueryClientOffline","options","key","databaseName","store","slot","ended","warnedEnded","timer","inFlight","afterPendingClear","pending","warnEnded","save","write","queryClient","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","snapshot","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called.\n */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. Idempotent. */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000 } = options;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let warnedEnded = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n async function save(): Promise<void> {\n if (ended) return;\n await afterPendingClear();\n if (ended) return;\n const write = store.put({ id: key, state: dehydrate(queryClient) });\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) queryClient.clear();\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (snapshot && !ended) hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n cancelTimer();\n unsubscribeCache();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":"A,iIAkBA,IAAMM,EAAgB,IAAI,IAgH1B,SAAgBC,EACZ,EACuB,CACvB,GAAM,CAAE,cAAa,aAAa,KAASC,EACrCC,EAAMD,EAAQ,KAAO,iBACrBE,EAAeF,EAAQ,cAAgB,oBACvCG,EAAQX,EAAAA,mBAAqC,CAC/C,eACA,QAAS,EACT,UAAW,QACX,QAAS,MACT,QAAS,IACb,CAAC,EAEKY,EAAO,GAAGF,EAAa,QAAQD,IACjCI,EAAQ,GACRC,EAAc,GACdC,EAA8C,KAC5CC,EAAW,IAAI,IAErB,eAAeC,GAAmC,CAC9C,IAAMC,EAAUZ,EAAc,IAAIM,CAAI,EAClCM,GAAS,MAAMA,CACvB,CAEA,SAASC,GAAkB,CACnB,IAAgBlB,EAAAA,WAAW,IAC/B,EAAc,GACd,QAAQ,KACJ,0HAEJ,EACJ,CAEA,eAAemB,GAAsB,CAGjC,GAFIP,IACJ,MAAMI,EAAkB,EACpBJ,GAAO,OACX,IAAMQ,EAAQV,EAAM,IAAI,CAAE,GAAIF,EAAK,OAAA,EAAOP,EAAAA,UAAAA,CAAUoB,CAAW,CAAE,CAAC,EAClEN,EAAS,IAAIK,CAAK,EAClB,GAAI,CACA,MAAMA,CACV,QAAU,CACNL,EAAS,OAAOK,CAAK,CACzB,CACJ,CAEA,SAASE,GAAoB,CACzB,AAEIR,KADA,aAAaA,CAAK,EACV,KAEhB,CAEA,SAASS,GAAiB,CAClBT,GAASF,IACb,EAAQ,eAAiB,CACrB,EAAQ,KACRO,EAAU,CACd,EAAGK,CAAU,EACjB,CAEA,SAASC,EAAa,EAAoC,CACjDb,GAIDc,EAAM,OAAS,SAAWA,EAAM,OAAS,aAC7CC,EAAiB,EACjBT,EAAU,GALNK,EAAS,CAMjB,CAEA,IAAMI,EAAmBN,EAAY,cAAc,CAAC,CAAC,UAAUI,CAAY,EAE3E,eAAeG,EAAW,EAAqC,CAC3D,GAAI,CACA,MAAM,QAAQ,WAAW,CAAC,GAAGb,CAAQ,CAAC,EACtC,MAAML,EAAM,OAAOF,CAAG,CAC1B,QAAU,CACFqB,GAAaR,EAAY,MAAM,CACvC,CACJ,CAEA,MAAO,CACH,MAAM,SAAU,CACZ,GAAIT,EAAO,CACPM,EAAU,EACV,MACJ,CAEA,GADA,MAAMF,EAAkB,EACpBJ,EAAO,OACX,IAAMkB,EAAW,MAAMpB,EAAM,IAAIF,CAAG,EAChCsB,GAAY,CAAClB,IAAO,EAAAR,EAAA,QAAA,CAAQiB,EAAaS,EAAS,KAAK,CAC/D,EACA,MAAM,OAAQ,CACNlB,EACAM,EAAU,GAGdI,EAAY,EACZ,MAAMH,EAAK,EACf,EACA,aAAc,CACVG,EAAY,EACZK,EAAiB,CACrB,EACA,MAAM,MAAM,EAAe,CAAC,EAAG,CAC3B,EAAQ,GACRL,EAAY,EACZ,IAAMS,EAAW1B,EAAc,IAAIM,CAAI,EACjCqB,EAAMJ,EAAWK,EAAa,aAAe,EAAK,EAClDC,EAAU,QAAQ,WAAW,CAACH,EAAUC,CAAG,CAAC,EAClD3B,EAAc,IAAIM,EAAMuB,CAAO,EAC/BA,EAAa,SAAW,CAChB7B,EAAc,IAAIM,CAAI,IAAMuB,GAAS7B,EAAc,OAAOM,CAAI,CACtE,CAAC,EACD,MAAMqB,CACV,CACJ,CACJ,C"}
|
|
1
|
+
{"version":3,"file":"offline-persistence.cjs","names":["ownerFingerprint","createOfflineStore","isDevBuild","dehydrate","require_dev_mode","require_owner_fingerprint","require_create_offline_store","_tanstack_react_query","pendingClears","currentOwner","getPrincipal","persistQueryClientOffline","options","key","databaseName","store","slot","ended","warnedEnded","timer","inFlight","afterPendingClear","pending","warnEnded","save","snapshot","queryClient","write","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\n /**\n * Who is signed in right now — a stable, non-secret id such as the user id;\n * `null`/`undefined` when nobody is. Read on every write, which stamps the\n * snapshot with a one-way fingerprint of it (never the id itself), and on\n * `restore()`, which hydrates only a snapshot stamped with the same\n * fingerprint.\n *\n * `clear()` on logout already deletes the snapshot; this covers the session\n * that ends without it — a token that expired, a crash, a tab closed while\n * signed in. Without it, the next account to boot on the browser is shown\n * the previous account's cached responses (#531). A snapshot written before\n * this option was set carries no stamp and is not restored either; it is a\n * cache, so the cost is one cold start.\n *\n * A foreign snapshot is skipped, not deleted: a cookie app that only learns\n * the principal after a `/me` round trip would otherwise wipe its own cache\n * on every cold start. The next write replaces it.\n *\n * Read synchronously. A non-string, non-number value (the `Promise` of an\n * `async` getter) or a getter that throws reads as nobody. Omit it to restore\n * every snapshot, as before 0.82.0.\n */\n getPrincipal?: () => string | number | null | undefined;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called, and — with\n * `getPrincipal` — for a snapshot written under another principal.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called.\n */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. Idempotent. */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n /** Owner fingerprint when `getPrincipal` was set at write time; absent otherwise. */\n owner?: string | null;\n}\n\n/**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param getPrincipal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a throw.\n */\nfunction currentOwner(getPrincipal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", getPrincipal());\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000, getPrincipal } = options;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let warnedEnded = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n async function save(): Promise<void> {\n if (ended) return;\n await afterPendingClear();\n if (ended) return;\n const snapshot: Snapshot = { id: key, state: dehydrate(queryClient) };\n if (getPrincipal) snapshot.owner = currentOwner(getPrincipal);\n const write = store.put(snapshot);\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) queryClient.clear();\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (!snapshot || ended) return;\n if (getPrincipal && snapshot.owner !== currentOwner(getPrincipal)) return;\n hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n cancelTimer();\n unsubscribeCache();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":"A,6KAmBA,IAAMQ,EAAgB,IAAI,IAsH1B,SAASC,EAAa,EAA4C,CAC9D,GAAI,CACA,OAAOT,EAAAA,iBAAiB,IAAKU,EAAa,CAAC,CAC/C,MAAQ,CACJ,OAAO,IACX,CACJ,CA4BA,SAAgBC,EACZ,EACuB,CACvB,GAAM,CAAE,cAAa,aAAa,IAAM,gBAAiBC,EACnDC,EAAMD,EAAQ,KAAO,iBACrBE,EAAeF,EAAQ,cAAgB,oBACvCG,EAAQd,EAAAA,mBAAqC,CAC/C,eACA,QAAS,EACT,UAAW,QACX,QAAS,MACT,QAAS,IACb,CAAC,EAEKe,EAAO,GAAGF,EAAa,QAAQD,IACjCI,EAAQ,GACRC,EAAc,GACdC,EAA8C,KAC5CC,EAAW,IAAI,IAErB,eAAeC,GAAmC,CAC9C,IAAMC,EAAUd,EAAc,IAAIQ,CAAI,EAClCM,GAAS,MAAMA,CACvB,CAEA,SAASC,GAAkB,CACnB,IAAgBrB,EAAAA,WAAW,IAC/B,EAAc,GACd,QAAQ,KACJ,0HAEJ,EACJ,CAEA,eAAesB,GAAsB,CAGjC,GAFIP,IACJ,MAAMI,EAAkB,EACpBJ,GAAO,OACX,IAAMQ,EAAqB,CAAE,GAAIZ,EAAK,OAAA,EAAOV,EAAAA,UAAAA,CAAUuB,CAAW,CAAE,EAChEhB,IAAc,EAAS,MAAQD,EAAaC,CAAY,GAC5D,IAAMiB,EAAQZ,EAAM,IAAIU,CAAQ,EAChCL,EAAS,IAAIO,CAAK,EAClB,GAAI,CACA,MAAMA,CACV,QAAU,CACNP,EAAS,OAAOO,CAAK,CACzB,CACJ,CAEA,SAASC,GAAoB,CACzB,AAEIT,KADA,aAAaA,CAAK,EACV,KAEhB,CAEA,SAASU,GAAiB,CAClBV,GAASF,IACb,EAAQ,eAAiB,CACrB,EAAQ,KACRO,EAAU,CACd,EAAGM,CAAU,EACjB,CAEA,SAASC,EAAa,EAAoC,CACjDd,GAIDe,EAAM,OAAS,SAAWA,EAAM,OAAS,aAC7CC,EAAiB,EACjBV,EAAU,GALNM,EAAS,CAMjB,CAEA,IAAMI,EAAmBP,EAAY,cAAc,CAAC,CAAC,UAAUK,CAAY,EAE3E,eAAeG,EAAW,EAAqC,CAC3D,GAAI,CACA,MAAM,QAAQ,WAAW,CAAC,GAAGd,CAAQ,CAAC,EACtC,MAAML,EAAM,OAAOF,CAAG,CAC1B,QAAU,CACFsB,GAAaT,EAAY,MAAM,CACvC,CACJ,CAEA,MAAO,CACH,MAAM,SAAU,CACZ,GAAIT,EAAO,CACPM,EAAU,EACV,MACJ,CAEA,GADA,MAAMF,EAAkB,EACpBJ,EAAO,OACX,IAAMQ,EAAW,MAAMV,EAAM,IAAIF,CAAG,EAC/BY,GAAY,KACbf,GAAgBe,EAAS,QAAUhB,EAAaC,CAAY,IAChE,EAAAH,EAAA,QAAA,CAAQmB,EAAaD,EAAS,KAAK,EACvC,EACA,MAAM,OAAQ,CACNR,EACAM,EAAU,GAGdK,EAAY,EACZ,MAAMJ,EAAK,EACf,EACA,aAAc,CACVI,EAAY,EACZK,EAAiB,CACrB,EACA,MAAM,MAAM,EAAe,CAAC,EAAG,CAC3B,EAAQ,GACRL,EAAY,EACZ,IAAMQ,EAAW5B,EAAc,IAAIQ,CAAI,EACjCqB,EAAMH,EAAWI,EAAa,aAAe,EAAK,EAClDC,EAAU,QAAQ,WAAW,CAACH,EAAUC,CAAG,CAAC,EAClD7B,EAAc,IAAIQ,EAAMuB,CAAO,EAC/BA,EAAa,SAAW,CAChB/B,EAAc,IAAIQ,CAAI,IAAMuB,GAAS/B,EAAc,OAAOQ,CAAI,CACtE,CAAC,EACD,MAAMqB,CACV,CACJ,CACJ,C"}
|
|
@@ -1,81 +1,91 @@
|
|
|
1
1
|
import { isDevBuild as e } from "../utils/dev-mode.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { ownerFingerprint as t } from "../utils/owner-fingerprint.js";
|
|
3
|
+
import { createOfflineStore as n } from "../offline/create-offline-store.js";
|
|
4
|
+
import { dehydrate as r, hydrate as i } from "@tanstack/react-query";
|
|
4
5
|
//#region src/query/offline-persistence.ts
|
|
5
|
-
var
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
var a = /* @__PURE__ */ new Map();
|
|
7
|
+
function o(e) {
|
|
8
|
+
try {
|
|
9
|
+
return t("p", e());
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function s(t) {
|
|
15
|
+
let { queryClient: s, throttleMs: c = 1e3, getPrincipal: l } = t, u = t.key ?? "tanstack-query", d = t.databaseName ?? "TempestQueryCache", f = n({
|
|
16
|
+
databaseName: d,
|
|
9
17
|
version: 1,
|
|
10
18
|
tableName: "cache",
|
|
11
19
|
indexes: "&id",
|
|
12
20
|
keyPath: "id"
|
|
13
|
-
}),
|
|
14
|
-
async function
|
|
15
|
-
let e =
|
|
21
|
+
}), p = `${d}\u0000${u}`, m = !1, h = !1, g = null, _ = /* @__PURE__ */ new Set();
|
|
22
|
+
async function v() {
|
|
23
|
+
let e = a.get(p);
|
|
16
24
|
e && await e;
|
|
17
25
|
}
|
|
18
|
-
function
|
|
19
|
-
!
|
|
26
|
+
function y() {
|
|
27
|
+
!h && e() && (h = !0, console.warn("[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); create a new one after login to keep persisting."));
|
|
20
28
|
}
|
|
21
|
-
async function
|
|
22
|
-
if (
|
|
23
|
-
let e =
|
|
24
|
-
id:
|
|
25
|
-
state:
|
|
26
|
-
}
|
|
27
|
-
|
|
29
|
+
async function b() {
|
|
30
|
+
if (m || (await v(), m)) return;
|
|
31
|
+
let e = {
|
|
32
|
+
id: u,
|
|
33
|
+
state: r(s)
|
|
34
|
+
};
|
|
35
|
+
l && (e.owner = o(l));
|
|
36
|
+
let t = f.put(e);
|
|
37
|
+
_.add(t);
|
|
28
38
|
try {
|
|
29
|
-
await
|
|
39
|
+
await t;
|
|
30
40
|
} finally {
|
|
31
|
-
|
|
41
|
+
_.delete(t);
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
|
-
function
|
|
35
|
-
|
|
44
|
+
function x() {
|
|
45
|
+
g &&= (clearTimeout(g), null);
|
|
36
46
|
}
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
47
|
+
function S() {
|
|
48
|
+
g || m || (g = setTimeout(() => {
|
|
49
|
+
g = null, b();
|
|
50
|
+
}, c));
|
|
41
51
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
52
|
+
function C(e) {
|
|
53
|
+
m ? (e.type === "added" || e.type === "updated") && (w(), y()) : S();
|
|
44
54
|
}
|
|
45
|
-
let
|
|
46
|
-
async function
|
|
55
|
+
let w = s.getQueryCache().subscribe(C);
|
|
56
|
+
async function T(e) {
|
|
47
57
|
try {
|
|
48
|
-
await Promise.allSettled([...
|
|
58
|
+
await Promise.allSettled([..._]), await f.delete(u);
|
|
49
59
|
} finally {
|
|
50
|
-
e &&
|
|
60
|
+
e && s.clear();
|
|
51
61
|
}
|
|
52
62
|
}
|
|
53
63
|
return {
|
|
54
64
|
async restore() {
|
|
55
|
-
if (
|
|
56
|
-
|
|
65
|
+
if (m) {
|
|
66
|
+
y();
|
|
57
67
|
return;
|
|
58
68
|
}
|
|
59
|
-
if (await
|
|
60
|
-
let e = await
|
|
61
|
-
e && !
|
|
69
|
+
if (await v(), m) return;
|
|
70
|
+
let e = await f.get(u);
|
|
71
|
+
e && !m && (l && e.owner !== o(l) || i(s, e.state));
|
|
62
72
|
},
|
|
63
73
|
async flush() {
|
|
64
|
-
|
|
74
|
+
m ? y() : (x(), await b());
|
|
65
75
|
},
|
|
66
76
|
unsubscribe() {
|
|
67
|
-
|
|
77
|
+
x(), w();
|
|
68
78
|
},
|
|
69
79
|
async clear(e = {}) {
|
|
70
|
-
|
|
71
|
-
let t =
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
m = !0, x();
|
|
81
|
+
let t = a.get(p), n = T(e.resetClient ?? !1), r = Promise.allSettled([t, n]);
|
|
82
|
+
a.set(p, r), r.then(() => {
|
|
83
|
+
a.get(p) === r && a.delete(p);
|
|
74
84
|
}), await n;
|
|
75
85
|
}
|
|
76
86
|
};
|
|
77
87
|
}
|
|
78
88
|
//#endregion
|
|
79
|
-
export {
|
|
89
|
+
export { s as persistQueryClientOffline };
|
|
80
90
|
|
|
81
91
|
//# sourceMappingURL=offline-persistence.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-persistence.js","names":["pendingClears","persistQueryClientOffline","options","key","databaseName","store","createOfflineStore","slot","ended","warnedEnded","timer","inFlight","afterPendingClear","pending","warnEnded","isDevBuild","save","write","dehydrate","queryClient","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","snapshot","hydrate","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called.\n */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. Idempotent. */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000 } = options;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let warnedEnded = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n async function save(): Promise<void> {\n if (ended) return;\n await afterPendingClear();\n if (ended) return;\n const write = store.put({ id: key, state: dehydrate(queryClient) });\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) queryClient.clear();\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (snapshot && !ended) hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n cancelTimer();\n unsubscribeCache();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":";;;;AAkBA,IAAMA,oBAAgB,IAAI,IAA8B;AAgHxD,SAAgBC,EACZ,GACuB;CACvB,IAAM,EAAE,gBAAa,gBAAa,QAASC,GACrCC,IAAMD,EAAQ,OAAO,kBACrBE,IAAeF,EAAQ,gBAAgB,qBACvCG,IAAQC,EAAqC;EAC/C;EACA,SAAS;EACT,WAAW;EACX,SAAS;EACT,SAAS;CACb,CAAC,GAEKC,IAAO,GAAGH,EAAa,QAAQD,KACjCK,IAAQ,IACRC,IAAc,IACdC,IAA8C,MAC5CC,oBAAW,IAAI,IAAsB;CAE3C,eAAeC,IAAmC;EAC9C,IAAMC,IAAUb,EAAc,IAAIO,CAAI;EACtC,AAAIM,KAAS,MAAMA;CACvB;CAEA,SAASC,IAAkB;EACvB,AAAI,MAAgBC,EAAW,MAC/B,IAAc,IACd,QAAQ,KACJ,0HAEJ;CACJ;CAEA,eAAeC,IAAsB;EAGjC,IAFIR,MACJ,MAAMI,EAAkB,GACpBJ,IAAO;EACX,IAAMS,IAAQZ,EAAM,IAAI;GAAE,IAAIF;GAAK,OAAOe,EAAUC,CAAW;EAAE,CAAC;EAClE,EAAS,IAAIF,CAAK;EAClB,IAAI;GACA,MAAMA;EACV,UAAU;GACN,EAAS,OAAOA,CAAK;EACzB;CACJ;CAEA,SAASG,IAAoB;EACzB,AAEIV,OADA,aAAaA,CAAK,GACV;CAEhB;CAEA,SAASW,IAAiB;EACtB,AAAIX,KAASF,MACb,IAAQ,iBAAiB;GAErB,AADA,IAAQ,MACRQ,EAAU;EACd,GAAGM,CAAU;CACjB;CAEA,SAASC,EAAa,GAAoC;EACtD,AAAKf,KAIDgB,EAAM,SAAS,WAAWA,EAAM,SAAS,eAC7CC,EAAiB,GACjBX,EAAU,KALNO,EAAS;CAMjB;CAEA,IAAMI,IAAmBN,EAAY,cAAc,CAAC,CAAC,UAAUI,CAAY;CAE3E,eAAeG,EAAW,GAAqC;EAC3D,IAAI;GAEA,AADA,MAAM,QAAQ,WAAW,CAAC,GAAGf,CAAQ,CAAC,GACtC,MAAMN,EAAM,OAAOF,CAAG;EAC1B,UAAU;GACN,AAAIwB,KAAaR,EAAY,MAAM;EACvC;CACJ;CAEA,OAAO;EACH,MAAM,UAAU;GACZ,IAAIX,GAAO;IACP,EAAU;IACV;GACJ;GAEA,IADA,MAAMI,EAAkB,GACpBJ,GAAO;GACX,IAAMoB,IAAW,MAAMvB,EAAM,IAAIF,CAAG;GACpC,AAAIyB,KAAY,CAACpB,KAAOqB,EAAQV,GAAaS,EAAS,KAAK;EAC/D;EACA,MAAM,QAAQ;GACV,AAAIpB,IACAM,EAAU,KAGdM,EAAY,GACZ,MAAMJ,EAAK;EACf;EACA,cAAc;GAEV,AADAI,EAAY,GACZK,EAAiB;EACrB;EACA,MAAM,MAAM,IAAe,CAAC,GAAG;GAE3B,AADA,IAAQ,IACRL,EAAY;GACZ,IAAMU,IAAW9B,EAAc,IAAIO,CAAI,GACjCwB,IAAML,EAAWM,EAAa,eAAe,EAAK,GAClDC,IAAU,QAAQ,WAAW,CAACH,GAAUC,CAAG,CAAC;GAKlD,AAJA/B,EAAc,IAAIO,GAAM0B,CAAO,GAC/BA,EAAa,WAAW;IACpB,AAAIjC,EAAc,IAAIO,CAAI,MAAM0B,KAASjC,EAAc,OAAOO,CAAI;GACtE,CAAC,GACD,MAAMwB;EACV;CACJ;AACJ"}
|
|
1
|
+
{"version":3,"file":"offline-persistence.js","names":["pendingClears","currentOwner","ownerFingerprint","getPrincipal","persistQueryClientOffline","options","key","databaseName","store","createOfflineStore","slot","ended","warnedEnded","timer","inFlight","afterPendingClear","pending","warnEnded","isDevBuild","save","snapshot","dehydrate","queryClient","write","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","hydrate","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\n /**\n * Who is signed in right now — a stable, non-secret id such as the user id;\n * `null`/`undefined` when nobody is. Read on every write, which stamps the\n * snapshot with a one-way fingerprint of it (never the id itself), and on\n * `restore()`, which hydrates only a snapshot stamped with the same\n * fingerprint.\n *\n * `clear()` on logout already deletes the snapshot; this covers the session\n * that ends without it — a token that expired, a crash, a tab closed while\n * signed in. Without it, the next account to boot on the browser is shown\n * the previous account's cached responses (#531). A snapshot written before\n * this option was set carries no stamp and is not restored either; it is a\n * cache, so the cost is one cold start.\n *\n * A foreign snapshot is skipped, not deleted: a cookie app that only learns\n * the principal after a `/me` round trip would otherwise wipe its own cache\n * on every cold start. The next write replaces it.\n *\n * Read synchronously. A non-string, non-number value (the `Promise` of an\n * `async` getter) or a getter that throws reads as nobody. Omit it to restore\n * every snapshot, as before 0.82.0.\n */\n getPrincipal?: () => string | number | null | undefined;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called, and — with\n * `getPrincipal` — for a snapshot written under another principal.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called.\n */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. Idempotent. */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n /** Owner fingerprint when `getPrincipal` was set at write time; absent otherwise. */\n owner?: string | null;\n}\n\n/**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param getPrincipal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a throw.\n */\nfunction currentOwner(getPrincipal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", getPrincipal());\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000, getPrincipal } = options;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let warnedEnded = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n async function save(): Promise<void> {\n if (ended) return;\n await afterPendingClear();\n if (ended) return;\n const snapshot: Snapshot = { id: key, state: dehydrate(queryClient) };\n if (getPrincipal) snapshot.owner = currentOwner(getPrincipal);\n const write = store.put(snapshot);\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) queryClient.clear();\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (!snapshot || ended) return;\n if (getPrincipal && snapshot.owner !== currentOwner(getPrincipal)) return;\n hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n cancelTimer();\n unsubscribeCache();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":";;;;;AAmBA,IAAMA,oBAAgB,IAAI,IAA8B;AAsHxD,SAASC,EAAa,GAA4C;CAC9D,IAAI;EACA,OAAOC,EAAiB,KAAKC,EAAa,CAAC;CAC/C,QAAQ;EACJ,OAAO;CACX;AACJ;AA4BA,SAAgBC,EACZ,GACuB;CACvB,IAAM,EAAE,gBAAa,gBAAa,KAAM,oBAAiBC,GACnDC,IAAMD,EAAQ,OAAO,kBACrBE,IAAeF,EAAQ,gBAAgB,qBACvCG,IAAQC,EAAqC;EAC/C;EACA,SAAS;EACT,WAAW;EACX,SAAS;EACT,SAAS;CACb,CAAC,GAEKC,IAAO,GAAGH,EAAa,QAAQD,KACjCK,IAAQ,IACRC,IAAc,IACdC,IAA8C,MAC5CC,oBAAW,IAAI,IAAsB;CAE3C,eAAeC,IAAmC;EAC9C,IAAMC,IAAUhB,EAAc,IAAIU,CAAI;EACtC,AAAIM,KAAS,MAAMA;CACvB;CAEA,SAASC,IAAkB;EACvB,AAAI,MAAgBC,EAAW,MAC/B,IAAc,IACd,QAAQ,KACJ,0HAEJ;CACJ;CAEA,eAAeC,IAAsB;EAGjC,IAFIR,MACJ,MAAMI,EAAkB,GACpBJ,IAAO;EACX,IAAMS,IAAqB;GAAE,IAAId;GAAK,OAAOe,EAAUC,CAAW;EAAE;EACpE,AAAInB,MAAc,EAAS,QAAQF,EAAaE,CAAY;EAC5D,IAAMoB,IAAQf,EAAM,IAAIY,CAAQ;EAChC,EAAS,IAAIG,CAAK;EAClB,IAAI;GACA,MAAMA;EACV,UAAU;GACN,EAAS,OAAOA,CAAK;EACzB;CACJ;CAEA,SAASC,IAAoB;EACzB,AAEIX,OADA,aAAaA,CAAK,GACV;CAEhB;CAEA,SAASY,IAAiB;EACtB,AAAIZ,KAASF,MACb,IAAQ,iBAAiB;GAErB,AADA,IAAQ,MACRQ,EAAU;EACd,GAAGO,CAAU;CACjB;CAEA,SAASC,EAAa,GAAoC;EACtD,AAAKhB,KAIDiB,EAAM,SAAS,WAAWA,EAAM,SAAS,eAC7CC,EAAiB,GACjBZ,EAAU,KALNQ,EAAS;CAMjB;CAEA,IAAMI,IAAmBP,EAAY,cAAc,CAAC,CAAC,UAAUK,CAAY;CAE3E,eAAeG,EAAW,GAAqC;EAC3D,IAAI;GAEA,AADA,MAAM,QAAQ,WAAW,CAAC,GAAGhB,CAAQ,CAAC,GACtC,MAAMN,EAAM,OAAOF,CAAG;EAC1B,UAAU;GACN,AAAIyB,KAAaT,EAAY,MAAM;EACvC;CACJ;CAEA,OAAO;EACH,MAAM,UAAU;GACZ,IAAIX,GAAO;IACP,EAAU;IACV;GACJ;GAEA,IADA,MAAMI,EAAkB,GACpBJ,GAAO;GACX,IAAMS,IAAW,MAAMZ,EAAM,IAAIF,CAAG;GACpC,AAAKc,KAAY,OACbjB,KAAgBiB,EAAS,UAAUnB,EAAaE,CAAY,KAChE6B,EAAQV,GAAaF,EAAS,KAAK;EACvC;EACA,MAAM,QAAQ;GACV,AAAIT,IACAM,EAAU,KAGdO,EAAY,GACZ,MAAML,EAAK;EACf;EACA,cAAc;GAEV,AADAK,EAAY,GACZK,EAAiB;EACrB;EACA,MAAM,MAAM,IAAe,CAAC,GAAG;GAE3B,AADA,IAAQ,IACRL,EAAY;GACZ,IAAMS,IAAWjC,EAAc,IAAIU,CAAI,GACjCwB,IAAMJ,EAAWK,EAAa,eAAe,EAAK,GAClDC,IAAU,QAAQ,WAAW,CAACH,GAAUC,CAAG,CAAC;GAKlD,AAJAlC,EAAc,IAAIU,GAAM0B,CAAO,GAC/BA,EAAa,WAAW;IACpB,AAAIpC,EAAc,IAAIU,CAAI,MAAM0B,KAASpC,EAAc,OAAOU,CAAI;GACtE,CAAC,GACD,MAAMwB;EACV;CACJ;AACJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../utils/json-frame.cjs");function t(t,n={}){let{withCredentials:r=!1,namedEvents:i=[],heartbeatEvents:a=[`ping`],maxRetries:o=10,initialBackoff:s=1e3,maxBackoff:c=3e4,parser:l,onOpen:u,onMessage:d,onError:f,onParseError:p,schema:m,onValidationError:h,onUrlError:g,shouldRetryUrl:_=()=>!0,onStatusChange:
|
|
1
|
+
const e=require("../utils/json-frame.cjs");function t(t,n={}){let{withCredentials:r=!1,namedEvents:i=[],heartbeatEvents:a=[`ping`],maxRetries:o=10,initialBackoff:s=1e3,maxBackoff:c=3e4,parser:l,onOpen:u,onMessage:d,onError:f,onParseError:p,schema:m,onValidationError:h,onUrlError:g,shouldRetryUrl:_=()=>!0,sessionKey:v,onStatusChange:y}=n,b=null,x=null,S=0,C=`idle`,w=!1,T=0,E;function D(e){C!==e&&(C=e,y?.(e))}function O(){return v!==void 0&&!Object.is(v(),E)}function k(t,n,r){if(t!==b||a.includes(n))return;if(O()){F();return}let i=e.decodeFrame(typeof r.data==`string`?r.data:``,`createEventStream`,{parser:l,onParseError:p,schema:m,onValidationError:h});i.delivered&&d?.({event:n,data:i.data,id:r.lastEventId||void 0,raw:r})}function A(){if(w)return;if(S>=o){D(`error`);return}let e=Math.min(s*2**S,c);S+=1,x=setTimeout(M,e)}function j(e){g?.(e),_(e)?(D(`closed`),A()):D(`error`)}function M(){if(w)return;if(b&&=(b.close(),null),D(`connecting`),T+=1,E=v?.(),typeof t==`string`){N(t);return}let e=T;Promise.resolve().then(()=>w||e!==T?null:t()).then(t=>{t===null||w||e!==T||(O()?M():N(t))},t=>{!w&&e===T&&j(t)})}function N(e){let t=new EventSource(e,{withCredentials:r});b=t,t.onopen=()=>{S=0,D(`open`),u?.()},t.onmessage=e=>k(t,`message`,e);for(let e of i)t.addEventListener(e,n=>k(t,e,n));for(let e of a)t.addEventListener(e,()=>{});t.onerror=e=>{f?.(e),t.close(),b=null,D(`closed`),A()}}function P(){w=!0,x&&=(clearTimeout(x),null),S=0,b&&=(b.close(),null),D(`closed`)}function F(){x&&=(clearTimeout(x),null),S=0,w=!1,M()}return M(),{close:P,reconnect:F,get status(){return C}}}exports.createEventStream=t;
|
|
2
2
|
//# sourceMappingURL=create-event-stream.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-event-stream.cjs","names":["decodeFrame","require_json_frame","createEventStream","options","source","retryTimer","retries","status","closed","attempt","setStatus","next","onStatusChange","emit","heartbeatEvents","eventName","decoded","event","onMessage","scheduleReconnect","maxRetries","delay","initialBackoff","maxBackoff","connect","failUrl","onUrlError","error","shouldRetryUrl","url","open","current","resolved","es","target","onOpen","name","namedEvents","onError","close","reconnect"],"sources":["../../src/sse/create-event-stream.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body is the SSE line protocol — accumulate\n * `data:` lines until a blank line, track `id:` for Last-Event-ID, honour `retry:` —\n * plus the reconnect that uses the id it just tracked.\n */\nimport { decodeFrame } from \"../utils/json-frame\";\nimport type { SchemaIssue, SchemaLike } from \"../utils/schema-like\";\nexport type EventStreamStatus = \"idle\" | \"connecting\" | \"open\" | \"closed\" | \"error\";\n\n/**\n * Where the stream connects: a fixed URL, or a function resolved again before\n * every connection, the first one and each reconnect.\n *\n * The function form exists for short-lived credentials carried in the URL,\n * such as the `?ticket=` of the `tempest-fastapi-sdk` SSE router. `EventSource`\n * cannot send headers, so the credential has to ride in the query, and one that\n * expires makes a fixed URL a trap: the reconnect after the expiry presents the\n * same dead credential, gets a `401`, and burns every retry on it while the user\n * is still signed in.\n */\nexport type EventStreamUrl = string | (() => string | Promise<string>);\n\nexport interface EventStreamMessage<T> {\n /** Server-named event (default `\"message\"`). */\n event: string;\n /** Parsed payload — validated when `schema` is set, JSON-decoded when possible, raw string otherwise. */\n data: T;\n /** Server-supplied id, if any. */\n id?: string;\n /** Raw `MessageEvent` for advanced cases. */\n raw: MessageEvent;\n}\n\nexport interface CreateEventStreamOptions<T> {\n /** Send cookies with the EventSource handshake. Default: false. */\n withCredentials?: boolean;\n /** Subscribe to named events in addition to `message`. */\n namedEvents?: readonly string[];\n /** Treat these named events as heartbeat-only (no callback). Default: `[\"ping\"]`. */\n heartbeatEvents?: readonly string[];\n /** Max reconnect attempts. Default: 10. Pass 0 to disable reconnect. */\n maxRetries?: number;\n /** Initial backoff in ms; doubles per attempt, capped at `maxBackoff`. Default: 1000. */\n initialBackoff?: number;\n /** Maximum backoff in ms. Default: 30000. */\n maxBackoff?: number;\n /** Parse `event.data`. Defaults to JSON with raw-string fallback. */\n parser?: (raw: string) => T;\n /**\n * A frame arrived that is not valid JSON, and no `parser` was supplied.\n *\n * Registering this drops the frame instead of delivering it: the previous\n * behaviour handed `onMessage` the raw `string` announced as your message\n * type, so the failure surfaced later, at the first property access, with\n * nothing left pointing at the parse. Leave it out and that behaviour is\n * kept, with a one-time warning in development builds.\n */\n onParseError?: (error: unknown, raw: string) => void;\n /**\n * Schema every decoded frame must satisfy, from zod, valibot, arktype or\n * anything else exposing `~standard` or `.safeParse`.\n *\n * Without it nothing changes: the payload reaches `onMessage` announced as\n * `T` on the strength of the type argument alone, which is a promise about\n * the server that TypeScript cannot keep. With it, a frame that does not\n * match is **not** delivered — the same rule `onParseError` already follows —\n * and `onValidationError` hears why. The value delivered is the schema's\n * output, so coercions and defaults are honoured.\n *\n * When `parser` is also supplied, it decodes first and the schema validates\n * what it returned.\n *\n * The validation must be synchronous. A frame is decoded inside the\n * `message` handler and delivered from it, so an async schema would deliver\n * frames in whatever order their validations settled; that case is reported\n * through `onValidationError` instead of awaited.\n */\n schema?: SchemaLike<T>;\n /**\n * A frame was decoded but the `schema` refused it, so it was dropped.\n *\n * The one signal that does not depend on how the app's bundler resolves\n * `process`: the one-time development warning behind `onParseError` needs\n * `isDevBuild()` to be able to answer, and this callback is the app's own.\n */\n onValidationError?: (issues: SchemaIssue[], raw: string) => void;\n /**\n * The URL function threw or rejected, so no connection was attempted.\n *\n * `onError` cannot carry this: it receives the `EventSource` `Event`, and a\n * URL that never resolved never had an `EventSource`.\n */\n onUrlError?: (error: unknown) => void;\n /**\n * Whether a URL-resolution failure is worth another attempt. Default: always.\n *\n * Return `false` for failures no backoff can fix — a `401` or a `403` when\n * fetching a ticket is a session decision, not a network blip — and the stream\n * stops at `status: \"error\"` instead of spending `maxRetries` on it.\n */\n shouldRetryUrl?: (error: unknown) => boolean;\n onOpen?: () => void;\n onMessage?: (message: EventStreamMessage<T>) => void;\n onError?: (error: Event) => void;\n onStatusChange?: (status: EventStreamStatus) => void;\n}\n\nexport interface EventStreamController {\n close: () => void;\n /** Force an immediate reconnect, resetting the retry counter. */\n reconnect: () => void;\n /** Current connection status. */\n readonly status: EventStreamStatus;\n}\n\n/**\n * Open a Server-Sent Events stream with automatic exponential-backoff reconnect.\n *\n * Heartbeat events (default `\"ping\"`) keep the socket alive without firing\n * `onMessage`. Pass `withCredentials: true` when the backend authenticates via\n * cookies. Call `close()` from the returned controller to tear down.\n *\n * When `url` is a function it is called before every connection, so a\n * reconnect presents a fresh credential instead of the one that expired. A\n * resolution still pending when `close()` or `reconnect()` runs is discarded.\n *\n * @param url - Full SSE endpoint URL, or a function returning it per connection.\n * @param options - Stream configuration and callbacks.\n * @returns A controller exposing `close`, `reconnect` and the current `status`.\n */\nexport function createEventStream<T = unknown>(\n url: EventStreamUrl,\n options: CreateEventStreamOptions<T> = {},\n): EventStreamController {\n const {\n withCredentials = false,\n namedEvents = [],\n heartbeatEvents = [\"ping\"],\n maxRetries = 10,\n initialBackoff = 1000,\n maxBackoff = 30000,\n parser,\n onOpen,\n onMessage,\n onError,\n onParseError,\n schema,\n onValidationError,\n onUrlError,\n shouldRetryUrl = () => true,\n onStatusChange,\n } = options;\n\n let source: EventSource | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let retries = 0;\n let status: EventStreamStatus = \"idle\";\n let closed = false;\n let attempt = 0;\n\n function setStatus(next: EventStreamStatus): void {\n if (status === next) return;\n status = next;\n onStatusChange?.(next);\n }\n\n function emit(eventName: string, event: MessageEvent): void {\n if (heartbeatEvents.includes(eventName)) return;\n const decoded = decodeFrame<T>(\n typeof event.data === \"string\" ? event.data : \"\",\n \"createEventStream\",\n { parser, onParseError, schema, onValidationError },\n );\n if (!decoded.delivered) return;\n onMessage?.({\n event: eventName,\n data: decoded.data,\n id: event.lastEventId || undefined,\n raw: event,\n });\n }\n\n function scheduleReconnect(): void {\n if (closed) return;\n if (retries >= maxRetries) {\n setStatus(\"error\");\n return;\n }\n const delay = Math.min(initialBackoff * 2 ** retries, maxBackoff);\n retries += 1;\n retryTimer = setTimeout(connect, delay);\n }\n\n function failUrl(error: unknown): void {\n onUrlError?.(error);\n if (!shouldRetryUrl(error)) {\n setStatus(\"error\");\n return;\n }\n setStatus(\"closed\");\n scheduleReconnect();\n }\n\n function connect(): void {\n if (closed) return;\n if (source) {\n source.close();\n source = null;\n }\n setStatus(\"connecting\");\n attempt += 1;\n if (typeof url === \"string\") {\n open(url);\n return;\n }\n const current = attempt;\n Promise.resolve()\n .then(url)\n .then(\n (resolved) => {\n if (!closed && current === attempt) open(resolved);\n },\n (error: unknown) => {\n if (!closed && current === attempt) failUrl(error);\n },\n );\n }\n\n function open(target: string): void {\n const es = new EventSource(target, { withCredentials });\n source = es;\n\n es.onopen = () => {\n retries = 0;\n setStatus(\"open\");\n onOpen?.();\n };\n\n es.onmessage = (event) => emit(\"message\", event);\n for (const name of namedEvents) {\n es.addEventListener(name, (event) => emit(name, event as MessageEvent));\n }\n for (const name of heartbeatEvents) {\n es.addEventListener(name, () => {\n /* heartbeat — keep socket alive */\n });\n }\n\n es.onerror = (event) => {\n onError?.(event);\n es.close();\n source = null;\n setStatus(\"closed\");\n scheduleReconnect();\n };\n }\n\n function close(): void {\n closed = true;\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n if (source) {\n source.close();\n source = null;\n }\n setStatus(\"closed\");\n }\n\n function reconnect(): void {\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n closed = false;\n connect();\n }\n\n connect();\n\n return {\n close,\n reconnect,\n get status() {\n return status;\n },\n };\n}\n"],"mappings":"A,2CAkIA,SAAgBE,EACZ,EACA,EAAuC,CAAC,EACnB,CACrB,GAAM,CACF,kBAAkB,GAClB,cAAc,CAAC,EACf,kBAAkB,CAAC,MAAM,EACzB,aAAa,GACb,iBAAiB,IACjB,aAAa,IACb,SACA,SACA,YACA,UACA,eACA,SACA,oBACA,aACA,qBAAuB,GACvB,kBACAC,EAEAC,EAA6B,KAC7BC,EAAmD,KACnDC,EAAU,EACVC,EAA4B,OAC5BC,EAAS,GACTC,EAAU,EAEd,SAASC,EAAU,EAA+B,CAC1CH,IAAWI,IACf,EAASA,EACTC,IAAiBD,CAAI,EACzB,CAEA,SAASE,EAAK,EAAmB,EAA2B,CACxD,GAAIC,EAAgB,SAASC,CAAS,EAAG,OACzC,IAAMC,EAAUhB,EAAAA,YACZ,OAAOiB,EAAM,MAAS,SAAWA,EAAM,KAAO,GAC9C,oBACA,CAAE,SAAQ,eAAc,SAAQ,mBAAkB,CACtD,EACKD,EAAQ,WACbE,IAAY,CACR,MAAOH,EACP,KAAMC,EAAQ,KACd,GAAIC,EAAM,aAAe,IAAA,GACzB,IAAKA,CACT,CAAC,CACL,CAEA,SAASE,GAA0B,CAC/B,GAAIX,EAAQ,OACZ,GAAIF,GAAWc,EAAY,CACvBV,EAAU,OAAO,EACjB,MACJ,CACA,IAAMW,EAAQ,KAAK,IAAIC,EAAiB,GAAKhB,EAASiB,CAAU,EAChE,GAAW,EACX,EAAa,WAAWC,EAASH,CAAK,CAC1C,CAEA,SAASI,EAAQ,EAAsB,CACnCC,IAAaC,CAAK,EACbC,EAAeD,CAAK,GAIzBjB,EAAU,QAAQ,EAClBS,EAAkB,GAJdT,EAAU,OAAO,CAKzB,CAEA,SAASc,GAAgB,CACrB,GAAIhB,EAAQ,OAOZ,GANA,AAEIJ,KADAA,EAAO,MAAM,EACJ,MAEbM,EAAU,YAAY,EACtB,GAAW,EACP,OAAOmB,GAAQ,SAAU,CACzBC,EAAKD,CAAG,EACR,MACJ,CACA,IAAME,EAAUtB,EAChB,QAAQ,QAAQ,CAAC,CACZ,KAAKoB,CAAG,CAAC,CACT,KACI,GAAa,CACN,CAACrB,GAAUuB,IAAYtB,GAASqB,EAAKE,CAAQ,CACrD,EACC,GAAmB,CACZ,CAACxB,GAAUuB,IAAYtB,GAASgB,EAAQE,CAAK,CACrD,CACJ,CACR,CAEA,SAASG,EAAK,EAAsB,CAChC,IAAMG,EAAK,IAAI,YAAYC,EAAQ,CAAE,iBAAgB,CAAC,EACtD,EAASD,EAET,EAAG,WAAe,CACd,EAAU,EACVvB,EAAU,MAAM,EAChByB,IAAS,CACb,EAEA,EAAG,UAAa,GAAUtB,EAAK,UAAWI,CAAK,EAC/C,IAAK,IAAMmB,KAAQC,EACfJ,EAAG,iBAAiBG,EAAO,GAAUvB,EAAKuB,EAAMnB,CAAqB,CAAC,EAE1E,IAAK,IAAMmB,KAAQtB,EACfmB,EAAG,iBAAiBG,MAAY,CAEhC,CAAC,EAGL,EAAG,QAAW,GAAU,CACpBE,IAAUrB,CAAK,EACfgB,EAAG,MAAM,EACT,EAAS,KACTvB,EAAU,QAAQ,EAClBS,EAAkB,CACtB,CACJ,CAEA,SAASoB,GAAc,CACnB,EAAS,GACT,AAEIlC,KADA,aAAaA,CAAU,EACV,MAEjB,EAAU,EACV,AAEID,KADAA,EAAO,MAAM,EACJ,MAEbM,EAAU,QAAQ,CACtB,CAEA,SAAS8B,GAAkB,CACvB,AAEInC,KADA,aAAaA,CAAU,EACV,MAEjB,EAAU,EACV,EAAS,GACTmB,EAAQ,CACZ,CAIA,OAFAA,EAAQ,EAED,CACH,QACA,YACA,IAAI,QAAS,CACT,OAAOjB,CACX,CACJ,CACJ,C"}
|
|
1
|
+
{"version":3,"file":"create-event-stream.cjs","names":["decodeFrame","require_json_frame","createEventStream","options","source","retryTimer","retries","status","closed","attempt","boundKey","setStatus","next","onStatusChange","sessionChanged","sessionKey","emit","from","heartbeatEvents","eventName","reconnect","decoded","event","onMessage","scheduleReconnect","maxRetries","delay","initialBackoff","maxBackoff","connect","failUrl","onUrlError","error","shouldRetryUrl","url","open","current","resolved","es","target","onOpen","name","namedEvents","onError","close"],"sources":["../../src/sse/create-event-stream.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body is the SSE line protocol — accumulate\n * `data:` lines until a blank line, track `id:` for Last-Event-ID, honour `retry:` —\n * plus the reconnect that uses the id it just tracked.\n */\nimport { decodeFrame } from \"../utils/json-frame\";\nimport type { SchemaIssue, SchemaLike } from \"../utils/schema-like\";\nexport type EventStreamStatus = \"idle\" | \"connecting\" | \"open\" | \"closed\" | \"error\";\n\n/**\n * Where the stream connects: a fixed URL, or a function resolved again before\n * every connection, the first one and each reconnect.\n *\n * The function form exists for short-lived credentials carried in the URL,\n * such as the `?ticket=` of the `tempest-fastapi-sdk` SSE router. `EventSource`\n * cannot send headers, so the credential has to ride in the query, and one that\n * expires makes a fixed URL a trap: the reconnect after the expiry presents the\n * same dead credential, gets a `401`, and burns every retry on it while the user\n * is still signed in.\n */\nexport type EventStreamUrl = string | (() => string | Promise<string>);\n\n/**\n * Identifies the session a stream belongs to: the signed-in user's id, an\n * auth epoch, `null` when signed out.\n *\n * A primitive on purpose. The hook form lists it in its effect dependencies,\n * and an object built inline would be a new identity on every render, so the\n * stream would reopen on every render.\n */\nexport type SessionKey = string | number | boolean | null | undefined;\n\nexport interface EventStreamMessage<T> {\n /** Server-named event (default `\"message\"`). */\n event: string;\n /** Parsed payload — validated when `schema` is set, JSON-decoded when possible, raw string otherwise. */\n data: T;\n /** Server-supplied id, if any. */\n id?: string;\n /** Raw `MessageEvent` for advanced cases. */\n raw: MessageEvent;\n}\n\nexport interface CreateEventStreamOptions<T> {\n /** Send cookies with the EventSource handshake. Default: false. */\n withCredentials?: boolean;\n /** Subscribe to named events in addition to `message`. */\n namedEvents?: readonly string[];\n /** Treat these named events as heartbeat-only (no callback). Default: `[\"ping\"]`. */\n heartbeatEvents?: readonly string[];\n /** Max reconnect attempts. Default: 10. Pass 0 to disable reconnect. */\n maxRetries?: number;\n /** Initial backoff in ms; doubles per attempt, capped at `maxBackoff`. Default: 1000. */\n initialBackoff?: number;\n /** Maximum backoff in ms. Default: 30000. */\n maxBackoff?: number;\n /** Parse `event.data`. Defaults to JSON with raw-string fallback. */\n parser?: (raw: string) => T;\n /**\n * A frame arrived that is not valid JSON, and no `parser` was supplied.\n *\n * Registering this drops the frame instead of delivering it: the previous\n * behaviour handed `onMessage` the raw `string` announced as your message\n * type, so the failure surfaced later, at the first property access, with\n * nothing left pointing at the parse. Leave it out and that behaviour is\n * kept, with a one-time warning in development builds.\n */\n onParseError?: (error: unknown, raw: string) => void;\n /**\n * Schema every decoded frame must satisfy, from zod, valibot, arktype or\n * anything else exposing `~standard` or `.safeParse`.\n *\n * Without it nothing changes: the payload reaches `onMessage` announced as\n * `T` on the strength of the type argument alone, which is a promise about\n * the server that TypeScript cannot keep. With it, a frame that does not\n * match is **not** delivered — the same rule `onParseError` already follows —\n * and `onValidationError` hears why. The value delivered is the schema's\n * output, so coercions and defaults are honoured.\n *\n * When `parser` is also supplied, it decodes first and the schema validates\n * what it returned.\n *\n * The validation must be synchronous. A frame is decoded inside the\n * `message` handler and delivered from it, so an async schema would deliver\n * frames in whatever order their validations settled; that case is reported\n * through `onValidationError` instead of awaited.\n */\n schema?: SchemaLike<T>;\n /**\n * A frame was decoded but the `schema` refused it, so it was dropped.\n *\n * The one signal that does not depend on how the app's bundler resolves\n * `process`: the one-time development warning behind `onParseError` needs\n * `isDevBuild()` to be able to answer, and this callback is the app's own.\n */\n onValidationError?: (issues: SchemaIssue[], raw: string) => void;\n /**\n * The URL function threw or rejected, so no connection was attempted.\n *\n * `onError` cannot carry this: it receives the `EventSource` `Event`, and a\n * URL that never resolved never had an `EventSource`.\n */\n onUrlError?: (error: unknown) => void;\n /**\n * Read the current session key; the stream belongs to the session that was\n * current when it connected.\n *\n * The server cannot end the stream for you. The `tempest-fastapi-sdk` SSE\n * router checks the ticket only when the stream opens, and neither logout\n * nor refresh-token revocation touches an open stream: it lives until the\n * client leaves. So a tab that switches from user A to user B without\n * closing the stream keeps receiving A's events under B.\n *\n * With this set, the key is read when each connection starts, again when\n * its URL resolved, and before every frame is delivered. A frame that\n * arrives after the key changed is dropped, never delivered, and the stream\n * reconnects for the session now current — with a fresh ticket, for the\n * ticket stream. A URL resolved while the key changed is discarded the same\n * way, since the ticket inside it may belong to either session.\n *\n * The check runs when a frame arrives: a stream that stays quiet holds its\n * connection until the next frame or until `close()`. The hook form closes\n * it as soon as the key changes.\n *\n * Keys are compared with `Object.is`. Leave it out and nothing changes.\n */\n sessionKey?: () => SessionKey;\n /**\n * Whether a URL-resolution failure is worth another attempt. Default: always.\n *\n * Return `false` for failures no backoff can fix — a `401` or a `403` when\n * fetching a ticket is a session decision, not a network blip — and the stream\n * stops at `status: \"error\"` instead of spending `maxRetries` on it.\n */\n shouldRetryUrl?: (error: unknown) => boolean;\n onOpen?: () => void;\n onMessage?: (message: EventStreamMessage<T>) => void;\n onError?: (error: Event) => void;\n onStatusChange?: (status: EventStreamStatus) => void;\n}\n\nexport interface EventStreamController {\n close: () => void;\n /** Force an immediate reconnect, resetting the retry counter. */\n reconnect: () => void;\n /** Current connection status. */\n readonly status: EventStreamStatus;\n}\n\n/**\n * Open a Server-Sent Events stream with automatic exponential-backoff reconnect.\n *\n * Heartbeat events (default `\"ping\"`) keep the socket alive without firing\n * `onMessage`. Pass `withCredentials: true` when the backend authenticates via\n * cookies. Call `close()` from the returned controller to tear down.\n *\n * When `url` is a function it is called before every connection, so a\n * reconnect presents a fresh credential instead of the one that expired. A\n * resolution still pending when `close()` or `reconnect()` runs is discarded, and\n * one that has not started yet is never made: a stream closed in the same tick\n * it was created asks for no URL — no ticket request, for the ticket stream.\n *\n * Pass `sessionKey` to bind the stream to the session it opened under: once\n * the key changes, no frame of the previous session reaches `onMessage`, and\n * the stream reconnects for the new one.\n *\n * @param url - Full SSE endpoint URL, or a function returning it per connection.\n * @param options - Stream configuration and callbacks.\n * @returns A controller exposing `close`, `reconnect` and the current `status`.\n */\nexport function createEventStream<T = unknown>(\n url: EventStreamUrl,\n options: CreateEventStreamOptions<T> = {},\n): EventStreamController {\n const {\n withCredentials = false,\n namedEvents = [],\n heartbeatEvents = [\"ping\"],\n maxRetries = 10,\n initialBackoff = 1000,\n maxBackoff = 30000,\n parser,\n onOpen,\n onMessage,\n onError,\n onParseError,\n schema,\n onValidationError,\n onUrlError,\n shouldRetryUrl = () => true,\n sessionKey,\n onStatusChange,\n } = options;\n\n let source: EventSource | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let retries = 0;\n let status: EventStreamStatus = \"idle\";\n let closed = false;\n let attempt = 0;\n let boundKey: SessionKey;\n\n function setStatus(next: EventStreamStatus): void {\n if (status === next) return;\n status = next;\n onStatusChange?.(next);\n }\n\n function sessionChanged(): boolean {\n return sessionKey !== undefined && !Object.is(sessionKey(), boundKey);\n }\n\n /**\n * Deliver one frame, unless it no longer belongs to this stream.\n *\n * A frame from a source other than the current one — closed by `close()`,\n * replaced by a reconnect — is ignored: it would otherwise reach `onMessage`\n * after the caller let go of the stream, and the session check below would\n * reopen a controller that `close()` already ended. A frame that arrives\n * after `sessionKey` changed is dropped and the stream reconnects for the\n * session now current.\n *\n * @param from - The `EventSource` the frame came from.\n * @param eventName - The SSE event name.\n * @param event - The raw message event.\n */\n function emit(from: EventSource, eventName: string, event: MessageEvent): void {\n if (from !== source || heartbeatEvents.includes(eventName)) return;\n if (sessionChanged()) {\n reconnect();\n return;\n }\n const decoded = decodeFrame<T>(\n typeof event.data === \"string\" ? event.data : \"\",\n \"createEventStream\",\n { parser, onParseError, schema, onValidationError },\n );\n if (!decoded.delivered) return;\n onMessage?.({\n event: eventName,\n data: decoded.data,\n id: event.lastEventId || undefined,\n raw: event,\n });\n }\n\n function scheduleReconnect(): void {\n if (closed) return;\n if (retries >= maxRetries) {\n setStatus(\"error\");\n return;\n }\n const delay = Math.min(initialBackoff * 2 ** retries, maxBackoff);\n retries += 1;\n retryTimer = setTimeout(connect, delay);\n }\n\n function failUrl(error: unknown): void {\n onUrlError?.(error);\n if (!shouldRetryUrl(error)) {\n setStatus(\"error\");\n return;\n }\n setStatus(\"closed\");\n scheduleReconnect();\n }\n\n function connect(): void {\n if (closed) return;\n if (source) {\n source.close();\n source = null;\n }\n setStatus(\"connecting\");\n attempt += 1;\n boundKey = sessionKey?.();\n if (typeof url === \"string\") {\n open(url);\n return;\n }\n const current = attempt;\n Promise.resolve()\n .then(() => (closed || current !== attempt ? null : url()))\n .then(\n (resolved) => {\n if (resolved === null || closed || current !== attempt) return;\n if (sessionChanged()) {\n connect();\n return;\n }\n open(resolved);\n },\n (error: unknown) => {\n if (!closed && current === attempt) failUrl(error);\n },\n );\n }\n\n function open(target: string): void {\n const es = new EventSource(target, { withCredentials });\n source = es;\n\n es.onopen = () => {\n retries = 0;\n setStatus(\"open\");\n onOpen?.();\n };\n\n es.onmessage = (event) => emit(es, \"message\", event);\n for (const name of namedEvents) {\n es.addEventListener(name, (event) => emit(es, name, event as MessageEvent));\n }\n for (const name of heartbeatEvents) {\n es.addEventListener(name, () => {\n /* heartbeat — keep socket alive */\n });\n }\n\n es.onerror = (event) => {\n onError?.(event);\n es.close();\n source = null;\n setStatus(\"closed\");\n scheduleReconnect();\n };\n }\n\n function close(): void {\n closed = true;\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n if (source) {\n source.close();\n source = null;\n }\n setStatus(\"closed\");\n }\n\n function reconnect(): void {\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n closed = false;\n connect();\n }\n\n connect();\n\n return {\n close,\n reconnect,\n get status() {\n return status;\n },\n };\n}\n"],"mappings":"A,2CA0KA,SAAgBE,EACZ,EACA,EAAuC,CAAC,EACnB,CACrB,GAAM,CACF,kBAAkB,GAClB,cAAc,CAAC,EACf,kBAAkB,CAAC,MAAM,EACzB,aAAa,GACb,iBAAiB,IACjB,aAAa,IACb,SACA,SACA,YACA,UACA,eACA,SACA,oBACA,aACA,qBAAuB,GACvB,aACA,kBACAC,EAEAC,EAA6B,KAC7BC,EAAmD,KACnDC,EAAU,EACVC,EAA4B,OAC5BC,EAAS,GACTC,EAAU,EACVC,EAEJ,SAASC,EAAU,EAA+B,CAC1CJ,IAAWK,IACf,EAASA,EACTC,IAAiBD,CAAI,EACzB,CAEA,SAASE,GAA0B,CAC/B,OAAOC,IAAe,IAAA,IAAa,CAAC,OAAO,GAAGA,EAAW,EAAGL,CAAQ,CACxE,CAgBA,SAASM,EAAK,EAAmB,EAAmB,EAA2B,CAC3E,GAAIC,IAASb,GAAUc,EAAgB,SAASC,CAAS,EAAG,OAC5D,GAAIL,EAAe,EAAG,CAClBM,EAAU,EACV,MACJ,CACA,IAAMC,EAAUrB,EAAAA,YACZ,OAAOsB,EAAM,MAAS,SAAWA,EAAM,KAAO,GAC9C,oBACA,CAAE,SAAQ,eAAc,SAAQ,mBAAkB,CACtD,EACKD,EAAQ,WACbE,IAAY,CACR,MAAOJ,EACP,KAAME,EAAQ,KACd,GAAIC,EAAM,aAAe,IAAA,GACzB,IAAKA,CACT,CAAC,CACL,CAEA,SAASE,GAA0B,CAC/B,GAAIhB,EAAQ,OACZ,GAAIF,GAAWmB,EAAY,CACvBd,EAAU,OAAO,EACjB,MACJ,CACA,IAAMe,EAAQ,KAAK,IAAIC,EAAiB,GAAKrB,EAASsB,CAAU,EAChE,GAAW,EACX,EAAa,WAAWC,EAASH,CAAK,CAC1C,CAEA,SAASI,EAAQ,EAAsB,CACnCC,IAAaC,CAAK,EACbC,EAAeD,CAAK,GAIzBrB,EAAU,QAAQ,EAClBa,EAAkB,GAJdb,EAAU,OAAO,CAKzB,CAEA,SAASkB,GAAgB,CACrB,GAAIrB,EAAQ,OAQZ,GAPA,AAEIJ,KADAA,EAAO,MAAM,EACJ,MAEbO,EAAU,YAAY,EACtB,GAAW,EACX,EAAWI,IAAa,EACpB,OAAOmB,GAAQ,SAAU,CACzBC,EAAKD,CAAG,EACR,MACJ,CACA,IAAME,EAAU3B,EAChB,QAAQ,QAAQ,CAAC,CACZ,SAAYD,GAAU4B,IAAY3B,EAAU,KAAOyB,EAAI,CAAE,CAAC,CAC1D,KACI,GAAa,CACNG,IAAa,MAAQ7B,GAAU4B,IAAY3B,IAC3CK,EAAe,EACfe,EAAQ,EAGZM,EAAKE,CAAQ,EACjB,EACC,GAAmB,CACZ,CAAC7B,GAAU4B,IAAY3B,GAASqB,EAAQE,CAAK,CACrD,CACJ,CACR,CAEA,SAASG,EAAK,EAAsB,CAChC,IAAMG,EAAK,IAAI,YAAYC,EAAQ,CAAE,iBAAgB,CAAC,EACtD,EAASD,EAET,EAAG,WAAe,CACd,EAAU,EACV3B,EAAU,MAAM,EAChB6B,IAAS,CACb,EAEA,EAAG,UAAa,GAAUxB,EAAKsB,EAAI,UAAWhB,CAAK,EACnD,IAAK,IAAMmB,KAAQC,EACfJ,EAAG,iBAAiBG,EAAO,GAAUzB,EAAKsB,EAAIG,EAAMnB,CAAqB,CAAC,EAE9E,IAAK,IAAMmB,KAAQvB,EACfoB,EAAG,iBAAiBG,MAAY,CAEhC,CAAC,EAGL,EAAG,QAAW,GAAU,CACpBE,IAAUrB,CAAK,EACfgB,EAAG,MAAM,EACT,EAAS,KACT3B,EAAU,QAAQ,EAClBa,EAAkB,CACtB,CACJ,CAEA,SAASoB,GAAc,CACnB,EAAS,GACT,AAEIvC,KADA,aAAaA,CAAU,EACV,MAEjB,EAAU,EACV,AAEID,KADAA,EAAO,MAAM,EACJ,MAEbO,EAAU,QAAQ,CACtB,CAEA,SAASS,GAAkB,CACvB,AAEIf,KADA,aAAaA,CAAU,EACV,MAEjB,EAAU,EACV,EAAS,GACTwB,EAAQ,CACZ,CAIA,OAFAA,EAAQ,EAED,CACH,QACA,YACA,IAAI,QAAS,CACT,OAAOtB,CACX,CACJ,CACJ,C"}
|
|
@@ -1,72 +1,79 @@
|
|
|
1
1
|
import { decodeFrame as e } from "../utils/json-frame.js";
|
|
2
2
|
//#region src/sse/create-event-stream.ts
|
|
3
3
|
function t(t, n = {}) {
|
|
4
|
-
let { withCredentials: r = !1, namedEvents: i = [], heartbeatEvents: a = ["ping"], maxRetries: o = 10, initialBackoff: s = 1e3, maxBackoff: c = 3e4, parser: l, onOpen: u, onMessage: d, onError: f, onParseError: p, schema: m, onValidationError: h, onUrlError: g, shouldRetryUrl: _ = () => !0,
|
|
5
|
-
function
|
|
6
|
-
|
|
4
|
+
let { withCredentials: r = !1, namedEvents: i = [], heartbeatEvents: a = ["ping"], maxRetries: o = 10, initialBackoff: s = 1e3, maxBackoff: c = 3e4, parser: l, onOpen: u, onMessage: d, onError: f, onParseError: p, schema: m, onValidationError: h, onUrlError: g, shouldRetryUrl: _ = () => !0, sessionKey: v, onStatusChange: y } = n, b = null, x = null, S = 0, C = "idle", w = !1, T = 0, E;
|
|
5
|
+
function D(e) {
|
|
6
|
+
C !== e && (C = e, y?.(e));
|
|
7
7
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
function O() {
|
|
9
|
+
return v !== void 0 && !Object.is(v(), E);
|
|
10
|
+
}
|
|
11
|
+
function k(t, n, r) {
|
|
12
|
+
if (t !== b || a.includes(n)) return;
|
|
13
|
+
if (O()) {
|
|
14
|
+
F();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let i = e(typeof r.data == "string" ? r.data : "", "createEventStream", {
|
|
11
18
|
parser: l,
|
|
12
19
|
onParseError: p,
|
|
13
20
|
schema: m,
|
|
14
21
|
onValidationError: h
|
|
15
22
|
});
|
|
16
|
-
|
|
17
|
-
event:
|
|
18
|
-
data:
|
|
19
|
-
id:
|
|
20
|
-
raw:
|
|
23
|
+
i.delivered && d?.({
|
|
24
|
+
event: n,
|
|
25
|
+
data: i.data,
|
|
26
|
+
id: r.lastEventId || void 0,
|
|
27
|
+
raw: r
|
|
21
28
|
});
|
|
22
29
|
}
|
|
23
|
-
function
|
|
24
|
-
if (
|
|
25
|
-
if (
|
|
26
|
-
|
|
30
|
+
function A() {
|
|
31
|
+
if (w) return;
|
|
32
|
+
if (S >= o) {
|
|
33
|
+
D("error");
|
|
27
34
|
return;
|
|
28
35
|
}
|
|
29
|
-
let e = Math.min(s * 2 **
|
|
30
|
-
|
|
36
|
+
let e = Math.min(s * 2 ** S, c);
|
|
37
|
+
S += 1, x = setTimeout(M, e);
|
|
31
38
|
}
|
|
32
|
-
function
|
|
33
|
-
g?.(e), _(e) ? (
|
|
39
|
+
function j(e) {
|
|
40
|
+
g?.(e), _(e) ? (D("closed"), A()) : D("error");
|
|
34
41
|
}
|
|
35
|
-
function
|
|
36
|
-
if (
|
|
37
|
-
if (
|
|
38
|
-
|
|
42
|
+
function M() {
|
|
43
|
+
if (w) return;
|
|
44
|
+
if (b &&= (b.close(), null), D("connecting"), T += 1, E = v?.(), typeof t == "string") {
|
|
45
|
+
N(t);
|
|
39
46
|
return;
|
|
40
47
|
}
|
|
41
|
-
let e =
|
|
42
|
-
Promise.resolve().then(t).then((t) => {
|
|
43
|
-
|
|
48
|
+
let e = T;
|
|
49
|
+
Promise.resolve().then(() => w || e !== T ? null : t()).then((t) => {
|
|
50
|
+
t === null || w || e !== T || (O() ? M() : N(t));
|
|
44
51
|
}, (t) => {
|
|
45
|
-
!
|
|
52
|
+
!w && e === T && j(t);
|
|
46
53
|
});
|
|
47
54
|
}
|
|
48
|
-
function
|
|
55
|
+
function N(e) {
|
|
49
56
|
let t = new EventSource(e, { withCredentials: r });
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}, t.onmessage = (e) =>
|
|
53
|
-
for (let e of i) t.addEventListener(e, (
|
|
57
|
+
b = t, t.onopen = () => {
|
|
58
|
+
S = 0, D("open"), u?.();
|
|
59
|
+
}, t.onmessage = (e) => k(t, "message", e);
|
|
60
|
+
for (let e of i) t.addEventListener(e, (n) => k(t, e, n));
|
|
54
61
|
for (let e of a) t.addEventListener(e, () => {});
|
|
55
62
|
t.onerror = (e) => {
|
|
56
|
-
f?.(e), t.close(),
|
|
63
|
+
f?.(e), t.close(), b = null, D("closed"), A();
|
|
57
64
|
};
|
|
58
65
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
66
|
+
function P() {
|
|
67
|
+
w = !0, x &&= (clearTimeout(x), null), S = 0, b &&= (b.close(), null), D("closed");
|
|
61
68
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
69
|
+
function F() {
|
|
70
|
+
x &&= (clearTimeout(x), null), S = 0, w = !1, M();
|
|
64
71
|
}
|
|
65
|
-
return
|
|
66
|
-
close:
|
|
67
|
-
reconnect:
|
|
72
|
+
return M(), {
|
|
73
|
+
close: P,
|
|
74
|
+
reconnect: F,
|
|
68
75
|
get status() {
|
|
69
|
-
return
|
|
76
|
+
return C;
|
|
70
77
|
}
|
|
71
78
|
};
|
|
72
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-event-stream.js","names":["createEventStream","options","source","retryTimer","retries","status","closed","attempt","setStatus","next","onStatusChange","emit","heartbeatEvents","eventName","decoded","decodeFrame","event","onMessage","scheduleReconnect","maxRetries","delay","initialBackoff","maxBackoff","connect","failUrl","onUrlError","error","shouldRetryUrl","url","current","open","resolved","es","target","onOpen","name","namedEvents","onError","close","reconnect"],"sources":["../../src/sse/create-event-stream.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body is the SSE line protocol — accumulate\n * `data:` lines until a blank line, track `id:` for Last-Event-ID, honour `retry:` —\n * plus the reconnect that uses the id it just tracked.\n */\nimport { decodeFrame } from \"../utils/json-frame\";\nimport type { SchemaIssue, SchemaLike } from \"../utils/schema-like\";\nexport type EventStreamStatus = \"idle\" | \"connecting\" | \"open\" | \"closed\" | \"error\";\n\n/**\n * Where the stream connects: a fixed URL, or a function resolved again before\n * every connection, the first one and each reconnect.\n *\n * The function form exists for short-lived credentials carried in the URL,\n * such as the `?ticket=` of the `tempest-fastapi-sdk` SSE router. `EventSource`\n * cannot send headers, so the credential has to ride in the query, and one that\n * expires makes a fixed URL a trap: the reconnect after the expiry presents the\n * same dead credential, gets a `401`, and burns every retry on it while the user\n * is still signed in.\n */\nexport type EventStreamUrl = string | (() => string | Promise<string>);\n\nexport interface EventStreamMessage<T> {\n /** Server-named event (default `\"message\"`). */\n event: string;\n /** Parsed payload — validated when `schema` is set, JSON-decoded when possible, raw string otherwise. */\n data: T;\n /** Server-supplied id, if any. */\n id?: string;\n /** Raw `MessageEvent` for advanced cases. */\n raw: MessageEvent;\n}\n\nexport interface CreateEventStreamOptions<T> {\n /** Send cookies with the EventSource handshake. Default: false. */\n withCredentials?: boolean;\n /** Subscribe to named events in addition to `message`. */\n namedEvents?: readonly string[];\n /** Treat these named events as heartbeat-only (no callback). Default: `[\"ping\"]`. */\n heartbeatEvents?: readonly string[];\n /** Max reconnect attempts. Default: 10. Pass 0 to disable reconnect. */\n maxRetries?: number;\n /** Initial backoff in ms; doubles per attempt, capped at `maxBackoff`. Default: 1000. */\n initialBackoff?: number;\n /** Maximum backoff in ms. Default: 30000. */\n maxBackoff?: number;\n /** Parse `event.data`. Defaults to JSON with raw-string fallback. */\n parser?: (raw: string) => T;\n /**\n * A frame arrived that is not valid JSON, and no `parser` was supplied.\n *\n * Registering this drops the frame instead of delivering it: the previous\n * behaviour handed `onMessage` the raw `string` announced as your message\n * type, so the failure surfaced later, at the first property access, with\n * nothing left pointing at the parse. Leave it out and that behaviour is\n * kept, with a one-time warning in development builds.\n */\n onParseError?: (error: unknown, raw: string) => void;\n /**\n * Schema every decoded frame must satisfy, from zod, valibot, arktype or\n * anything else exposing `~standard` or `.safeParse`.\n *\n * Without it nothing changes: the payload reaches `onMessage` announced as\n * `T` on the strength of the type argument alone, which is a promise about\n * the server that TypeScript cannot keep. With it, a frame that does not\n * match is **not** delivered — the same rule `onParseError` already follows —\n * and `onValidationError` hears why. The value delivered is the schema's\n * output, so coercions and defaults are honoured.\n *\n * When `parser` is also supplied, it decodes first and the schema validates\n * what it returned.\n *\n * The validation must be synchronous. A frame is decoded inside the\n * `message` handler and delivered from it, so an async schema would deliver\n * frames in whatever order their validations settled; that case is reported\n * through `onValidationError` instead of awaited.\n */\n schema?: SchemaLike<T>;\n /**\n * A frame was decoded but the `schema` refused it, so it was dropped.\n *\n * The one signal that does not depend on how the app's bundler resolves\n * `process`: the one-time development warning behind `onParseError` needs\n * `isDevBuild()` to be able to answer, and this callback is the app's own.\n */\n onValidationError?: (issues: SchemaIssue[], raw: string) => void;\n /**\n * The URL function threw or rejected, so no connection was attempted.\n *\n * `onError` cannot carry this: it receives the `EventSource` `Event`, and a\n * URL that never resolved never had an `EventSource`.\n */\n onUrlError?: (error: unknown) => void;\n /**\n * Whether a URL-resolution failure is worth another attempt. Default: always.\n *\n * Return `false` for failures no backoff can fix — a `401` or a `403` when\n * fetching a ticket is a session decision, not a network blip — and the stream\n * stops at `status: \"error\"` instead of spending `maxRetries` on it.\n */\n shouldRetryUrl?: (error: unknown) => boolean;\n onOpen?: () => void;\n onMessage?: (message: EventStreamMessage<T>) => void;\n onError?: (error: Event) => void;\n onStatusChange?: (status: EventStreamStatus) => void;\n}\n\nexport interface EventStreamController {\n close: () => void;\n /** Force an immediate reconnect, resetting the retry counter. */\n reconnect: () => void;\n /** Current connection status. */\n readonly status: EventStreamStatus;\n}\n\n/**\n * Open a Server-Sent Events stream with automatic exponential-backoff reconnect.\n *\n * Heartbeat events (default `\"ping\"`) keep the socket alive without firing\n * `onMessage`. Pass `withCredentials: true` when the backend authenticates via\n * cookies. Call `close()` from the returned controller to tear down.\n *\n * When `url` is a function it is called before every connection, so a\n * reconnect presents a fresh credential instead of the one that expired. A\n * resolution still pending when `close()` or `reconnect()` runs is discarded.\n *\n * @param url - Full SSE endpoint URL, or a function returning it per connection.\n * @param options - Stream configuration and callbacks.\n * @returns A controller exposing `close`, `reconnect` and the current `status`.\n */\nexport function createEventStream<T = unknown>(\n url: EventStreamUrl,\n options: CreateEventStreamOptions<T> = {},\n): EventStreamController {\n const {\n withCredentials = false,\n namedEvents = [],\n heartbeatEvents = [\"ping\"],\n maxRetries = 10,\n initialBackoff = 1000,\n maxBackoff = 30000,\n parser,\n onOpen,\n onMessage,\n onError,\n onParseError,\n schema,\n onValidationError,\n onUrlError,\n shouldRetryUrl = () => true,\n onStatusChange,\n } = options;\n\n let source: EventSource | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let retries = 0;\n let status: EventStreamStatus = \"idle\";\n let closed = false;\n let attempt = 0;\n\n function setStatus(next: EventStreamStatus): void {\n if (status === next) return;\n status = next;\n onStatusChange?.(next);\n }\n\n function emit(eventName: string, event: MessageEvent): void {\n if (heartbeatEvents.includes(eventName)) return;\n const decoded = decodeFrame<T>(\n typeof event.data === \"string\" ? event.data : \"\",\n \"createEventStream\",\n { parser, onParseError, schema, onValidationError },\n );\n if (!decoded.delivered) return;\n onMessage?.({\n event: eventName,\n data: decoded.data,\n id: event.lastEventId || undefined,\n raw: event,\n });\n }\n\n function scheduleReconnect(): void {\n if (closed) return;\n if (retries >= maxRetries) {\n setStatus(\"error\");\n return;\n }\n const delay = Math.min(initialBackoff * 2 ** retries, maxBackoff);\n retries += 1;\n retryTimer = setTimeout(connect, delay);\n }\n\n function failUrl(error: unknown): void {\n onUrlError?.(error);\n if (!shouldRetryUrl(error)) {\n setStatus(\"error\");\n return;\n }\n setStatus(\"closed\");\n scheduleReconnect();\n }\n\n function connect(): void {\n if (closed) return;\n if (source) {\n source.close();\n source = null;\n }\n setStatus(\"connecting\");\n attempt += 1;\n if (typeof url === \"string\") {\n open(url);\n return;\n }\n const current = attempt;\n Promise.resolve()\n .then(url)\n .then(\n (resolved) => {\n if (!closed && current === attempt) open(resolved);\n },\n (error: unknown) => {\n if (!closed && current === attempt) failUrl(error);\n },\n );\n }\n\n function open(target: string): void {\n const es = new EventSource(target, { withCredentials });\n source = es;\n\n es.onopen = () => {\n retries = 0;\n setStatus(\"open\");\n onOpen?.();\n };\n\n es.onmessage = (event) => emit(\"message\", event);\n for (const name of namedEvents) {\n es.addEventListener(name, (event) => emit(name, event as MessageEvent));\n }\n for (const name of heartbeatEvents) {\n es.addEventListener(name, () => {\n /* heartbeat — keep socket alive */\n });\n }\n\n es.onerror = (event) => {\n onError?.(event);\n es.close();\n source = null;\n setStatus(\"closed\");\n scheduleReconnect();\n };\n }\n\n function close(): void {\n closed = true;\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n if (source) {\n source.close();\n source = null;\n }\n setStatus(\"closed\");\n }\n\n function reconnect(): void {\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n closed = false;\n connect();\n }\n\n connect();\n\n return {\n close,\n reconnect,\n get status() {\n return status;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-event-stream.js","names":["createEventStream","options","source","retryTimer","retries","status","closed","attempt","boundKey","setStatus","next","onStatusChange","sessionChanged","sessionKey","emit","from","heartbeatEvents","eventName","decoded","decodeFrame","event","onMessage","scheduleReconnect","maxRetries","delay","initialBackoff","maxBackoff","connect","failUrl","onUrlError","error","shouldRetryUrl","url","current","resolved","open","es","target","onOpen","name","namedEvents","onError","close","reconnect"],"sources":["../../src/sse/create-event-stream.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body is the SSE line protocol — accumulate\n * `data:` lines until a blank line, track `id:` for Last-Event-ID, honour `retry:` —\n * plus the reconnect that uses the id it just tracked.\n */\nimport { decodeFrame } from \"../utils/json-frame\";\nimport type { SchemaIssue, SchemaLike } from \"../utils/schema-like\";\nexport type EventStreamStatus = \"idle\" | \"connecting\" | \"open\" | \"closed\" | \"error\";\n\n/**\n * Where the stream connects: a fixed URL, or a function resolved again before\n * every connection, the first one and each reconnect.\n *\n * The function form exists for short-lived credentials carried in the URL,\n * such as the `?ticket=` of the `tempest-fastapi-sdk` SSE router. `EventSource`\n * cannot send headers, so the credential has to ride in the query, and one that\n * expires makes a fixed URL a trap: the reconnect after the expiry presents the\n * same dead credential, gets a `401`, and burns every retry on it while the user\n * is still signed in.\n */\nexport type EventStreamUrl = string | (() => string | Promise<string>);\n\n/**\n * Identifies the session a stream belongs to: the signed-in user's id, an\n * auth epoch, `null` when signed out.\n *\n * A primitive on purpose. The hook form lists it in its effect dependencies,\n * and an object built inline would be a new identity on every render, so the\n * stream would reopen on every render.\n */\nexport type SessionKey = string | number | boolean | null | undefined;\n\nexport interface EventStreamMessage<T> {\n /** Server-named event (default `\"message\"`). */\n event: string;\n /** Parsed payload — validated when `schema` is set, JSON-decoded when possible, raw string otherwise. */\n data: T;\n /** Server-supplied id, if any. */\n id?: string;\n /** Raw `MessageEvent` for advanced cases. */\n raw: MessageEvent;\n}\n\nexport interface CreateEventStreamOptions<T> {\n /** Send cookies with the EventSource handshake. Default: false. */\n withCredentials?: boolean;\n /** Subscribe to named events in addition to `message`. */\n namedEvents?: readonly string[];\n /** Treat these named events as heartbeat-only (no callback). Default: `[\"ping\"]`. */\n heartbeatEvents?: readonly string[];\n /** Max reconnect attempts. Default: 10. Pass 0 to disable reconnect. */\n maxRetries?: number;\n /** Initial backoff in ms; doubles per attempt, capped at `maxBackoff`. Default: 1000. */\n initialBackoff?: number;\n /** Maximum backoff in ms. Default: 30000. */\n maxBackoff?: number;\n /** Parse `event.data`. Defaults to JSON with raw-string fallback. */\n parser?: (raw: string) => T;\n /**\n * A frame arrived that is not valid JSON, and no `parser` was supplied.\n *\n * Registering this drops the frame instead of delivering it: the previous\n * behaviour handed `onMessage` the raw `string` announced as your message\n * type, so the failure surfaced later, at the first property access, with\n * nothing left pointing at the parse. Leave it out and that behaviour is\n * kept, with a one-time warning in development builds.\n */\n onParseError?: (error: unknown, raw: string) => void;\n /**\n * Schema every decoded frame must satisfy, from zod, valibot, arktype or\n * anything else exposing `~standard` or `.safeParse`.\n *\n * Without it nothing changes: the payload reaches `onMessage` announced as\n * `T` on the strength of the type argument alone, which is a promise about\n * the server that TypeScript cannot keep. With it, a frame that does not\n * match is **not** delivered — the same rule `onParseError` already follows —\n * and `onValidationError` hears why. The value delivered is the schema's\n * output, so coercions and defaults are honoured.\n *\n * When `parser` is also supplied, it decodes first and the schema validates\n * what it returned.\n *\n * The validation must be synchronous. A frame is decoded inside the\n * `message` handler and delivered from it, so an async schema would deliver\n * frames in whatever order their validations settled; that case is reported\n * through `onValidationError` instead of awaited.\n */\n schema?: SchemaLike<T>;\n /**\n * A frame was decoded but the `schema` refused it, so it was dropped.\n *\n * The one signal that does not depend on how the app's bundler resolves\n * `process`: the one-time development warning behind `onParseError` needs\n * `isDevBuild()` to be able to answer, and this callback is the app's own.\n */\n onValidationError?: (issues: SchemaIssue[], raw: string) => void;\n /**\n * The URL function threw or rejected, so no connection was attempted.\n *\n * `onError` cannot carry this: it receives the `EventSource` `Event`, and a\n * URL that never resolved never had an `EventSource`.\n */\n onUrlError?: (error: unknown) => void;\n /**\n * Read the current session key; the stream belongs to the session that was\n * current when it connected.\n *\n * The server cannot end the stream for you. The `tempest-fastapi-sdk` SSE\n * router checks the ticket only when the stream opens, and neither logout\n * nor refresh-token revocation touches an open stream: it lives until the\n * client leaves. So a tab that switches from user A to user B without\n * closing the stream keeps receiving A's events under B.\n *\n * With this set, the key is read when each connection starts, again when\n * its URL resolved, and before every frame is delivered. A frame that\n * arrives after the key changed is dropped, never delivered, and the stream\n * reconnects for the session now current — with a fresh ticket, for the\n * ticket stream. A URL resolved while the key changed is discarded the same\n * way, since the ticket inside it may belong to either session.\n *\n * The check runs when a frame arrives: a stream that stays quiet holds its\n * connection until the next frame or until `close()`. The hook form closes\n * it as soon as the key changes.\n *\n * Keys are compared with `Object.is`. Leave it out and nothing changes.\n */\n sessionKey?: () => SessionKey;\n /**\n * Whether a URL-resolution failure is worth another attempt. Default: always.\n *\n * Return `false` for failures no backoff can fix — a `401` or a `403` when\n * fetching a ticket is a session decision, not a network blip — and the stream\n * stops at `status: \"error\"` instead of spending `maxRetries` on it.\n */\n shouldRetryUrl?: (error: unknown) => boolean;\n onOpen?: () => void;\n onMessage?: (message: EventStreamMessage<T>) => void;\n onError?: (error: Event) => void;\n onStatusChange?: (status: EventStreamStatus) => void;\n}\n\nexport interface EventStreamController {\n close: () => void;\n /** Force an immediate reconnect, resetting the retry counter. */\n reconnect: () => void;\n /** Current connection status. */\n readonly status: EventStreamStatus;\n}\n\n/**\n * Open a Server-Sent Events stream with automatic exponential-backoff reconnect.\n *\n * Heartbeat events (default `\"ping\"`) keep the socket alive without firing\n * `onMessage`. Pass `withCredentials: true` when the backend authenticates via\n * cookies. Call `close()` from the returned controller to tear down.\n *\n * When `url` is a function it is called before every connection, so a\n * reconnect presents a fresh credential instead of the one that expired. A\n * resolution still pending when `close()` or `reconnect()` runs is discarded, and\n * one that has not started yet is never made: a stream closed in the same tick\n * it was created asks for no URL — no ticket request, for the ticket stream.\n *\n * Pass `sessionKey` to bind the stream to the session it opened under: once\n * the key changes, no frame of the previous session reaches `onMessage`, and\n * the stream reconnects for the new one.\n *\n * @param url - Full SSE endpoint URL, or a function returning it per connection.\n * @param options - Stream configuration and callbacks.\n * @returns A controller exposing `close`, `reconnect` and the current `status`.\n */\nexport function createEventStream<T = unknown>(\n url: EventStreamUrl,\n options: CreateEventStreamOptions<T> = {},\n): EventStreamController {\n const {\n withCredentials = false,\n namedEvents = [],\n heartbeatEvents = [\"ping\"],\n maxRetries = 10,\n initialBackoff = 1000,\n maxBackoff = 30000,\n parser,\n onOpen,\n onMessage,\n onError,\n onParseError,\n schema,\n onValidationError,\n onUrlError,\n shouldRetryUrl = () => true,\n sessionKey,\n onStatusChange,\n } = options;\n\n let source: EventSource | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let retries = 0;\n let status: EventStreamStatus = \"idle\";\n let closed = false;\n let attempt = 0;\n let boundKey: SessionKey;\n\n function setStatus(next: EventStreamStatus): void {\n if (status === next) return;\n status = next;\n onStatusChange?.(next);\n }\n\n function sessionChanged(): boolean {\n return sessionKey !== undefined && !Object.is(sessionKey(), boundKey);\n }\n\n /**\n * Deliver one frame, unless it no longer belongs to this stream.\n *\n * A frame from a source other than the current one — closed by `close()`,\n * replaced by a reconnect — is ignored: it would otherwise reach `onMessage`\n * after the caller let go of the stream, and the session check below would\n * reopen a controller that `close()` already ended. A frame that arrives\n * after `sessionKey` changed is dropped and the stream reconnects for the\n * session now current.\n *\n * @param from - The `EventSource` the frame came from.\n * @param eventName - The SSE event name.\n * @param event - The raw message event.\n */\n function emit(from: EventSource, eventName: string, event: MessageEvent): void {\n if (from !== source || heartbeatEvents.includes(eventName)) return;\n if (sessionChanged()) {\n reconnect();\n return;\n }\n const decoded = decodeFrame<T>(\n typeof event.data === \"string\" ? event.data : \"\",\n \"createEventStream\",\n { parser, onParseError, schema, onValidationError },\n );\n if (!decoded.delivered) return;\n onMessage?.({\n event: eventName,\n data: decoded.data,\n id: event.lastEventId || undefined,\n raw: event,\n });\n }\n\n function scheduleReconnect(): void {\n if (closed) return;\n if (retries >= maxRetries) {\n setStatus(\"error\");\n return;\n }\n const delay = Math.min(initialBackoff * 2 ** retries, maxBackoff);\n retries += 1;\n retryTimer = setTimeout(connect, delay);\n }\n\n function failUrl(error: unknown): void {\n onUrlError?.(error);\n if (!shouldRetryUrl(error)) {\n setStatus(\"error\");\n return;\n }\n setStatus(\"closed\");\n scheduleReconnect();\n }\n\n function connect(): void {\n if (closed) return;\n if (source) {\n source.close();\n source = null;\n }\n setStatus(\"connecting\");\n attempt += 1;\n boundKey = sessionKey?.();\n if (typeof url === \"string\") {\n open(url);\n return;\n }\n const current = attempt;\n Promise.resolve()\n .then(() => (closed || current !== attempt ? null : url()))\n .then(\n (resolved) => {\n if (resolved === null || closed || current !== attempt) return;\n if (sessionChanged()) {\n connect();\n return;\n }\n open(resolved);\n },\n (error: unknown) => {\n if (!closed && current === attempt) failUrl(error);\n },\n );\n }\n\n function open(target: string): void {\n const es = new EventSource(target, { withCredentials });\n source = es;\n\n es.onopen = () => {\n retries = 0;\n setStatus(\"open\");\n onOpen?.();\n };\n\n es.onmessage = (event) => emit(es, \"message\", event);\n for (const name of namedEvents) {\n es.addEventListener(name, (event) => emit(es, name, event as MessageEvent));\n }\n for (const name of heartbeatEvents) {\n es.addEventListener(name, () => {\n /* heartbeat — keep socket alive */\n });\n }\n\n es.onerror = (event) => {\n onError?.(event);\n es.close();\n source = null;\n setStatus(\"closed\");\n scheduleReconnect();\n };\n }\n\n function close(): void {\n closed = true;\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n if (source) {\n source.close();\n source = null;\n }\n setStatus(\"closed\");\n }\n\n function reconnect(): void {\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n closed = false;\n connect();\n }\n\n connect();\n\n return {\n close,\n reconnect,\n get status() {\n return status;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../http/errors.cjs"),t=require("./create-event-stream.cjs"),
|
|
1
|
+
const e=require("../http/errors.cjs"),t=require("../auth/session-identity.cjs"),n=require("./create-event-stream.cjs"),r=require("./use-event-stream.cjs");let i=require("react");function a(t){return e.isApiError(t)&&e.isRetriableStatus(t.status)}function o(e){let{client:t,prefix:n=`/sse`,ticketParam:r=`ticket`}=e;return async()=>{let e=await t.post(`${n}/ticket`);if(typeof e?.ticket!=`string`||e.ticket===``)throw TypeError(`createTicketEventStream: POST ${n}/ticket returned no "ticket" string.`);return t.url(`${n}/stream`,{[r]:e.ticket})}}function s(e){let{client:r,prefix:i,ticketParam:s,sessionKey:l,shouldRetryUrl:u=a,...d}=e,f=l===void 0?t.sessionIdentityOf(r):void 0,p=n.createEventStream(o({client:r,prefix:i,ticketParam:s}),{...d,sessionKey:l??f?.current,shouldRetryUrl:u});return f?c(p,f):p}function c(e,t){let n=t.current(),r=null;function i(){r??=t.subscribe(()=>{let r=t.current();Object.is(r,n)||(n=r,r===null?e.close():e.reconnect())})}return i(),n===null&&e.close(),{close:()=>{r?.(),r=null,e.close()},reconnect:()=>{n=t.current(),i(),e.reconnect()},get status(){return e.status}}}function l(e){let t=(0,i.useCallback)(t=>e?.subscribe(t)??(()=>void 0),[e]),n=()=>e?.current();return(0,i.useSyncExternalStore)(t,n,n)}function u(e){let{client:n,prefix:i,ticketParam:s,enabled:c=!0,sessionKey:u,shouldRetryUrl:d=a,...f}=e,p=l(u===void 0?t.sessionIdentityOf(n):void 0);return r.useEventStream(o({client:n,prefix:i,ticketParam:s}),{...f,enabled:c&&p!==null,sessionKey:u??p,shouldRetryUrl:d})}exports.createTicketEventStream=s,exports.isRetriableTicketError=a,exports.useTicketEventStream=u;
|
|
2
2
|
//# sourceMappingURL=ticket-event-stream.cjs.map
|