@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
@@ -1,612 +0,0 @@
1
- /**
2
- * Core server-side transport, parameterized by codec.
3
- *
4
- * Composes TurnManager and pipeStream to handle the full server-side turn
5
- * lifecycle. Cancel message routing is handled directly by the transport's
6
- * single channel subscription — no separate cancel manager needed.
7
- *
8
- * The transport exposes a single factory method — `newTurn()` — which returns
9
- * a Turn object with explicit lifecycle methods: start(), addMessages(),
10
- * streamResponse(), and end().
11
- */
12
-
13
- import * as Ably from 'ably';
14
-
15
- import {
16
- EVENT_CANCEL,
17
- HEADER_CANCEL_ALL,
18
- HEADER_CANCEL_CLIENT_ID,
19
- HEADER_CANCEL_OWN,
20
- HEADER_CANCEL_TURN_ID,
21
- } from '../../constants.js';
22
- import { ErrorCode } from '../../errors.js';
23
- import type { Logger } from '../../logger.js';
24
- import { getHeaders, mergeHeaders } from '../../utils.js';
25
- import { buildTransportHeaders } from './headers.js';
26
- import { pipeStream } from './pipe-stream.js';
27
- import type { TurnManager } from './turn-manager.js';
28
- import { createTurnManager } from './turn-manager.js';
29
- import type {
30
- AddMessageOptions,
31
- AddMessagesResult,
32
- CancelFilter,
33
- CancelRequest,
34
- EventsNode,
35
- MessageNode,
36
- NewTurnOptions,
37
- ServerTransport,
38
- ServerTransportOptions,
39
- StreamResponseOptions,
40
- StreamResult,
41
- Turn,
42
- TurnEndReason,
43
- } from './types.js';
44
-
45
- // ---------------------------------------------------------------------------
46
- // Internal turn record for cancel routing
47
- // ---------------------------------------------------------------------------
48
-
49
- interface RegisteredTurn {
50
- turnId: string;
51
- clientId: string;
52
- controller: AbortController;
53
- /** Composite signal that fires when either the internal controller or the external signal aborts. */
54
- signal: AbortSignal;
55
- onCancel?: (request: CancelRequest) => Promise<boolean>;
56
- onError?: (error: Ably.ErrorInfo) => void;
57
- }
58
-
59
- // ---------------------------------------------------------------------------
60
- // Internal state machines
61
- // ---------------------------------------------------------------------------
62
-
63
- enum ServerTransportState {
64
- READY = 'ready',
65
- CLOSED = 'closed',
66
- }
67
-
68
- enum TurnState {
69
- INITIALIZED = 'initialized',
70
- STARTED = 'started',
71
- ENDED = 'ended',
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // Implementation
76
- // ---------------------------------------------------------------------------
77
-
78
- // Spec: AIT-ST1
79
- class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent, TMessage> {
80
- private readonly _channel: Ably.RealtimeChannel;
81
- private readonly _codec: ServerTransportOptions<TEvent, TMessage>['codec'];
82
- private readonly _logger: Logger | undefined;
83
- private readonly _onError: ((error: Ably.ErrorInfo) => void) | undefined;
84
- private readonly _turnManager: TurnManager;
85
- private readonly _registeredTurns = new Map<string, RegisteredTurn>();
86
- private readonly _channelListener: (msg: Ably.InboundMessage) => void;
87
- private readonly _attachPromise: Promise<void>;
88
-
89
- private _state = ServerTransportState.READY;
90
- private _hasAttachedOnce: boolean;
91
- private readonly _onChannelStateChange: Ably.channelEventCallback;
92
-
93
- constructor(options: ServerTransportOptions<TEvent, TMessage>) {
94
- this._channel = options.channel;
95
- this._codec = options.codec;
96
- this._logger = options.logger?.withContext({ component: 'ServerTransport' });
97
- this._onError = options.onError;
98
- this._turnManager = createTurnManager(this._channel, this._logger);
99
-
100
- this._channelListener = (msg: Ably.InboundMessage) => {
101
- this._handleChannelMessage(msg);
102
- };
103
-
104
- // Spec: AIT-ST2
105
- // Subscribe before attach (RTL7g) — ensures no messages are missed.
106
- this._attachPromise = this._channel.subscribe(EVENT_CANCEL, this._channelListener).then(
107
- /* eslint-disable @typescript-eslint/no-empty-function -- discard subscription handle */
108
- () => {},
109
- /* eslint-enable @typescript-eslint/no-empty-function */
110
- (error: unknown) => {
111
- const errInfo = new Ably.ErrorInfo(
112
- `unable to subscribe to cancel messages; ${error instanceof Error ? error.message : String(error)}`,
113
- ErrorCode.TransportSubscriptionError,
114
- 500,
115
- error instanceof Ably.ErrorInfo ? error : undefined,
116
- );
117
- this._logger?.error('DefaultServerTransport(); subscribe failed');
118
- this._onError?.(errInfo);
119
- },
120
- );
121
-
122
- // Spec: AIT-ST12, AIT-ST12a
123
- // Listen for channel state changes that break message continuity. The
124
- // server only consumes cancel messages from the channel, so losing one
125
- // is survivable — but the developer needs to know so they can decide
126
- // whether to abort in-flight work. _hasAttachedOnce is seeded from the
127
- // channel's current state so pre-attached channels are handled correctly;
128
- // it distinguishes the initial attach from a genuine discontinuity.
129
- this._hasAttachedOnce = this._channel.state === 'attached';
130
- this._onChannelStateChange = (stateChange: Ably.ChannelStateChange) => {
131
- this._handleChannelStateChange(stateChange);
132
- };
133
- this._channel.on(this._onChannelStateChange);
134
-
135
- this._logger?.debug('DefaultServerTransport(); transport created');
136
- }
137
-
138
- // -------------------------------------------------------------------------
139
- // Public API
140
- // -------------------------------------------------------------------------
141
-
142
- // Spec: AIT-ST3
143
- newTurn(turnOpts: NewTurnOptions<TEvent>): Turn<TEvent, TMessage> {
144
- this._logger?.trace('DefaultServerTransport.newTurn();', { turnId: turnOpts.turnId });
145
- return this._createTurn(turnOpts);
146
- }
147
-
148
- // Spec: AIT-ST11
149
- close(): void {
150
- if (this._state === ServerTransportState.CLOSED) return;
151
- this._state = ServerTransportState.CLOSED;
152
- this._logger?.trace('DefaultServerTransport.close();');
153
- this._channel.unsubscribe(EVENT_CANCEL, this._channelListener);
154
- this._channel.off(this._onChannelStateChange);
155
- for (const reg of this._registeredTurns.values()) {
156
- reg.controller.abort();
157
- }
158
- this._registeredTurns.clear();
159
- this._turnManager.close();
160
- this._logger?.debug('DefaultServerTransport.close(); transport closed');
161
- }
162
-
163
- // -------------------------------------------------------------------------
164
- // Cancel message routing
165
- // -------------------------------------------------------------------------
166
-
167
- private _resolveFilter(filter: CancelFilter, senderClientId?: string): string[] {
168
- const turnIds = [...this._registeredTurns.keys()];
169
-
170
- if (filter.all) return turnIds;
171
- if (filter.own && senderClientId) {
172
- return turnIds.filter((id) => this._registeredTurns.get(id)?.clientId === senderClientId);
173
- }
174
- if (filter.clientId) {
175
- return turnIds.filter((id) => this._registeredTurns.get(id)?.clientId === filter.clientId);
176
- }
177
- if (filter.turnId && this._registeredTurns.has(filter.turnId)) {
178
- return [filter.turnId];
179
- }
180
- return [];
181
- }
182
-
183
- // Spec: AIT-ST8, AIT-ST8a, AIT-ST8b, AIT-ST8c, AIT-ST8d, AIT-ST9, AIT-ST9a
184
- private async _handleCancelMessage(msg: Ably.InboundMessage): Promise<void> {
185
- const headers = getHeaders(msg);
186
-
187
- // Spec: AIT-ST8a, AIT-ST8b, AIT-ST8c, AIT-ST8d
188
- const filter: CancelFilter = {};
189
- if (headers[HEADER_CANCEL_TURN_ID]) {
190
- filter.turnId = headers[HEADER_CANCEL_TURN_ID];
191
- } else if (headers[HEADER_CANCEL_OWN] === 'true') {
192
- filter.own = true;
193
- } else if (headers[HEADER_CANCEL_CLIENT_ID]) {
194
- filter.clientId = headers[HEADER_CANCEL_CLIENT_ID];
195
- } else if (headers[HEADER_CANCEL_ALL] === 'true') {
196
- filter.all = true;
197
- }
198
-
199
- const matchedTurnIds = this._resolveFilter(filter, msg.clientId);
200
- if (matchedTurnIds.length === 0) return;
201
-
202
- this._logger?.debug('DefaultServerTransport._handleCancelMessage(); matched turns', {
203
- matchedTurnIds,
204
- filter,
205
- });
206
-
207
- const owners = new Map<string, string>();
208
- for (const tid of matchedTurnIds) {
209
- const reg = this._registeredTurns.get(tid);
210
- owners.set(tid, reg?.clientId ?? '');
211
- }
212
- const request: CancelRequest = { message: msg, filter, matchedTurnIds, turnOwners: owners };
213
-
214
- for (const turnId of matchedTurnIds) {
215
- const reg = this._registeredTurns.get(turnId);
216
- if (!reg) continue;
217
-
218
- try {
219
- if (reg.onCancel) {
220
- const allowed = await reg.onCancel(request);
221
- if (!allowed) {
222
- this._logger?.debug('DefaultServerTransport._handleCancelMessage(); cancel rejected by onCancel', {
223
- turnId,
224
- });
225
- continue;
226
- }
227
- }
228
- reg.controller.abort();
229
- this._logger?.debug('DefaultServerTransport._handleCancelMessage(); turn aborted', { turnId });
230
- } catch (error) {
231
- // A throwing onCancel handler must not prevent other turns from being cancelled.
232
- const errInfo = new Ably.ErrorInfo(
233
- `unable to process cancel for turn ${turnId}; onCancel handler threw: ${error instanceof Error ? error.message : String(error)}`,
234
- ErrorCode.CancelListenerError,
235
- 500,
236
- error instanceof Ably.ErrorInfo ? error : undefined,
237
- );
238
- this._logger?.error('DefaultServerTransport._handleCancelMessage(); onCancel threw', { turnId });
239
- (reg.onError ?? this._onError)?.(errInfo);
240
- }
241
- }
242
- }
243
-
244
- // -------------------------------------------------------------------------
245
- // Channel state change handler
246
- // -------------------------------------------------------------------------
247
-
248
- // Spec: AIT-ST12, AIT-ST12a
249
- private _handleChannelStateChange(stateChange: Ably.ChannelStateChange): void {
250
- if (this._state === ServerTransportState.CLOSED) return;
251
-
252
- const { current, resumed } = stateChange;
253
-
254
- // Track the initial attach so we don't treat it as a discontinuity
255
- if (current === 'attached' && !this._hasAttachedOnce) {
256
- this._hasAttachedOnce = true;
257
- return;
258
- }
259
-
260
- // Continuity-breaking states:
261
- // - FAILED, SUSPENDED, DETACHED: no more messages expected (or gap)
262
- // - ATTACHED with resumed: false (UPDATE): messages were lost
263
- const continuityLost =
264
- current === 'failed' || current === 'suspended' || current === 'detached' || (current === 'attached' && !resumed);
265
-
266
- if (!continuityLost) return;
267
-
268
- this._logger?.error('DefaultServerTransport._handleChannelStateChange(); channel continuity lost', {
269
- current,
270
- resumed,
271
- previous: stateChange.previous,
272
- });
273
-
274
- const err = new Ably.ErrorInfo(
275
- `unable to deliver cancel messages; channel continuity lost (${current}${current === 'attached' ? ', resumed: false' : ''})`,
276
- ErrorCode.ChannelContinuityLost,
277
- 500,
278
- stateChange.reason,
279
- );
280
-
281
- // Transport-level notification only: continuity loss is not scoped to any
282
- // turn. Per-turn onError handlers are reserved for errors from that turn's
283
- // own operations (publish failures, encoder errors). Developers that need
284
- // per-turn reaction can iterate active turns from the transport handler.
285
- this._onError?.(err);
286
- }
287
-
288
- // -------------------------------------------------------------------------
289
- // Channel subscription handler
290
- // -------------------------------------------------------------------------
291
-
292
- private _handleChannelMessage(msg: Ably.InboundMessage): void {
293
- try {
294
- if (msg.name === EVENT_CANCEL) {
295
- // Fire-and-forget async handler — errors are caught internally.
296
- this._handleCancelMessage(msg).catch((error: unknown) => {
297
- const errInfo = new Ably.ErrorInfo(
298
- `unable to route cancel message; ${error instanceof Error ? error.message : String(error)}`,
299
- ErrorCode.CancelListenerError,
300
- 500,
301
- error instanceof Ably.ErrorInfo ? error : undefined,
302
- );
303
- this._logger?.error('DefaultServerTransport._handleChannelMessage(); cancel routing error');
304
- this._onError?.(errInfo);
305
- });
306
- }
307
- } catch (error) {
308
- const errInfo = new Ably.ErrorInfo(
309
- `unable to process channel message; ${error instanceof Error ? error.message : String(error)}`,
310
- ErrorCode.TransportSubscriptionError,
311
- 500,
312
- error instanceof Ably.ErrorInfo ? error : undefined,
313
- );
314
- this._logger?.error('DefaultServerTransport._handleChannelMessage(); subscription error');
315
- this._onError?.(errInfo);
316
- }
317
- }
318
-
319
- // -------------------------------------------------------------------------
320
- // Turn creation
321
- // -------------------------------------------------------------------------
322
-
323
- private _createTurn(turnOpts: NewTurnOptions<TEvent>): Turn<TEvent, TMessage> {
324
- const {
325
- turnId,
326
- clientId: turnClientId,
327
- onMessage,
328
- onAbort,
329
- onCancel,
330
- onError: turnOnError,
331
- parent: turnParent,
332
- forkOf: turnForkOf,
333
- signal: externalSignal,
334
- } = turnOpts;
335
-
336
- const controller = new AbortController();
337
- let state = TurnState.INITIALIZED;
338
-
339
- // Compose the internal controller signal with the external signal (e.g.
340
- // req.signal) so platform-level cancellation (request cancellation, function
341
- // timeout) aborts the turn through the same path as Ably cancel messages.
342
- const signal = externalSignal ? AbortSignal.any([controller.signal, externalSignal]) : controller.signal;
343
-
344
- // Spec: AIT-ST3a — register immediately so early cancels can fire the abort signal.
345
- const registration: RegisteredTurn = {
346
- turnId,
347
- clientId: turnClientId ?? '',
348
- controller,
349
- signal,
350
- onCancel,
351
- onError: turnOnError,
352
- };
353
- this._registeredTurns.set(turnId, registration);
354
-
355
- // Capture instance members as locals so arrow functions close over them
356
- // without needing `this` (avoids unicorn/no-this-assignment).
357
- const logger = this._logger;
358
- const turnManager = this._turnManager;
359
- const attachPromise = this._attachPromise;
360
- const codec = this._codec;
361
- const channel = this._channel;
362
- const registeredTurns = this._registeredTurns;
363
-
364
- const turn: Turn<TEvent, TMessage> = {
365
- get turnId() {
366
- return turnId;
367
- },
368
- get abortSignal() {
369
- return signal;
370
- },
371
-
372
- // Spec: AIT-ST4, AIT-ST4a, AIT-ST4b
373
- start: async (): Promise<void> => {
374
- logger?.trace('Turn.start();', { turnId });
375
-
376
- // Spec: AIT-ST4a
377
- if (signal.aborted) {
378
- throw new Ably.ErrorInfo(
379
- `unable to start turn; turn ${turnId} was cancelled before start()`,
380
- ErrorCode.InvalidArgument,
381
- 400,
382
- );
383
- }
384
- if (state !== TurnState.INITIALIZED) return;
385
- state = TurnState.STARTED;
386
-
387
- try {
388
- await turnManager.startTurn(turnId, turnClientId, controller, {
389
- parent: turnParent,
390
- forkOf: turnForkOf,
391
- });
392
- } catch (error) {
393
- const errInfo = new Ably.ErrorInfo(
394
- `unable to publish turn-start for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
395
- ErrorCode.TurnLifecycleError,
396
- 500,
397
- error instanceof Ably.ErrorInfo ? error : undefined,
398
- );
399
- logger?.error('Turn.start(); failed to publish turn-start', { turnId });
400
- throw errInfo;
401
- }
402
-
403
- logger?.debug('Turn.start(); turn started', { turnId });
404
- },
405
-
406
- // Spec: AIT-ST5, AIT-ST5a, AIT-ST5b, AIT-ST5c
407
- addMessages: async (nodes: MessageNode<TMessage>[], opts?: AddMessageOptions): Promise<AddMessagesResult> => {
408
- logger?.trace('Turn.addMessages();', { turnId, count: nodes.length });
409
-
410
- if (state === TurnState.INITIALIZED) {
411
- throw new Ably.ErrorInfo(
412
- `unable to add messages; start() must be called before addMessages() (turn ${turnId})`,
413
- ErrorCode.InvalidArgument,
414
- 400,
415
- );
416
- }
417
- await attachPromise;
418
-
419
- const msgIds: string[] = [];
420
-
421
- try {
422
- for (const node of nodes) {
423
- // Build transport headers from the node's typed fields, then merge
424
- // any extra headers from the node (e.g. domain-specific headers).
425
- const headers = mergeHeaders(
426
- buildTransportHeaders({
427
- role: 'user',
428
- turnId,
429
- msgId: node.msgId,
430
- turnClientId: opts?.clientId,
431
- parent: node.parentId ?? turnParent,
432
- forkOf: node.forkOf ?? turnForkOf,
433
- }),
434
- node.headers,
435
- );
436
-
437
- const encoder = codec.createEncoder(channel, {
438
- extras: { headers },
439
- onMessage,
440
- });
441
-
442
- await encoder.writeMessages([node.message], opts?.clientId ? { clientId: opts.clientId } : undefined);
443
-
444
- msgIds.push(node.msgId);
445
- }
446
- } catch (error) {
447
- const errInfo = new Ably.ErrorInfo(
448
- `unable to publish messages for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
449
- ErrorCode.TurnLifecycleError,
450
- 500,
451
- error instanceof Ably.ErrorInfo ? error : undefined,
452
- );
453
- logger?.error('Turn.addMessages(); publish failed', { turnId });
454
- throw errInfo;
455
- }
456
-
457
- logger?.debug('Turn.addMessages(); messages published', { turnId, count: nodes.length });
458
- return { msgIds };
459
- },
460
-
461
- // Spec: AIT-ST5c
462
- addEvents: async (nodes: EventsNode<TEvent>[]): Promise<void> => {
463
- logger?.trace('Turn.addEvents();', { turnId, count: nodes.length });
464
-
465
- if (state === TurnState.INITIALIZED) {
466
- throw new Ably.ErrorInfo(
467
- `unable to add events; start() must be called before addEvents() (turn ${turnId})`,
468
- ErrorCode.InvalidArgument,
469
- 400,
470
- );
471
- }
472
- await attachPromise;
473
-
474
- const turnOwnerClientId = turnManager.getClientId(turnId);
475
-
476
- try {
477
- for (const node of nodes) {
478
- const headers = buildTransportHeaders({
479
- role: 'assistant',
480
- turnId,
481
- msgId: node.msgId,
482
- turnClientId: turnOwnerClientId,
483
- amend: node.msgId,
484
- });
485
-
486
- const encoder = codec.createEncoder(channel, {
487
- extras: { headers },
488
- onMessage,
489
- });
490
-
491
- for (const event of node.events) {
492
- await encoder.writeEvent(event);
493
- }
494
-
495
- await encoder.close();
496
- }
497
- } catch (error) {
498
- const errInfo = new Ably.ErrorInfo(
499
- `unable to publish events for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
500
- ErrorCode.TurnLifecycleError,
501
- 500,
502
- error instanceof Ably.ErrorInfo ? error : undefined,
503
- );
504
- logger?.error('Turn.addEvents(); publish failed', { turnId });
505
- throw errInfo;
506
- }
507
-
508
- logger?.debug('Turn.addEvents(); events published', { turnId, count: nodes.length });
509
- },
510
-
511
- // Spec: AIT-ST6, AIT-ST6a, AIT-ST6b, AIT-ST6b1, AIT-ST6b2, AIT-ST6b3, AIT-ST6b4, AIT-ST6c
512
- streamResponse: async (
513
- stream: ReadableStream<TEvent>,
514
- streamOpts?: StreamResponseOptions<TEvent>,
515
- ): Promise<StreamResult> => {
516
- logger?.trace('Turn.streamResponse();', { turnId });
517
-
518
- if (state === TurnState.INITIALIZED) {
519
- throw new Ably.ErrorInfo(
520
- `unable to stream response; start() must be called before streamResponse() (turn ${turnId})`,
521
- ErrorCode.InvalidArgument,
522
- 400,
523
- );
524
- }
525
- await attachPromise;
526
-
527
- const turnOwnerClientId = turnManager.getClientId(turnId);
528
-
529
- // Per-operation parent overrides the turn-level default.
530
- const assistantParent = streamOpts?.parent === undefined ? turnParent : streamOpts.parent;
531
-
532
- const msgId = crypto.randomUUID();
533
- const defaultHeaders = buildTransportHeaders({
534
- role: 'assistant',
535
- turnId,
536
- msgId,
537
- turnClientId: turnOwnerClientId,
538
- parent: assistantParent,
539
- forkOf: streamOpts?.forkOf ?? turnForkOf,
540
- });
541
- const encoder = codec.createEncoder(channel, {
542
- extras: { headers: defaultHeaders },
543
- onMessage,
544
- messageId: msgId,
545
- });
546
-
547
- const result = await pipeStream(stream, encoder, signal, onAbort, streamOpts?.resolveWriteOptions, logger);
548
-
549
- if (result.error) {
550
- const errInfo = new Ably.ErrorInfo(
551
- `unable to stream response for turn ${turnId}; ${result.error.message}`,
552
- ErrorCode.StreamError,
553
- 500,
554
- result.error instanceof Ably.ErrorInfo ? result.error : undefined,
555
- );
556
- logger?.error('Turn.streamResponse(); stream error', { turnId });
557
- turnOnError?.(errInfo);
558
- }
559
-
560
- logger?.debug('Turn.streamResponse(); stream finished', { turnId, reason: result.reason });
561
- return result;
562
- },
563
-
564
- // Spec: AIT-ST7, AIT-ST7a, AIT-ST7b
565
- end: async (reason: TurnEndReason): Promise<void> => {
566
- logger?.trace('Turn.end();', { turnId, reason });
567
-
568
- if (state === TurnState.INITIALIZED) {
569
- throw new Ably.ErrorInfo(
570
- `unable to end turn; start() must be called before end() (turn ${turnId})`,
571
- ErrorCode.InvalidArgument,
572
- 400,
573
- );
574
- }
575
- if (state === TurnState.ENDED) return;
576
- state = TurnState.ENDED;
577
-
578
- try {
579
- await turnManager.endTurn(turnId, reason);
580
- } catch (error) {
581
- const errInfo = new Ably.ErrorInfo(
582
- `unable to publish turn-end for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
583
- ErrorCode.TurnLifecycleError,
584
- 500,
585
- error instanceof Ably.ErrorInfo ? error : undefined,
586
- );
587
- logger?.error('Turn.end(); failed to publish turn-end', { turnId });
588
- throw errInfo;
589
- } finally {
590
- registeredTurns.delete(turnId);
591
- }
592
-
593
- logger?.debug('Turn.end(); turn ended', { turnId, reason });
594
- },
595
- };
596
-
597
- return turn;
598
- }
599
- }
600
-
601
- // ---------------------------------------------------------------------------
602
- // Factory
603
- // ---------------------------------------------------------------------------
604
-
605
- /**
606
- * Create a server transport bound to the given channel and codec.
607
- * @param options - Transport configuration.
608
- * @returns A new {@link ServerTransport} instance.
609
- */
610
- export const createServerTransport = <TEvent, TMessage>(
611
- options: ServerTransportOptions<TEvent, TMessage>,
612
- ): ServerTransport<TEvent, TMessage> => new DefaultServerTransport(options);