@ably/ai-transport 0.1.0 → 0.2.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 (163) hide show
  1. package/README.md +91 -100
  2. package/dist/ably-ai-transport.js +1553 -1238
  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 +29 -0
  8. package/dist/core/codec/decoder.d.ts +20 -23
  9. package/dist/core/codec/encoder.d.ts +11 -8
  10. package/dist/core/codec/index.d.ts +1 -2
  11. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  12. package/dist/core/codec/types.d.ts +407 -115
  13. package/dist/core/transport/agent-session.d.ts +10 -0
  14. package/dist/core/transport/branch-chain.d.ts +43 -0
  15. package/dist/core/transport/client-session.d.ts +13 -0
  16. package/dist/core/transport/decode-fold.d.ts +47 -0
  17. package/dist/core/transport/headers.d.ts +96 -18
  18. package/dist/core/transport/index.d.ts +5 -6
  19. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  20. package/dist/core/transport/invocation.d.ts +74 -0
  21. package/dist/core/transport/load-conversation.d.ts +128 -0
  22. package/dist/core/transport/load-history.d.ts +39 -0
  23. package/dist/core/transport/pipe-stream.d.ts +9 -9
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +373 -109
  26. package/dist/core/transport/types/agent.d.ts +353 -0
  27. package/dist/core/transport/types/client.d.ts +168 -0
  28. package/dist/core/transport/types/shared.d.ts +24 -0
  29. package/dist/core/transport/types/tree.d.ts +315 -0
  30. package/dist/core/transport/types/view.d.ts +222 -0
  31. package/dist/core/transport/types.d.ts +13 -553
  32. package/dist/core/transport/view.d.ts +272 -84
  33. package/dist/errors.d.ts +21 -10
  34. package/dist/index.d.ts +6 -8
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +976 -990
  37. package/dist/react/ably-ai-transport-react.js.map +1 -1
  38. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  39. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  40. package/dist/react/contexts/client-session-context.d.ts +36 -0
  41. package/dist/react/contexts/client-session-provider.d.ts +53 -0
  42. package/dist/react/create-session-hooks.d.ts +116 -0
  43. package/dist/react/index.d.ts +12 -12
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +17 -14
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +14 -13
  48. package/dist/react/use-tree.d.ts +30 -15
  49. package/dist/react/use-view.d.ts +82 -51
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2573 -2086
  52. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  53. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  55. package/dist/vercel/codec/decoder.d.ts +5 -18
  56. package/dist/vercel/codec/encoder.d.ts +6 -36
  57. package/dist/vercel/codec/events.d.ts +51 -0
  58. package/dist/vercel/codec/index.d.ts +24 -12
  59. package/dist/vercel/codec/reducer.d.ts +144 -0
  60. package/dist/vercel/codec/tool-transitions.d.ts +2 -2
  61. package/dist/vercel/index.d.ts +4 -5
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +3907 -3266
  63. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  64. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +33 -8
  65. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  66. package/dist/vercel/react/contexts/chat-transport-context.d.ts +7 -6
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  68. package/dist/vercel/react/index.d.ts +1 -2
  69. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  70. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +43 -24
  73. package/dist/vercel/transport/index.d.ts +25 -21
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +30 -23
  77. package/src/constants.ts +124 -51
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +113 -65
  81. package/src/core/codec/index.ts +13 -6
  82. package/src/core/codec/lifecycle-tracker.ts +10 -9
  83. package/src/core/codec/types.ts +436 -120
  84. package/src/core/transport/agent-session.ts +1344 -0
  85. package/src/core/transport/branch-chain.ts +58 -0
  86. package/src/core/transport/client-session.ts +775 -0
  87. package/src/core/transport/decode-fold.ts +91 -0
  88. package/src/core/transport/headers.ts +181 -22
  89. package/src/core/transport/index.ts +25 -26
  90. package/src/core/transport/internal/bounded-map.ts +27 -0
  91. package/src/core/transport/invocation.ts +98 -0
  92. package/src/core/transport/load-conversation.ts +355 -0
  93. package/src/core/transport/load-history.ts +269 -0
  94. package/src/core/transport/pipe-stream.ts +54 -39
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +926 -308
  97. package/src/core/transport/types/agent.ts +407 -0
  98. package/src/core/transport/types/client.ts +211 -0
  99. package/src/core/transport/types/shared.ts +27 -0
  100. package/src/core/transport/types/tree.ts +344 -0
  101. package/src/core/transport/types/view.ts +259 -0
  102. package/src/core/transport/types.ts +13 -706
  103. package/src/core/transport/view.ts +864 -433
  104. package/src/errors.ts +22 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +52 -41
  107. package/src/logger.ts +14 -1
  108. package/src/react/contexts/client-session-context.ts +41 -0
  109. package/src/react/contexts/client-session-provider.tsx +186 -0
  110. package/src/react/create-session-hooks.ts +141 -0
  111. package/src/react/index.ts +23 -13
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +32 -22
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +33 -29
  116. package/src/react/use-tree.ts +61 -30
  117. package/src/react/use-view.ts +139 -97
  118. package/src/utils.ts +63 -45
  119. package/src/vercel/codec/decoder.ts +336 -258
  120. package/src/vercel/codec/encoder.ts +343 -205
  121. package/src/vercel/codec/events.ts +87 -0
  122. package/src/vercel/codec/index.ts +60 -13
  123. package/src/vercel/codec/reducer.ts +977 -0
  124. package/src/vercel/codec/tool-transitions.ts +2 -2
  125. package/src/vercel/index.ts +6 -19
  126. package/src/vercel/react/contexts/chat-transport-context.ts +7 -6
  127. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  128. package/src/vercel/react/index.ts +3 -5
  129. package/src/vercel/react/use-chat-transport.ts +47 -49
  130. package/src/vercel/react/use-message-sync.ts +80 -39
  131. package/src/vercel/run-end-reason.ts +78 -0
  132. package/src/vercel/transport/chat-transport.ts +392 -98
  133. package/src/vercel/transport/index.ts +39 -38
  134. package/src/vercel/transport/run-output-stream.ts +170 -0
  135. package/src/version.ts +2 -0
  136. package/dist/core/transport/client-transport.d.ts +0 -10
  137. package/dist/core/transport/decode-history.d.ts +0 -43
  138. package/dist/core/transport/server-transport.d.ts +0 -7
  139. package/dist/core/transport/stream-router.d.ts +0 -29
  140. package/dist/core/transport/turn-manager.d.ts +0 -37
  141. package/dist/react/contexts/transport-context.d.ts +0 -31
  142. package/dist/react/contexts/transport-provider.d.ts +0 -49
  143. package/dist/react/create-transport-hooks.d.ts +0 -124
  144. package/dist/react/use-active-turns.d.ts +0 -12
  145. package/dist/react/use-client-transport.d.ts +0 -80
  146. package/dist/vercel/codec/accumulator.d.ts +0 -21
  147. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  148. package/dist/vercel/tool-approvals.d.ts +0 -124
  149. package/dist/vercel/tool-events.d.ts +0 -26
  150. package/src/core/transport/client-transport.ts +0 -977
  151. package/src/core/transport/decode-history.ts +0 -485
  152. package/src/core/transport/server-transport.ts +0 -612
  153. package/src/core/transport/stream-router.ts +0 -136
  154. package/src/core/transport/turn-manager.ts +0 -165
  155. package/src/react/contexts/transport-context.ts +0 -37
  156. package/src/react/contexts/transport-provider.tsx +0 -164
  157. package/src/react/create-transport-hooks.ts +0 -144
  158. package/src/react/use-active-turns.ts +0 -72
  159. package/src/react/use-client-transport.ts +0 -197
  160. package/src/vercel/codec/accumulator.ts +0 -588
  161. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  162. package/src/vercel/tool-approvals.ts +0 -380
  163. package/src/vercel/tool-events.ts +0 -53
package/src/errors.ts CHANGED
@@ -14,18 +14,24 @@ export enum ErrorCode {
14
14
  */
15
15
  InvalidArgument = 40003,
16
16
 
17
+ /**
18
+ * Operation not permitted with the provided capability (Ably 40160).
19
+ * Used when the Ably channel rejects a publish for a capability reason.
20
+ */
21
+ InsufficientCapability = 40160,
22
+
17
23
  // 104000 - 104999 are reserved for AI Transport SDK errors
18
24
 
19
25
  /**
20
- * Encoder recovery failed after flush — one or more updateMessage calls
26
+ * Encoder recovery failed during flush — one or more updateMessage calls
21
27
  * could not recover a failed append pipeline.
22
28
  */
23
29
  EncoderRecoveryFailed = 104000,
24
30
 
25
31
  /**
26
- * A transport-level channel subscription callback threw unexpectedly.
32
+ * A session-level channel subscription callback threw unexpectedly.
27
33
  */
28
- TransportSubscriptionError = 104001,
34
+ SessionSubscriptionError = 104001,
29
35
 
30
36
  /**
31
37
  * Cancel listener or onCancel hook threw while processing a cancel message.
@@ -33,19 +39,19 @@ export enum ErrorCode {
33
39
  CancelListenerError = 104002,
34
40
 
35
41
  /**
36
- * A publish within a turn failed (lifecycle event, message, or event).
42
+ * A publish within a run failed (lifecycle event, message, or event).
37
43
  */
38
- TurnLifecycleError = 104003,
44
+ RunLifecycleError = 104003,
39
45
 
40
46
  /**
41
- * An operation was attempted on a transport that has already been closed.
47
+ * An operation was attempted on a session that has already been closed.
42
48
  */
43
- TransportClosed = 104004,
49
+ SessionClosed = 104004,
44
50
 
45
51
  /**
46
- * The HTTP POST to the server endpoint failed (network error or non-2xx response).
52
+ * The HTTP POST to the agent endpoint failed (network error or non-2xx response).
47
53
  */
48
- TransportSendFailed = 104005,
54
+ SessionSendFailed = 104005,
49
55
 
50
56
  /**
51
57
  * The Ably channel lost message continuity — the channel entered FAILED,
@@ -66,6 +72,13 @@ export enum ErrorCode {
66
72
  * network failure) or an underlying publish failed mid-stream.
67
73
  */
68
74
  StreamError = 104008,
75
+
76
+ /**
77
+ * The agent attached to the channel and waited for the input event(s) the
78
+ * invocation points at (rewind + live wait) but `inputEventLookupTimeoutMs`
79
+ * lapsed without seeing them.
80
+ */
81
+ InputEventNotFound = 104010,
69
82
  }
70
83
 
71
84
  /**
@@ -76,8 +76,9 @@ const toAblyLogger = (logger: Logger): unknown => ({
76
76
  });
77
77
 
78
78
  // CAST: Access Ably's internal EventEmitter constructor. Not publicly exported
79
- // but available to other Ably SDKs. The logger parameter ensures listener
80
- // exceptions are caught and logged rather than crashing.
79
+ // but available to other Ably SDKs. Ably always catches listener exceptions
80
+ // internally; the logger parameter ensures those caught exceptions are logged
81
+ // rather than silently swallowed.
81
82
  const InternalEventEmitter: new <EventsMap>(logger: unknown) => InterfaceEventEmitter<EventsMap> = (
82
83
  Ably.Realtime as unknown as { EventEmitter: new <EventsMap>(logger: unknown) => InterfaceEventEmitter<EventsMap> }
83
84
  ).EventEmitter;
package/src/index.ts CHANGED
@@ -1,87 +1,98 @@
1
1
  // Core transport
2
2
  export type {
3
- ActiveTurn,
4
- AddMessageOptions,
5
- AddMessagesResult,
6
- CancelFilter,
3
+ ActiveRun,
4
+ AgentSession,
5
+ AgentSessionOptions,
6
+ BranchSelection,
7
7
  CancelRequest,
8
- ClientTransport,
9
- ClientTransportOptions,
10
- CloseOptions,
8
+ ClientSession,
9
+ ClientSessionOptions,
10
+ ConversationNode,
11
11
  EventsNode,
12
+ InputNode,
13
+ InvocationData,
14
+ LoadConversationOptions,
12
15
  MessageNode,
13
- NewTurnOptions,
16
+ OutputEvent,
17
+ PipeOptions,
18
+ Run,
19
+ RunEndReason,
20
+ RunInfo,
21
+ RunLifecycleEvent,
22
+ RunNode,
23
+ RunRuntime,
24
+ RunView,
14
25
  SendOptions,
15
- ServerTransport,
16
- ServerTransportOptions,
17
- StreamResponseOptions,
18
26
  StreamResult,
19
27
  Tree,
20
- Turn,
21
- TurnEndReason,
22
- TurnLifecycleEvent,
23
28
  View,
24
29
  } from './core/transport/index.js';
25
-
26
- // Deprecated aliases — intentional re-export of deprecated types for backwards compatibility.
27
- // eslint-disable-next-line @typescript-eslint/no-deprecated
28
- export type { EventNode } from './core/transport/index.js';
29
- // eslint-disable-next-line @typescript-eslint/no-deprecated
30
- export type { TreeNode } from './core/transport/index.js';
31
- export { buildTransportHeaders, createClientTransport, createServerTransport } from './core/transport/index.js';
30
+ export { buildTransportHeaders, createAgentSession, createClientSession, Invocation } from './core/transport/index.js';
32
31
 
33
32
  // Core codec
34
33
  export type {
35
34
  ChannelWriter,
36
35
  Codec,
36
+ CodecInputEvent,
37
+ CodecMessage,
38
+ CodecOutputEvent,
39
+ DecodedMessage,
40
+ Decoder,
37
41
  DecoderCore,
38
42
  DecoderCoreHooks,
39
43
  DecoderCoreOptions,
40
- DecoderOutput,
41
- DiscreteEncoder,
44
+ Encoder,
42
45
  EncoderCore,
43
46
  EncoderCoreOptions,
44
47
  EncoderOptions,
45
48
  Extras,
46
49
  LifecycleTracker,
47
- MessageAccumulator,
48
50
  MessagePayload,
49
51
  PhaseConfig,
50
- StreamDecoder,
51
- StreamEncoder,
52
+ Reducer,
53
+ ReducerMeta,
54
+ Regenerate,
52
55
  StreamPayload,
53
56
  StreamTrackerState,
57
+ ToolApprovalResponse,
58
+ ToolResult,
59
+ ToolResultError,
60
+ UserMessage,
54
61
  WriteOptions,
55
62
  } from './core/codec/index.js';
56
- export { createDecoderCore, createEncoderCore, createLifecycleTracker, eventOutput } from './core/codec/index.js';
63
+ export { createDecoderCore, createEncoderCore, createLifecycleTracker } from './core/codec/index.js';
57
64
 
58
65
  // Constants
59
66
  export {
60
- DOMAIN_HEADER_PREFIX,
61
- EVENT_ABORT,
62
67
  EVENT_CANCEL,
63
- EVENT_ERROR,
64
- EVENT_TURN_END,
65
- EVENT_TURN_START,
66
- HEADER_CANCEL_ALL,
67
- HEADER_CANCEL_CLIENT_ID,
68
- HEADER_CANCEL_OWN,
69
- HEADER_CANCEL_TURN_ID,
68
+ EVENT_RUN_END,
69
+ EVENT_RUN_START,
70
+ HEADER_CODEC_MESSAGE_ID,
71
+ HEADER_ERROR_CODE,
72
+ HEADER_ERROR_MESSAGE,
70
73
  HEADER_FORK_OF,
71
- HEADER_MSG_ID,
74
+ HEADER_INPUT_CLIENT_ID,
75
+ HEADER_MSG_REGENERATE,
72
76
  HEADER_PARENT,
73
77
  HEADER_ROLE,
78
+ HEADER_RUN_CLIENT_ID,
79
+ HEADER_RUN_ID,
80
+ HEADER_RUN_REASON,
74
81
  HEADER_STATUS,
75
82
  HEADER_STREAM,
76
83
  HEADER_STREAM_ID,
77
- HEADER_TURN_CLIENT_ID,
78
- HEADER_TURN_ID,
79
- HEADER_TURN_REASON,
80
84
  } from './constants.js';
81
85
 
82
86
  // Utilities
83
87
  export type { DomainHeaderReader, DomainHeaderWriter, Stripped } from './utils.js';
84
- export { getHeaders, headerReader, headerWriter, mergeHeaders, stripUndefined } from './utils.js';
88
+ export {
89
+ getCodecHeaders,
90
+ getTransportHeaders,
91
+ headerReader,
92
+ headerWriter,
93
+ mergeHeaders,
94
+ stripUndefined,
95
+ } from './utils.js';
85
96
 
86
97
  // Event emitter
87
98
  export { EventEmitter } from './event-emitter.js';
package/src/logger.ts CHANGED
@@ -143,10 +143,22 @@ export const consoleLogger = (message: string, level: LogLevel, context?: LogCon
143
143
  * Options for creating a logger.
144
144
  */
145
145
  export interface LoggerOptions {
146
+ /**
147
+ * The handler that receives formatted log messages. Defaults to {@link consoleLogger} when omitted.
148
+ */
146
149
  logHandler?: LogHandler;
150
+ /**
151
+ * The minimum level to emit; messages below this level are suppressed. Must be a valid {@link LogLevel}, otherwise logger creation throws.
152
+ */
147
153
  logLevel: LogLevel;
148
154
  }
149
155
 
156
+ /**
157
+ * Creates a {@link Logger} from the given options.
158
+ * @param options The handler and minimum level for the logger.
159
+ * @returns A logger that filters by level and delegates to the handler.
160
+ * @throws {@link Ably.ErrorInfo} with {@link ErrorCode.InvalidArgument} if `options.logLevel` is not a recognised {@link LogLevel}.
161
+ */
150
162
  export const makeLogger = (options: LoggerOptions): Logger => {
151
163
  const logHandler = options.logHandler ?? consoleLogger;
152
164
 
@@ -218,7 +230,8 @@ class DefaultLogger implements Logger {
218
230
  }
219
231
 
220
232
  withContext(context: LogContext): Logger {
221
- // Get the original log level by finding the key in logLevelNumberMap that matches this._levelNumber
233
+ // Get the original log level by finding the key in logLevelNumberMap that matches this._levelNumber.
234
+ // The Error fallback is defensive and unreachable in practice: _levelNumber always originates from the map.
222
235
  const originalLevel =
223
236
  [...logLevelNumberMap.entries()].find(([, value]) => value === this._levelNumber)?.[0] ?? LogLevel.Error;
224
237
 
@@ -0,0 +1,41 @@
1
+ import type * as Ably from 'ably';
2
+ import { createContext } from 'react';
3
+
4
+ import type { CodecInputEvent, CodecOutputEvent } from '../../core/codec/types.js';
5
+ import type { ClientSession } from '../../core/transport/types.js';
6
+
7
+ /**
8
+ * A single entry in the client-session registry, holding the session and any
9
+ * error that occurred during its construction.
10
+ *
11
+ * `session` is `undefined` when construction failed.
12
+ * `sessionError` is set when `createClientSession` threw during provider render.
13
+ */
14
+ export interface ClientSessionSlot {
15
+ /** The constructed session, or `undefined` if construction failed. */
16
+ session: ClientSession<CodecInputEvent, CodecOutputEvent, unknown, unknown> | undefined;
17
+ /** Construction error from `createClientSession`, or `undefined` on success. */
18
+ sessionError?: Ably.ErrorInfo | undefined;
19
+ }
20
+
21
+ /**
22
+ * The shape of the {@link ClientSessionContext} value.
23
+ *
24
+ * `nearest` is the slot from the innermost enclosing {@link ClientSessionProvider}.
25
+ * `providers` is the full registry of all enclosing providers, keyed by channelName.
26
+ */
27
+ export interface ClientSessionContextValue {
28
+ /** The innermost {@link ClientSessionProvider}'s slot. `undefined` when no provider is present. */
29
+ nearest: ClientSessionSlot | undefined;
30
+ /** All registered session slots from enclosing providers, keyed by channelName. */
31
+ providers: Readonly<Record<string, ClientSessionSlot>>;
32
+ }
33
+
34
+ /**
35
+ * Unified client-session context.
36
+ *
37
+ * Holds the nearest client-session slot and the full registry of all registered
38
+ * slots keyed by channelName. Populated by {@link ClientSessionProvider};
39
+ * read by {@link useClientSession} and internal hooks.
40
+ */
41
+ export const ClientSessionContext = createContext<ClientSessionContextValue>({ nearest: undefined, providers: {} });
@@ -0,0 +1,186 @@
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
+
27
+ import * as Ably from 'ably';
28
+ import { useAbly } from 'ably/react';
29
+ import { type PropsWithChildren, type ReactNode, useContext, useEffect, useMemo, useRef } from 'react';
30
+
31
+ import type { CodecInputEvent, CodecOutputEvent } from '../../core/codec/types.js';
32
+ import { createClientSession } from '../../core/transport/client-session.js';
33
+ import type { ClientSession, ClientSessionOptions } from '../../core/transport/types.js';
34
+ import { ErrorCode } from '../../errors.js';
35
+ import type { ClientSessionSlot } from './client-session-context.js';
36
+ import { ClientSessionContext } from './client-session-context.js';
37
+
38
+ /**
39
+ * Props for {@link ClientSessionProvider}.
40
+ *
41
+ * All {@link ClientSessionOptions} except `client` (read from the surrounding
42
+ * `<AblyProvider>`).
43
+ */
44
+ export interface ClientSessionProviderProps<
45
+ TInput extends CodecInputEvent,
46
+ TOutput extends CodecOutputEvent,
47
+ TProjection,
48
+ TMessage,
49
+ >
50
+ extends Omit<ClientSessionOptions<TInput, TOutput, TProjection, TMessage>, 'client'>, PropsWithChildren {}
51
+
52
+ /**
53
+ * Provide a {@link ClientSession} to descendant components.
54
+ *
55
+ * Reads the Ably Realtime client from the surrounding `<AblyProvider>`,
56
+ * creates a session bound to `channelName`, calls `connect()` on mount,
57
+ * and registers it in `ClientSessionContext` under `channelName`.
58
+ * Descendants call {@link useClientSession} with the same `channelName` to
59
+ * access the session.
60
+ *
61
+ * If `createClientSession` throws during construction, the error is surfaced
62
+ * through `useClientSession` as `sessionError` — the component tree does not
63
+ * crash and children are still rendered.
64
+ *
65
+ * ```tsx
66
+ * <AblyProvider client={ably}>
67
+ * <ClientSessionProvider channelName="ai:demo" codec={UIMessageCodec}>
68
+ * <Chat />
69
+ * </ClientSessionProvider>
70
+ * </AblyProvider>
71
+ *
72
+ * // Inside Chat:
73
+ * const { session, sessionError } = useClientSession({ channelName: 'ai:demo' });
74
+ * ```
75
+ *
76
+ * For multiple sessions, nest providers with distinct channelNames:
77
+ *
78
+ * ```tsx
79
+ * <ClientSessionProvider channelName="ai:main" codec={UIMessageCodec}>
80
+ * <ClientSessionProvider channelName="ai:aux" codec={UIMessageCodec}>
81
+ * <App />
82
+ * </ClientSessionProvider>
83
+ * </ClientSessionProvider>
84
+ *
85
+ * // Inside App:
86
+ * const { session: main } = useClientSession({ channelName: 'ai:main' });
87
+ * const { session: aux } = useClientSession({ channelName: 'ai:aux' });
88
+ * ```
89
+ * @param props - Provider configuration including `channelName`, `codec`, and all other {@link ClientSessionOptions} except `client`.
90
+ * @param props.children - Descendant components that consume the session via {@link useClientSession}.
91
+ * @returns A React element wrapping children with ClientSessionContext.
92
+ */
93
+ export const ClientSessionProvider = <
94
+ TInput extends CodecInputEvent,
95
+ TOutput extends CodecOutputEvent,
96
+ TProjection,
97
+ TMessage,
98
+ >({
99
+ children,
100
+ ...sessionOptions
101
+ }: ClientSessionProviderProps<TInput, TOutput, TProjection, TMessage>): ReactNode => {
102
+ const client = useAbly();
103
+ const { channelName } = sessionOptions;
104
+ const sessionRef = useRef<ClientSession<TInput, TOutput, TProjection, TMessage> | undefined>(undefined);
105
+ const sessionChannelRef = useRef<string>(channelName);
106
+ const sessionsToDisposeRef = useRef<ClientSession<CodecInputEvent, CodecOutputEvent, unknown, unknown>[]>([]);
107
+ const pendingCloseRef = useRef(false);
108
+ const constructionErrorRef = useRef<Ably.ErrorInfo | undefined>(undefined);
109
+
110
+ const alreadyCreatedOrFailed = !!sessionRef.current || !!constructionErrorRef.current;
111
+
112
+ if (!alreadyCreatedOrFailed || sessionChannelRef.current !== channelName) {
113
+ sessionChannelRef.current = channelName;
114
+ if (sessionRef.current) sessionsToDisposeRef.current.push(sessionRef.current);
115
+ try {
116
+ sessionRef.current = createClientSession({ ...sessionOptions, client });
117
+ constructionErrorRef.current = undefined;
118
+ } catch (error) {
119
+ sessionRef.current = undefined;
120
+ constructionErrorRef.current =
121
+ error instanceof Ably.ErrorInfo
122
+ ? error
123
+ : new Ably.ErrorInfo('Unknown error while creating client session', ErrorCode.BadRequest, 400);
124
+ }
125
+ }
126
+
127
+ const parentContext = useContext(ClientSessionContext);
128
+
129
+ // Capture ref values as locals so useMemo deps track changes correctly.
130
+ // CAST: ClientSessionContext stores sessions with erased generics.
131
+ // The generic types are fixed at the ClientSessionProvider<TInput, TOutput, TProjection, TMessage> boundary.
132
+ const currentSession = sessionRef.current as
133
+ | ClientSession<CodecInputEvent, CodecOutputEvent, unknown, unknown>
134
+ | undefined;
135
+ const currentError = constructionErrorRef.current;
136
+
137
+ const slot = useMemo<ClientSessionSlot>(
138
+ () => ({ session: currentSession, sessionError: currentError }),
139
+ [currentSession, currentError],
140
+ );
141
+
142
+ const contextValue = useMemo(
143
+ () => ({ nearest: slot, providers: { ...parentContext.providers, [channelName]: slot } }),
144
+ [channelName, parentContext, slot],
145
+ );
146
+
147
+ // Dispose sessions superseded by a channelName change. When channelName
148
+ // changes, the render path above pushes the now-stale session into
149
+ // sessionsToDisposeRef and creates a replacement. This effect's cleanup —
150
+ // which runs on the next channelName change or on unmount — closes every
151
+ // queued session.
152
+ useEffect(
153
+ () => () => {
154
+ for (const session of sessionsToDisposeRef.current) void session.close();
155
+ },
156
+ [channelName],
157
+ );
158
+
159
+ // Trigger connect() once the session is created. Re-runs when channelName
160
+ // changes so the freshly-recreated session connects too. Any error is
161
+ // stored on the session's emitter and surfaced via on('error');
162
+ // useClientSession doesn't need to await this.
163
+ useEffect(() => {
164
+ void sessionRef.current?.connect();
165
+ }, [channelName]);
166
+
167
+ // Close the session when the component truly unmounts. The close is
168
+ // scheduled as a microtask: in React Strict Mode (dev) the component
169
+ // remounts synchronously before any microtask can drain, so the remount's
170
+ // effect setup resets pendingCloseRef.current = false and cancels the
171
+ // close. On a real unmount no remount follows, the microtask fires, and
172
+ // the session is closed.
173
+ useEffect(() => {
174
+ pendingCloseRef.current = false;
175
+ return () => {
176
+ pendingCloseRef.current = true;
177
+ void Promise.resolve().then(() => {
178
+ if (pendingCloseRef.current) {
179
+ void sessionRef.current?.close();
180
+ }
181
+ });
182
+ };
183
+ }, []);
184
+
185
+ return <ClientSessionContext.Provider value={contextValue}>{children}</ClientSessionContext.Provider>;
186
+ };
@@ -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,28 @@
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 type { CodecMessage } from '../core/codec/types.js';
2
+ export type {
3
+ ActiveRun,
4
+ BranchSelection,
5
+ ClientSession,
6
+ ConversationNode,
7
+ EventsNode,
8
+ InputNode,
9
+ MessageNode,
10
+ RunInfo,
11
+ RunNode,
12
+ SendOptions,
13
+ } from '../core/transport/types.js';
14
+ export type { ClientSessionSlot } from './contexts/client-session-context.js';
15
+ export type { ClientSessionProviderProps } from './contexts/client-session-provider.js';
16
+ export { ClientSessionProvider } from './contexts/client-session-provider.js';
17
+ export type { SessionHooks } from './create-session-hooks.js';
18
+ export { createSessionHooks } from './create-session-hooks.js';
19
+ export type { UseAblyMessagesOptions } from './use-ably-messages.js';
10
20
  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';
21
+ export type { ClientSessionHandle } from './use-client-session.js';
22
+ export { useClientSession } from './use-client-session.js';
23
+ export type { UseCreateViewOptions } from './use-create-view.js';
14
24
  export { useCreateView } from './use-create-view.js';
15
- export type { TreeHandle } from './use-tree.js';
25
+ export type { TreeHandle, UseTreeOptions } from './use-tree.js';
16
26
  export { useTree } from './use-tree.js';
17
27
  export type { UseViewOptions, ViewHandle } from './use-view.js';
18
28
  export { useView } from './use-view.js';