@ably/ai-transport 0.1.0 → 0.3.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 (221) hide show
  1. package/README.md +93 -111
  2. package/dist/ably-ai-transport.js +2401 -1387
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +116 -42
  7. package/dist/core/agent.d.ts +44 -0
  8. package/dist/core/channel-options.d.ts +57 -0
  9. package/dist/core/codec/codec-event.d.ts +9 -0
  10. package/dist/core/codec/decoder.d.ts +24 -24
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +10 -12
  13. package/dist/core/codec/field-bag.d.ts +85 -0
  14. package/dist/core/codec/fields.d.ts +141 -0
  15. package/dist/core/codec/index.d.ts +8 -2
  16. package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
  17. package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
  18. package/dist/core/codec/input-descriptors.d.ts +281 -0
  19. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  20. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  21. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  22. package/dist/core/codec/output-descriptors.d.ts +237 -0
  23. package/dist/core/codec/types.d.ts +470 -119
  24. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  25. package/dist/core/transport/agent-session.d.ts +10 -0
  26. package/dist/core/transport/agent-view.d.ts +296 -0
  27. package/dist/core/transport/client-session.d.ts +13 -0
  28. package/dist/core/transport/decode-fold.d.ts +55 -0
  29. package/dist/core/transport/headers.d.ts +121 -14
  30. package/dist/core/transport/index.d.ts +5 -6
  31. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  32. package/dist/core/transport/invocation.d.ts +74 -0
  33. package/dist/core/transport/load-history-pages.d.ts +71 -0
  34. package/dist/core/transport/load-history.d.ts +44 -0
  35. package/dist/core/transport/pipe-stream.d.ts +9 -9
  36. package/dist/core/transport/run-manager.d.ts +76 -0
  37. package/dist/core/transport/session-support.d.ts +55 -0
  38. package/dist/core/transport/tree.d.ts +523 -109
  39. package/dist/core/transport/types/agent.d.ts +375 -0
  40. package/dist/core/transport/types/client.d.ts +201 -0
  41. package/dist/core/transport/types/shared.d.ts +24 -0
  42. package/dist/core/transport/types/tree.d.ts +357 -0
  43. package/dist/core/transport/types/view.d.ts +249 -0
  44. package/dist/core/transport/types.d.ts +13 -553
  45. package/dist/core/transport/view.d.ts +390 -84
  46. package/dist/core/transport/wire-log.d.ts +102 -0
  47. package/dist/errors.d.ts +27 -10
  48. package/dist/index.d.ts +8 -9
  49. package/dist/logger.d.ts +12 -0
  50. package/dist/react/ably-ai-transport-react.js +1365 -1010
  51. package/dist/react/ably-ai-transport-react.js.map +1 -1
  52. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  53. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  54. package/dist/react/contexts/client-session-context.d.ts +37 -0
  55. package/dist/react/contexts/client-session-provider.d.ts +56 -0
  56. package/dist/react/create-session-hooks.d.ts +116 -0
  57. package/dist/react/index.d.ts +13 -12
  58. package/dist/react/internal/skipped-session.d.ts +8 -0
  59. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  60. package/dist/react/use-ably-messages.d.ts +17 -14
  61. package/dist/react/use-client-session.d.ts +81 -0
  62. package/dist/react/use-create-view.d.ts +14 -13
  63. package/dist/react/use-tree.d.ts +30 -15
  64. package/dist/react/use-view.d.ts +81 -50
  65. package/dist/utils.d.ts +48 -71
  66. package/dist/vercel/ably-ai-transport-vercel.js +3257 -2499
  67. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  68. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  69. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  70. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  71. package/dist/vercel/codec/events.d.ts +50 -0
  72. package/dist/vercel/codec/fields.d.ts +44 -0
  73. package/dist/vercel/codec/fold-content.d.ts +16 -0
  74. package/dist/vercel/codec/fold-data.d.ts +16 -0
  75. package/dist/vercel/codec/fold-input.d.ts +67 -0
  76. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  77. package/dist/vercel/codec/fold-text.d.ts +16 -0
  78. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  79. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  80. package/dist/vercel/codec/index.d.ts +7 -20
  81. package/dist/vercel/codec/inputs.d.ts +11 -0
  82. package/dist/vercel/codec/outputs.d.ts +11 -0
  83. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  84. package/dist/vercel/codec/reducer.d.ts +62 -0
  85. package/dist/vercel/codec/tool-transitions.d.ts +2 -8
  86. package/dist/vercel/codec/wire-data.d.ts +34 -0
  87. package/dist/vercel/index.d.ts +5 -5
  88. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2859 -9705
  89. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  90. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -45
  91. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  92. package/dist/vercel/react/contexts/chat-transport-context.d.ts +9 -7
  93. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  94. package/dist/vercel/react/index.d.ts +1 -2
  95. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  96. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  97. package/dist/vercel/run-end-reason.d.ts +84 -0
  98. package/dist/vercel/tool-part.d.ts +21 -0
  99. package/dist/vercel/transport/chat-transport.d.ts +41 -24
  100. package/dist/vercel/transport/index.d.ts +24 -20
  101. package/dist/vercel/transport/run-output-stream.d.ts +54 -0
  102. package/dist/version.d.ts +2 -0
  103. package/package.json +31 -24
  104. package/src/constants.ts +124 -51
  105. package/src/core/agent.ts +92 -0
  106. package/src/core/channel-options.ts +89 -0
  107. package/src/core/codec/codec-event.ts +27 -0
  108. package/src/core/codec/decoder.ts +202 -105
  109. package/src/core/codec/define-codec.ts +432 -0
  110. package/src/core/codec/encoder.ts +114 -107
  111. package/src/core/codec/field-bag.ts +142 -0
  112. package/src/core/codec/fields.ts +193 -0
  113. package/src/core/codec/index.ts +56 -6
  114. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  115. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  116. package/src/core/codec/input-descriptors.ts +373 -0
  117. package/src/core/codec/lifecycle-tracker.ts +10 -9
  118. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  119. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  120. package/src/core/codec/output-descriptors.ts +307 -0
  121. package/src/core/codec/types.ts +505 -126
  122. package/src/core/codec/well-known-inputs.ts +96 -0
  123. package/src/core/transport/agent-session.ts +1085 -0
  124. package/src/core/transport/agent-view.ts +738 -0
  125. package/src/core/transport/client-session.ts +780 -0
  126. package/src/core/transport/decode-fold.ts +101 -0
  127. package/src/core/transport/headers.ts +234 -22
  128. package/src/core/transport/index.ts +27 -27
  129. package/src/core/transport/internal/bounded-map.ts +27 -0
  130. package/src/core/transport/invocation.ts +98 -0
  131. package/src/core/transport/load-history-pages.ts +220 -0
  132. package/src/core/transport/load-history.ts +271 -0
  133. package/src/core/transport/pipe-stream.ts +63 -39
  134. package/src/core/transport/run-manager.ts +243 -0
  135. package/src/core/transport/session-support.ts +96 -0
  136. package/src/core/transport/tree.ts +1293 -308
  137. package/src/core/transport/types/agent.ts +434 -0
  138. package/src/core/transport/types/client.ts +247 -0
  139. package/src/core/transport/types/shared.ts +27 -0
  140. package/src/core/transport/types/tree.ts +393 -0
  141. package/src/core/transport/types/view.ts +288 -0
  142. package/src/core/transport/types.ts +13 -706
  143. package/src/core/transport/view.ts +1229 -450
  144. package/src/core/transport/wire-log.ts +189 -0
  145. package/src/errors.ts +29 -9
  146. package/src/event-emitter.ts +3 -2
  147. package/src/index.ts +86 -42
  148. package/src/logger.ts +14 -1
  149. package/src/react/contexts/client-session-context.ts +41 -0
  150. package/src/react/contexts/client-session-provider.tsx +222 -0
  151. package/src/react/create-session-hooks.ts +141 -0
  152. package/src/react/index.ts +24 -13
  153. package/src/react/internal/skipped-session.ts +62 -0
  154. package/src/react/internal/use-resolved-session.ts +63 -0
  155. package/src/react/use-ably-messages.ts +32 -22
  156. package/src/react/use-client-session.ts +178 -0
  157. package/src/react/use-create-view.ts +33 -29
  158. package/src/react/use-tree.ts +61 -30
  159. package/src/react/use-view.ts +138 -96
  160. package/src/utils.ts +83 -131
  161. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  162. package/src/vercel/codec/events.ts +85 -0
  163. package/src/vercel/codec/fields.ts +58 -0
  164. package/src/vercel/codec/fold-content.ts +54 -0
  165. package/src/vercel/codec/fold-data.ts +46 -0
  166. package/src/vercel/codec/fold-input.ts +255 -0
  167. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  168. package/src/vercel/codec/fold-text.ts +55 -0
  169. package/src/vercel/codec/fold-tool-input.ts +86 -0
  170. package/src/vercel/codec/fold-tool-output.ts +79 -0
  171. package/src/vercel/codec/index.ts +28 -21
  172. package/src/vercel/codec/inputs.ts +116 -0
  173. package/src/vercel/codec/outputs.ts +207 -0
  174. package/src/vercel/codec/reducer-state.ts +169 -0
  175. package/src/vercel/codec/reducer.ts +191 -0
  176. package/src/vercel/codec/tool-transitions.ts +3 -14
  177. package/src/vercel/codec/wire-data.ts +64 -0
  178. package/src/vercel/index.ts +7 -19
  179. package/src/vercel/react/contexts/chat-transport-context.ts +8 -7
  180. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  181. package/src/vercel/react/index.ts +3 -5
  182. package/src/vercel/react/use-chat-transport.ts +44 -66
  183. package/src/vercel/react/use-message-sync.ts +75 -39
  184. package/src/vercel/run-end-reason.ts +157 -0
  185. package/src/vercel/tool-part.ts +25 -0
  186. package/src/vercel/transport/chat-transport.ts +380 -98
  187. package/src/vercel/transport/index.ts +38 -37
  188. package/src/vercel/transport/run-output-stream.ts +169 -0
  189. package/src/version.ts +2 -0
  190. package/dist/core/transport/client-transport.d.ts +0 -10
  191. package/dist/core/transport/decode-history.d.ts +0 -43
  192. package/dist/core/transport/server-transport.d.ts +0 -7
  193. package/dist/core/transport/stream-router.d.ts +0 -29
  194. package/dist/core/transport/turn-manager.d.ts +0 -37
  195. package/dist/react/contexts/transport-context.d.ts +0 -31
  196. package/dist/react/contexts/transport-provider.d.ts +0 -49
  197. package/dist/react/create-transport-hooks.d.ts +0 -124
  198. package/dist/react/use-active-turns.d.ts +0 -12
  199. package/dist/react/use-client-transport.d.ts +0 -80
  200. package/dist/vercel/codec/accumulator.d.ts +0 -21
  201. package/dist/vercel/codec/decoder.d.ts +0 -22
  202. package/dist/vercel/codec/encoder.d.ts +0 -41
  203. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  204. package/dist/vercel/tool-approvals.d.ts +0 -124
  205. package/dist/vercel/tool-events.d.ts +0 -26
  206. package/src/core/transport/client-transport.ts +0 -977
  207. package/src/core/transport/decode-history.ts +0 -485
  208. package/src/core/transport/server-transport.ts +0 -612
  209. package/src/core/transport/stream-router.ts +0 -136
  210. package/src/core/transport/turn-manager.ts +0 -165
  211. package/src/react/contexts/transport-context.ts +0 -37
  212. package/src/react/contexts/transport-provider.tsx +0 -164
  213. package/src/react/create-transport-hooks.ts +0 -144
  214. package/src/react/use-active-turns.ts +0 -72
  215. package/src/react/use-client-transport.ts +0 -197
  216. package/src/vercel/codec/accumulator.ts +0 -588
  217. package/src/vercel/codec/decoder.ts +0 -618
  218. package/src/vercel/codec/encoder.ts +0 -410
  219. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  220. package/src/vercel/tool-approvals.ts +0 -380
  221. package/src/vercel/tool-events.ts +0 -53
@@ -0,0 +1,222 @@
1
+ /**
2
+ * ClientSessionProvider: creates a ClientSession and makes it available to
3
+ * descendants via ClientSessionContext.
4
+ *
5
+ * Reads the Ably Realtime client from the surrounding `<AblyProvider>` and
6
+ * forwards it to `createClientSession` along with the supplied `channelName`.
7
+ *
8
+ * The session is created on first render (via useRef) and recreated when
9
+ * `channelName` changes; the previous session is queued for disposal.
10
+ * `connect()` is invoked from a `useEffect` so the session is
11
+ * subscribed/attached before the first descendant operation. If
12
+ * `createClientSession` throws,
13
+ * the error is stored in the ClientSessionSlot (alongside an undefined
14
+ * session) so that useClientSession can surface it as `sessionError`
15
+ * without crashing the component tree.
16
+ *
17
+ * The session is closed when the provider truly unmounts. The close is
18
+ * scheduled as a microtask so that React Strict Mode's synchronous
19
+ * remount cycle (mount → fake-unmount → remount) can cancel it before it
20
+ * fires, avoiding unnecessary session teardown in development.
21
+ *
22
+ * Multiple ClientSessionProviders can be nested using distinct channelNames.
23
+ * Each provider merges its slot into the parent record so descendants
24
+ * can access all registered sessions via useClientSession(channelName).
25
+ *
26
+ * The provider also wraps its children in ably-js's `<ChannelProvider>` for the
27
+ * session's channel, so descendants can use ably-js channel hooks
28
+ * (`usePresence`, `useChannel`, etc.) against it without adding their own. It
29
+ * seeds the ChannelProvider's `options` with this SDK's channel agent so the
30
+ * hooks' agent is appended rather than overwriting it (ably-js >= 2.22).
31
+ */
32
+
33
+ import * as Ably from 'ably';
34
+ import { ChannelProvider, useAbly } from 'ably/react';
35
+ import { type PropsWithChildren, type ReactNode, useContext, useEffect, useMemo, useRef } from 'react';
36
+
37
+ import { channelAgent } from '../../core/agent.js';
38
+ import { resolveChannelModes } from '../../core/channel-options.js';
39
+ import type { CodecInputEvent, CodecOutputEvent } from '../../core/codec/types.js';
40
+ import { createClientSession } from '../../core/transport/client-session.js';
41
+ import type { ClientSession, ClientSessionOptions } from '../../core/transport/types.js';
42
+ import { ErrorCode } from '../../errors.js';
43
+ import type { ClientSessionSlot } from './client-session-context.js';
44
+ import { ClientSessionContext } from './client-session-context.js';
45
+
46
+ /**
47
+ * Props for {@link ClientSessionProvider}.
48
+ *
49
+ * All {@link ClientSessionOptions} except `client` (read from the surrounding
50
+ * `<AblyProvider>`).
51
+ */
52
+ export interface ClientSessionProviderProps<
53
+ TInput extends CodecInputEvent,
54
+ TOutput extends CodecOutputEvent,
55
+ TProjection,
56
+ TMessage,
57
+ >
58
+ extends Omit<ClientSessionOptions<TInput, TOutput, TProjection, TMessage>, 'client'>, PropsWithChildren {}
59
+
60
+ /**
61
+ * Provide a {@link ClientSession} to descendant components.
62
+ *
63
+ * Reads the Ably Realtime client from the surrounding `<AblyProvider>`,
64
+ * creates a session bound to `channelName`, calls `connect()` on mount,
65
+ * and registers it in `ClientSessionContext` under `channelName`.
66
+ * Descendants call {@link useClientSession} with the same `channelName` to
67
+ * access the session.
68
+ *
69
+ * If `createClientSession` throws during construction, the error is surfaced
70
+ * through `useClientSession` as `sessionError` — the component tree does not
71
+ * crash and children are still rendered.
72
+ *
73
+ * ```tsx
74
+ * <AblyProvider client={ably}>
75
+ * <ClientSessionProvider channelName="ai:demo" codec={UIMessageCodec}>
76
+ * <Chat />
77
+ * </ClientSessionProvider>
78
+ * </AblyProvider>
79
+ *
80
+ * // Inside Chat:
81
+ * const { session, sessionError } = useClientSession({ channelName: 'ai:demo' });
82
+ * ```
83
+ *
84
+ * For multiple sessions, nest providers with distinct channelNames:
85
+ *
86
+ * ```tsx
87
+ * <ClientSessionProvider channelName="ai:main" codec={UIMessageCodec}>
88
+ * <ClientSessionProvider channelName="ai:aux" codec={UIMessageCodec}>
89
+ * <App />
90
+ * </ClientSessionProvider>
91
+ * </ClientSessionProvider>
92
+ *
93
+ * // Inside App:
94
+ * const { session: main } = useClientSession({ channelName: 'ai:main' });
95
+ * const { session: aux } = useClientSession({ channelName: 'ai:aux' });
96
+ * ```
97
+ * `channelModes` must stay constant for the provider's lifetime: the session is
98
+ * only recreated when `channelName` changes, and removing the modes after mount
99
+ * silently reverts the channel's mode set without a reattach.
100
+ * @param props - Provider configuration including `channelName`, `codec`, and all other {@link ClientSessionOptions} except `client`.
101
+ * @param props.children - Descendant components that consume the session via {@link useClientSession}.
102
+ * @returns A React element wrapping children with ClientSessionContext.
103
+ */
104
+ export const ClientSessionProvider = <
105
+ TInput extends CodecInputEvent,
106
+ TOutput extends CodecOutputEvent,
107
+ TProjection,
108
+ TMessage,
109
+ >({
110
+ children,
111
+ ...sessionOptions
112
+ }: ClientSessionProviderProps<TInput, TOutput, TProjection, TMessage>): ReactNode => {
113
+ const client = useAbly();
114
+ const { channelName } = sessionOptions;
115
+
116
+ // Seed the ChannelProvider with this SDK's channel agent so ably-js's React
117
+ // hooks append their agent (`channelOptionsForReactHooks`) rather than
118
+ // overwriting it. Memoised on the codec, which determines the agent string.
119
+ //
120
+ // Spec: AIT-CT23 — resolve the channel modes through the same helper the
121
+ // session uses so the provider and the session request an identical,
122
+ // identically-ordered mode set. ably-js compares modes order- and
123
+ // duplicate-sensitively, so matching arrays mean the provider's setOptions
124
+ // never triggers a reattach and never silently reverts the session's modes.
125
+ const channelOptions = useMemo<Ably.ChannelOptions>(() => {
126
+ const options: Ably.ChannelOptions = { params: { agent: channelAgent(sessionOptions.codec) } };
127
+ const modes = resolveChannelModes(sessionOptions.channelModes);
128
+ if (modes) options.modes = modes;
129
+ return options;
130
+ }, [sessionOptions.codec, sessionOptions.channelModes]);
131
+ const sessionRef = useRef<ClientSession<TInput, TOutput, TProjection, TMessage> | undefined>(undefined);
132
+ const sessionChannelRef = useRef<string>(channelName);
133
+ const sessionsToDisposeRef = useRef<ClientSession<CodecInputEvent, CodecOutputEvent, unknown, unknown>[]>([]);
134
+ const pendingCloseRef = useRef(false);
135
+ const constructionErrorRef = useRef<Ably.ErrorInfo | undefined>(undefined);
136
+
137
+ const alreadyCreatedOrFailed = !!sessionRef.current || !!constructionErrorRef.current;
138
+
139
+ if (!alreadyCreatedOrFailed || sessionChannelRef.current !== channelName) {
140
+ sessionChannelRef.current = channelName;
141
+ if (sessionRef.current) sessionsToDisposeRef.current.push(sessionRef.current);
142
+ try {
143
+ sessionRef.current = createClientSession({ ...sessionOptions, client });
144
+ constructionErrorRef.current = undefined;
145
+ } catch (error) {
146
+ sessionRef.current = undefined;
147
+ constructionErrorRef.current =
148
+ error instanceof Ably.ErrorInfo
149
+ ? error
150
+ : new Ably.ErrorInfo('Unknown error while creating client session', ErrorCode.BadRequest, 400);
151
+ }
152
+ }
153
+
154
+ const parentContext = useContext(ClientSessionContext);
155
+
156
+ // Capture ref values as locals so useMemo deps track changes correctly.
157
+ // CAST: ClientSessionContext stores sessions with erased generics.
158
+ // The generic types are fixed at the ClientSessionProvider<TInput, TOutput, TProjection, TMessage> boundary.
159
+ const currentSession = sessionRef.current as
160
+ | ClientSession<CodecInputEvent, CodecOutputEvent, unknown, unknown>
161
+ | undefined;
162
+ const currentError = constructionErrorRef.current;
163
+
164
+ const slot = useMemo<ClientSessionSlot>(
165
+ () => ({ session: currentSession, sessionError: currentError }),
166
+ [currentSession, currentError],
167
+ );
168
+
169
+ const contextValue = useMemo(
170
+ () => ({ nearest: slot, providers: { ...parentContext.providers, [channelName]: slot } }),
171
+ [channelName, parentContext, slot],
172
+ );
173
+
174
+ // Dispose sessions superseded by a channelName change. When channelName
175
+ // changes, the render path above pushes the now-stale session into
176
+ // sessionsToDisposeRef and creates a replacement. This effect's cleanup —
177
+ // which runs on the next channelName change or on unmount — closes every
178
+ // queued session.
179
+ useEffect(
180
+ () => () => {
181
+ for (const session of sessionsToDisposeRef.current) void session.close();
182
+ },
183
+ [channelName],
184
+ );
185
+
186
+ // Trigger connect() once the session is created. Re-runs when channelName
187
+ // changes so the freshly-recreated session connects too. Any error is
188
+ // stored on the session's emitter and surfaced via on('error');
189
+ // useClientSession doesn't need to await this.
190
+ useEffect(() => {
191
+ void sessionRef.current?.connect();
192
+ }, [channelName]);
193
+
194
+ // Close the session when the component truly unmounts. The close is
195
+ // scheduled as a microtask: in React Strict Mode (dev) the component
196
+ // remounts synchronously before any microtask can drain, so the remount's
197
+ // effect setup resets pendingCloseRef.current = false and cancels the
198
+ // close. On a real unmount no remount follows, the microtask fires, and
199
+ // the session is closed.
200
+ useEffect(() => {
201
+ pendingCloseRef.current = false;
202
+ return () => {
203
+ pendingCloseRef.current = true;
204
+ void Promise.resolve().then(() => {
205
+ if (pendingCloseRef.current) {
206
+ void sessionRef.current?.close();
207
+ }
208
+ });
209
+ };
210
+ }, []);
211
+
212
+ return (
213
+ <ClientSessionContext.Provider value={contextValue}>
214
+ <ChannelProvider
215
+ channelName={channelName}
216
+ options={channelOptions}
217
+ >
218
+ {children}
219
+ </ChannelProvider>
220
+ </ClientSessionContext.Provider>
221
+ );
222
+ };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * createSessionHooks: factory that captures the codec's type parameters once and
3
+ * returns a bundle of type-safe hooks + ClientSessionProvider. Hook call sites need
4
+ * no type parameters at every use — just call the hooks directly.
5
+ * @example
6
+ * // Once per app (e.g. in a shared session.ts):
7
+ * export const {
8
+ * ClientSessionProvider,
9
+ * useClientSession,
10
+ * useView,
11
+ * } = createSessionHooks<VercelInput, VercelOutput, VercelProjection, UIMessage>();
12
+ *
13
+ * // In page:
14
+ * <ClientSessionProvider channelName="ai:demo" codec={UIMessageCodec}>
15
+ * <Chat />
16
+ * </ClientSessionProvider>
17
+ *
18
+ * // In Chat — no type params needed, session is implicit from nearest provider:
19
+ * const { nodes } = useView({ limit: 30 });
20
+ */
21
+
22
+ import type * as Ably from 'ably';
23
+ import type { ComponentType } from 'react';
24
+
25
+ import type { CodecInputEvent, CodecOutputEvent } from '../core/codec/types.js';
26
+ import type { ClientSession, View } from '../core/transport/types.js';
27
+ import type { ClientSessionProviderProps } from './contexts/client-session-provider.js';
28
+ import { ClientSessionProvider as _ClientSessionProvider } from './contexts/client-session-provider.js';
29
+ import { useAblyMessages as _useAblyMessages } from './use-ably-messages.js';
30
+ import type { ClientSessionHandle } from './use-client-session.js';
31
+ import { useClientSession as _useClientSession } from './use-client-session.js';
32
+ import { useCreateView as _useCreateView } from './use-create-view.js';
33
+ import type { TreeHandle } from './use-tree.js';
34
+ import { useTree as _useTree } from './use-tree.js';
35
+ import type { ViewHandle } from './use-view.js';
36
+ import { useView as _useView } from './use-view.js';
37
+
38
+ /**
39
+ * Bundle of type-safe hooks and provider returned by {@link createSessionHooks}.
40
+ *
41
+ * The codec's `TInput`, `TOutput`, `TProjection`, and `TMessage` are baked in at
42
+ * factory creation time so no type params are needed at hook call sites.
43
+ */
44
+ export interface SessionHooks<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage> {
45
+ /**
46
+ * `ClientSessionProvider` narrowed to the codec's `TInput`/`TOutput`/`TMessage`. No JSX type params needed.
47
+ */
48
+ ClientSessionProvider: ComponentType<ClientSessionProviderProps<TInput, TOutput, TProjection, TMessage>>;
49
+ /**
50
+ * Read the session from context. No type params needed.
51
+ *
52
+ * Returns `{ session, sessionError }`. When no provider is found (or session
53
+ * construction failed), `sessionError` is set and `session` is a stub that
54
+ * throws on access — the hook never throws during render.
55
+ *
56
+ * Pass `onError` to subscribe to post-construction session errors
57
+ * (e.g. send failures, channel continuity loss) without wiring
58
+ * `session.on('error', …)` manually.
59
+ */
60
+ useClientSession: (props?: {
61
+ /** Channel name to look up; omit to use the nearest {@link ClientSessionProvider}. */
62
+ channelName?: string;
63
+ /** When `true`, return a stub session that throws on any access. */
64
+ skip?: boolean;
65
+ /** Called whenever the resolved session emits an error event. */
66
+ onError?: (error: Ably.ErrorInfo) => void;
67
+ }) => ClientSessionHandle<TInput, TOutput, TProjection, TMessage>;
68
+ /**
69
+ * Subscribe to the nearest session's view and return the visible message list with pagination.
70
+ * Pass `session` to use a session's default view, `view` to subscribe to a specific view
71
+ * directly. Pass `limit` to auto-load on mount. Pass `skip: true` for an empty handle.
72
+ */
73
+ useView: (props?: {
74
+ /** Client session whose default view to subscribe to; defaults to the nearest {@link ClientSessionProvider}. */
75
+ session?: ClientSession<TInput, TOutput, TProjection, TMessage> | null;
76
+ /** A specific {@link View} to subscribe to directly. Takes priority over `session`. */
77
+ view?: View<TInput, TMessage> | null;
78
+ /** When provided, auto-loads the first page on mount. */
79
+ limit?: number;
80
+ /** When `true`, skip all subscriptions and return an empty handle. */
81
+ skip?: boolean;
82
+ }) => ViewHandle<TInput, TMessage>;
83
+ /**
84
+ * Navigate conversation branches in the session tree.
85
+ * Pass `session` to override; defaults to the nearest {@link ClientSessionProvider}.
86
+ */
87
+ useTree: (props?: {
88
+ /** Override session; defaults to the nearest {@link ClientSessionProvider}. */
89
+ session?: ClientSession<TInput, TOutput, TProjection, TMessage>;
90
+ }) => TreeHandle<TProjection>;
91
+ /**
92
+ * Subscribe to raw Ably messages on the session channel.
93
+ * Pass `session` to override; defaults to the nearest {@link ClientSessionProvider}.
94
+ * Pass `skip: true` to return an empty array without subscribing.
95
+ */
96
+ useAblyMessages: (props?: {
97
+ /** Override session; defaults to the nearest {@link ClientSessionProvider}. */
98
+ session?: ClientSession<TInput, TOutput, TProjection, TMessage>;
99
+ /** When `true`, skip all subscriptions and return an empty array. */
100
+ skip?: boolean;
101
+ }) => Ably.InboundMessage[];
102
+ /**
103
+ * Create an independent view over the same tree.
104
+ * Pass `session` to override; defaults to the nearest {@link ClientSessionProvider}.
105
+ * Pass `skip: true` to return an empty handle without creating a view.
106
+ */
107
+ useCreateView: (props?: {
108
+ /** Override session; defaults to the nearest {@link ClientSessionProvider}. */
109
+ session?: ClientSession<TInput, TOutput, TProjection, TMessage> | null;
110
+ /** When provided, auto-loads the first page on mount. */
111
+ limit?: number;
112
+ /** When `true`, skip view creation and return an empty handle. */
113
+ skip?: boolean;
114
+ }) => ViewHandle<TInput, TMessage>;
115
+ }
116
+
117
+ /**
118
+ * Create a bundle of type-safe hooks and provider for a given codec's
119
+ * `TInput`/`TOutput`/`TProjection`/`TMessage`.
120
+ *
121
+ * These type parameters are captured at factory creation time; hook call sites need
122
+ * no type parameters. The returned hooks are thin wrappers around the standalone hooks
123
+ * with the types resolved.
124
+ * @returns A {@link SessionHooks} bundle.
125
+ */
126
+ export const createSessionHooks = <
127
+ TInput extends CodecInputEvent,
128
+ TOutput extends CodecOutputEvent,
129
+ TProjection,
130
+ TMessage,
131
+ >(): SessionHooks<TInput, TOutput, TProjection, TMessage> => ({
132
+ // CAST: ClientSessionProvider is generic; factory narrows it to the codec's TInput/TOutput/TProjection/TMessage.
133
+ ClientSessionProvider: _ClientSessionProvider as ComponentType<
134
+ ClientSessionProviderProps<TInput, TOutput, TProjection, TMessage>
135
+ >,
136
+ useClientSession: (props) => _useClientSession<TInput, TOutput, TProjection, TMessage>(props ?? {}),
137
+ useView: (props) => _useView<TInput, TOutput, TProjection, TMessage>(props ?? {}),
138
+ useTree: (props) => _useTree<TInput, TOutput, TProjection, TMessage>(props ?? {}),
139
+ useAblyMessages: (props) => _useAblyMessages<TInput, TOutput, TProjection, TMessage>(props ?? {}),
140
+ useCreateView: (props) => _useCreateView<TInput, TOutput, TProjection, TMessage>(props ?? {}),
141
+ });
@@ -1,18 +1,29 @@
1
- export type { EventsNode, MessageNode } from '../core/transport/types.js';
2
- export type { TransportSlot } from './contexts/transport-context.js';
3
- export { NearestTransportContext } from './contexts/transport-context.js';
4
- export type { TransportProviderProps } from './contexts/transport-provider.js';
5
- export { TransportProvider } from './contexts/transport-provider.js';
6
- export type { TransportHooks } from './create-transport-hooks.js';
7
- export { createTransportHooks } from './create-transport-hooks.js';
8
- // eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional re-export for backwards compatibility
9
- export type { EventNode, TreeNode } from '../core/transport/types.js';
1
+ export { OBJECT_MODES } from '../core/channel-options.js';
2
+ export type { CodecMessage } from '../core/codec/types.js';
3
+ export type {
4
+ ActiveRun,
5
+ BranchSelection,
6
+ ClientSession,
7
+ ConversationNode,
8
+ InputNode,
9
+ MessageNode,
10
+ RunInfo,
11
+ RunNode,
12
+ RunNodeState,
13
+ SendOptions,
14
+ } from '../core/transport/types.js';
15
+ export type { ClientSessionSlot } from './contexts/client-session-context.js';
16
+ export type { ClientSessionProviderProps } from './contexts/client-session-provider.js';
17
+ export { ClientSessionProvider } from './contexts/client-session-provider.js';
18
+ export type { SessionHooks } from './create-session-hooks.js';
19
+ export { createSessionHooks } from './create-session-hooks.js';
20
+ export type { UseAblyMessagesOptions } from './use-ably-messages.js';
10
21
  export { useAblyMessages } from './use-ably-messages.js';
11
- export { useActiveTurns } from './use-active-turns.js';
12
- export type { ClientTransportHandle } from './use-client-transport.js';
13
- export { useClientTransport } from './use-client-transport.js';
22
+ export type { ClientSessionHandle } from './use-client-session.js';
23
+ export { useClientSession } from './use-client-session.js';
24
+ export type { UseCreateViewOptions } from './use-create-view.js';
14
25
  export { useCreateView } from './use-create-view.js';
15
- export type { TreeHandle } from './use-tree.js';
26
+ export type { TreeHandle, UseTreeOptions } from './use-tree.js';
16
27
  export { useTree } from './use-tree.js';
17
28
  export type { UseViewOptions, ViewHandle } from './use-view.js';
18
29
  export { useView } from './use-view.js';
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The stub {@link ClientSession} returned by the context-reader hooks when they
3
+ * are skipped, when no provider is found, or when session construction failed.
4
+ * Every member throws so a held-but-unusable session fails loudly rather than
5
+ * silently no-ops. Generic so each consumer gets a session typed to its own
6
+ * codec parameters without a cast — the throwing bodies satisfy any instantiation.
7
+ */
8
+
9
+ import * as Ably from 'ably';
10
+ import type * as AblyObjects from 'ably/liveobjects';
11
+
12
+ import type { CodecInputEvent, CodecOutputEvent } from '../../core/codec/types.js';
13
+ import type { ClientSession, Tree, View } from '../../core/transport/types.js';
14
+ import { ErrorCode } from '../../errors.js';
15
+
16
+ /**
17
+ * Build the `hook is skipped` error for a stub member.
18
+ * @param operation - The attempted operation, phrased for the `unable to <operation>; hook is skipped` message.
19
+ * @returns The skipped-hook error.
20
+ */
21
+ const skipped = (operation: string): Ably.ErrorInfo =>
22
+ new Ably.ErrorInfo(`unable to ${operation}; hook is skipped`, ErrorCode.InvalidArgument, 400);
23
+
24
+ /**
25
+ * Create a throwing stub {@link ClientSession}. Held safely in state before a
26
+ * provider resolves; any access throws {@link Ably.ErrorInfo}.
27
+ * @returns A stub session whose every member throws.
28
+ */
29
+ export const makeSkippedClientSession = <
30
+ TInput extends CodecInputEvent,
31
+ TOutput extends CodecOutputEvent,
32
+ TProjection,
33
+ TMessage,
34
+ >(): ClientSession<TInput, TOutput, TProjection, TMessage> => ({
35
+ get tree(): Tree<TOutput, TProjection> {
36
+ throw skipped('access tree');
37
+ },
38
+ get view(): View<TInput, TMessage> {
39
+ throw skipped('access view');
40
+ },
41
+ get presence(): Ably.RealtimePresence {
42
+ throw skipped('access presence');
43
+ },
44
+ get object(): AblyObjects.RealtimeObject {
45
+ throw skipped('access object');
46
+ },
47
+ connect: () => {
48
+ throw skipped('connect');
49
+ },
50
+ createView: (): View<TInput, TMessage> => {
51
+ throw skipped('create view');
52
+ },
53
+ cancel: () => {
54
+ throw skipped('cancel');
55
+ },
56
+ on: () => {
57
+ throw skipped('subscribe');
58
+ },
59
+ close: () => {
60
+ throw skipped('close');
61
+ },
62
+ });
@@ -0,0 +1,63 @@
1
+ import { useContext } from 'react';
2
+
3
+ import type { CodecInputEvent, CodecOutputEvent } from '../../core/codec/types.js';
4
+ import type { ClientSession } from '../../core/transport/types.js';
5
+ import { ClientSessionContext } from '../contexts/client-session-context.js';
6
+
7
+ /**
8
+ * Shared base for hook options that accept an explicit session override.
9
+ * Extend this interface for any hook whose `session` option defaults to the
10
+ * nearest {@link ClientSessionProvider} when omitted. Pass `null` to defer
11
+ * resolution (e.g. when a split pane is collapsed) — the helper returns
12
+ * `undefined` rather than falling back to the nearest provider.
13
+ */
14
+ export interface BaseSessionOption<
15
+ TInput extends CodecInputEvent,
16
+ TOutput extends CodecOutputEvent,
17
+ TProjection,
18
+ TMessage,
19
+ > {
20
+ /**
21
+ * Session to operate on; defaults to the nearest {@link ClientSessionProvider}.
22
+ * Pass `null` to defer (returns undefined; nearest provider is not used).
23
+ */
24
+ session?: ClientSession<TInput, TOutput, TProjection, TMessage> | null;
25
+ }
26
+
27
+ /**
28
+ * Resolve the active `ClientSession` for a hook.
29
+ *
30
+ * Reads `ClientSessionContext` and applies the standard three-way
31
+ * priority: explicit `session` argument → nearest provider → `undefined`.
32
+ * When `skip` is `true`, returns `undefined` regardless of context.
33
+ * When `session` is `null`, returns `undefined` (caller is deferring).
34
+ *
35
+ * Internal — not part of the public API.
36
+ * @param root0 - Options.
37
+ * @param root0.session - Explicit session; takes priority over the nearest provider. `null` to defer.
38
+ * @param root0.skip - When `true`, returns `undefined` immediately; context is still read but its value is ignored.
39
+ * @returns The resolved session, or `undefined` if none is available or `skip` is `true`.
40
+ */
41
+ export const useResolvedSession = <
42
+ TInput extends CodecInputEvent,
43
+ TOutput extends CodecOutputEvent,
44
+ TProjection,
45
+ TMessage,
46
+ >({
47
+ session,
48
+ skip,
49
+ }: {
50
+ /** Explicit session; takes priority over the nearest provider. `null` to defer. */
51
+ session?: ClientSession<TInput, TOutput, TProjection, TMessage> | null;
52
+ /** When `true`, return `undefined` immediately (context is still read, but ignored). */
53
+ skip?: boolean;
54
+ } = {}): ClientSession<TInput, TOutput, TProjection, TMessage> | undefined => {
55
+ const { nearest } = useContext(ClientSessionContext);
56
+ // CAST: ClientSessionContext stores session with erased generics; types fixed at call site.
57
+ const nearestSession = nearest?.session as unknown as
58
+ | ClientSession<TInput, TOutput, TProjection, TMessage>
59
+ | undefined;
60
+ if (skip) return undefined;
61
+ if (session === null) return undefined;
62
+ return session ?? nearestSession;
63
+ };
@@ -1,43 +1,53 @@
1
1
  /**
2
- * useAblyMessages — reactive raw Ably message log from a ClientTransport.
2
+ * useAblyMessages — reactive raw Ably message log from a ClientSession.
3
3
  *
4
- * Accumulates raw Ably InboundMessages from the transport's tree
4
+ * Accumulates raw Ably InboundMessages from the session's tree
5
5
  * 'ably-message' event. Messages are appended in arrival order.
6
6
  *
7
- * When `transport` is omitted, defaults to the nearest
8
- * {@link TransportProvider}'s transport via context.
7
+ * When `session` is omitted, defaults to the nearest
8
+ * {@link ClientSessionProvider}'s session via context.
9
9
  * Pass `skip: true` to bypass all subscriptions and return an empty array.
10
10
  */
11
11
 
12
12
  import type * as Ably from 'ably';
13
- import { useContext, useEffect, useRef, useState } from 'react';
13
+ import { useEffect, useRef, useState } from 'react';
14
14
 
15
- import type { ClientTransport } from '../core/transport/types.js';
16
- import { NearestTransportContext } from './contexts/transport-context.js';
15
+ import type { CodecInputEvent, CodecOutputEvent } from '../core/codec/types.js';
16
+ import type { BaseSessionOption } from './internal/use-resolved-session.js';
17
+ import { useResolvedSession } from './internal/use-resolved-session.js';
18
+
19
+ /** Options for {@link useAblyMessages}. */
20
+ export interface UseAblyMessagesOptions<
21
+ TInput extends CodecInputEvent,
22
+ TOutput extends CodecOutputEvent,
23
+ TProjection,
24
+ TMessage,
25
+ > extends BaseSessionOption<TInput, TOutput, TProjection, TMessage> {
26
+ /** When `true`, skip all subscriptions and return an empty array. */
27
+ skip?: boolean;
28
+ }
17
29
 
18
30
  /**
19
- * Subscribe to raw Ably message updates from a client transport's tree.
20
- * When `transport` is omitted, uses the nearest {@link TransportProvider}'s transport via context.
21
- * @param props - Options including optional `transport` and `skip`.
22
- * @param props.transport - Transport to subscribe to; defaults to the nearest provider.
31
+ * Subscribe to raw Ably message updates from a client session's tree.
32
+ * When `session` is omitted, uses the nearest {@link ClientSessionProvider}'s session via context.
33
+ * @param props - Options including optional `session` and `skip`.
34
+ * @param props.session - Session to subscribe to; defaults to the nearest provider.
23
35
  * @param props.skip - When `true`, skip all subscriptions and return an empty array.
24
- * @returns The accumulated raw Ably messages in chronological order.
36
+ * @returns The accumulated raw Ably messages in event-arrival order — older history messages loaded later are appended after the live messages already present, so this is not strictly chronological.
25
37
  */
26
- export const useAblyMessages = <TEvent, TMessage>({
27
- transport,
28
- skip,
29
- }: { transport?: ClientTransport<TEvent, TMessage>; skip?: boolean } = {}): Ably.InboundMessage[] => {
30
- const nearestSlot = useContext(NearestTransportContext);
31
- // CAST: NearestTransportContext stores transport with erased generics; types fixed at call site.
32
- const resolved = skip
33
- ? undefined
34
- : ((transport ?? nearestSlot?.transport) as ClientTransport<TEvent, TMessage> | undefined);
38
+ export const useAblyMessages = <
39
+ TInput extends CodecInputEvent,
40
+ TOutput extends CodecOutputEvent,
41
+ TProjection,
42
+ TMessage,
43
+ >({ session, skip }: UseAblyMessagesOptions<TInput, TOutput, TProjection, TMessage> = {}): Ably.InboundMessage[] => {
44
+ const resolved = useResolvedSession({ session, skip });
35
45
 
36
46
  const [messages, setMessages] = useState<Ably.InboundMessage[]>([]);
37
47
  const messagesRef = useRef<Ably.InboundMessage[]>([]);
38
48
 
39
49
  useEffect(() => {
40
- // Reset on transport change
50
+ // Reset on session change
41
51
  messagesRef.current = [];
42
52
  setMessages([]);
43
53