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.
Files changed (179) hide show
  1. package/README.md +5 -6
  2. package/bin/create-tempest-app.e2e.test.mjs +31 -0
  3. package/bin/create-tempest-app.mjs +11 -1
  4. package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
  5. package/bin/lib/doctor/vitest.mjs +120 -0
  6. package/bin/lib/doctor/vitest.test.mjs +139 -0
  7. package/bin/lib/fs/contain.mjs +7 -0
  8. package/bin/lib/openapi/escape.mjs +68 -4
  9. package/bin/lib/openapi/generate.hostile.test.mjs +137 -0
  10. package/bin/lib/openapi/generate.mjs +78 -13
  11. package/bin/lib/openapi/pattern-risk.mjs +134 -0
  12. package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
  13. package/bin/tempest-gen.test.mjs +100 -0
  14. package/bin/tempest.mjs +24 -8
  15. package/dist/access/use-can.cjs +1 -1
  16. package/dist/access/use-can.cjs.map +1 -1
  17. package/dist/access/use-can.js +25 -28
  18. package/dist/access/use-can.js.map +1 -1
  19. package/dist/auth/create-tempest-auth.cjs +1 -1
  20. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  21. package/dist/auth/create-tempest-auth.js +192 -149
  22. package/dist/auth/create-tempest-auth.js.map +1 -1
  23. package/dist/auth/session-identity.cjs +2 -0
  24. package/dist/auth/session-identity.cjs.map +1 -0
  25. package/dist/auth/session-identity.js +12 -0
  26. package/dist/auth/session-identity.js.map +1 -0
  27. package/dist/br/boleto.cjs.map +1 -1
  28. package/dist/br/boleto.js.map +1 -1
  29. package/dist/components/AuthForm/AuthLinks.cjs +1 -1
  30. package/dist/components/AuthForm/AuthLinks.cjs.map +1 -1
  31. package/dist/components/AuthForm/AuthLinks.js +18 -15
  32. package/dist/components/AuthForm/AuthLinks.js.map +1 -1
  33. package/dist/components/AuthPage/AuthPage.cjs +1 -1
  34. package/dist/components/AuthPage/AuthPage.cjs.map +1 -1
  35. package/dist/components/AuthPage/AuthPage.js +58 -52
  36. package/dist/components/AuthPage/AuthPage.js.map +1 -1
  37. package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
  38. package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
  39. package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
  40. package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
  41. package/dist/components/Button/Button.cjs +1 -1
  42. package/dist/components/Button/Button.cjs.map +1 -1
  43. package/dist/components/Button/Button.js +41 -40
  44. package/dist/components/Button/Button.js.map +1 -1
  45. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  46. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  47. package/dist/components/Chat/ChatAttachments.js +38 -38
  48. package/dist/components/Form/Form.cjs +1 -1
  49. package/dist/components/Form/Form.cjs.map +1 -1
  50. package/dist/components/Form/Form.js +30 -32
  51. package/dist/components/Form/Form.js.map +1 -1
  52. package/dist/components/Layout/Layout.cjs +1 -1
  53. package/dist/components/Layout/Layout.cjs.map +1 -1
  54. package/dist/components/Layout/Layout.js +25 -24
  55. package/dist/components/Layout/Layout.js.map +1 -1
  56. package/dist/components/Layout/gap.cjs +2 -0
  57. package/dist/components/Layout/gap.cjs.map +1 -0
  58. package/dist/components/Layout/gap.js +15 -0
  59. package/dist/components/Layout/gap.js.map +1 -0
  60. package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
  61. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  62. package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
  63. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  64. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  65. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  66. package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
  67. package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
  68. package/dist/components/Sidebar/SidebarEntry.js +30 -29
  69. package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
  70. package/dist/data/create-data-provider.cjs +1 -1
  71. package/dist/data/create-data-provider.cjs.map +1 -1
  72. package/dist/data/create-data-provider.js +1 -2
  73. package/dist/data/create-data-provider.js.map +1 -1
  74. package/dist/forms/validate-form.cjs +1 -1
  75. package/dist/forms/validate-form.cjs.map +1 -1
  76. package/dist/forms/validate-form.js +6 -1
  77. package/dist/forms/validate-form.js.map +1 -1
  78. package/dist/forms/zod-resolver.cjs +1 -1
  79. package/dist/forms/zod-resolver.cjs.map +1 -1
  80. package/dist/forms/zod-resolver.js +8 -3
  81. package/dist/forms/zod-resolver.js.map +1 -1
  82. package/dist/http/api-client.cjs +1 -1
  83. package/dist/http/api-client.cjs.map +1 -1
  84. package/dist/http/api-client.js +90 -72
  85. package/dist/http/api-client.js.map +1 -1
  86. package/dist/http/credential-scope.cjs +1 -1
  87. package/dist/http/credential-scope.cjs.map +1 -1
  88. package/dist/http/credential-scope.js +33 -9
  89. package/dist/http/credential-scope.js.map +1 -1
  90. package/dist/http/presigned-post.cjs.map +1 -1
  91. package/dist/http/presigned-post.js.map +1 -1
  92. package/dist/http/resumable-upload.cjs +1 -1
  93. package/dist/http/resumable-upload.cjs.map +1 -1
  94. package/dist/http/resumable-upload.js +90 -84
  95. package/dist/http/resumable-upload.js.map +1 -1
  96. package/dist/http/upload-with-progress.cjs +1 -1
  97. package/dist/http/upload-with-progress.cjs.map +1 -1
  98. package/dist/http/upload-with-progress.js +8 -8
  99. package/dist/http/upload-with-progress.js.map +1 -1
  100. package/dist/oauth/GoogleSignIn.cjs +1 -1
  101. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  102. package/dist/oauth/GoogleSignIn.js +5 -4
  103. package/dist/oauth/GoogleSignIn.js.map +1 -1
  104. package/dist/offline/create-offline-sync.cjs +1 -1
  105. package/dist/offline/create-offline-sync.cjs.map +1 -1
  106. package/dist/offline/create-offline-sync.js +87 -69
  107. package/dist/offline/create-offline-sync.js.map +1 -1
  108. package/dist/query/offline-persistence.cjs +1 -1
  109. package/dist/query/offline-persistence.cjs.map +1 -1
  110. package/dist/query/offline-persistence.js +54 -44
  111. package/dist/query/offline-persistence.js.map +1 -1
  112. package/dist/sse/create-event-stream.cjs +1 -1
  113. package/dist/sse/create-event-stream.cjs.map +1 -1
  114. package/dist/sse/create-event-stream.js +48 -41
  115. package/dist/sse/create-event-stream.js.map +1 -1
  116. package/dist/sse/ticket-event-stream.cjs +1 -1
  117. package/dist/sse/ticket-event-stream.cjs.map +1 -1
  118. package/dist/sse/ticket-event-stream.js +48 -19
  119. package/dist/sse/ticket-event-stream.js.map +1 -1
  120. package/dist/sse/use-event-stream.cjs +1 -1
  121. package/dist/sse/use-event-stream.cjs.map +1 -1
  122. package/dist/sse/use-event-stream.js +19 -14
  123. package/dist/sse/use-event-stream.js.map +1 -1
  124. package/dist/sw/background-sync.cjs +1 -1
  125. package/dist/sw/background-sync.cjs.map +1 -1
  126. package/dist/sw/background-sync.js +10 -8
  127. package/dist/sw/background-sync.js.map +1 -1
  128. package/dist/sw/cache.cjs +1 -1
  129. package/dist/sw/cache.cjs.map +1 -1
  130. package/dist/sw/cache.js +36 -30
  131. package/dist/sw/cache.js.map +1 -1
  132. package/dist/sw/create-push-handler.cjs +1 -1
  133. package/dist/sw/create-push-handler.cjs.map +1 -1
  134. package/dist/sw/create-push-handler.js +20 -10
  135. package/dist/sw/create-push-handler.js.map +1 -1
  136. package/dist/sw.d.ts +44 -13
  137. package/dist/tabular/compact.cjs +1 -1
  138. package/dist/tabular/compact.cjs.map +1 -1
  139. package/dist/tabular/compact.js +35 -16
  140. package/dist/tabular/compact.js.map +1 -1
  141. package/dist/tabular/manifest.cjs +1 -1
  142. package/dist/tabular/manifest.cjs.map +1 -1
  143. package/dist/tabular/manifest.js +33 -15
  144. package/dist/tabular/manifest.js.map +1 -1
  145. package/dist/tabular.d.ts +11 -1
  146. package/dist/tempest-react-sdk.cjs +1 -1
  147. package/dist/tempest-react-sdk.d.ts +457 -68
  148. package/dist/tempest-react-sdk.js +224 -224
  149. package/dist/theme/initial-theme.cjs +4 -4
  150. package/dist/theme/initial-theme.cjs.map +1 -1
  151. package/dist/theme/initial-theme.js +8 -5
  152. package/dist/theme/initial-theme.js.map +1 -1
  153. package/dist/utils/compressed-storage.cjs +1 -1
  154. package/dist/utils/compressed-storage.cjs.map +1 -1
  155. package/dist/utils/compressed-storage.js +13 -7
  156. package/dist/utils/compressed-storage.js.map +1 -1
  157. package/dist/utils/objects.cjs +1 -1
  158. package/dist/utils/objects.cjs.map +1 -1
  159. package/dist/utils/objects.js +13 -7
  160. package/dist/utils/objects.js.map +1 -1
  161. package/dist/utils/safe-url.cjs +1 -1
  162. package/dist/utils/safe-url.cjs.map +1 -1
  163. package/dist/utils/safe-url.js +4 -1
  164. package/dist/utils/safe-url.js.map +1 -1
  165. package/dist/webrtc/peer-mesh.cjs +1 -1
  166. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  167. package/dist/webrtc/peer-mesh.js +7 -1
  168. package/dist/webrtc/peer-mesh.js.map +1 -1
  169. package/dist/ws/create-web-socket.cjs +1 -1
  170. package/dist/ws/create-web-socket.cjs.map +1 -1
  171. package/dist/ws/create-web-socket.js +77 -61
  172. package/dist/ws/create-web-socket.js.map +1 -1
  173. package/dist/ws/use-web-socket.cjs +1 -1
  174. package/dist/ws/use-web-socket.cjs.map +1 -1
  175. package/dist/ws/use-web-socket.js +4 -3
  176. package/dist/ws/use-web-socket.js.map +1 -1
  177. package/package.json +1 -1
  178. package/template-pwa/src/pwa.tsx +42 -0
  179. package/template-pwa/src/sw.ts +5 -4
@@ -1 +1 @@
1
- {"version":3,"file":"ticket-event-stream.cjs","names":["isApiError","isRetriableStatus","createEventStream","useEventStream","require_errors","require_create_event_stream","require_use_event_stream","isRetriableTicketError","error","ticketStreamUrl","source","response","client","prefix","ticketParam","createTicketEventStream","rest","options","useTicketEventStream"],"sources":["../../src/sse/ticket-event-stream.ts"],"sourcesContent":["import { isApiError, isRetriableStatus } from \"../http/errors\";\nimport type { ApiClient } from \"../http/types\";\nimport {\n createEventStream,\n type CreateEventStreamOptions,\n type EventStreamController,\n} from \"./create-event-stream\";\nimport {\n useEventStream,\n type UseEventStreamOptions,\n type UseEventStreamResult,\n} from \"./use-event-stream\";\n\n/** Body of `POST <prefix>/ticket` on the `tempest-fastapi-sdk` SSE router. */\nexport interface SseTicketResponse {\n /** Signed stream ticket, presented as a query param when opening the stream. */\n ticket: string;\n /** Seconds the ticket is accepted for opening a stream. */\n expires_in: number;\n}\n\n/** Where the ticket comes from and how the stream URL is built. */\nexport interface TicketEventStreamSource {\n /**\n * Authenticated client the ticket is requested through, so it carries the\n * `Authorization` header or the session cookie, the CSRF header, and the\n * 401 refresh-and-replay that a hand-rolled `fetch` would lose.\n */\n client: Pick<ApiClient, \"post\" | \"url\">;\n /** Router prefix, relative to the client's base URL. Default: `\"/sse\"`. */\n prefix?: string;\n /** Query param the stream reads the ticket from. Default: `\"ticket\"`. */\n ticketParam?: string;\n}\n\nexport type CreateTicketEventStreamOptions<T> = TicketEventStreamSource &\n CreateEventStreamOptions<T>;\n\nexport type UseTicketEventStreamOptions<T> = TicketEventStreamSource & UseEventStreamOptions<T>;\n\n/**\n * Whether a failure to obtain a ticket is worth another attempt.\n *\n * Only a failure the backoff can outlast is: no response at all, a timeout, a\n * rate limit, a `5xx`. A `401` reached the caller after the client already\n * tried its refresh, and a `403` (`ACCOUNT_INACTIVE`) is the backend saying no;\n * both are session decisions, and retrying them only delays the error state the\n * app has to show. Anything that is not an API error is a contract mismatch,\n * which a retry cannot fix either.\n *\n * @param error - What the ticket request threw.\n * @returns Whether to schedule a reconnect.\n */\nexport function isRetriableTicketError(error: unknown): boolean {\n return isApiError(error) && isRetriableStatus(error.status);\n}\n\n/**\n * Build the URL function that requests a ticket and points at the stream.\n *\n * A fresh ticket is requested for every connection rather than cached for its\n * `expires_in`. Every connection after the first follows a failure, and the\n * failure may have been the ticket itself; reusing it would spend the retries\n * on a credential that is already refused, while a new ticket costs one `POST`\n * per reconnect, which the backoff already spaces out.\n *\n * @param source - Client, router prefix and ticket param name.\n * @returns A function resolving the stream URL with a fresh ticket.\n * @throws TypeError When the ticket response has no `ticket` string.\n */\nfunction ticketStreamUrl(source: TicketEventStreamSource): () => Promise<string> {\n const { client, prefix = \"/sse\", ticketParam = \"ticket\" } = source;\n return async () => {\n const response = await client.post<SseTicketResponse>(`${prefix}/ticket`);\n if (typeof response?.ticket !== \"string\" || response.ticket === \"\") {\n throw new TypeError(\n `createTicketEventStream: POST ${prefix}/ticket returned no \"ticket\" string.`,\n );\n }\n return client.url(`${prefix}/stream`, { [ticketParam]: response.ticket });\n };\n}\n\n/**\n * Open the ticket-authenticated stream of the `tempest-fastapi-sdk` SSE router.\n *\n * `POST <prefix>/ticket` through the authenticated client, then\n * `GET <prefix>/stream?ticket=…` — and again for every reconnect, because the\n * ticket only opens streams for `expires_in` seconds and a reconnect after that\n * with the old one is a `401`. A ticket request refused with a status no\n * backoff fixes (`401`, `403 ACCOUNT_INACTIVE`) stops the stream at\n * `status: \"error\"` and reaches `onUrlError`; pass `shouldRetryUrl` to replace\n * {@link isRetriableTicketError}.\n *\n * The access token never enters the URL: it travels as the client's header (or\n * cookie) on the ticket request, and the ticket in the URL opens that stream\n * and nothing else.\n *\n * @example\n * const stream = createTicketEventStream<Notification>({\n * client: api,\n * onMessage: ({ data }) => addNotification(data),\n * onUrlError: (error) => toast.error(describeApiError(error)),\n * });\n *\n * @param options - Ticket source plus every {@link createEventStream} option.\n * @returns The stream controller.\n */\nexport function createTicketEventStream<T = unknown>(\n options: CreateTicketEventStreamOptions<T>,\n): EventStreamController {\n const {\n client,\n prefix,\n ticketParam,\n shouldRetryUrl = isRetriableTicketError,\n ...rest\n } = options;\n return createEventStream<T>(ticketStreamUrl({ client, prefix, ticketParam }), {\n ...rest,\n shouldRetryUrl,\n });\n}\n\n/**\n * React form of {@link createTicketEventStream}: same ticket per connection,\n * lifecycle tied to the component like {@link useEventStream}.\n *\n * Changing `client`, `prefix` or `ticketParam` does not drop an open stream:\n * the next connection uses the new values, and `reconnect()` applies them now.\n * Reopening on identity would reconnect on every render for a client built\n * inline.\n *\n * @example\n * const { status } = useTicketEventStream<Notification>({\n * client: api,\n * enabled: isAuthenticated,\n * schema: notificationSchema,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @param options - Ticket source plus every {@link useEventStream} option.\n * @returns Status, last message and `reconnect`.\n */\nexport function useTicketEventStream<T = unknown>(\n options: UseTicketEventStreamOptions<T>,\n): UseEventStreamResult<T> {\n const {\n client,\n prefix,\n ticketParam,\n shouldRetryUrl = isRetriableTicketError,\n ...rest\n } = options;\n return useEventStream<T>(ticketStreamUrl({ client, prefix, ticketParam }), {\n ...rest,\n shouldRetryUrl,\n });\n}\n"],"mappings":"A,iHAqDA,SAAgBO,EAAuB,EAAyB,CAC5D,OAAOP,EAAAA,WAAWQ,CAAK,GAAKP,EAAAA,kBAAkBO,EAAM,MAAM,CAC9D,CAeA,SAASC,EAAgB,EAAwD,CAC7E,GAAM,CAAE,SAAQ,SAAS,OAAQ,cAAc,UAAaC,EAC5D,OAAO,SAAY,CACf,IAAMC,EAAW,MAAMC,EAAO,KAAwB,GAAGC,EAAO,QAAQ,EACxE,GAAI,OAAOF,GAAU,QAAW,UAAYA,EAAS,SAAW,GAC5D,MAAU,UACN,iCAAiCE,EAAO,qCAC5C,EAEJ,OAAOD,EAAO,IAAI,GAAGC,EAAO,SAAU,EAAGC,GAAcH,EAAS,MAAO,CAAC,CAC5E,CACJ,CA2BA,SAAgBI,EACZ,EACqB,CACrB,GAAM,CACF,SACA,SACA,cACA,iBAAiBR,EACjB,GAAGS,GACHC,EACJ,OAAOf,EAAAA,kBAAqBO,EAAgB,CAAE,SAAQ,SAAQ,aAAY,CAAC,EAAG,CAC1E,GAAGO,EACH,gBACJ,CAAC,CACL,CAsBA,SAAgBE,EACZ,EACuB,CACvB,GAAM,CACF,SACA,SACA,cACA,iBAAiBX,EACjB,GAAGS,GACHC,EACJ,OAAOd,EAAAA,eAAkBM,EAAgB,CAAE,SAAQ,SAAQ,aAAY,CAAC,EAAG,CACvE,GAAGO,EACH,gBACJ,CAAC,CACL,C"}
1
+ {"version":3,"file":"ticket-event-stream.cjs","names":["isApiError","isRetriableStatus","sessionIdentityOf","createEventStream","useCallback","useSyncExternalStore","useEventStream","require_errors","require_session_identity","require_create_event_stream","require_use_event_stream","react","isRetriableTicketError","error","ticketStreamUrl","source","response","client","prefix","ticketParam","createTicketEventStream","rest","options","identity","sessionKey","controller","followSession","bound","unsubscribe","follow","next","useClientSession","subscribe","listener","read","useTicketEventStream","clientSession","enabled"],"sources":["../../src/sse/ticket-event-stream.ts"],"sourcesContent":["import { useCallback, useSyncExternalStore } from \"react\";\nimport { sessionIdentityOf, type SessionIdentity } from \"../auth/session-identity\";\nimport { isApiError, isRetriableStatus } from \"../http/errors\";\nimport type { ApiClient } from \"../http/types\";\nimport {\n createEventStream,\n type CreateEventStreamOptions,\n type EventStreamController,\n} from \"./create-event-stream\";\nimport {\n useEventStream,\n type UseEventStreamOptions,\n type UseEventStreamResult,\n} from \"./use-event-stream\";\n\n/** Body of `POST <prefix>/ticket` on the `tempest-fastapi-sdk` SSE router. */\nexport interface SseTicketResponse {\n /** Signed stream ticket, presented as a query param when opening the stream. */\n ticket: string;\n /** Seconds the ticket is accepted for opening a stream. */\n expires_in: number;\n}\n\n/** Where the ticket comes from and how the stream URL is built. */\nexport interface TicketEventStreamSource {\n /**\n * Authenticated client the ticket is requested through, so it carries the\n * `Authorization` header or the session cookie, the CSRF header, and the\n * 401 refresh-and-replay that a hand-rolled `fetch` would lose.\n *\n * The `api` of `createTempestAuth` also carries the session it acts for,\n * and the stream binds to it when `sessionKey` is not passed — see\n * {@link useTicketEventStream}.\n */\n client: Pick<ApiClient, \"post\" | \"url\">;\n /** Router prefix, relative to the client's base URL. Default: `\"/sse\"`. */\n prefix?: string;\n /** Query param the stream reads the ticket from. Default: `\"ticket\"`. */\n ticketParam?: string;\n}\n\nexport type CreateTicketEventStreamOptions<T> = TicketEventStreamSource &\n CreateEventStreamOptions<T>;\n\nexport type UseTicketEventStreamOptions<T> = TicketEventStreamSource & UseEventStreamOptions<T>;\n\n/**\n * Whether a failure to obtain a ticket is worth another attempt.\n *\n * Only a failure the backoff can outlast is: no response at all, a timeout, a\n * rate limit, a `5xx`. A `401` reached the caller after the client already\n * tried its refresh, and a `403` (`ACCOUNT_INACTIVE`) is the backend saying no;\n * both are session decisions, and retrying them only delays the error state the\n * app has to show. Anything that is not an API error is a contract mismatch,\n * which a retry cannot fix either.\n *\n * @param error - What the ticket request threw.\n * @returns Whether to schedule a reconnect.\n */\nexport function isRetriableTicketError(error: unknown): boolean {\n return isApiError(error) && isRetriableStatus(error.status);\n}\n\n/**\n * Build the URL function that requests a ticket and points at the stream.\n *\n * A fresh ticket is requested for every connection rather than cached for its\n * `expires_in`. Every connection after the first follows a failure, and the\n * failure may have been the ticket itself; reusing it would spend the retries\n * on a credential that is already refused, while a new ticket costs one `POST`\n * per reconnect, which the backoff already spaces out.\n *\n * @param source - Client, router prefix and ticket param name.\n * @returns A function resolving the stream URL with a fresh ticket.\n * @throws TypeError When the ticket response has no `ticket` string.\n */\nfunction ticketStreamUrl(source: TicketEventStreamSource): () => Promise<string> {\n const { client, prefix = \"/sse\", ticketParam = \"ticket\" } = source;\n return async () => {\n const response = await client.post<SseTicketResponse>(`${prefix}/ticket`);\n if (typeof response?.ticket !== \"string\" || response.ticket === \"\") {\n throw new TypeError(\n `createTicketEventStream: POST ${prefix}/ticket returned no \"ticket\" string.`,\n );\n }\n return client.url(`${prefix}/stream`, { [ticketParam]: response.ticket });\n };\n}\n\n/**\n * Open the ticket-authenticated stream of the `tempest-fastapi-sdk` SSE router.\n *\n * `POST <prefix>/ticket` through the authenticated client, then\n * `GET <prefix>/stream?ticket=…` — and again for every reconnect, because the\n * ticket only opens streams for `expires_in` seconds and a reconnect after that\n * with the old one is a `401`. A ticket request refused with a status no\n * backoff fixes (`401`, `403 ACCOUNT_INACTIVE`) stops the stream at\n * `status: \"error\"` and reaches `onUrlError`; pass `shouldRetryUrl` to replace\n * {@link isRetriableTicketError}.\n *\n * The access token never enters the URL: it travels as the client's header (or\n * cookie) on the ticket request, and the ticket in the URL opens that stream\n * and nothing else.\n *\n * With the `api` of `createTempestAuth` as `client` and no `sessionKey`, the\n * stream follows that preset's session (since 0.82.0): it closes the moment\n * the session is replaced or ends, reopens with a fresh ticket for the next\n * one, stays closed while nobody is signed in, and ignores token rotation.\n * Any other client binds only through `sessionKey`, checked per frame.\n *\n * @example\n * const stream = createTicketEventStream<Notification>({\n * client: auth.api,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @example\n * // A client createTempestAuth did not build\n * const stream = createTicketEventStream<Notification>({\n * client: api,\n * sessionKey: () => useAuthStore.getState().user?.id ?? null,\n * onMessage: ({ data }) => addNotification(data),\n * onUrlError: (error) => toast.error(describeApiError(error)),\n * });\n *\n * @param options - Ticket source plus every {@link createEventStream} option.\n * @returns The stream controller.\n */\nexport function createTicketEventStream<T = unknown>(\n options: CreateTicketEventStreamOptions<T>,\n): EventStreamController {\n const {\n client,\n prefix,\n ticketParam,\n sessionKey,\n shouldRetryUrl = isRetriableTicketError,\n ...rest\n } = options;\n const identity = sessionKey === undefined ? sessionIdentityOf(client) : undefined;\n const controller = createEventStream<T>(ticketStreamUrl({ client, prefix, ticketParam }), {\n ...rest,\n sessionKey: sessionKey ?? identity?.current,\n shouldRetryUrl,\n });\n return identity ? followSession(controller, identity) : controller;\n}\n\n/**\n * Close the stream the moment the session it opened under ends, instead of\n * at its next frame, and reopen it for the session that follows.\n *\n * Signed out, the stream stays closed: a ticket request with no session only\n * comes back `401`. Following stops at `close()`; `reconnect()` resumes it.\n *\n * @param controller - The stream, already bound to `identity` frame by frame.\n * @param identity - The session of the client the tickets go through.\n * @returns A controller whose `close()` also stops following.\n */\nfunction followSession(\n controller: EventStreamController,\n identity: SessionIdentity,\n): EventStreamController {\n let bound = identity.current();\n let unsubscribe: (() => void) | null = null;\n function follow(): void {\n unsubscribe ??= identity.subscribe(() => {\n const next = identity.current();\n if (Object.is(next, bound)) return;\n bound = next;\n if (next === null) controller.close();\n else controller.reconnect();\n });\n }\n follow();\n if (bound === null) controller.close();\n return {\n close: () => {\n unsubscribe?.();\n unsubscribe = null;\n controller.close();\n },\n reconnect: () => {\n bound = identity.current();\n follow();\n controller.reconnect();\n },\n get status() {\n return controller.status;\n },\n };\n}\n\n/**\n * Read the session key of a client `createTempestAuth` built, as React state.\n *\n * @param identity - The client's session, or `undefined` for any other client.\n * @returns The key, or `undefined` when the client carries no session.\n */\nfunction useClientSession(identity: SessionIdentity | undefined): string | null | undefined {\n const subscribe = useCallback(\n (listener: () => void) => identity?.subscribe(listener) ?? (() => undefined),\n [identity],\n );\n const read = (): string | null | undefined => identity?.current();\n return useSyncExternalStore(subscribe, read, read);\n}\n\n/**\n * React form of {@link createTicketEventStream}: same ticket per connection,\n * lifecycle tied to the component like {@link useEventStream}.\n *\n * Changing `client`, `prefix` or `ticketParam` does not drop an open stream:\n * the next connection uses the new values, and `reconnect()` applies them now.\n * Reopening on identity would reconnect on every render for a client built\n * inline.\n *\n * The stream is bound to a session: the ticket names the channel of the user\n * who asked for it, the server never ends an open stream on logout, and a tab\n * that switches accounts without `enabled` going `false` would otherwise keep\n * delivering the previous user's events. A change of session closes that\n * stream and asks a ticket for the new one.\n *\n * With the `api` of `createTempestAuth` as `client`, that binding is automatic\n * (since 0.82.0): the session is read from the client — `auth.sessionKey()`,\n * the lineage `login()` mints — so a switch closes the stream, a token\n * rotation of the same session does not, and while nobody is signed in the\n * stream stays `idle` without asking for a ticket. Any other client keeps the\n * behaviour it had: pass `sessionKey` (the signed-in user's id). A\n * `sessionKey` passed explicitly always wins over the client's.\n *\n * @example\n * const { status } = useTicketEventStream<Notification>({\n * client: auth.api,\n * schema: notificationSchema,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @example\n * // A client createTempestAuth did not build\n * const { status } = useTicketEventStream<Notification>({\n * client: api,\n * enabled: isAuthenticated,\n * sessionKey: user?.id ?? null,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @param options - Ticket source plus every {@link useEventStream} option.\n * @returns Status, last message and `reconnect`.\n */\nexport function useTicketEventStream<T = unknown>(\n options: UseTicketEventStreamOptions<T>,\n): UseEventStreamResult<T> {\n const {\n client,\n prefix,\n ticketParam,\n enabled = true,\n sessionKey,\n shouldRetryUrl = isRetriableTicketError,\n ...rest\n } = options;\n const identity = sessionKey === undefined ? sessionIdentityOf(client) : undefined;\n const clientSession = useClientSession(identity);\n return useEventStream<T>(ticketStreamUrl({ client, prefix, ticketParam }), {\n ...rest,\n enabled: enabled && clientSession !== null,\n sessionKey: sessionKey ?? clientSession,\n shouldRetryUrl,\n });\n}\n"],"mappings":"A,kLA2DA,SAAgBY,EAAuB,EAAyB,CAC5D,OAAOZ,EAAAA,WAAWa,CAAK,GAAKZ,EAAAA,kBAAkBY,EAAM,MAAM,CAC9D,CAeA,SAASC,EAAgB,EAAwD,CAC7E,GAAM,CAAE,SAAQ,SAAS,OAAQ,cAAc,UAAaC,EAC5D,OAAO,SAAY,CACf,IAAMC,EAAW,MAAMC,EAAO,KAAwB,GAAGC,EAAO,QAAQ,EACxE,GAAI,OAAOF,GAAU,QAAW,UAAYA,EAAS,SAAW,GAC5D,MAAU,UACN,iCAAiCE,EAAO,qCAC5C,EAEJ,OAAOD,EAAO,IAAI,GAAGC,EAAO,SAAU,EAAGC,GAAcH,EAAS,MAAO,CAAC,CAC5E,CACJ,CAyCA,SAAgBI,EACZ,EACqB,CACrB,GAAM,CACF,SACA,SACA,cACA,aACA,iBAAiBR,EACjB,GAAGS,GACHC,EACEC,EAAWC,IAAe,IAAA,GAAYtB,EAAAA,kBAAkBe,CAAM,EAAI,IAAA,GAClEQ,EAAatB,EAAAA,kBAAqBW,EAAgB,CAAE,SAAQ,SAAQ,aAAY,CAAC,EAAG,CACtF,GAAGO,EACH,WAAYG,GAAcD,GAAU,QACpC,gBACJ,CAAC,EACD,OAAOA,EAAWG,EAAcD,EAAYF,CAAQ,EAAIE,CAC5D,CAaA,SAASC,EACL,EACA,EACqB,CACrB,IAAIC,EAAQJ,EAAS,QAAQ,EACzBK,EAAmC,KACvC,SAASC,GAAe,CACpB,IAAgBN,EAAS,cAAgB,CACrC,IAAMO,EAAOP,EAAS,QAAQ,EAC1B,OAAO,GAAGO,EAAMH,CAAK,IACzB,EAAQG,EACJA,IAAS,KAAML,EAAW,MAAM,EAC/BA,EAAW,UAAU,EAC9B,CAAC,CACL,CAGA,OAFAI,EAAO,EACHF,IAAU,MAAMF,EAAW,MAAM,EAC9B,CACH,UAAa,CACTG,IAAc,EACd,EAAc,KACdH,EAAW,MAAM,CACrB,EACA,cAAiB,CACb,EAAQF,EAAS,QAAQ,EACzBM,EAAO,EACPJ,EAAW,UAAU,CACzB,EACA,IAAI,QAAS,CACT,OAAOA,EAAW,MACtB,CACJ,CACJ,CAQA,SAASM,EAAiB,EAAkE,CACxF,IAAMC,GAAA,EAAY5B,EAAAA,YAAAA,CACb,GAAyBmB,GAAU,UAAUU,CAAQ,QAAY,IAAA,IAClE,CAACV,CAAQ,CACb,EACMW,MAAwCX,GAAU,QAAQ,EAChE,OAAA,EAAOlB,EAAAA,qBAAAA,CAAqB2B,EAAWE,EAAMA,CAAI,CACrD,CA4CA,SAAgBC,EACZ,EACuB,CACvB,GAAM,CACF,SACA,SACA,cACA,UAAU,GACV,aACA,iBAAiBvB,EACjB,GAAGS,GACHC,EAEEc,EAAgBL,EADLP,IAAe,IAAA,GAAYtB,EAAAA,kBAAkBe,CAAM,EAAI,IAAA,EACzB,EAC/C,OAAOX,EAAAA,eAAkBQ,EAAgB,CAAE,SAAQ,SAAQ,aAAY,CAAC,EAAG,CACvE,GAAGO,EACH,QAASgB,GAAWD,IAAkB,KACtC,WAAYZ,GAAcY,EAC1B,gBACJ,CAAC,CACL,C"}
@@ -1,11 +1,13 @@
1
1
  import { isApiError as e, isRetriableStatus as t } from "../http/errors.js";
2
- import { createEventStream as n } from "./create-event-stream.js";
3
- import { useEventStream as r } from "./use-event-stream.js";
2
+ import { sessionIdentityOf as n } from "../auth/session-identity.js";
3
+ import { createEventStream as r } from "./create-event-stream.js";
4
+ import { useEventStream as i } from "./use-event-stream.js";
5
+ import { useCallback as a, useSyncExternalStore as o } from "react";
4
6
  //#region src/sse/ticket-event-stream.ts
5
- function i(n) {
7
+ function s(n) {
6
8
  return e(n) && t(n.status);
7
9
  }
8
- function a(e) {
10
+ function c(e) {
9
11
  let { client: t, prefix: n = "/sse", ticketParam: r = "ticket" } = e;
10
12
  return async () => {
11
13
  let e = await t.post(`${n}/ticket`);
@@ -13,29 +15,56 @@ function a(e) {
13
15
  return t.url(`${n}/stream`, { [r]: e.ticket });
14
16
  };
15
17
  }
16
- function o(e) {
17
- let { client: t, prefix: r, ticketParam: o, shouldRetryUrl: s = i, ...c } = e;
18
- return n(a({
18
+ function l(e) {
19
+ let { client: t, prefix: i, ticketParam: a, sessionKey: o, shouldRetryUrl: l = s, ...d } = e, f = o === void 0 ? n(t) : void 0, p = r(c({
19
20
  client: t,
20
- prefix: r,
21
- ticketParam: o
21
+ prefix: i,
22
+ ticketParam: a
22
23
  }), {
23
- ...c,
24
- shouldRetryUrl: s
24
+ ...d,
25
+ sessionKey: o ?? f?.current,
26
+ shouldRetryUrl: l
25
27
  });
28
+ return f ? u(p, f) : p;
29
+ }
30
+ function u(e, t) {
31
+ let n = t.current(), r = null;
32
+ function i() {
33
+ r ??= t.subscribe(() => {
34
+ let r = t.current();
35
+ Object.is(r, n) || (n = r, r === null ? e.close() : e.reconnect());
36
+ });
37
+ }
38
+ return i(), n === null && e.close(), {
39
+ close: () => {
40
+ r?.(), r = null, e.close();
41
+ },
42
+ reconnect: () => {
43
+ n = t.current(), i(), e.reconnect();
44
+ },
45
+ get status() {
46
+ return e.status;
47
+ }
48
+ };
26
49
  }
27
- function s(e) {
28
- let { client: t, prefix: n, ticketParam: o, shouldRetryUrl: s = i, ...c } = e;
29
- return r(a({
50
+ function d(e) {
51
+ let t = a((t) => e?.subscribe(t) ?? (() => void 0), [e]), n = () => e?.current();
52
+ return o(t, n, n);
53
+ }
54
+ function f(e) {
55
+ let { client: t, prefix: r, ticketParam: a, enabled: o = !0, sessionKey: l, shouldRetryUrl: u = s, ...f } = e, p = d(l === void 0 ? n(t) : void 0);
56
+ return i(c({
30
57
  client: t,
31
- prefix: n,
32
- ticketParam: o
58
+ prefix: r,
59
+ ticketParam: a
33
60
  }), {
34
- ...c,
35
- shouldRetryUrl: s
61
+ ...f,
62
+ enabled: o && p !== null,
63
+ sessionKey: l ?? p,
64
+ shouldRetryUrl: u
36
65
  });
37
66
  }
38
67
  //#endregion
39
- export { o as createTicketEventStream, i as isRetriableTicketError, s as useTicketEventStream };
68
+ export { l as createTicketEventStream, s as isRetriableTicketError, f as useTicketEventStream };
40
69
 
41
70
  //# sourceMappingURL=ticket-event-stream.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ticket-event-stream.js","names":["isRetriableTicketError","isApiError","error","isRetriableStatus","ticketStreamUrl","source","response","client","prefix","ticketParam","createTicketEventStream","rest","options","createEventStream","useTicketEventStream","useEventStream"],"sources":["../../src/sse/ticket-event-stream.ts"],"sourcesContent":["import { isApiError, isRetriableStatus } from \"../http/errors\";\nimport type { ApiClient } from \"../http/types\";\nimport {\n createEventStream,\n type CreateEventStreamOptions,\n type EventStreamController,\n} from \"./create-event-stream\";\nimport {\n useEventStream,\n type UseEventStreamOptions,\n type UseEventStreamResult,\n} from \"./use-event-stream\";\n\n/** Body of `POST <prefix>/ticket` on the `tempest-fastapi-sdk` SSE router. */\nexport interface SseTicketResponse {\n /** Signed stream ticket, presented as a query param when opening the stream. */\n ticket: string;\n /** Seconds the ticket is accepted for opening a stream. */\n expires_in: number;\n}\n\n/** Where the ticket comes from and how the stream URL is built. */\nexport interface TicketEventStreamSource {\n /**\n * Authenticated client the ticket is requested through, so it carries the\n * `Authorization` header or the session cookie, the CSRF header, and the\n * 401 refresh-and-replay that a hand-rolled `fetch` would lose.\n */\n client: Pick<ApiClient, \"post\" | \"url\">;\n /** Router prefix, relative to the client's base URL. Default: `\"/sse\"`. */\n prefix?: string;\n /** Query param the stream reads the ticket from. Default: `\"ticket\"`. */\n ticketParam?: string;\n}\n\nexport type CreateTicketEventStreamOptions<T> = TicketEventStreamSource &\n CreateEventStreamOptions<T>;\n\nexport type UseTicketEventStreamOptions<T> = TicketEventStreamSource & UseEventStreamOptions<T>;\n\n/**\n * Whether a failure to obtain a ticket is worth another attempt.\n *\n * Only a failure the backoff can outlast is: no response at all, a timeout, a\n * rate limit, a `5xx`. A `401` reached the caller after the client already\n * tried its refresh, and a `403` (`ACCOUNT_INACTIVE`) is the backend saying no;\n * both are session decisions, and retrying them only delays the error state the\n * app has to show. Anything that is not an API error is a contract mismatch,\n * which a retry cannot fix either.\n *\n * @param error - What the ticket request threw.\n * @returns Whether to schedule a reconnect.\n */\nexport function isRetriableTicketError(error: unknown): boolean {\n return isApiError(error) && isRetriableStatus(error.status);\n}\n\n/**\n * Build the URL function that requests a ticket and points at the stream.\n *\n * A fresh ticket is requested for every connection rather than cached for its\n * `expires_in`. Every connection after the first follows a failure, and the\n * failure may have been the ticket itself; reusing it would spend the retries\n * on a credential that is already refused, while a new ticket costs one `POST`\n * per reconnect, which the backoff already spaces out.\n *\n * @param source - Client, router prefix and ticket param name.\n * @returns A function resolving the stream URL with a fresh ticket.\n * @throws TypeError When the ticket response has no `ticket` string.\n */\nfunction ticketStreamUrl(source: TicketEventStreamSource): () => Promise<string> {\n const { client, prefix = \"/sse\", ticketParam = \"ticket\" } = source;\n return async () => {\n const response = await client.post<SseTicketResponse>(`${prefix}/ticket`);\n if (typeof response?.ticket !== \"string\" || response.ticket === \"\") {\n throw new TypeError(\n `createTicketEventStream: POST ${prefix}/ticket returned no \"ticket\" string.`,\n );\n }\n return client.url(`${prefix}/stream`, { [ticketParam]: response.ticket });\n };\n}\n\n/**\n * Open the ticket-authenticated stream of the `tempest-fastapi-sdk` SSE router.\n *\n * `POST <prefix>/ticket` through the authenticated client, then\n * `GET <prefix>/stream?ticket=…` — and again for every reconnect, because the\n * ticket only opens streams for `expires_in` seconds and a reconnect after that\n * with the old one is a `401`. A ticket request refused with a status no\n * backoff fixes (`401`, `403 ACCOUNT_INACTIVE`) stops the stream at\n * `status: \"error\"` and reaches `onUrlError`; pass `shouldRetryUrl` to replace\n * {@link isRetriableTicketError}.\n *\n * The access token never enters the URL: it travels as the client's header (or\n * cookie) on the ticket request, and the ticket in the URL opens that stream\n * and nothing else.\n *\n * @example\n * const stream = createTicketEventStream<Notification>({\n * client: api,\n * onMessage: ({ data }) => addNotification(data),\n * onUrlError: (error) => toast.error(describeApiError(error)),\n * });\n *\n * @param options - Ticket source plus every {@link createEventStream} option.\n * @returns The stream controller.\n */\nexport function createTicketEventStream<T = unknown>(\n options: CreateTicketEventStreamOptions<T>,\n): EventStreamController {\n const {\n client,\n prefix,\n ticketParam,\n shouldRetryUrl = isRetriableTicketError,\n ...rest\n } = options;\n return createEventStream<T>(ticketStreamUrl({ client, prefix, ticketParam }), {\n ...rest,\n shouldRetryUrl,\n });\n}\n\n/**\n * React form of {@link createTicketEventStream}: same ticket per connection,\n * lifecycle tied to the component like {@link useEventStream}.\n *\n * Changing `client`, `prefix` or `ticketParam` does not drop an open stream:\n * the next connection uses the new values, and `reconnect()` applies them now.\n * Reopening on identity would reconnect on every render for a client built\n * inline.\n *\n * @example\n * const { status } = useTicketEventStream<Notification>({\n * client: api,\n * enabled: isAuthenticated,\n * schema: notificationSchema,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @param options - Ticket source plus every {@link useEventStream} option.\n * @returns Status, last message and `reconnect`.\n */\nexport function useTicketEventStream<T = unknown>(\n options: UseTicketEventStreamOptions<T>,\n): UseEventStreamResult<T> {\n const {\n client,\n prefix,\n ticketParam,\n shouldRetryUrl = isRetriableTicketError,\n ...rest\n } = options;\n return useEventStream<T>(ticketStreamUrl({ client, prefix, ticketParam }), {\n ...rest,\n shouldRetryUrl,\n });\n}\n"],"mappings":";;;;AAqDA,SAAgBA,EAAuB,GAAyB;CAC5D,OAAOC,EAAWC,CAAK,KAAKC,EAAkBD,EAAM,MAAM;AAC9D;AAeA,SAASE,EAAgB,GAAwD;CAC7E,IAAM,EAAE,WAAQ,YAAS,QAAQ,iBAAc,aAAaC;CAC5D,OAAO,YAAY;EACf,IAAMC,IAAW,MAAMC,EAAO,KAAwB,GAAGC,EAAO,QAAQ;EACxE,IAAI,OAAOF,GAAU,UAAW,YAAYA,EAAS,WAAW,IAC5D,MAAU,UACN,iCAAiCE,EAAO,qCAC5C;EAEJ,OAAOD,EAAO,IAAI,GAAGC,EAAO,UAAU,GAAGC,IAAcH,EAAS,OAAO,CAAC;CAC5E;AACJ;AA2BA,SAAgBI,EACZ,GACqB;CACrB,IAAM,EACF,WACA,WACA,gBACA,oBAAiBV,GACjB,GAAGW,MACHC;CACJ,OAAOC,EAAqBT,EAAgB;EAAE;EAAQ;EAAQ;CAAY,CAAC,GAAG;EAC1E,GAAGO;EACH;CACJ,CAAC;AACL;AAsBA,SAAgBG,EACZ,GACuB;CACvB,IAAM,EACF,WACA,WACA,gBACA,oBAAiBd,GACjB,GAAGW,MACHC;CACJ,OAAOG,EAAkBX,EAAgB;EAAE;EAAQ;EAAQ;CAAY,CAAC,GAAG;EACvE,GAAGO;EACH;CACJ,CAAC;AACL"}
1
+ {"version":3,"file":"ticket-event-stream.js","names":["isRetriableTicketError","isApiError","error","isRetriableStatus","ticketStreamUrl","source","response","client","prefix","ticketParam","createTicketEventStream","rest","options","identity","sessionKey","sessionIdentityOf","controller","createEventStream","followSession","bound","unsubscribe","follow","next","useClientSession","subscribe","useCallback","listener","read","useSyncExternalStore","useTicketEventStream","clientSession","useEventStream","enabled"],"sources":["../../src/sse/ticket-event-stream.ts"],"sourcesContent":["import { useCallback, useSyncExternalStore } from \"react\";\nimport { sessionIdentityOf, type SessionIdentity } from \"../auth/session-identity\";\nimport { isApiError, isRetriableStatus } from \"../http/errors\";\nimport type { ApiClient } from \"../http/types\";\nimport {\n createEventStream,\n type CreateEventStreamOptions,\n type EventStreamController,\n} from \"./create-event-stream\";\nimport {\n useEventStream,\n type UseEventStreamOptions,\n type UseEventStreamResult,\n} from \"./use-event-stream\";\n\n/** Body of `POST <prefix>/ticket` on the `tempest-fastapi-sdk` SSE router. */\nexport interface SseTicketResponse {\n /** Signed stream ticket, presented as a query param when opening the stream. */\n ticket: string;\n /** Seconds the ticket is accepted for opening a stream. */\n expires_in: number;\n}\n\n/** Where the ticket comes from and how the stream URL is built. */\nexport interface TicketEventStreamSource {\n /**\n * Authenticated client the ticket is requested through, so it carries the\n * `Authorization` header or the session cookie, the CSRF header, and the\n * 401 refresh-and-replay that a hand-rolled `fetch` would lose.\n *\n * The `api` of `createTempestAuth` also carries the session it acts for,\n * and the stream binds to it when `sessionKey` is not passed — see\n * {@link useTicketEventStream}.\n */\n client: Pick<ApiClient, \"post\" | \"url\">;\n /** Router prefix, relative to the client's base URL. Default: `\"/sse\"`. */\n prefix?: string;\n /** Query param the stream reads the ticket from. Default: `\"ticket\"`. */\n ticketParam?: string;\n}\n\nexport type CreateTicketEventStreamOptions<T> = TicketEventStreamSource &\n CreateEventStreamOptions<T>;\n\nexport type UseTicketEventStreamOptions<T> = TicketEventStreamSource & UseEventStreamOptions<T>;\n\n/**\n * Whether a failure to obtain a ticket is worth another attempt.\n *\n * Only a failure the backoff can outlast is: no response at all, a timeout, a\n * rate limit, a `5xx`. A `401` reached the caller after the client already\n * tried its refresh, and a `403` (`ACCOUNT_INACTIVE`) is the backend saying no;\n * both are session decisions, and retrying them only delays the error state the\n * app has to show. Anything that is not an API error is a contract mismatch,\n * which a retry cannot fix either.\n *\n * @param error - What the ticket request threw.\n * @returns Whether to schedule a reconnect.\n */\nexport function isRetriableTicketError(error: unknown): boolean {\n return isApiError(error) && isRetriableStatus(error.status);\n}\n\n/**\n * Build the URL function that requests a ticket and points at the stream.\n *\n * A fresh ticket is requested for every connection rather than cached for its\n * `expires_in`. Every connection after the first follows a failure, and the\n * failure may have been the ticket itself; reusing it would spend the retries\n * on a credential that is already refused, while a new ticket costs one `POST`\n * per reconnect, which the backoff already spaces out.\n *\n * @param source - Client, router prefix and ticket param name.\n * @returns A function resolving the stream URL with a fresh ticket.\n * @throws TypeError When the ticket response has no `ticket` string.\n */\nfunction ticketStreamUrl(source: TicketEventStreamSource): () => Promise<string> {\n const { client, prefix = \"/sse\", ticketParam = \"ticket\" } = source;\n return async () => {\n const response = await client.post<SseTicketResponse>(`${prefix}/ticket`);\n if (typeof response?.ticket !== \"string\" || response.ticket === \"\") {\n throw new TypeError(\n `createTicketEventStream: POST ${prefix}/ticket returned no \"ticket\" string.`,\n );\n }\n return client.url(`${prefix}/stream`, { [ticketParam]: response.ticket });\n };\n}\n\n/**\n * Open the ticket-authenticated stream of the `tempest-fastapi-sdk` SSE router.\n *\n * `POST <prefix>/ticket` through the authenticated client, then\n * `GET <prefix>/stream?ticket=…` — and again for every reconnect, because the\n * ticket only opens streams for `expires_in` seconds and a reconnect after that\n * with the old one is a `401`. A ticket request refused with a status no\n * backoff fixes (`401`, `403 ACCOUNT_INACTIVE`) stops the stream at\n * `status: \"error\"` and reaches `onUrlError`; pass `shouldRetryUrl` to replace\n * {@link isRetriableTicketError}.\n *\n * The access token never enters the URL: it travels as the client's header (or\n * cookie) on the ticket request, and the ticket in the URL opens that stream\n * and nothing else.\n *\n * With the `api` of `createTempestAuth` as `client` and no `sessionKey`, the\n * stream follows that preset's session (since 0.82.0): it closes the moment\n * the session is replaced or ends, reopens with a fresh ticket for the next\n * one, stays closed while nobody is signed in, and ignores token rotation.\n * Any other client binds only through `sessionKey`, checked per frame.\n *\n * @example\n * const stream = createTicketEventStream<Notification>({\n * client: auth.api,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @example\n * // A client createTempestAuth did not build\n * const stream = createTicketEventStream<Notification>({\n * client: api,\n * sessionKey: () => useAuthStore.getState().user?.id ?? null,\n * onMessage: ({ data }) => addNotification(data),\n * onUrlError: (error) => toast.error(describeApiError(error)),\n * });\n *\n * @param options - Ticket source plus every {@link createEventStream} option.\n * @returns The stream controller.\n */\nexport function createTicketEventStream<T = unknown>(\n options: CreateTicketEventStreamOptions<T>,\n): EventStreamController {\n const {\n client,\n prefix,\n ticketParam,\n sessionKey,\n shouldRetryUrl = isRetriableTicketError,\n ...rest\n } = options;\n const identity = sessionKey === undefined ? sessionIdentityOf(client) : undefined;\n const controller = createEventStream<T>(ticketStreamUrl({ client, prefix, ticketParam }), {\n ...rest,\n sessionKey: sessionKey ?? identity?.current,\n shouldRetryUrl,\n });\n return identity ? followSession(controller, identity) : controller;\n}\n\n/**\n * Close the stream the moment the session it opened under ends, instead of\n * at its next frame, and reopen it for the session that follows.\n *\n * Signed out, the stream stays closed: a ticket request with no session only\n * comes back `401`. Following stops at `close()`; `reconnect()` resumes it.\n *\n * @param controller - The stream, already bound to `identity` frame by frame.\n * @param identity - The session of the client the tickets go through.\n * @returns A controller whose `close()` also stops following.\n */\nfunction followSession(\n controller: EventStreamController,\n identity: SessionIdentity,\n): EventStreamController {\n let bound = identity.current();\n let unsubscribe: (() => void) | null = null;\n function follow(): void {\n unsubscribe ??= identity.subscribe(() => {\n const next = identity.current();\n if (Object.is(next, bound)) return;\n bound = next;\n if (next === null) controller.close();\n else controller.reconnect();\n });\n }\n follow();\n if (bound === null) controller.close();\n return {\n close: () => {\n unsubscribe?.();\n unsubscribe = null;\n controller.close();\n },\n reconnect: () => {\n bound = identity.current();\n follow();\n controller.reconnect();\n },\n get status() {\n return controller.status;\n },\n };\n}\n\n/**\n * Read the session key of a client `createTempestAuth` built, as React state.\n *\n * @param identity - The client's session, or `undefined` for any other client.\n * @returns The key, or `undefined` when the client carries no session.\n */\nfunction useClientSession(identity: SessionIdentity | undefined): string | null | undefined {\n const subscribe = useCallback(\n (listener: () => void) => identity?.subscribe(listener) ?? (() => undefined),\n [identity],\n );\n const read = (): string | null | undefined => identity?.current();\n return useSyncExternalStore(subscribe, read, read);\n}\n\n/**\n * React form of {@link createTicketEventStream}: same ticket per connection,\n * lifecycle tied to the component like {@link useEventStream}.\n *\n * Changing `client`, `prefix` or `ticketParam` does not drop an open stream:\n * the next connection uses the new values, and `reconnect()` applies them now.\n * Reopening on identity would reconnect on every render for a client built\n * inline.\n *\n * The stream is bound to a session: the ticket names the channel of the user\n * who asked for it, the server never ends an open stream on logout, and a tab\n * that switches accounts without `enabled` going `false` would otherwise keep\n * delivering the previous user's events. A change of session closes that\n * stream and asks a ticket for the new one.\n *\n * With the `api` of `createTempestAuth` as `client`, that binding is automatic\n * (since 0.82.0): the session is read from the client — `auth.sessionKey()`,\n * the lineage `login()` mints — so a switch closes the stream, a token\n * rotation of the same session does not, and while nobody is signed in the\n * stream stays `idle` without asking for a ticket. Any other client keeps the\n * behaviour it had: pass `sessionKey` (the signed-in user's id). A\n * `sessionKey` passed explicitly always wins over the client's.\n *\n * @example\n * const { status } = useTicketEventStream<Notification>({\n * client: auth.api,\n * schema: notificationSchema,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @example\n * // A client createTempestAuth did not build\n * const { status } = useTicketEventStream<Notification>({\n * client: api,\n * enabled: isAuthenticated,\n * sessionKey: user?.id ?? null,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @param options - Ticket source plus every {@link useEventStream} option.\n * @returns Status, last message and `reconnect`.\n */\nexport function useTicketEventStream<T = unknown>(\n options: UseTicketEventStreamOptions<T>,\n): UseEventStreamResult<T> {\n const {\n client,\n prefix,\n ticketParam,\n enabled = true,\n sessionKey,\n shouldRetryUrl = isRetriableTicketError,\n ...rest\n } = options;\n const identity = sessionKey === undefined ? sessionIdentityOf(client) : undefined;\n const clientSession = useClientSession(identity);\n return useEventStream<T>(ticketStreamUrl({ client, prefix, ticketParam }), {\n ...rest,\n enabled: enabled && clientSession !== null,\n sessionKey: sessionKey ?? clientSession,\n shouldRetryUrl,\n });\n}\n"],"mappings":";;;;;;AA2DA,SAAgBA,EAAuB,GAAyB;CAC5D,OAAOC,EAAWC,CAAK,KAAKC,EAAkBD,EAAM,MAAM;AAC9D;AAeA,SAASE,EAAgB,GAAwD;CAC7E,IAAM,EAAE,WAAQ,YAAS,QAAQ,iBAAc,aAAaC;CAC5D,OAAO,YAAY;EACf,IAAMC,IAAW,MAAMC,EAAO,KAAwB,GAAGC,EAAO,QAAQ;EACxE,IAAI,OAAOF,GAAU,UAAW,YAAYA,EAAS,WAAW,IAC5D,MAAU,UACN,iCAAiCE,EAAO,qCAC5C;EAEJ,OAAOD,EAAO,IAAI,GAAGC,EAAO,UAAU,GAAGC,IAAcH,EAAS,OAAO,CAAC;CAC5E;AACJ;AAyCA,SAAgBI,EACZ,GACqB;CACrB,IAAM,EACF,WACA,WACA,gBACA,eACA,oBAAiBV,GACjB,GAAGW,MACHC,GACEC,IAAWC,MAAe,KAAA,IAAYC,EAAkBR,CAAM,IAAI,KAAA,GAClES,IAAaC,EAAqBb,EAAgB;EAAE;EAAQ;EAAQ;CAAY,CAAC,GAAG;EACtF,GAAGO;EACH,YAAYG,KAAcD,GAAU;EACpC;CACJ,CAAC;CACD,OAAOA,IAAWK,EAAcF,GAAYH,CAAQ,IAAIG;AAC5D;AAaA,SAASE,EACL,GACA,GACqB;CACrB,IAAIC,IAAQN,EAAS,QAAQ,GACzBO,IAAmC;CACvC,SAASC,IAAe;EACpB,MAAgBR,EAAS,gBAAgB;GACrC,IAAMS,IAAOT,EAAS,QAAQ;GAC9B,AAAI,OAAO,GAAGS,GAAMH,CAAK,MACzB,IAAQG,GACJA,MAAS,OAAMN,EAAW,MAAM,IAC/BA,EAAW,UAAU;EAC9B,CAAC;CACL;CAGA,OAFAK,EAAO,GACHF,MAAU,QAAMH,EAAW,MAAM,GAC9B;EACH,aAAa;GAGT,AAFAI,IAAc,GACd,IAAc,MACdJ,EAAW,MAAM;EACrB;EACA,iBAAiB;GAGb,AAFA,IAAQH,EAAS,QAAQ,GACzBQ,EAAO,GACPL,EAAW,UAAU;EACzB;EACA,IAAI,SAAS;GACT,OAAOA,EAAW;EACtB;CACJ;AACJ;AAQA,SAASO,EAAiB,GAAkE;CACxF,IAAMC,IAAYC,GACb,MAAyBZ,GAAU,UAAUa,CAAQ,YAAY,KAAA,IAClE,CAACb,CAAQ,CACb,GACMc,UAAwCd,GAAU,QAAQ;CAChE,OAAOe,EAAqBJ,GAAWG,GAAMA,CAAI;AACrD;AA4CA,SAAgBE,EACZ,GACuB;CACvB,IAAM,EACF,WACA,WACA,gBACA,aAAU,IACV,eACA,oBAAiB7B,GACjB,GAAGW,MACHC,GAEEkB,IAAgBP,EADLT,MAAe,KAAA,IAAYC,EAAkBR,CAAM,IAAI,KAAA,CACzB;CAC/C,OAAOwB,EAAkB3B,EAAgB;EAAE;EAAQ;EAAQ;CAAY,CAAC,GAAG;EACvE,GAAGO;EACH,SAASqB,KAAWF,MAAkB;EACtC,YAAYhB,KAAcgB;EAC1B;CACJ,CAAC;AACL"}
@@ -1,2 +1,2 @@
1
- const e=require("../hooks/use-latest-ref.cjs"),t=require("./create-event-stream.cjs");let n=require("react");function r(r,i={}){let{enabled:a=!0,onMessage:o,...s}=i,[c,l]=(0,n.useState)(`idle`),[u,d]=(0,n.useState)(null),f=(0,n.useRef)(null),p=e.useLatestRef(o),m=e.useLatestRef(r),h=typeof r==`string`?r:null;return(0,n.useEffect)(()=>{if(!a||h===``){l(`idle`);return}let e=t.createEventStream(h??(()=>{let e=m.current;return typeof e==`string`?e:e()}),{...s,onStatusChange:l,onMessage:e=>{d(e),p.current?.(e)}});return f.current=e.reconnect,()=>{e.close(),f.current=null}},[h,a]),{status:c,lastMessage:u,reconnect:()=>f.current?.()}}exports.useEventStream=r;
1
+ const e=require("../hooks/use-latest-ref.cjs"),t=require("./create-event-stream.cjs");let n=require("react");function r(r,i={}){let{enabled:a=!0,sessionKey:o,onMessage:s,...c}=i,[l,u]=(0,n.useState)(`idle`),[d,f]=(0,n.useState)(null),p=(0,n.useRef)(null),m=(0,n.useRef)(o),h=e.useLatestRef(s),g=e.useLatestRef(r),_=e.useLatestRef(o),v=typeof r==`string`?r:null;return(0,n.useEffect)(()=>{if(Object.is(m.current,o)||(m.current=o,f(null)),!a||v===``){u(`idle`);return}let e=t.createEventStream(v??(()=>{let e=g.current;return typeof e==`string`?e:e()}),{...c,sessionKey:()=>_.current,onStatusChange:u,onMessage:e=>{f(e),h.current?.(e)}});return p.current=e.reconnect,()=>{e.close(),p.current=null}},[v,a,o]),{status:l,lastMessage:d,reconnect:()=>p.current?.()}}exports.useEventStream=r;
2
2
  //# sourceMappingURL=use-event-stream.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-event-stream.cjs","names":["useState","useRef","useLatestRef","createEventStream","require_use_latest_ref","require_create_event_stream","react","useEventStream","rest","options","status","setStatus","lastMessage","setLastMessage","reconnectRef","onMessageRef","onMessage","urlRef","url","fixedUrl","enabled","controller","latest","message"],"sources":["../../src/sse/use-event-stream.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport {\n createEventStream,\n type CreateEventStreamOptions,\n type EventStreamMessage,\n type EventStreamStatus,\n type EventStreamUrl,\n} from \"./create-event-stream\";\n\nexport interface UseEventStreamOptions<T> extends Omit<\n CreateEventStreamOptions<T>,\n \"onStatusChange\"\n> {\n /** When false, the stream is not opened. Useful for \"wait for auth\". Default: true. */\n enabled?: boolean;\n}\n\nexport interface UseEventStreamResult<T> {\n status: EventStreamStatus;\n /** Last message received (excluding heartbeats). */\n lastMessage: EventStreamMessage<T> | null;\n /** Force a reconnect. */\n reconnect: () => void;\n}\n\n/**\n * React hook wrapper around {@link createEventStream}. Connection lifecycle is\n * tied to the component (and the `url`/`enabled` dependencies); the stream\n * closes on unmount.\n *\n * A string `url` reopens the stream when it changes. A function `url` does not:\n * an inline arrow is a new function every render, and keying on its identity\n * would reconnect on every render. The latest function is the one called on the\n * next connection, so call `reconnect()` when what it resolves to must change now.\n *\n * Pass `schema` to have every frame validated before it reaches `onMessage`,\n * instead of trusting the type argument: without it `data` is announced as `T`\n * on the strength of the generic alone, which is a promise about the server\n * that TypeScript cannot keep. It is read when the stream opens, so declare it\n * outside the component rather than building one inline per render.\n *\n * @example\n * useEventStream<Notification>(`${API}/notifications/stream`, {\n * enabled: !!user,\n * withCredentials: true,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @example\n * // Validated: a frame that does not match never reaches `onMessage`\n * const notificationSchema = z.object({ id: z.string(), message: z.string() });\n *\n * useEventStream<Notification>(`${API}/notifications/stream`, {\n * schema: notificationSchema,\n * onValidationError: (issues) => logger.warn(\"stream drift\", { issues }),\n * onMessage: ({ data }) => addNotification(data),\n * });\n */\nexport function useEventStream<T = unknown>(\n url: EventStreamUrl,\n options: UseEventStreamOptions<T> = {},\n): UseEventStreamResult<T> {\n const { enabled = true, onMessage, ...rest } = options;\n const [status, setStatus] = useState<EventStreamStatus>(\"idle\");\n const [lastMessage, setLastMessage] = useState<EventStreamMessage<T> | null>(null);\n const reconnectRef = useRef<(() => void) | null>(null);\n\n const onMessageRef = useLatestRef(onMessage);\n const urlRef = useLatestRef(url);\n const fixedUrl = typeof url === \"string\" ? url : null;\n\n useEffect(() => {\n if (!enabled || fixedUrl === \"\") {\n setStatus(\"idle\");\n return;\n }\n\n const target: EventStreamUrl =\n fixedUrl ??\n (() => {\n const latest = urlRef.current;\n return typeof latest === \"string\" ? latest : latest();\n });\n const controller = createEventStream<T>(target, {\n ...rest,\n onStatusChange: setStatus,\n onMessage: (message) => {\n setLastMessage(message);\n onMessageRef.current?.(message);\n },\n });\n reconnectRef.current = controller.reconnect;\n\n return () => {\n controller.close();\n reconnectRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fixedUrl, enabled]);\n\n return {\n status,\n lastMessage,\n reconnect: () => reconnectRef.current?.(),\n };\n}\n"],"mappings":"A,6GA2DA,SAAgBO,EACZ,EACA,EAAoC,CAAC,EACd,CACvB,GAAM,CAAE,UAAU,GAAM,YAAW,GAAGC,GAASC,EACzC,CAACC,EAAQC,IAAA,EAAaX,EAAAA,SAAAA,CAA4B,MAAM,EACxD,CAACY,EAAaC,IAAA,EAAkBb,EAAAA,SAAAA,CAAuC,IAAI,EAC3Ec,GAAA,EAAeb,EAAAA,OAAAA,CAA4B,IAAI,EAE/Cc,EAAeb,EAAAA,aAAac,CAAS,EACrCC,EAASf,EAAAA,aAAagB,CAAG,EACzBC,EAAW,OAAOD,GAAQ,SAAWA,EAAM,KA+BjD,OA7BA,EAAAZ,EAAA,UAAA,KAAgB,CACZ,GAAI,CAACc,GAAWD,IAAa,GAAI,CAC7BR,EAAU,MAAM,EAChB,MACJ,CAQA,IAAMU,EAAalB,EAAAA,kBALfgB,QACO,CACH,IAAMG,EAASL,EAAO,QACtB,OAAO,OAAOK,GAAW,SAAWA,EAASA,EAAO,CACxD,GAC4C,CAC5C,GAAGd,EACH,eAAgBG,EAChB,UAAY,GAAY,CACpBE,EAAeU,CAAO,EACtBR,EAAa,UAAUQ,CAAO,CAClC,CACJ,CAAC,EAGD,MAFA,GAAa,QAAUF,EAAW,cAErB,CACTA,EAAW,MAAM,EACjB,EAAa,QAAU,IAC3B,CAEJ,EAAG,CAACF,EAAUC,CAAO,CAAC,EAEf,CACH,SACA,cACA,cAAiBN,EAAa,UAAU,CAC5C,CACJ,C"}
1
+ {"version":3,"file":"use-event-stream.cjs","names":["useState","useRef","useLatestRef","createEventStream","require_use_latest_ref","require_create_event_stream","react","useEventStream","rest","options","status","setStatus","lastMessage","setLastMessage","reconnectRef","lastSessionRef","sessionKey","onMessageRef","onMessage","urlRef","url","sessionKeyRef","fixedUrl","enabled","controller","latest","message"],"sources":["../../src/sse/use-event-stream.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport {\n createEventStream,\n type CreateEventStreamOptions,\n type EventStreamMessage,\n type EventStreamStatus,\n type EventStreamUrl,\n type SessionKey,\n} from \"./create-event-stream\";\n\nexport interface UseEventStreamOptions<T> extends Omit<\n CreateEventStreamOptions<T>,\n \"onStatusChange\" | \"sessionKey\"\n> {\n /** When false, the stream is not opened. Useful for \"wait for auth\". Default: true. */\n enabled?: boolean;\n /**\n * The session the stream belongs to — the signed-in user's id, typically.\n *\n * A change closes the stream, clears `lastMessage` and, while `enabled`,\n * opens a new connection for the new session (a fresh ticket, for\n * {@link useTicketEventStream}). Without it, a tab that switches accounts\n * without passing through `enabled: false` keeps the previous user's\n * stream open and delivers its events under the new user: the server does\n * not end an open stream on logout or revocation, so the client has to.\n *\n * A frame that arrives between the render that changed the key and the\n * effect that closes the stream is dropped as well. Default: `undefined`,\n * which never changes, so the stream follows `url` and `enabled` alone.\n */\n sessionKey?: SessionKey;\n}\n\nexport interface UseEventStreamResult<T> {\n status: EventStreamStatus;\n /** Last message received (excluding heartbeats). */\n lastMessage: EventStreamMessage<T> | null;\n /** Force a reconnect. */\n reconnect: () => void;\n}\n\n/**\n * React hook wrapper around {@link createEventStream}. Connection lifecycle is\n * tied to the component (and the `url`/`enabled`/`sessionKey` dependencies);\n * the stream closes on unmount.\n *\n * Pass `sessionKey` (the signed-in user's id) whenever the stream carries one\n * user's data: a change of account then closes the previous user's stream and\n * clears `lastMessage`, even when `enabled` never went `false` in between.\n *\n * A string `url` reopens the stream when it changes. A function `url` does not:\n * an inline arrow is a new function every render, and keying on its identity\n * would reconnect on every render. The latest function is the one called on the\n * next connection, so call `reconnect()` when what it resolves to must change now.\n *\n * Pass `schema` to have every frame validated before it reaches `onMessage`,\n * instead of trusting the type argument: without it `data` is announced as `T`\n * on the strength of the generic alone, which is a promise about the server\n * that TypeScript cannot keep. It is read when the stream opens, so declare it\n * outside the component rather than building one inline per render.\n *\n * @example\n * useEventStream<Notification>(`${API}/notifications/stream`, {\n * enabled: !!user,\n * withCredentials: true,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @example\n * // Validated: a frame that does not match never reaches `onMessage`\n * const notificationSchema = z.object({ id: z.string(), message: z.string() });\n *\n * useEventStream<Notification>(`${API}/notifications/stream`, {\n * schema: notificationSchema,\n * onValidationError: (issues) => logger.warn(\"stream drift\", { issues }),\n * onMessage: ({ data }) => addNotification(data),\n * });\n */\nexport function useEventStream<T = unknown>(\n url: EventStreamUrl,\n options: UseEventStreamOptions<T> = {},\n): UseEventStreamResult<T> {\n const { enabled = true, sessionKey, onMessage, ...rest } = options;\n const [status, setStatus] = useState<EventStreamStatus>(\"idle\");\n const [lastMessage, setLastMessage] = useState<EventStreamMessage<T> | null>(null);\n const reconnectRef = useRef<(() => void) | null>(null);\n const lastSessionRef = useRef<SessionKey>(sessionKey);\n\n const onMessageRef = useLatestRef(onMessage);\n const urlRef = useLatestRef(url);\n const sessionKeyRef = useLatestRef(sessionKey);\n const fixedUrl = typeof url === \"string\" ? url : null;\n\n useEffect(() => {\n if (!Object.is(lastSessionRef.current, sessionKey)) {\n lastSessionRef.current = sessionKey;\n setLastMessage(null);\n }\n if (!enabled || fixedUrl === \"\") {\n setStatus(\"idle\");\n return;\n }\n\n const target: EventStreamUrl =\n fixedUrl ??\n (() => {\n const latest = urlRef.current;\n return typeof latest === \"string\" ? latest : latest();\n });\n const controller = createEventStream<T>(target, {\n ...rest,\n sessionKey: () => sessionKeyRef.current,\n onStatusChange: setStatus,\n onMessage: (message) => {\n setLastMessage(message);\n onMessageRef.current?.(message);\n },\n });\n reconnectRef.current = controller.reconnect;\n\n return () => {\n controller.close();\n reconnectRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fixedUrl, enabled, sessionKey]);\n\n return {\n status,\n lastMessage,\n reconnect: () => reconnectRef.current?.(),\n };\n}\n"],"mappings":"A,6GA+EA,SAAgBO,EACZ,EACA,EAAoC,CAAC,EACd,CACvB,GAAM,CAAE,UAAU,GAAM,aAAY,YAAW,GAAGC,GAASC,EACrD,CAACC,EAAQC,IAAA,EAAaX,EAAAA,SAAAA,CAA4B,MAAM,EACxD,CAACY,EAAaC,IAAA,EAAkBb,EAAAA,SAAAA,CAAuC,IAAI,EAC3Ec,GAAA,EAAeb,EAAAA,OAAAA,CAA4B,IAAI,EAC/Cc,GAAA,EAAiBd,EAAAA,OAAAA,CAAmBe,CAAU,EAE9CC,EAAef,EAAAA,aAAagB,CAAS,EACrCC,EAASjB,EAAAA,aAAakB,CAAG,EACzBC,EAAgBnB,EAAAA,aAAac,CAAU,EACvCM,EAAW,OAAOF,GAAQ,SAAWA,EAAM,KAoCjD,OAlCA,EAAAd,EAAA,UAAA,KAAgB,CAKZ,GAJK,OAAO,GAAGS,EAAe,QAASC,CAAU,IAC7C,EAAe,QAAUA,EACzBH,EAAe,IAAI,GAEnB,CAACU,GAAWD,IAAa,GAAI,CAC7BX,EAAU,MAAM,EAChB,MACJ,CAQA,IAAMa,EAAarB,EAAAA,kBALfmB,QACO,CACH,IAAMG,EAASN,EAAO,QACtB,OAAO,OAAOM,GAAW,SAAWA,EAASA,EAAO,CACxD,GAC4C,CAC5C,GAAGjB,EACH,eAAkBa,EAAc,QAChC,eAAgBV,EAChB,UAAY,GAAY,CACpBE,EAAea,CAAO,EACtBT,EAAa,UAAUS,CAAO,CAClC,CACJ,CAAC,EAGD,MAFA,GAAa,QAAUF,EAAW,cAErB,CACTA,EAAW,MAAM,EACjB,EAAa,QAAU,IAC3B,CAEJ,EAAG,CAACF,EAAUC,EAASP,CAAU,CAAC,EAE3B,CACH,SACA,cACA,cAAiBF,EAAa,UAAU,CAC5C,CACJ,C"}
@@ -3,29 +3,34 @@ import { createEventStream as t } from "./create-event-stream.js";
3
3
  import { useEffect as n, useRef as r, useState as i } from "react";
4
4
  //#region src/sse/use-event-stream.ts
5
5
  function a(a, o = {}) {
6
- let { enabled: s = !0, onMessage: c, ...l } = o, [u, d] = i("idle"), [f, p] = i(null), m = r(null), h = e(c), g = e(a), _ = typeof a == "string" ? a : null;
6
+ let { enabled: s = !0, sessionKey: c, onMessage: l, ...u } = o, [d, f] = i("idle"), [p, m] = i(null), h = r(null), g = r(c), _ = e(l), v = e(a), y = e(c), b = typeof a == "string" ? a : null;
7
7
  return n(() => {
8
- if (!s || _ === "") {
9
- d("idle");
8
+ if (Object.is(g.current, c) || (g.current = c, m(null)), !s || b === "") {
9
+ f("idle");
10
10
  return;
11
11
  }
12
- let e = t(_ ?? (() => {
13
- let e = g.current;
12
+ let e = t(b ?? (() => {
13
+ let e = v.current;
14
14
  return typeof e == "string" ? e : e();
15
15
  }), {
16
- ...l,
17
- onStatusChange: d,
16
+ ...u,
17
+ sessionKey: () => y.current,
18
+ onStatusChange: f,
18
19
  onMessage: (e) => {
19
- p(e), h.current?.(e);
20
+ m(e), _.current?.(e);
20
21
  }
21
22
  });
22
- return m.current = e.reconnect, () => {
23
- e.close(), m.current = null;
23
+ return h.current = e.reconnect, () => {
24
+ e.close(), h.current = null;
24
25
  };
25
- }, [_, s]), {
26
- status: u,
27
- lastMessage: f,
28
- reconnect: () => m.current?.()
26
+ }, [
27
+ b,
28
+ s,
29
+ c
30
+ ]), {
31
+ status: d,
32
+ lastMessage: p,
33
+ reconnect: () => h.current?.()
29
34
  };
30
35
  }
31
36
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-event-stream.js","names":["useEventStream","rest","options","status","setStatus","useState","lastMessage","setLastMessage","reconnectRef","useRef","onMessageRef","useLatestRef","onMessage","urlRef","url","fixedUrl","useEffect","enabled","controller","createEventStream","latest","message"],"sources":["../../src/sse/use-event-stream.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport {\n createEventStream,\n type CreateEventStreamOptions,\n type EventStreamMessage,\n type EventStreamStatus,\n type EventStreamUrl,\n} from \"./create-event-stream\";\n\nexport interface UseEventStreamOptions<T> extends Omit<\n CreateEventStreamOptions<T>,\n \"onStatusChange\"\n> {\n /** When false, the stream is not opened. Useful for \"wait for auth\". Default: true. */\n enabled?: boolean;\n}\n\nexport interface UseEventStreamResult<T> {\n status: EventStreamStatus;\n /** Last message received (excluding heartbeats). */\n lastMessage: EventStreamMessage<T> | null;\n /** Force a reconnect. */\n reconnect: () => void;\n}\n\n/**\n * React hook wrapper around {@link createEventStream}. Connection lifecycle is\n * tied to the component (and the `url`/`enabled` dependencies); the stream\n * closes on unmount.\n *\n * A string `url` reopens the stream when it changes. A function `url` does not:\n * an inline arrow is a new function every render, and keying on its identity\n * would reconnect on every render. The latest function is the one called on the\n * next connection, so call `reconnect()` when what it resolves to must change now.\n *\n * Pass `schema` to have every frame validated before it reaches `onMessage`,\n * instead of trusting the type argument: without it `data` is announced as `T`\n * on the strength of the generic alone, which is a promise about the server\n * that TypeScript cannot keep. It is read when the stream opens, so declare it\n * outside the component rather than building one inline per render.\n *\n * @example\n * useEventStream<Notification>(`${API}/notifications/stream`, {\n * enabled: !!user,\n * withCredentials: true,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @example\n * // Validated: a frame that does not match never reaches `onMessage`\n * const notificationSchema = z.object({ id: z.string(), message: z.string() });\n *\n * useEventStream<Notification>(`${API}/notifications/stream`, {\n * schema: notificationSchema,\n * onValidationError: (issues) => logger.warn(\"stream drift\", { issues }),\n * onMessage: ({ data }) => addNotification(data),\n * });\n */\nexport function useEventStream<T = unknown>(\n url: EventStreamUrl,\n options: UseEventStreamOptions<T> = {},\n): UseEventStreamResult<T> {\n const { enabled = true, onMessage, ...rest } = options;\n const [status, setStatus] = useState<EventStreamStatus>(\"idle\");\n const [lastMessage, setLastMessage] = useState<EventStreamMessage<T> | null>(null);\n const reconnectRef = useRef<(() => void) | null>(null);\n\n const onMessageRef = useLatestRef(onMessage);\n const urlRef = useLatestRef(url);\n const fixedUrl = typeof url === \"string\" ? url : null;\n\n useEffect(() => {\n if (!enabled || fixedUrl === \"\") {\n setStatus(\"idle\");\n return;\n }\n\n const target: EventStreamUrl =\n fixedUrl ??\n (() => {\n const latest = urlRef.current;\n return typeof latest === \"string\" ? latest : latest();\n });\n const controller = createEventStream<T>(target, {\n ...rest,\n onStatusChange: setStatus,\n onMessage: (message) => {\n setLastMessage(message);\n onMessageRef.current?.(message);\n },\n });\n reconnectRef.current = controller.reconnect;\n\n return () => {\n controller.close();\n reconnectRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fixedUrl, enabled]);\n\n return {\n status,\n lastMessage,\n reconnect: () => reconnectRef.current?.(),\n };\n}\n"],"mappings":";;;;AA2DA,SAAgBA,EACZ,GACA,IAAoC,CAAC,GACd;CACvB,IAAM,EAAE,aAAU,IAAM,cAAW,GAAGC,MAASC,GACzC,CAACC,GAAQC,KAAaC,EAA4B,MAAM,GACxD,CAACC,GAAaC,KAAkBF,EAAuC,IAAI,GAC3EG,IAAeC,EAA4B,IAAI,GAE/CC,IAAeC,EAAaC,CAAS,GACrCC,IAASF,EAAaG,CAAG,GACzBC,IAAW,OAAOD,KAAQ,WAAWA,IAAM;CA+BjD,OA7BAE,QAAgB;EACZ,IAAI,CAACC,KAAWF,MAAa,IAAI;GAC7B,EAAU,MAAM;GAChB;EACJ;EAQA,IAAMG,IAAaC,EALfJ,YACO;GACH,IAAMK,IAASP,EAAO;GACtB,OAAO,OAAOO,KAAW,WAAWA,IAASA,EAAO;EACxD,IAC4C;GAC5C,GAAGnB;GACH,gBAAgBG;GAChB,YAAY,MAAY;IAEpB,AADAG,EAAec,CAAO,GACtBX,EAAa,UAAUW,CAAO;GAClC;EACJ,CAAC;EAGD,OAFA,EAAa,UAAUH,EAAW,iBAErB;GAET,AADAA,EAAW,MAAM,GACjB,EAAa,UAAU;EAC3B;CAEJ,GAAG,CAACH,GAAUE,CAAO,CAAC,GAEf;EACH;EACA;EACA,iBAAiBT,EAAa,UAAU;CAC5C;AACJ"}
1
+ {"version":3,"file":"use-event-stream.js","names":["useEventStream","rest","options","status","setStatus","useState","lastMessage","setLastMessage","reconnectRef","useRef","lastSessionRef","sessionKey","onMessageRef","useLatestRef","onMessage","urlRef","url","sessionKeyRef","fixedUrl","useEffect","enabled","controller","createEventStream","latest","message"],"sources":["../../src/sse/use-event-stream.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport {\n createEventStream,\n type CreateEventStreamOptions,\n type EventStreamMessage,\n type EventStreamStatus,\n type EventStreamUrl,\n type SessionKey,\n} from \"./create-event-stream\";\n\nexport interface UseEventStreamOptions<T> extends Omit<\n CreateEventStreamOptions<T>,\n \"onStatusChange\" | \"sessionKey\"\n> {\n /** When false, the stream is not opened. Useful for \"wait for auth\". Default: true. */\n enabled?: boolean;\n /**\n * The session the stream belongs to — the signed-in user's id, typically.\n *\n * A change closes the stream, clears `lastMessage` and, while `enabled`,\n * opens a new connection for the new session (a fresh ticket, for\n * {@link useTicketEventStream}). Without it, a tab that switches accounts\n * without passing through `enabled: false` keeps the previous user's\n * stream open and delivers its events under the new user: the server does\n * not end an open stream on logout or revocation, so the client has to.\n *\n * A frame that arrives between the render that changed the key and the\n * effect that closes the stream is dropped as well. Default: `undefined`,\n * which never changes, so the stream follows `url` and `enabled` alone.\n */\n sessionKey?: SessionKey;\n}\n\nexport interface UseEventStreamResult<T> {\n status: EventStreamStatus;\n /** Last message received (excluding heartbeats). */\n lastMessage: EventStreamMessage<T> | null;\n /** Force a reconnect. */\n reconnect: () => void;\n}\n\n/**\n * React hook wrapper around {@link createEventStream}. Connection lifecycle is\n * tied to the component (and the `url`/`enabled`/`sessionKey` dependencies);\n * the stream closes on unmount.\n *\n * Pass `sessionKey` (the signed-in user's id) whenever the stream carries one\n * user's data: a change of account then closes the previous user's stream and\n * clears `lastMessage`, even when `enabled` never went `false` in between.\n *\n * A string `url` reopens the stream when it changes. A function `url` does not:\n * an inline arrow is a new function every render, and keying on its identity\n * would reconnect on every render. The latest function is the one called on the\n * next connection, so call `reconnect()` when what it resolves to must change now.\n *\n * Pass `schema` to have every frame validated before it reaches `onMessage`,\n * instead of trusting the type argument: without it `data` is announced as `T`\n * on the strength of the generic alone, which is a promise about the server\n * that TypeScript cannot keep. It is read when the stream opens, so declare it\n * outside the component rather than building one inline per render.\n *\n * @example\n * useEventStream<Notification>(`${API}/notifications/stream`, {\n * enabled: !!user,\n * withCredentials: true,\n * onMessage: ({ data }) => addNotification(data),\n * });\n *\n * @example\n * // Validated: a frame that does not match never reaches `onMessage`\n * const notificationSchema = z.object({ id: z.string(), message: z.string() });\n *\n * useEventStream<Notification>(`${API}/notifications/stream`, {\n * schema: notificationSchema,\n * onValidationError: (issues) => logger.warn(\"stream drift\", { issues }),\n * onMessage: ({ data }) => addNotification(data),\n * });\n */\nexport function useEventStream<T = unknown>(\n url: EventStreamUrl,\n options: UseEventStreamOptions<T> = {},\n): UseEventStreamResult<T> {\n const { enabled = true, sessionKey, onMessage, ...rest } = options;\n const [status, setStatus] = useState<EventStreamStatus>(\"idle\");\n const [lastMessage, setLastMessage] = useState<EventStreamMessage<T> | null>(null);\n const reconnectRef = useRef<(() => void) | null>(null);\n const lastSessionRef = useRef<SessionKey>(sessionKey);\n\n const onMessageRef = useLatestRef(onMessage);\n const urlRef = useLatestRef(url);\n const sessionKeyRef = useLatestRef(sessionKey);\n const fixedUrl = typeof url === \"string\" ? url : null;\n\n useEffect(() => {\n if (!Object.is(lastSessionRef.current, sessionKey)) {\n lastSessionRef.current = sessionKey;\n setLastMessage(null);\n }\n if (!enabled || fixedUrl === \"\") {\n setStatus(\"idle\");\n return;\n }\n\n const target: EventStreamUrl =\n fixedUrl ??\n (() => {\n const latest = urlRef.current;\n return typeof latest === \"string\" ? latest : latest();\n });\n const controller = createEventStream<T>(target, {\n ...rest,\n sessionKey: () => sessionKeyRef.current,\n onStatusChange: setStatus,\n onMessage: (message) => {\n setLastMessage(message);\n onMessageRef.current?.(message);\n },\n });\n reconnectRef.current = controller.reconnect;\n\n return () => {\n controller.close();\n reconnectRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fixedUrl, enabled, sessionKey]);\n\n return {\n status,\n lastMessage,\n reconnect: () => reconnectRef.current?.(),\n };\n}\n"],"mappings":";;;;AA+EA,SAAgBA,EACZ,GACA,IAAoC,CAAC,GACd;CACvB,IAAM,EAAE,aAAU,IAAM,eAAY,cAAW,GAAGC,MAASC,GACrD,CAACC,GAAQC,KAAaC,EAA4B,MAAM,GACxD,CAACC,GAAaC,KAAkBF,EAAuC,IAAI,GAC3EG,IAAeC,EAA4B,IAAI,GAC/CC,IAAiBD,EAAmBE,CAAU,GAE9CC,IAAeC,EAAaC,CAAS,GACrCC,IAASF,EAAaG,CAAG,GACzBC,IAAgBJ,EAAaF,CAAU,GACvCO,IAAW,OAAOF,KAAQ,WAAWA,IAAM;CAoCjD,OAlCAG,QAAgB;EAKZ,IAJK,OAAO,GAAGT,EAAe,SAASC,CAAU,MAC7C,EAAe,UAAUA,GACzBJ,EAAe,IAAI,IAEnB,CAACa,KAAWF,MAAa,IAAI;GAC7B,EAAU,MAAM;GAChB;EACJ;EAQA,IAAMG,IAAaC,EALfJ,YACO;GACH,IAAMK,IAASR,EAAO;GACtB,OAAO,OAAOQ,KAAW,WAAWA,IAASA,EAAO;EACxD,IAC4C;GAC5C,GAAGtB;GACH,kBAAkBgB,EAAc;GAChC,gBAAgBb;GAChB,YAAY,MAAY;IAEpB,AADAG,EAAeiB,CAAO,GACtBZ,EAAa,UAAUY,CAAO;GAClC;EACJ,CAAC;EAGD,OAFA,EAAa,UAAUH,EAAW,iBAErB;GAET,AADAA,EAAW,MAAM,GACjB,EAAa,UAAU;EAC3B;CAEJ,GAAG;EAACH;EAAUE;EAAST;CAAU,CAAC,GAE3B;EACH;EACA;EACA,iBAAiBH,EAAa,UAAU;CAC5C;AACJ"}
@@ -1,2 +1,2 @@
1
- const e=require("../utils/dev-mode.cjs"),t=require("../http/credential-scope.cjs"),n=require("./credential-headers.cjs");function r(){return globalThis}var i=`tempest-bg-sync`,a=2,o=`requests`,s=`meta`,c=`epoch`,l=`tempest:bg-sync-credentials`,u=3e3,d=u*2;function f(e,t){let n=[],r=[];for(let[i,a]of e)t.has(i.toLowerCase())?r.push(i.toLowerCase()):n.push([i,a]);return{kept:n,stripped:r}}function p(e,t){let n=e.openCursor();n.onsuccess=()=>{let e=n.result;if(!e)return;let r=e.value,{kept:i,stripped:a}=f(r.headers,t);e.update({...r,headers:i,strippedHeaders:[...new Set([...r.strippedHeaders??[],...a])],ownerState:r.ownerState??`unknown`}),e.continue()}}function m(e,t=n.credentialHeaderSet()){return new Promise((n,r)=>{let i=indexedDB.open(e,a);i.onupgradeneeded=e=>{let n=i.result;n.objectStoreNames.contains(o)?e.oldVersion<2&&i.transaction&&p(i.transaction.objectStore(o),t):n.createObjectStore(o,{keyPath:`id`,autoIncrement:!0}),n.objectStoreNames.contains(s)||n.createObjectStore(s)},i.onsuccess=()=>{let e=i.result;e.onversionchange=()=>e.close(),n(e)},i.onerror=()=>r(i.error)})}function h(e){return new Promise((t,n)=>{e.oncomplete=()=>t(),e.onerror=()=>n(e.error),e.onabort=()=>n(e.error)})}async function g(e,t){let n=await m(e,t),r=n.transaction(s,`readonly`),i=r.objectStore(s).get(c);return await h(r),n.close(),i.result??0}async function _(e,t,n,r){let i=await m(e,r),a=i.transaction([o,s],`readwrite`),l,u=a.objectStore(s).get(c);return u.onsuccess=()=>{if((u.result??0)!==n)return;let e=a.objectStore(o).add(t);e.onsuccess=()=>{l=e.result}},await h(a),i.close(),l}async function v(e,t,n,r){let i=await m(e),a=i.transaction(o,`readwrite`),s=a.objectStore(o),c=s.get(t);c.onsuccess=()=>{let e=c.result;e&&(e.ownerState=n,n===`bound`&&(e.owner=r??null),s.put(e))},await h(a),i.close()}async function y(e,t){let n=await m(e,t),r=n.transaction(o,`readonly`),i=await new Promise((e,t)=>{let n=r.objectStore(o).getAll();n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)});return n.close(),i}async function b(e,t){let n=await m(e),r=n.transaction(o,`readonly`),i=r.objectStore(o).count(t);return await h(r),n.close(),i.result>0}async function x(e,t){let n=await m(e),r=n.transaction(o,`readwrite`);r.objectStore(o).delete(t),await h(r),n.close()}async function S(e,t,n,r){let i=await e.clone().arrayBuffer(),{kept:a,stripped:o}=f(e.headers.entries(),n);return{url:e.url,method:e.method,headers:a,body:i.byteLength>0?i:null,timestamp:t,credentials:e.credentials,strippedHeaders:o,ownerState:r}}function C(e,t){let{kept:n,stripped:r}=f(e.headers,t);return{request:new Request(e.url,{method:e.method,headers:n,body:e.body??void 0,credentials:e.credentials}),strippedHeaders:[...new Set([...e.strippedHeaders??[],...r])]}}async function w(e,t,n){let r=new MessageChannel,i=await Promise.race([new Promise(n=>{r.port1.onmessage=e=>n(e.data),e.postMessage({type:l,...t},[r.port2])}),n.then(()=>void 0)]);if(r.port1.close(),typeof i!=`object`||!i||!(`headers`in i))return;let{headers:a,owner:o}=i,s=`owner`in i;if(!(s&&typeof o!=`string`&&o!==null))return{headers:typeof a==`object`&&a?a:null,hasPrincipal:s,owner:s?o:null}}function T(){let e;return{promise:new Promise(t=>{e=setTimeout(t,u)}),cancel:()=>clearTimeout(e)}}async function E(e,t){let n=await e.clients?.matchAll({type:`window`})??[];if(n.length===0)return[];let r=T(),i=await Promise.all(n.map(e=>w(e,t,r.promise)));return r.cancel(),i.filter(e=>e!==void 0)}function D(t){let n=new Map,r=new Set,i=!1;return{ask(e){let r=`${e.method} ${e.url}`,i=n.get(r);return i||(i=E(t,{url:e.url,method:e.method,phase:`replay`}),n.set(r,i)),i},warnNoAnswer(){!i&&e.isDevBuild()&&(i=!0,console.warn(`[tempest-react-sdk/sw] background-sync: a queued request needs an open page to confirm who is signed in or to supply the current credentials, and no open page answered. Call provideBackgroundSyncCredentials() in the page; the entry stays pending.`))},warnOutOfScope(t){let n=new URL(t).origin;!r.has(n)&&e.isDevBuild()&&(r.add(n),console.warn(`[tempest-react-sdk/sw] background-sync: dropped a credentialed request to ${n}; list it in trustedOrigins to replay it.`))}}}function O(e,t){switch(t.ownerState){case`bound`:return e.find(e=>e.hasPrincipal&&e.owner===t.owner);case`unknown`:return e.every(e=>!e.hasPrincipal)?e[0]:void 0;case`unbound`:return e.find(e=>!e.hasPrincipal)??e[0]}}async function k(e,t,n,r){let i=n.strippedHeaders.length>0,a=n.ownerState===`unbound`||r===`lenient`&&n.ownerState===`unknown`;if(!i&&a)return t;let o=await e.ask(t);if(o.length===0)throw e.warnNoAnswer(),Error(`background-sync: no client answered`);let s=O(o,n);if(!s)return null;if(!i)return t;if(s.headers===null)return null;let c=new Map(Object.entries(s.headers).map(([e,t])=>[e.toLowerCase(),t]));if(n.strippedHeaders.some(e=>!c.has(e)))throw Error(`background-sync: the answer lacks a stripped credential header`);let l=new Headers(t.headers);for(let e of n.strippedHeaders)l.set(e,c.get(e));return new Request(t,{headers:l})}function A(e,t){return[...e.headers.keys()].some(e=>t.has(e))}async function j(e,t){let{sw:n,name:r,maxRetentionMs:i,credentialHeaders:a,beforeReplay:o,ownerPolicy:s,inCredentialScope:c}=e,l=D(n),u=o??((e,t)=>k(l,e,t,s)),f=await y(r,a),p=0;for(let e of f){if(e.id===void 0)continue;if(t-e.timestamp>i){await x(r,e.id);continue}let n=e.ownerState??`unknown`;if(n===`pending`&&t-e.timestamp<d){p+=1;continue}let o=n===`pending`?`unknown`:n;try{let{request:t,strippedHeaders:n}=C(e,a);if(n.length>0&&!c(t.url)){l.warnOutOfScope(t.url),await x(r,e.id);continue}let i=await u(t,{queuedAt:e.timestamp,strippedHeaders:n,ownerState:o,owner:o===`bound`?e.owner??null:void 0});if(!await b(r,e.id))continue;if(i===null){await x(r,e.id);continue}if(A(i,a)&&!c(i.url)){l.warnOutOfScope(i.url),await x(r,e.id);continue}let s=await fetch(i);s.ok||s.status>=400&&s.status<500?await x(r,e.id):p+=1}catch{p+=1}}if(p>0)throw Error(`background-sync: ${p} request(s) still pending`)}var M=new Map;function N(e){let t=M.get(e.name);if(t)return t;let n=j(e,Date.now()).finally(()=>{M.delete(e.name)});return M.set(e.name,n),n}function P(e,t,n){return e?typeof e==`function`?e(t,n):e.test(t.href):!0}async function F(e,t,n,r){let i=T(),a=await w(n,{url:r.url,method:r.method,phase:`enqueue`},i.promise);i.cancel(),await v(e,t,a?a.hasPrincipal?`bound`:`unbound`:`unknown`,a?.owner)}function I(e={}){let a=r(),{match:o,queueName:s=i,maxRetentionMinutes:c=1440,periodicSyncTag:l=`${e.queueName??i}-periodic`,credentialHeaders:u=n.DEFAULT_CREDENTIAL_HEADERS,beforeReplay:d,ownerPolicy:f=`strict`,trustedOrigins:p=[]}=e,m=n.credentialHeaderSet(u),h=globalThis.location?.href??``,v={sw:a,name:s,maxRetentionMs:c*60*1e3,credentialHeaders:m,beforeReplay:d,ownerPolicy:f,inCredentialScope:e=>t.isTrustedCredentialTarget(e,h,p)};a.addEventListener(`fetch`,e=>{let t=e.request;if(t.method===`GET`||t.method===`HEAD`)return;let n=new URL(t.url);if(!P(o,n,t))return;let r=g(s,m);r.catch(()=>void 0);let i=fetch(t.clone()),c=i.then(()=>void 0,async()=>{let n=e.clientId?await a.clients?.get(e.clientId):void 0,i=await S(t,Date.now(),m,n?`pending`:`unknown`),o=await _(s,i,await r,m);if(o!==void 0){try{await a.registration.sync?.register(s)}catch{}return n?{id:o,issuer:n}:void 0}});e.respondWith(i.catch(async e=>{throw await c,e})),e.waitUntil(c.then(e=>e?F(s,e.id,e.issuer,t):void 0).catch(()=>void 0))}),a.addEventListener(`sync`,e=>{e.tag===s&&e.waitUntil(N(v))}),a.addEventListener(`periodicsync`,e=>{e.tag===l&&e.waitUntil(N(v).catch(()=>void 0))}),a.addEventListener(`fetch`,e=>{a.registration.sync||e.request.method===`GET`&&e.waitUntil(N(v).catch(()=>void 0))})}async function L(e=i){if(typeof indexedDB>`u`)return!1;let t=await m(e),n=t.transaction([o,s],`readwrite`);n.objectStore(o).clear();let r=n.objectStore(s),a=r.get(c);return a.onsuccess=()=>{r.put((a.result??0)+1,c)},await h(n),t.close(),!0}async function R(e){if(e==null||e===``)return null;let t=await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(`tempest-bg-sync:${e}`));return[...new Uint8Array(t)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}function z(e,t={}){if(typeof navigator>`u`||!navigator.serviceWorker)return()=>void 0;let n=navigator.serviceWorker,{getPrincipal:r}=t,i=t=>{let n=t.data,i=t.ports[0];if(n?.type!==`tempest:bg-sync-credentials`||!i)return;let a={url:String(n.url),method:String(n.method),phase:n.phase===`enqueue`?`enqueue`:`replay`};(async()=>{try{let t={headers:a.phase===`enqueue`?null:await e(a)};r&&(t.owner=await R(await r())),i.postMessage(t)}catch{i.postMessage({error:!0})}})()};return n.addEventListener(`message`,i),()=>n.removeEventListener(`message`,i)}exports.BACKGROUND_SYNC_CREDENTIALS_MESSAGE=l,exports.clearBackgroundSyncQueue=L,exports.installBackgroundSync=I,exports.provideBackgroundSyncCredentials=z;
1
+ const e=require("../utils/dev-mode.cjs"),t=require("../http/credential-scope.cjs"),n=require("./credential-headers.cjs");function r(){return globalThis}var i=`tempest-bg-sync`,a=2,o=`requests`,s=`meta`,c=`epoch`,l=`tempest:bg-sync-credentials`,u=3e3,d=u*2;function f(e,t){let n=[],r=[];for(let[i,a]of e)t.has(i.toLowerCase())?r.push(i.toLowerCase()):n.push([i,a]);return{kept:n,stripped:r}}function p(e,t){let n=e.openCursor();n.onsuccess=()=>{let e=n.result;if(!e)return;let r=e.value,{kept:i,stripped:a}=f(r.headers,t);e.update({...r,headers:i,strippedHeaders:[...new Set([...r.strippedHeaders??[],...a])],ownerState:r.ownerState??`unknown`}),e.continue()}}function m(e,t=n.credentialHeaderSet()){return new Promise((n,r)=>{let i=indexedDB.open(e,a);i.onupgradeneeded=e=>{let n=i.result;n.objectStoreNames.contains(o)?e.oldVersion<2&&i.transaction&&p(i.transaction.objectStore(o),t):n.createObjectStore(o,{keyPath:`id`,autoIncrement:!0}),n.objectStoreNames.contains(s)||n.createObjectStore(s)},i.onsuccess=()=>{let e=i.result;e.onversionchange=()=>e.close(),n(e)},i.onerror=()=>r(i.error)})}function h(e){return new Promise((t,n)=>{e.oncomplete=()=>t(),e.onerror=()=>n(e.error),e.onabort=()=>n(e.error)})}async function g(e,t){let n=await m(e,t),r=n.transaction(s,`readonly`),i=r.objectStore(s).get(c);return await h(r),n.close(),i.result??0}async function _(e,t,n,r){let i=await m(e,r),a=i.transaction([o,s],`readwrite`),l,u=a.objectStore(s).get(c);return u.onsuccess=()=>{if((u.result??0)!==n)return;let e=a.objectStore(o).add(t);e.onsuccess=()=>{l=e.result}},await h(a),i.close(),l}async function v(e,t,n,r){let i=await m(e),a=i.transaction(o,`readwrite`),s=a.objectStore(o),c=s.get(t);c.onsuccess=()=>{let e=c.result;e&&(e.ownerState=n,n===`bound`&&(e.owner=r??null),s.put(e))},await h(a),i.close()}async function y(e,t){let n=await m(e,t),r=n.transaction(o,`readonly`),i=await new Promise((e,t)=>{let n=r.objectStore(o).getAll();n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)});return n.close(),i}async function b(e,t){let n=await m(e),r=n.transaction(o,`readonly`),i=r.objectStore(o).count(t);return await h(r),n.close(),i.result>0}async function x(e,t){let n=await m(e),r=n.transaction(o,`readwrite`);r.objectStore(o).delete(t),await h(r),n.close()}async function S(e,t,n,r){let i=await e.clone().arrayBuffer(),{kept:a,stripped:o}=f(e.headers.entries(),n);return{url:e.url,method:e.method,headers:a,body:i.byteLength>0?i:null,timestamp:t,credentials:e.credentials,strippedHeaders:o,ownerState:r}}function C(e,t){let{kept:n,stripped:r}=f(e.headers,t);return{request:new Request(e.url,{method:e.method,headers:n,body:e.body??void 0,credentials:e.credentials}),strippedHeaders:[...new Set([...e.strippedHeaders??[],...r])]}}async function w(e,t,n){let r=new MessageChannel,i=await Promise.race([new Promise(n=>{r.port1.onmessage=e=>n(e.data),e.postMessage({type:l,...t},[r.port2])}),n.then(()=>void 0)]);if(r.port1.close(),typeof i!=`object`||!i||!(`headers`in i))return;let{headers:a,owner:o}=i,s=`owner`in i;if(!(s&&typeof o!=`string`&&o!==null))return{headers:typeof a==`object`&&a?a:null,hasPrincipal:s,owner:s?o:null}}function T(){let e;return{promise:new Promise(t=>{e=setTimeout(t,u)}),cancel:()=>clearTimeout(e)}}async function E(e,t){let n=await e.clients?.matchAll({type:`window`})??[];if(n.length===0)return[];let r=T(),i=await Promise.all(n.map(e=>w(e,t,r.promise)));return r.cancel(),i.filter(e=>e!==void 0)}function D(t){let n=new Map,r=new Set,i=!1;return{ask(e){let r=`${e.method} ${e.url}`,i=n.get(r);return i||(i=E(t,{url:e.url,method:e.method,phase:`replay`}),n.set(r,i)),i},warnNoAnswer(){!i&&e.isDevBuild()&&(i=!0,console.warn(`[tempest-react-sdk/sw] background-sync: a queued request needs an open page to confirm who is signed in or to supply the current credentials, and no open page answered. Call provideBackgroundSyncCredentials() in the page; the entry stays pending.`))},warnOutOfScope(t){let n=new URL(t).origin;!r.has(n)&&e.isDevBuild()&&(r.add(n),console.warn(`[tempest-react-sdk/sw] background-sync: dropped a credentialed request to ${n}; list it in trustedOrigins to replay it.`))}}}function O(e,t){switch(t.ownerState){case`bound`:return e.find(e=>e.hasPrincipal&&e.owner===t.owner);case`unknown`:return e.every(e=>!e.hasPrincipal)?e[0]:void 0;case`unbound`:return e.find(e=>!e.hasPrincipal)??e[0]}}async function k(e,t,n,r){let i=n.strippedHeaders.length>0,a=n.ownerState===`unbound`||r===`lenient`&&n.ownerState===`unknown`;if(!i&&a)return t;let o=await e.ask(t);if(o.length===0)throw e.warnNoAnswer(),Error(`background-sync: no client answered`);let s=O(o,n);if(!s)return null;if(!i)return t;if(s.headers===null)return null;let c=new Map(Object.entries(s.headers).map(([e,t])=>[e.toLowerCase(),t]));if(n.strippedHeaders.some(e=>!c.has(e)))throw Error(`background-sync: the answer lacks a stripped credential header`);let l=new Headers(t.headers);for(let e of n.strippedHeaders)l.set(e,c.get(e));return new Request(t,{headers:l})}function A(e,t){return[...e.headers.keys()].some(e=>t.has(e))}async function j(e,t){let{sw:n,name:r,maxRetentionMs:i,credentialHeaders:a,beforeReplay:o,ownerPolicy:s,inCredentialScope:c}=e,l=D(n),u=o??((e,t)=>k(l,e,t,s)),f=await y(r,a),p=0;for(let e of f){if(e.id===void 0)continue;if(t-e.timestamp>i){await x(r,e.id);continue}let n=e.ownerState??`unknown`;if(n===`pending`&&t-e.timestamp<d){p+=1;continue}let o=n===`pending`?`unknown`:n;try{let{request:t,strippedHeaders:n}=C(e,a);if(n.length>0&&!c(t.url)){l.warnOutOfScope(t.url),await x(r,e.id);continue}let i=await u(t,{queuedAt:e.timestamp,strippedHeaders:n,ownerState:o,owner:o===`bound`?e.owner??null:void 0});if(!await b(r,e.id))continue;if(i===null){await x(r,e.id);continue}if(A(i,a)&&!c(i.url)){l.warnOutOfScope(i.url),await x(r,e.id);continue}let s=await fetch(i);s.ok||s.status>=400&&s.status<500?await x(r,e.id):p+=1}catch{p+=1}}if(p>0)throw Error(`background-sync: ${p} request(s) still pending`)}var M=new Map;function N(e){let t=M.get(e.name);if(t)return t;let n=j(e,Date.now()).finally(()=>{M.delete(e.name)});return M.set(e.name,n),n}function P(e,t,n){return e?typeof e==`function`?e(t,n):e.test(t.href):!0}async function F(e,t,n,r){let i=T(),a=await w(n,{url:r.url,method:r.method,phase:`enqueue`},i.promise);i.cancel(),await v(e,t,a?a.hasPrincipal?`bound`:`unbound`:`unknown`,a?.owner)}function I(e={}){let a=r(),{match:o,queueName:s=i,maxRetentionMinutes:c=1440,periodicSyncTag:l=`${e.queueName??i}-periodic`,credentialHeaders:u=n.DEFAULT_CREDENTIAL_HEADERS,beforeReplay:d,ownerPolicy:f=`strict`,trustedOrigins:p=[]}=e,m=n.credentialHeaderSet(u),h=globalThis.location?.href??``,v={sw:a,name:s,maxRetentionMs:c*60*1e3,credentialHeaders:m,beforeReplay:d,ownerPolicy:f,inCredentialScope:e=>t.isTrustedCredentialTarget(e,h,p)};a.addEventListener(`fetch`,e=>{let t=e.request;if(t.method===`GET`||t.method===`HEAD`)return;let n=new URL(t.url);if(!P(o,n,t))return;let r=g(s,m);r.catch(()=>void 0);let i=fetch(t.clone()),c=i.then(()=>void 0,async()=>{let n=e.clientId?await a.clients?.get(e.clientId):void 0,i=await S(t,Date.now(),m,n?`pending`:`unknown`),o=await _(s,i,await r,m);if(o!==void 0){try{await a.registration.sync?.register(s)}catch{}return n?{id:o,issuer:n}:void 0}});e.respondWith(i.catch(async e=>{throw await c,e})),e.waitUntil(c.then(e=>e?F(s,e.id,e.issuer,t):void 0).catch(()=>void 0))}),a.addEventListener(`sync`,e=>{e.tag===s&&e.waitUntil(N(v))}),a.addEventListener(`periodicsync`,e=>{e.tag===l&&e.waitUntil(N(v).catch(()=>void 0))}),a.addEventListener(`fetch`,e=>{a.registration.sync||e.request.method===`GET`&&e.waitUntil(N(v).catch(()=>void 0))})}async function L(e=i){if(typeof indexedDB>`u`)return!1;let t=await m(e),n=t.transaction([o,s],`readwrite`);n.objectStore(o).clear();let r=n.objectStore(s),a=r.get(c);return a.onsuccess=()=>{r.put((a.result??0)+1,c)},await h(n),t.close(),!0}async function R(e){if(e==null||e===``)return null;let t=await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(`tempest-bg-sync:${e}`));return[...new Uint8Array(t)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}function z(t,n={}){if(typeof navigator>`u`||!navigator.serviceWorker)return()=>void 0;let r=navigator.serviceWorker,{getPrincipal:i}=n;!i&&e.isDevBuild()&&console.warn("[tempest-react-sdk/sw] provideBackgroundSyncCredentials() without `getPrincipal`: queued mutations are not bound to the account that made them and replay with whichever session answers next, unless clearSessionData() runs on logout. Pass { getPrincipal: () => currentUserId } to bind them.");let a=e=>{let n=e.data,r=e.ports[0];if(n?.type!==`tempest:bg-sync-credentials`||!r)return;let a={url:String(n.url),method:String(n.method),phase:n.phase===`enqueue`?`enqueue`:`replay`};(async()=>{try{let e={headers:a.phase===`enqueue`?null:await t(a)};i&&(e.owner=await R(await i())),r.postMessage(e)}catch{r.postMessage({error:!0})}})()};return r.addEventListener(`message`,a),()=>r.removeEventListener(`message`,a)}exports.BACKGROUND_SYNC_CREDENTIALS_MESSAGE=l,exports.clearBackgroundSyncQueue=L,exports.installBackgroundSync=I,exports.provideBackgroundSyncCredentials=z;
2
2
  //# sourceMappingURL=background-sync.cjs.map