@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,7 +1,7 @@
1
1
  /**
2
2
  * Encoder core — message append lifecycle machinery.
3
3
  *
4
- * Provides Ably primitives (publish, append, close, abort, flush) that
4
+ * Provides Ably primitives (publish, append, close, cancel, flush) that
5
5
  * domain-specific encoders wire their event types to.
6
6
  *
7
7
  * Domain encoders call `createEncoderCore(writer, options)` and use the
@@ -11,7 +11,13 @@
11
11
 
12
12
  import * as Ably from 'ably';
13
13
 
14
- import { HEADER_DISCRETE, HEADER_MSG_ID, HEADER_STATUS, HEADER_STREAM, HEADER_STREAM_ID } from '../../constants.js';
14
+ import {
15
+ HEADER_CODEC_MESSAGE_ID,
16
+ HEADER_DISCRETE,
17
+ HEADER_STATUS,
18
+ HEADER_STREAM,
19
+ HEADER_STREAM_ID,
20
+ } from '../../constants.js';
15
21
  import { ErrorCode } from '../../errors.js';
16
22
  import type { Logger } from '../../logger.js';
17
23
  import { mergeHeaders } from '../../utils.js';
@@ -36,8 +42,22 @@ interface StreamState {
36
42
  name: string;
37
43
  streamId: string;
38
44
  accumulated: string;
39
- persistentHeaders: Record<string, string>;
40
- aborted: boolean;
45
+ /** Transport-tier headers repeated on every append (`extras.ai.transport`). */
46
+ persistentTransport: Record<string, string>;
47
+ /** Codec-tier headers repeated on every append (`extras.ai.codec`). */
48
+ persistentCodec: Record<string, string>;
49
+ cancelled: boolean;
50
+ /** Set by `closeStream` — a completed stream must never receive a cancelled terminal. */
51
+ completed: boolean;
52
+ }
53
+
54
+ /**
55
+ * The SDK's `extras.ai` namespace as written to the wire: a `transport` tier
56
+ * (always present on SDK-published messages) and an optional `codec` tier.
57
+ */
58
+ interface AiExtras {
59
+ transport: Record<string, string>;
60
+ codec?: Record<string, string>;
41
61
  }
42
62
 
43
63
  interface PendingAppend {
@@ -57,32 +77,29 @@ export interface EncoderCore {
57
77
  /** Publish multiple discrete messages atomically in a single channel publish. */
58
78
  publishDiscreteBatch(payloads: MessagePayload[], opts?: WriteOptions): Promise<Ably.PublishResult>;
59
79
 
60
- /** Start a streamed message with x-ably-status:streaming. */
80
+ /** Start a streamed message with status:streaming. */
61
81
  startStream(streamId: string, payload: StreamPayload, opts?: WriteOptions): Promise<void>;
62
82
 
63
83
  /**
64
84
  * Append data to an in-flight streamed message. Fire-and-forget: errors are
65
- * collected internally and surfaced by {@link closeStream} or {@link close}.
85
+ * collected internally and surfaced by {@link closeStream},
86
+ * {@link cancelAllStreams} or {@link close}.
87
+ * @throws {Ably.ErrorInfo} InvalidArgument if there is no active stream for `streamId` or the core is closed.
66
88
  */
67
89
  appendStream(streamId: string, data: string): void;
68
90
 
69
91
  /**
70
- * Close a streamed message with x-ably-status:finished. Flushes all pending
92
+ * Close a streamed message with status:complete. Flushes all pending
71
93
  * appends for recovery before returning. Repeats persistent and payload headers.
94
+ * @throws {Ably.ErrorInfo} InvalidArgument if there is no active stream for `streamId`, or the encoder has been closed; EncoderRecoveryFailed if a failed append cannot be recovered during the flush.
72
95
  */
73
96
  closeStream(streamId: string, payload: StreamPayload): Promise<void>;
74
97
 
75
98
  /**
76
- * Abort a single in-progress stream (x-ably-status:aborted) and flush all
77
- * pending appends for recovery before returning.
78
- */
79
- abortStream(streamId: string, opts?: WriteOptions): Promise<void>;
80
-
81
- /**
82
- * Abort all in-progress streams (x-ably-status:aborted) and flush all
99
+ * Cancel all in-progress streams (status:cancelled) and flush all
83
100
  * pending appends for recovery before returning.
84
101
  */
85
- abortAllStreams(opts?: WriteOptions): Promise<void>;
102
+ cancelAllStreams(opts?: WriteOptions): Promise<void>;
86
103
 
87
104
  /** Flush + clear trackers. Idempotent. */
88
105
  close(): Promise<void>;
@@ -95,7 +112,6 @@ export interface EncoderCore {
95
112
  // Spec: AIT-CD1
96
113
  class DefaultEncoderCore implements EncoderCore {
97
114
  private readonly _writer: ChannelWriter;
98
- private readonly _defaultClientId: string | undefined;
99
115
  private readonly _defaultExtras: Extras | undefined;
100
116
  private readonly _onMessageHook: (message: Ably.Message) => void;
101
117
  private readonly _logger: Logger | undefined;
@@ -106,7 +122,6 @@ class DefaultEncoderCore implements EncoderCore {
106
122
 
107
123
  constructor(writer: ChannelWriter, options: EncoderCoreOptions = {}) {
108
124
  this._writer = writer;
109
- this._defaultClientId = options.clientId;
110
125
  this._defaultExtras = options.extras;
111
126
  this._onMessageHook =
112
127
  options.onMessage ??
@@ -128,14 +143,7 @@ class DefaultEncoderCore implements EncoderCore {
128
143
  async publishDiscreteBatch(payloads: MessagePayload[], opts?: WriteOptions): Promise<Ably.PublishResult> {
129
144
  this._assertNotClosed();
130
145
  this._logger?.trace('DefaultEncoderCore.publishDiscreteBatch();', { count: payloads.length });
131
- const msgs = payloads.map((p) => this._buildDiscreteMessage(p, opts));
132
- // Mark batch-published payloads as discrete message parts (from writeMessages).
133
- // The decoder relies on this header to distinguish message parts from lifecycle
134
- // events that also happen to be discrete (x-ably-stream: false).
135
- for (const msg of msgs) {
136
- // CAST: extras is built by _buildDiscreteMessage with a known { headers } shape.
137
- (msg.extras as { headers: Record<string, string> }).headers[HEADER_DISCRETE] = 'true';
138
- }
146
+ const msgs = payloads.map((p) => this._buildDiscreteMessage(p, opts, true));
139
147
  return this._writer.publish(msgs);
140
148
  }
141
149
 
@@ -144,17 +152,16 @@ class DefaultEncoderCore implements EncoderCore {
144
152
  this._assertNotClosed();
145
153
  this._logger?.trace('DefaultEncoderCore.startStream();', { name: payload.name, streamId });
146
154
 
147
- const allHeaders = this._buildHeaders(payload.headers ?? {}, opts);
148
- allHeaders[HEADER_STREAM] = 'true';
149
- allHeaders[HEADER_STATUS] = 'streaming';
150
- allHeaders[HEADER_STREAM_ID] = streamId;
155
+ const transport = this._buildTransport(payload.transportHeaders, opts);
156
+ transport[HEADER_STREAM] = 'true';
157
+ transport[HEADER_STATUS] = 'streaming';
158
+ transport[HEADER_STREAM_ID] = streamId;
159
+ const codec = payload.codecHeaders ?? {};
151
160
 
152
- const clientId = this._resolveClientId(opts);
153
161
  const msg: Ably.Message = {
154
162
  name: payload.name,
155
163
  data: payload.data,
156
- extras: { headers: allHeaders },
157
- ...(clientId ? { clientId } : {}),
164
+ extras: { ai: this._aiExtras(transport, codec) },
158
165
  };
159
166
 
160
167
  this._invokeOnMessage(msg);
@@ -175,8 +182,10 @@ class DefaultEncoderCore implements EncoderCore {
175
182
  name: payload.name,
176
183
  streamId,
177
184
  accumulated: payload.data,
178
- persistentHeaders: allHeaders,
179
- aborted: false,
185
+ persistentTransport: transport,
186
+ persistentCodec: codec,
187
+ cancelled: false,
188
+ completed: false,
180
189
  });
181
190
 
182
191
  this._logger?.debug('DefaultEncoderCore.startStream(); stream started', {
@@ -204,7 +213,7 @@ class DefaultEncoderCore implements EncoderCore {
204
213
  const appendMsg: Ably.Message = {
205
214
  serial: tracker.serial,
206
215
  data,
207
- extras: { headers: { ...tracker.persistentHeaders } },
216
+ extras: { ai: this._aiExtras({ ...tracker.persistentTransport }, { ...tracker.persistentCodec }) },
208
217
  };
209
218
 
210
219
  this._invokeOnMessage(appendMsg);
@@ -228,14 +237,17 @@ class DefaultEncoderCore implements EncoderCore {
228
237
 
229
238
  // Accumulate closing data so recovery has the full content
230
239
  tracker.accumulated += payload.data;
240
+ // Mark completed so a later cancelAllStreams (e.g. pipeStream terminating
241
+ // streams left open by an agent self-abort) skips this stream.
242
+ tracker.completed = true;
231
243
 
232
- const allHeaders = this._buildClosingHeaders(tracker, payload.headers ?? {});
233
- allHeaders[HEADER_STATUS] = 'finished';
244
+ const { transport, codec } = this._buildClosing(tracker, payload);
245
+ transport[HEADER_STATUS] = 'complete';
234
246
 
235
247
  const msg: Ably.Message = {
236
248
  serial: tracker.serial,
237
249
  data: payload.data,
238
- extras: { headers: allHeaders },
250
+ extras: { ai: this._aiExtras(transport, codec) },
239
251
  };
240
252
 
241
253
  this._invokeOnMessage(msg);
@@ -247,55 +259,25 @@ class DefaultEncoderCore implements EncoderCore {
247
259
  this._logger?.debug('DefaultEncoderCore.closeStream(); stream closed', { streamId });
248
260
  }
249
261
 
250
- // Spec: AIT-CD5, AIT-CD5b
251
- async abortStream(streamId: string, opts?: WriteOptions): Promise<void> {
262
+ // Spec: AIT-CD5, AIT-CD5a
263
+ async cancelAllStreams(opts?: WriteOptions): Promise<void> {
252
264
  this._assertNotClosed();
253
- this._logger?.trace('DefaultEncoderCore.abortStream();', { streamId });
254
-
255
- const tracker = this._trackers.get(streamId);
256
- if (!tracker) {
257
- throw new Ably.ErrorInfo(
258
- `unable to abort stream; no active stream for streamId '${streamId}'`,
259
- ErrorCode.InvalidArgument,
260
- 400,
261
- );
262
- }
263
-
264
- tracker.aborted = true;
265
-
266
- const allHeaders = this._buildClosingHeaders(tracker, {}, opts);
267
- allHeaders[HEADER_STATUS] = 'aborted';
268
-
269
- const msg: Ably.Message = {
270
- serial: tracker.serial,
271
- data: '',
272
- extras: { headers: allHeaders },
273
- };
274
-
275
- this._invokeOnMessage(msg);
276
- const p = this._writer.appendMessage(msg);
277
- this._pending.push({ promise: p, streamId });
278
-
279
- await this._flushPending();
280
-
281
- this._logger?.debug('DefaultEncoderCore.abortStream(); stream aborted', { streamId });
282
- }
283
-
284
- // Spec: AIT-CD5a
285
- async abortAllStreams(opts?: WriteOptions): Promise<void> {
286
- this._assertNotClosed();
287
- this._logger?.trace('DefaultEncoderCore.abortAllStreams();', { streamCount: this._trackers.size });
265
+ this._logger?.trace('DefaultEncoderCore.cancelAllStreams();', { streamCount: this._trackers.size });
288
266
 
289
267
  for (const tracker of this._trackers.values()) {
290
- tracker.aborted = true;
268
+ // Idempotent and complete-safe: a stream already cancelled must not be
269
+ // re-appended on a repeat call, and a stream that closed with
270
+ // status:complete must never receive a cancelled terminal.
271
+ if (tracker.cancelled || tracker.completed) continue;
272
+ tracker.cancelled = true;
291
273
 
292
- const allHeaders = this._buildClosingHeaders(tracker, {}, opts);
293
- allHeaders[HEADER_STATUS] = 'aborted';
274
+ const { transport, codec } = this._buildClosing(tracker, undefined, opts);
275
+ transport[HEADER_STATUS] = 'cancelled';
294
276
 
295
277
  const msg: Ably.Message = {
296
278
  serial: tracker.serial,
297
279
  data: '',
298
- extras: { headers: allHeaders },
280
+ extras: { ai: this._aiExtras(transport, codec) },
299
281
  };
300
282
 
301
283
  this._invokeOnMessage(msg);
@@ -354,11 +336,16 @@ class DefaultEncoderCore implements EncoderCore {
354
336
  const tracker = this._trackers.get(streamId);
355
337
  if (!tracker) continue;
356
338
 
357
- const recoveryStatus = tracker.aborted ? 'aborted' : 'finished';
339
+ const recoveryStatus = tracker.cancelled ? 'cancelled' : 'complete';
358
340
  const msg: Ably.Message = {
359
341
  serial: tracker.serial,
360
342
  data: tracker.accumulated,
361
- extras: { headers: { ...tracker.persistentHeaders, [HEADER_STATUS]: recoveryStatus } },
343
+ extras: {
344
+ ai: this._aiExtras(
345
+ { ...tracker.persistentTransport, [HEADER_STATUS]: recoveryStatus },
346
+ { ...tracker.persistentCodec },
347
+ ),
348
+ },
362
349
  };
363
350
 
364
351
  try {
@@ -411,32 +398,53 @@ class DefaultEncoderCore implements EncoderCore {
411
398
  }
412
399
  }
413
400
 
414
- private _resolveClientId(opts?: WriteOptions): string | undefined {
415
- return opts?.clientId ?? this._defaultClientId;
416
- }
417
-
418
- private _buildHeaders(codecHeaders: Record<string, string>, opts?: WriteOptions): Record<string, string> {
401
+ /**
402
+ * Build the transport-tier header record for a message: caller-configured
403
+ * transport headers (default extras + per-write overrides) layered with any
404
+ * transport headers the codec payload stamps directly, plus the message-id.
405
+ * @param payloadTransport - Transport headers carried on the codec payload.
406
+ * @param opts - Optional per-write overrides.
407
+ * @returns The transport-tier headers record (`extras.ai.transport`).
408
+ */
409
+ private _buildTransport(
410
+ payloadTransport: Record<string, string> | undefined,
411
+ opts?: WriteOptions,
412
+ ): Record<string, string> {
419
413
  const callerHeaders = mergeHeaders(this._defaultExtras?.headers, opts?.extras?.headers);
420
- const merged = { ...callerHeaders, ...codecHeaders };
414
+ const transport = { ...callerHeaders, ...payloadTransport };
421
415
  if (opts?.messageId !== undefined) {
422
- merged[HEADER_MSG_ID] = opts.messageId;
416
+ transport[HEADER_CODEC_MESSAGE_ID] = opts.messageId;
423
417
  }
424
- return merged;
418
+ return transport;
425
419
  }
426
420
 
427
- private _buildDiscreteMessage(payload: MessagePayload, opts?: WriteOptions): Ably.Message {
428
- const headers = this._buildHeaders(payload.headers ?? {}, opts);
429
- headers[HEADER_STREAM] = 'false';
430
- const clientId = this._resolveClientId(opts);
421
+ /**
422
+ * Assemble the `extras.ai` namespace from its two tiers, omitting the codec
423
+ * tier when empty.
424
+ * @param transport - Transport-tier headers (always present on SDK messages).
425
+ * @param codec - Codec-tier headers; omitted from the wire when empty.
426
+ * @returns The `extras.ai` object.
427
+ */
428
+ private _aiExtras(transport: Record<string, string>, codec: Record<string, string>): AiExtras {
429
+ return Object.keys(codec).length > 0 ? { transport, codec } : { transport };
430
+ }
431
431
 
432
+ private _buildDiscreteMessage(payload: MessagePayload, opts?: WriteOptions, discrete = false): Ably.Message {
433
+ const transport = this._buildTransport(payload.transportHeaders, opts);
434
+ transport[HEADER_STREAM] = 'false';
435
+ if (discrete) {
436
+ // Mark batch-published payloads as discrete message parts (from writeMessages).
437
+ // The decoder relies on this header to distinguish message parts from lifecycle
438
+ // events that also happen to be discrete (stream: false).
439
+ transport[HEADER_DISCRETE] = 'true';
440
+ }
432
441
  const msg: Ably.Message = {
433
442
  name: payload.name,
434
443
  data: payload.data,
435
444
  extras: {
436
- headers,
445
+ ai: this._aiExtras(transport, payload.codecHeaders ?? {}),
437
446
  ...(payload.ephemeral ? { ephemeral: true } : {}),
438
447
  },
439
- ...(clientId ? { clientId } : {}),
440
448
  };
441
449
 
442
450
  this._invokeOnMessage(msg);
@@ -444,24 +452,23 @@ class DefaultEncoderCore implements EncoderCore {
444
452
  }
445
453
 
446
454
  /**
447
- * Build headers for a closing append. Closing appends must repeat ALL
448
- * persistent headers (Ably replaces the entire extras object on append).
455
+ * Build both header tiers for a closing append. Closing appends must repeat
456
+ * ALL persistent headers (Ably replaces the entire extras object on append).
449
457
  * Then layer caller and codec overrides.
450
458
  * @param tracker - The stream tracker with persistent headers.
451
- * @param codecHeaders - Codec-layer headers to merge.
459
+ * @param payload - The closing stream payload (codec + transport headers).
452
460
  * @param opts - Optional per-write overrides.
453
- * @returns Merged headers for the closing append.
461
+ * @returns The two tiers for the closing append.
454
462
  */
455
- private _buildClosingHeaders(
463
+ private _buildClosing(
456
464
  tracker: StreamState,
457
- codecHeaders: Record<string, string>,
465
+ payload: StreamPayload | undefined,
458
466
  opts?: WriteOptions,
459
- ): Record<string, string> {
460
- const h = { ...tracker.persistentHeaders };
467
+ ): { transport: Record<string, string>; codec: Record<string, string> } {
461
468
  const callerHeaders = mergeHeaders(this._defaultExtras?.headers, opts?.extras?.headers);
462
- Object.assign(h, callerHeaders);
463
- Object.assign(h, codecHeaders);
464
- return h;
469
+ const transport = { ...tracker.persistentTransport, ...callerHeaders, ...payload?.transportHeaders };
470
+ const codec = { ...tracker.persistentCodec, ...payload?.codecHeaders };
471
+ return { transport, codec };
465
472
  }
466
473
  }
467
474
 
@@ -472,7 +479,7 @@ class DefaultEncoderCore implements EncoderCore {
472
479
  /**
473
480
  * Create an encoder core bound to the given channel writer.
474
481
  * @param writer - The channel writer to publish messages through.
475
- * @param options - Encoder configuration (clientId, extras, hooks, logger).
482
+ * @param options - Encoder configuration (extras, hooks, logger).
476
483
  * @returns A new {@link EncoderCore} instance.
477
484
  */
478
485
  export const createEncoderCore = (writer: ChannelWriter, options: EncoderCoreOptions = {}): EncoderCore =>
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Shared header-field bag helpers.
3
+ *
4
+ * The wire dispatch discriminator (`kind`) plus the symmetric field↔headers
5
+ * write and read used by the descriptor drivers. Centralised so encode and
6
+ * decode operate on the same record shape and the dispatch key has one home —
7
+ * and so the input drivers can reuse the same primitives as the output drivers.
8
+ */
9
+
10
+ import type { HeaderField } from './fields.js';
11
+ import type { OutputDescriptor, OutputEventDescriptor } from './output-descriptors.js';
12
+
13
+ /** The codec header carrying the SDK-controlled dispatch kind / stream family id. */
14
+ export const KIND_HEADER = 'kind';
15
+
16
+ /** The sentinel suffix marking a descriptor literal as a wildcard family. */
17
+ const WILDCARD_SUFFIX = '-*';
18
+
19
+ /**
20
+ * Derive a wildcard dispatch predicate from a descriptor literal: a literal
21
+ * ending in `-*` matches any value sharing its prefix, so the literal and its
22
+ * predicate can never disagree. Returns `undefined` for an exact literal.
23
+ * Shared by the output event builder and the input part builder so the `-*`
24
+ * sentinel rule lives in one place, next to the {@link partFor} that consumes it.
25
+ * @param literal - The declared descriptor literal (`type` / `partType`).
26
+ * @returns A prefix-match predicate for a wildcard literal, else `undefined`.
27
+ */
28
+ export const wildcardMatcher = (literal: string): ((value: string) => boolean) | undefined =>
29
+ literal.endsWith(WILDCARD_SUFFIX) ? (value: string): boolean => value.startsWith(literal.slice(0, -1)) : undefined;
30
+
31
+ /**
32
+ * The codec header carrying a batch part's sub-discriminator. A batch stamps it
33
+ * on every exploded part on encode; the decoder reads it back to resolve the
34
+ * matching part descriptor. Centralised so the key has one home across the
35
+ * input encode and decode drivers and cannot drift between them.
36
+ */
37
+ export const PART_TYPE_HEADER = 'partType';
38
+
39
+ /**
40
+ * Read the value at a declared field key off a source object.
41
+ * @param source - The object to index (a chunk, or a lensed sub-object such as a payload).
42
+ * @param key - The declared field key.
43
+ * @returns The value at `key`, typed `unknown`.
44
+ */
45
+ // CAST: a descriptor indexes a source object's props by a declared key. The
46
+ // source's indexed type isn't statically known here, but a descriptor only ever
47
+ // runs against the member it matches, so the value has the field's type at runtime.
48
+ export const prop = (source: object, key: string): unknown => (source as Record<string, unknown>)[key];
49
+
50
+ /**
51
+ * Build a codec-headers record from a source object through declared fields,
52
+ * seeded with the dispatch `kind`. Each field writes the value at its key on
53
+ * `source`; an optional `keys` subset restricts which fields are written.
54
+ * @param fields - The declared header fields.
55
+ * @param kindValue - The dispatch kind / stream family id to seed under {@link KIND_HEADER}.
56
+ * @param source - The object to read field values from (a chunk, or a lensed payload).
57
+ * @param keys - Optional subset of field keys to write; omit to write all.
58
+ * @returns The codec-headers record.
59
+ */
60
+ export const writeFields = (
61
+ fields: readonly HeaderField<unknown>[],
62
+ kindValue: string,
63
+ source: object,
64
+ keys?: readonly string[],
65
+ ): Record<string, string> => {
66
+ const rec: Record<string, string> = { [KIND_HEADER]: kindValue };
67
+ for (const field of fields) {
68
+ if (keys && !keys.includes(field.key)) continue;
69
+ field.write(rec, prop(source, field.key));
70
+ }
71
+ return rec;
72
+ };
73
+
74
+ /**
75
+ * Read declared fields out of a codec-headers record into a bag keyed by field key.
76
+ * A field that reads `undefined` (absent, with no default) contributes no key — the
77
+ * bag carries only the values that are actually present.
78
+ * @param fields - The declared header fields.
79
+ * @param headers - The inbound codec-tier headers.
80
+ * @returns A bag of the present field values, keyed by each field's key.
81
+ */
82
+ /** The structural slice of a part descriptor {@link partFor} dispatches on. */
83
+ interface PartDispatch {
84
+ /** The exact `partType` literal, or the `-*` wildcard literal for a family. */
85
+ partType: string;
86
+ /** Wildcard dispatch predicate; absent for an exact part. */
87
+ match?: (partType: string) => boolean;
88
+ }
89
+
90
+ /**
91
+ * Resolve the part descriptor for a `partType`: an exact non-wildcard match,
92
+ * else a wildcard whose derived predicate accepts it. Wildcards are excluded
93
+ * from the exact pass — only their predicate may route to them. Shared by the
94
+ * input encode and decode drivers.
95
+ * @param parts - The batch's part descriptor sub-table.
96
+ * @param partType - The `partType` to resolve (from `partTypeOf` on encode, the wire header on decode).
97
+ * @returns The matching part descriptor, or undefined when none matches.
98
+ */
99
+ export const partFor = <P extends PartDispatch>(parts: readonly P[], partType: string): P | undefined =>
100
+ parts.find((part) => !part.match && part.partType === partType) ?? parts.find((part) => part.match?.(partType));
101
+
102
+ /** An output descriptor set's event descriptors, split for dispatch. */
103
+ export interface OutputEventDispatch<U> {
104
+ /** Exact (non-wildcard) event descriptors, keyed by `type`. */
105
+ discreteByType: Map<string, OutputEventDescriptor<U>>;
106
+ /** Wildcard event descriptors, dispatched by their `match` predicate. */
107
+ wildcards: OutputEventDescriptor<U>[];
108
+ }
109
+
110
+ /**
111
+ * Partition an output descriptor set's `event` descriptors into an exact-type
112
+ * map and a wildcard list. Stream descriptors are skipped — each driver indexes
113
+ * those by its own key (phase on encode, kind on decode). Shared by the output
114
+ * encode and decode drivers so the exact-vs-wildcard split has one home.
115
+ * @template U - The codec's event union.
116
+ * @param descriptors - The full descriptor set (events + streamed families).
117
+ * @returns The event descriptors split into {@link OutputEventDispatch}.
118
+ */
119
+ export const partitionOutputEvents = <U extends { type: string }>(
120
+ descriptors: readonly OutputDescriptor<U>[],
121
+ ): OutputEventDispatch<U> => {
122
+ const discreteByType = new Map<string, OutputEventDescriptor<U>>();
123
+ const wildcards: OutputEventDescriptor<U>[] = [];
124
+ for (const descriptor of descriptors) {
125
+ if (descriptor.construct !== 'event') continue;
126
+ if (descriptor.match) wildcards.push(descriptor);
127
+ else discreteByType.set(descriptor.type, descriptor);
128
+ }
129
+ return { discreteByType, wildcards };
130
+ };
131
+
132
+ export const readFields = (
133
+ fields: readonly HeaderField<unknown>[],
134
+ headers: Record<string, string>,
135
+ ): Record<string, unknown> => {
136
+ const bag: Record<string, unknown> = {};
137
+ for (const field of fields) {
138
+ const value = field.read(headers);
139
+ if (value !== undefined) bag[field.key] = value;
140
+ }
141
+ return bag;
142
+ };