@ably/ai-transport 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +93 -111
  2. package/dist/ably-ai-transport.js +2401 -1387
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +116 -42
  7. package/dist/core/agent.d.ts +44 -0
  8. package/dist/core/channel-options.d.ts +57 -0
  9. package/dist/core/codec/codec-event.d.ts +9 -0
  10. package/dist/core/codec/decoder.d.ts +24 -24
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +10 -12
  13. package/dist/core/codec/field-bag.d.ts +85 -0
  14. package/dist/core/codec/fields.d.ts +141 -0
  15. package/dist/core/codec/index.d.ts +8 -2
  16. package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
  17. package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
  18. package/dist/core/codec/input-descriptors.d.ts +281 -0
  19. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  20. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  21. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  22. package/dist/core/codec/output-descriptors.d.ts +237 -0
  23. package/dist/core/codec/types.d.ts +470 -119
  24. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  25. package/dist/core/transport/agent-session.d.ts +10 -0
  26. package/dist/core/transport/agent-view.d.ts +296 -0
  27. package/dist/core/transport/client-session.d.ts +13 -0
  28. package/dist/core/transport/decode-fold.d.ts +55 -0
  29. package/dist/core/transport/headers.d.ts +121 -14
  30. package/dist/core/transport/index.d.ts +5 -6
  31. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  32. package/dist/core/transport/invocation.d.ts +74 -0
  33. package/dist/core/transport/load-history-pages.d.ts +71 -0
  34. package/dist/core/transport/load-history.d.ts +44 -0
  35. package/dist/core/transport/pipe-stream.d.ts +9 -9
  36. package/dist/core/transport/run-manager.d.ts +76 -0
  37. package/dist/core/transport/session-support.d.ts +55 -0
  38. package/dist/core/transport/tree.d.ts +523 -109
  39. package/dist/core/transport/types/agent.d.ts +375 -0
  40. package/dist/core/transport/types/client.d.ts +201 -0
  41. package/dist/core/transport/types/shared.d.ts +24 -0
  42. package/dist/core/transport/types/tree.d.ts +357 -0
  43. package/dist/core/transport/types/view.d.ts +249 -0
  44. package/dist/core/transport/types.d.ts +13 -553
  45. package/dist/core/transport/view.d.ts +390 -84
  46. package/dist/core/transport/wire-log.d.ts +102 -0
  47. package/dist/errors.d.ts +27 -10
  48. package/dist/index.d.ts +8 -9
  49. package/dist/logger.d.ts +12 -0
  50. package/dist/react/ably-ai-transport-react.js +1365 -1010
  51. package/dist/react/ably-ai-transport-react.js.map +1 -1
  52. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  53. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  54. package/dist/react/contexts/client-session-context.d.ts +37 -0
  55. package/dist/react/contexts/client-session-provider.d.ts +56 -0
  56. package/dist/react/create-session-hooks.d.ts +116 -0
  57. package/dist/react/index.d.ts +13 -12
  58. package/dist/react/internal/skipped-session.d.ts +8 -0
  59. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  60. package/dist/react/use-ably-messages.d.ts +17 -14
  61. package/dist/react/use-client-session.d.ts +81 -0
  62. package/dist/react/use-create-view.d.ts +14 -13
  63. package/dist/react/use-tree.d.ts +30 -15
  64. package/dist/react/use-view.d.ts +81 -50
  65. package/dist/utils.d.ts +48 -71
  66. package/dist/vercel/ably-ai-transport-vercel.js +3257 -2499
  67. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  68. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  69. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  70. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  71. package/dist/vercel/codec/events.d.ts +50 -0
  72. package/dist/vercel/codec/fields.d.ts +44 -0
  73. package/dist/vercel/codec/fold-content.d.ts +16 -0
  74. package/dist/vercel/codec/fold-data.d.ts +16 -0
  75. package/dist/vercel/codec/fold-input.d.ts +67 -0
  76. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  77. package/dist/vercel/codec/fold-text.d.ts +16 -0
  78. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  79. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  80. package/dist/vercel/codec/index.d.ts +7 -20
  81. package/dist/vercel/codec/inputs.d.ts +11 -0
  82. package/dist/vercel/codec/outputs.d.ts +11 -0
  83. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  84. package/dist/vercel/codec/reducer.d.ts +62 -0
  85. package/dist/vercel/codec/tool-transitions.d.ts +2 -8
  86. package/dist/vercel/codec/wire-data.d.ts +34 -0
  87. package/dist/vercel/index.d.ts +5 -5
  88. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2859 -9705
  89. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  90. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -45
  91. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  92. package/dist/vercel/react/contexts/chat-transport-context.d.ts +9 -7
  93. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  94. package/dist/vercel/react/index.d.ts +1 -2
  95. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  96. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  97. package/dist/vercel/run-end-reason.d.ts +84 -0
  98. package/dist/vercel/tool-part.d.ts +21 -0
  99. package/dist/vercel/transport/chat-transport.d.ts +41 -24
  100. package/dist/vercel/transport/index.d.ts +24 -20
  101. package/dist/vercel/transport/run-output-stream.d.ts +54 -0
  102. package/dist/version.d.ts +2 -0
  103. package/package.json +31 -24
  104. package/src/constants.ts +124 -51
  105. package/src/core/agent.ts +92 -0
  106. package/src/core/channel-options.ts +89 -0
  107. package/src/core/codec/codec-event.ts +27 -0
  108. package/src/core/codec/decoder.ts +202 -105
  109. package/src/core/codec/define-codec.ts +432 -0
  110. package/src/core/codec/encoder.ts +114 -107
  111. package/src/core/codec/field-bag.ts +142 -0
  112. package/src/core/codec/fields.ts +193 -0
  113. package/src/core/codec/index.ts +56 -6
  114. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  115. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  116. package/src/core/codec/input-descriptors.ts +373 -0
  117. package/src/core/codec/lifecycle-tracker.ts +10 -9
  118. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  119. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  120. package/src/core/codec/output-descriptors.ts +307 -0
  121. package/src/core/codec/types.ts +505 -126
  122. package/src/core/codec/well-known-inputs.ts +96 -0
  123. package/src/core/transport/agent-session.ts +1085 -0
  124. package/src/core/transport/agent-view.ts +738 -0
  125. package/src/core/transport/client-session.ts +780 -0
  126. package/src/core/transport/decode-fold.ts +101 -0
  127. package/src/core/transport/headers.ts +234 -22
  128. package/src/core/transport/index.ts +27 -27
  129. package/src/core/transport/internal/bounded-map.ts +27 -0
  130. package/src/core/transport/invocation.ts +98 -0
  131. package/src/core/transport/load-history-pages.ts +220 -0
  132. package/src/core/transport/load-history.ts +271 -0
  133. package/src/core/transport/pipe-stream.ts +63 -39
  134. package/src/core/transport/run-manager.ts +243 -0
  135. package/src/core/transport/session-support.ts +96 -0
  136. package/src/core/transport/tree.ts +1293 -308
  137. package/src/core/transport/types/agent.ts +434 -0
  138. package/src/core/transport/types/client.ts +247 -0
  139. package/src/core/transport/types/shared.ts +27 -0
  140. package/src/core/transport/types/tree.ts +393 -0
  141. package/src/core/transport/types/view.ts +288 -0
  142. package/src/core/transport/types.ts +13 -706
  143. package/src/core/transport/view.ts +1229 -450
  144. package/src/core/transport/wire-log.ts +189 -0
  145. package/src/errors.ts +29 -9
  146. package/src/event-emitter.ts +3 -2
  147. package/src/index.ts +86 -42
  148. package/src/logger.ts +14 -1
  149. package/src/react/contexts/client-session-context.ts +41 -0
  150. package/src/react/contexts/client-session-provider.tsx +222 -0
  151. package/src/react/create-session-hooks.ts +141 -0
  152. package/src/react/index.ts +24 -13
  153. package/src/react/internal/skipped-session.ts +62 -0
  154. package/src/react/internal/use-resolved-session.ts +63 -0
  155. package/src/react/use-ably-messages.ts +32 -22
  156. package/src/react/use-client-session.ts +178 -0
  157. package/src/react/use-create-view.ts +33 -29
  158. package/src/react/use-tree.ts +61 -30
  159. package/src/react/use-view.ts +138 -96
  160. package/src/utils.ts +83 -131
  161. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  162. package/src/vercel/codec/events.ts +85 -0
  163. package/src/vercel/codec/fields.ts +58 -0
  164. package/src/vercel/codec/fold-content.ts +54 -0
  165. package/src/vercel/codec/fold-data.ts +46 -0
  166. package/src/vercel/codec/fold-input.ts +255 -0
  167. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  168. package/src/vercel/codec/fold-text.ts +55 -0
  169. package/src/vercel/codec/fold-tool-input.ts +86 -0
  170. package/src/vercel/codec/fold-tool-output.ts +79 -0
  171. package/src/vercel/codec/index.ts +28 -21
  172. package/src/vercel/codec/inputs.ts +116 -0
  173. package/src/vercel/codec/outputs.ts +207 -0
  174. package/src/vercel/codec/reducer-state.ts +169 -0
  175. package/src/vercel/codec/reducer.ts +191 -0
  176. package/src/vercel/codec/tool-transitions.ts +3 -14
  177. package/src/vercel/codec/wire-data.ts +64 -0
  178. package/src/vercel/index.ts +7 -19
  179. package/src/vercel/react/contexts/chat-transport-context.ts +8 -7
  180. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  181. package/src/vercel/react/index.ts +3 -5
  182. package/src/vercel/react/use-chat-transport.ts +44 -66
  183. package/src/vercel/react/use-message-sync.ts +75 -39
  184. package/src/vercel/run-end-reason.ts +157 -0
  185. package/src/vercel/tool-part.ts +25 -0
  186. package/src/vercel/transport/chat-transport.ts +380 -98
  187. package/src/vercel/transport/index.ts +38 -37
  188. package/src/vercel/transport/run-output-stream.ts +169 -0
  189. package/src/version.ts +2 -0
  190. package/dist/core/transport/client-transport.d.ts +0 -10
  191. package/dist/core/transport/decode-history.d.ts +0 -43
  192. package/dist/core/transport/server-transport.d.ts +0 -7
  193. package/dist/core/transport/stream-router.d.ts +0 -29
  194. package/dist/core/transport/turn-manager.d.ts +0 -37
  195. package/dist/react/contexts/transport-context.d.ts +0 -31
  196. package/dist/react/contexts/transport-provider.d.ts +0 -49
  197. package/dist/react/create-transport-hooks.d.ts +0 -124
  198. package/dist/react/use-active-turns.d.ts +0 -12
  199. package/dist/react/use-client-transport.d.ts +0 -80
  200. package/dist/vercel/codec/accumulator.d.ts +0 -21
  201. package/dist/vercel/codec/decoder.d.ts +0 -22
  202. package/dist/vercel/codec/encoder.d.ts +0 -41
  203. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  204. package/dist/vercel/tool-approvals.d.ts +0 -124
  205. package/dist/vercel/tool-events.d.ts +0 -26
  206. package/src/core/transport/client-transport.ts +0 -977
  207. package/src/core/transport/decode-history.ts +0 -485
  208. package/src/core/transport/server-transport.ts +0 -612
  209. package/src/core/transport/stream-router.ts +0 -136
  210. package/src/core/transport/turn-manager.ts +0 -165
  211. package/src/react/contexts/transport-context.ts +0 -37
  212. package/src/react/contexts/transport-provider.tsx +0 -164
  213. package/src/react/create-transport-hooks.ts +0 -144
  214. package/src/react/use-active-turns.ts +0 -72
  215. package/src/react/use-client-transport.ts +0 -197
  216. package/src/vercel/codec/accumulator.ts +0 -588
  217. package/src/vercel/codec/decoder.ts +0 -618
  218. package/src/vercel/codec/encoder.ts +0 -410
  219. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  220. package/src/vercel/tool-approvals.ts +0 -380
  221. package/src/vercel/tool-events.ts +0 -53
@@ -0,0 +1,432 @@
1
+ /**
2
+ * `defineCodec` — composition packaging for a codec.
3
+ *
4
+ * A codec author supplies only its **parts** — a reducer, a per-direction
5
+ * descriptor table (the `output` and `input` builder functions), an optional
6
+ * decode lifecycle policy, and an optional agent identifier — and `defineCodec`
7
+ * assembles a fully-formed {@link Codec}: the generic encoder/decoder skeletons
8
+ * (built here, codec-agnostic), the reducer methods, and the well-known input
9
+ * factories (merged internally).
10
+ *
11
+ * Both directions are declarative descriptor tables driven by the generic
12
+ * encode/decode drivers. `defineCodec` hands each table a direction-scoped
13
+ * builder typed to that direction's union — `{ event, stream }` for outputs,
14
+ * `{ event, batch }` for inputs — so each construct's spec stays type-correct
15
+ * per direction under shared construct names, with no per-entry casts. Both
16
+ * sides build/read wire headers through the same shared field bindings, so
17
+ * encode and decode cannot drift.
18
+ */
19
+
20
+ import * as Ably from 'ably';
21
+
22
+ import { EVENT_AI_INPUT, EVENT_AI_OUTPUT, HEADER_RUN_ID } from '../../constants.js';
23
+ import { ErrorCode } from '../../errors.js';
24
+ import type { DecoderCore, DecoderCoreHooks } from './decoder.js';
25
+ import { createDecoderCore } from './decoder.js';
26
+ import type { EncoderCore, EncoderCoreOptions } from './encoder.js';
27
+ import { createEncoderCore } from './encoder.js';
28
+ import { KIND_HEADER, PART_TYPE_HEADER } from './field-bag.js';
29
+ import type { HeaderField } from './fields.js';
30
+ import { createInputDescriptorDecoder, type InputDescriptorDecoder } from './input-descriptor-decoder.js';
31
+ import { createInputDescriptorEncoder, type InputDescriptorEncoder } from './input-descriptor-encoder.js';
32
+ import { type InputBuilder, inputBuilder, type InputDescriptor } from './input-descriptors.js';
33
+ import { createOutputDescriptorDecoder } from './output-descriptor-decoder.js';
34
+ import { createOutputDescriptorEncoder, type OutputDescriptorEncoder } from './output-descriptor-encoder.js';
35
+ import { type OutputBuilder, outputBuilder, type OutputDescriptor } from './output-descriptors.js';
36
+ import type {
37
+ ChannelWriter,
38
+ Codec,
39
+ CodecEvent,
40
+ CodecInputEvent,
41
+ CodecMessage,
42
+ CodecOutputEvent,
43
+ DecodedMessage,
44
+ Decoder,
45
+ Encoder,
46
+ MessagePayload,
47
+ ReducerMeta,
48
+ StreamTrackerState,
49
+ WriteOptions,
50
+ } from './types.js';
51
+ import { type WellKnownInputFactories, wellKnownInputs } from './well-known-inputs.js';
52
+
53
+ // Re-exported so codec descriptor tables (e.g. the Vercel `inputs.ts` / `outputs.ts`)
54
+ // can type their builder parameter without reaching into the descriptor modules directly.
55
+ export type { InputBuilder } from './input-descriptors.js';
56
+ export type { OutputBuilder } from './output-descriptors.js';
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Decode lifecycle policy
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /** Context passed to a {@link LifecyclePolicy} `onDiscrete` repair function. */
63
+ export interface LifecycleDiscreteContext {
64
+ /** The inbound codec-tier headers (e.g. to recover a stream's message id). */
65
+ codecHeaders: Record<string, string>;
66
+ }
67
+
68
+ /**
69
+ * Declarative decode-time lifecycle repair, applied when joining a stream
70
+ * mid-flight (history compaction, rewind miss, partial page). Each function
71
+ * performs its side effect on the codec's lifecycle tracker (captured by the
72
+ * factory that builds the policy) and RETURNS lead-in events to PREPEND; the
73
+ * generic decoder ALWAYS runs the descriptor driver after and appends its
74
+ * output, so the policy never replaces a decode. A codec with no repair
75
+ * supplies no policy.
76
+ * @template TOutput - The codec's output union.
77
+ */
78
+ export interface LifecyclePolicy<TOutput> {
79
+ /**
80
+ * Keyed on the discrete codec `kind`. Returns lead-in events to prepend
81
+ * (empty array = none) after applying any tracker side effect.
82
+ */
83
+ onDiscrete?: Record<string, (runId: string, ctx: LifecycleDiscreteContext) => TOutput[]>;
84
+ /** Lead-in prepended to a stream's start events (mid-stream-join pre-roll). */
85
+ onStreamStart?: (runId: string, tracker: StreamTrackerState) => TOutput[];
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // defineCodec config + result
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * The reducer parts a codec supplies. `TProjection` and `TMessage` infer from
94
+ * these, so `defineCodec` callers need not spell them out.
95
+ * @template TInput - The codec's input union.
96
+ * @template TOutput - The codec's output union.
97
+ * @template TProjection - The per-node projection the reducer folds into.
98
+ * @template TMessage - The per-message domain type.
99
+ */
100
+ export interface CodecReducer<TInput, TOutput, TProjection, TMessage> {
101
+ /** Build an empty projection for a node. */
102
+ init: () => TProjection;
103
+ /** Fold one direction-tagged input or output event into the projection. */
104
+ fold: (state: TProjection, event: CodecEvent<TInput, TOutput>, meta: ReducerMeta) => TProjection;
105
+ /** Extract the per-message list (each paired with its codec-message-id). */
106
+ getMessages: (projection: TProjection) => CodecMessage<TMessage>[];
107
+ }
108
+
109
+ /**
110
+ * The parts a codec supplies to {@link defineCodec}.
111
+ * @template TInput - The codec's input union.
112
+ * @template TOutput - The codec's output union.
113
+ * @template TProjection - The per-node projection the reducer folds into.
114
+ * @template TMessage - The per-message domain type.
115
+ */
116
+ export interface DefineCodecConfig<
117
+ TInput extends { kind: string },
118
+ TOutput extends { type: string },
119
+ TProjection,
120
+ TMessage,
121
+ > {
122
+ /** Optional Ably-Agent identifier registered on the channel; omit to opt out. */
123
+ adapterTag?: string;
124
+ /** Reducer parts; `TProjection` / `TMessage` infer from here. */
125
+ reducer: CodecReducer<TInput, TOutput, TProjection, TMessage>;
126
+ /**
127
+ * The declarative output (`ai-output`) descriptor table, returned from the
128
+ * injected `{ event, stream }` builder (both curried on `TOutput`).
129
+ */
130
+ output: (b: OutputBuilder<TOutput>) => readonly OutputDescriptor<TOutput>[];
131
+ /**
132
+ * The declarative input (`ai-input`) descriptor table, returned from the
133
+ * injected `{ event, batch }` builder (both curried on `TInput`).
134
+ */
135
+ input: (b: InputBuilder<TInput>) => readonly InputDescriptor<TInput>[];
136
+ /**
137
+ * Factory for a fresh decode lifecycle policy per decoder instance (the
138
+ * policy's closures capture a fresh, per-decoder lifecycle tracker). Omit
139
+ * for a codec with no mid-stream-join repair.
140
+ */
141
+ decodeLifecycle?: () => LifecyclePolicy<TOutput>;
142
+ }
143
+
144
+ /**
145
+ * A codec assembled by {@link defineCodec}: a conforming {@link Codec} whose
146
+ * well-known input factories are typed concretely by {@link WellKnownInputFactories}
147
+ * (so `createToolResult` etc. are callable without a guard). The factory methods
148
+ * are sourced from `WellKnownInputFactories` rather than `Codec` because the
149
+ * former types them against `UserMessageOf<TInput>` / `ToolResultPayloadOf<TInput>`
150
+ * — equal to the codec's `TMessage` / payloads for every real codec, but not
151
+ * provably so to the generic type system. At a concrete call site a
152
+ * `DefinedCodec` is assignable to the corresponding `Codec`.
153
+ */
154
+ export type DefinedCodec<
155
+ TInput extends CodecInputEvent,
156
+ TOutput extends CodecOutputEvent,
157
+ TProjection,
158
+ TMessage,
159
+ > = Omit<Codec<TInput, TOutput, TProjection, TMessage>, keyof WellKnownInputFactories<TInput>> &
160
+ WellKnownInputFactories<TInput>;
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Generic encoder
164
+ // ---------------------------------------------------------------------------
165
+
166
+ class DefaultCodecEncoder<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent> implements Encoder<
167
+ TInput,
168
+ TOutput
169
+ > {
170
+ private readonly _core: EncoderCore;
171
+ private readonly _messageId: string | undefined;
172
+ private readonly _outputEncoder: OutputDescriptorEncoder<TOutput>;
173
+ private readonly _inputEncoder: InputDescriptorEncoder<TInput>;
174
+
175
+ constructor(
176
+ writer: ChannelWriter,
177
+ options: EncoderCoreOptions,
178
+ outputEncoder: OutputDescriptorEncoder<TOutput>,
179
+ inputEncoder: InputDescriptorEncoder<TInput>,
180
+ ) {
181
+ this._core = createEncoderCore(writer, options);
182
+ this._messageId = options.messageId;
183
+ this._outputEncoder = outputEncoder;
184
+ this._inputEncoder = inputEncoder;
185
+ }
186
+
187
+ async publishInput(input: TInput, options?: WriteOptions): Promise<void> {
188
+ // No `messageId` threads into inputs — user-message parts carry no
189
+ // transport codec-message-id today; inputs rely on opts.messageId stamped
190
+ // by the client session.
191
+ await this._inputEncoder.encode(input, this._core, { opts: options });
192
+ }
193
+
194
+ async publishOutput(output: TOutput, options?: WriteOptions): Promise<void> {
195
+ await this._outputEncoder.encode(output, this._core, { messageId: this._messageId, opts: options });
196
+ }
197
+
198
+ async cancelStreams(): Promise<void> {
199
+ await this._core.cancelAllStreams();
200
+ }
201
+
202
+ async close(): Promise<void> {
203
+ await this._core.close();
204
+ }
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Generic decoder
209
+ // ---------------------------------------------------------------------------
210
+
211
+ const decodeDiscretePayload = <TInput extends { kind: string }, TOutput>(
212
+ payload: MessagePayload,
213
+ outputDecoder: ReturnType<typeof createOutputDescriptorDecoder<TOutput & { type: string }>>,
214
+ inputDecoder: InputDescriptorDecoder<TInput>,
215
+ lifecycle: LifecyclePolicy<TOutput> | undefined,
216
+ ): (TInput | TOutput)[] => {
217
+ const codecHeaders = payload.codecHeaders ?? {};
218
+ const transportHeaders = payload.transportHeaders ?? {};
219
+ const codecKind = codecHeaders[KIND_HEADER] ?? '';
220
+
221
+ if (payload.name === EVENT_AI_INPUT) {
222
+ return inputDecoder.decode({ codecKind, data: payload.data, codecHeaders, transportHeaders });
223
+ }
224
+
225
+ if (payload.name === EVENT_AI_OUTPUT) {
226
+ const runId = transportHeaders[HEADER_RUN_ID] ?? '';
227
+ // Lifecycle repair runs its side effect and returns lead-in events; the
228
+ // descriptor driver always decodes after and its output is appended.
229
+ // The `kind` comes off the wire, so the policy lookup must be own-property
230
+ // only — a crafted kind such as 'valueOf' or 'toString' would otherwise
231
+ // resolve through Object.prototype and corrupt the decode.
232
+ const onDiscrete = lifecycle?.onDiscrete;
233
+ const repair = onDiscrete !== undefined && Object.hasOwn(onDiscrete, codecKind) ? onDiscrete[codecKind] : undefined;
234
+ const pre = repair?.(runId, { codecHeaders }) ?? [];
235
+ return [...pre, ...outputDecoder.decodeDiscrete(codecKind, codecHeaders, transportHeaders, payload.data)];
236
+ }
237
+
238
+ return [];
239
+ };
240
+
241
+ // Only outputs stream: a streamed message under any other wire name (a
242
+ // foreign or crafted ai-input stream) must not rebuild through the output
243
+ // stream path — its events would be mislabelled as inputs by the
244
+ // direction-routing decode. Enforces the invariant the decode cast relies on.
245
+ const isOutputStream = (tracker: StreamTrackerState): boolean => tracker.name === EVENT_AI_OUTPUT;
246
+
247
+ const buildHooks = <TInput extends { kind: string }, TOutput extends { type: string }>(
248
+ outputDecoder: ReturnType<typeof createOutputDescriptorDecoder<TOutput>>,
249
+ inputDecoder: InputDescriptorDecoder<TInput>,
250
+ lifecycle: LifecyclePolicy<TOutput> | undefined,
251
+ ): DecoderCoreHooks<TInput | TOutput> => ({
252
+ buildStartEvents: (tracker) => {
253
+ if (!isOutputStream(tracker)) return [];
254
+ const runId = tracker.transportHeaders[HEADER_RUN_ID] ?? '';
255
+ const pre = lifecycle?.onStreamStart?.(runId, tracker) ?? [];
256
+ return [...pre, ...outputDecoder.buildStart(tracker)];
257
+ },
258
+ buildDeltaEvents: (tracker, delta) => (isOutputStream(tracker) ? outputDecoder.buildDelta(tracker, delta) : []),
259
+ buildEndEvents: (tracker, closingCodecHeaders) =>
260
+ isOutputStream(tracker) ? outputDecoder.buildEnd(tracker, closingCodecHeaders) : [],
261
+ decodeDiscrete: (payload) => decodeDiscretePayload(payload, outputDecoder, inputDecoder, lifecycle),
262
+ });
263
+
264
+ class DefaultCodecDecoder<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent> implements Decoder<
265
+ TInput,
266
+ TOutput
267
+ > {
268
+ private readonly _core: DecoderCore<TInput | TOutput>;
269
+
270
+ constructor(core: DecoderCore<TInput | TOutput>) {
271
+ this._core = core;
272
+ }
273
+
274
+ decode(message: Ably.InboundMessage): DecodedMessage<TInput, TOutput> {
275
+ const events = this._core.decode(message);
276
+ // A single inbound message carries one wire name (ai-input XOR ai-output), so the
277
+ // name fixes the direction of every event decoded from it. The wire name is the
278
+ // authoritative direction signal — never the event's in-memory shape.
279
+ if (message.name === EVENT_AI_INPUT) {
280
+ // CAST: an ai-input message decodes only to inputs.
281
+ return { inputs: events as TInput[], outputs: [] };
282
+ }
283
+ // CAST: every other message is ai-output — the only other wire name the core decodes
284
+ // (unrecognised names yield no events) — so its events are all outputs.
285
+ return { inputs: [], outputs: events as TOutput[] };
286
+ }
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Table validation
291
+ // ---------------------------------------------------------------------------
292
+
293
+ /**
294
+ * Reserve `literal` in `seen` under a human-readable owner description,
295
+ * throwing if another descriptor already holds it. Dispatch literals must be
296
+ * unique within their namespace — a duplicate would silently route through
297
+ * whichever descriptor registered last.
298
+ * @param seen - The namespace's literal → owner registry, mutated in place.
299
+ * @param literal - The dispatch literal to reserve.
300
+ * @param owner - Human-readable description of the declaring descriptor (used in the error).
301
+ */
302
+ const reserve = (seen: Map<string, string>, literal: string, owner: string): void => {
303
+ const holder = seen.get(literal);
304
+ if (holder !== undefined) {
305
+ throw new Ably.ErrorInfo(
306
+ `unable to define codec; dispatch literal '${literal}' is declared by both ${holder} and ${owner}`,
307
+ ErrorCode.InvalidArgument,
308
+ 400,
309
+ );
310
+ }
311
+ seen.set(literal, owner);
312
+ };
313
+
314
+ /**
315
+ * Throw when a declared field binds one of the driver-reserved header keys.
316
+ * @param fields - The descriptor's declared header fields.
317
+ * @param owner - Human-readable description of the declaring descriptor (used in the error).
318
+ */
319
+ const rejectReservedFieldKeys = (fields: readonly HeaderField<unknown>[], owner: string): void => {
320
+ for (const field of fields) {
321
+ if (field.key === KIND_HEADER || field.key === PART_TYPE_HEADER) {
322
+ throw new Ably.ErrorInfo(
323
+ `unable to define codec; ${owner} binds the driver-reserved header key '${field.key}'`,
324
+ ErrorCode.InvalidArgument,
325
+ 400,
326
+ );
327
+ }
328
+ }
329
+ };
330
+
331
+ /**
332
+ * Fail-fast validation of the assembled descriptor tables, run once per
333
+ * `defineCodec` call. Catches author mistakes the drivers would otherwise
334
+ * surface as silent last-wins routing or encode/decode asymmetry:
335
+ *
336
+ * - duplicate dispatch literals within a namespace — the domain chunk `type`
337
+ * namespace (discrete event types + stream phase types, which drive encode
338
+ * dispatch) and the wire `kind` namespace (discrete event types + stream
339
+ * family kinds, which drive decode dispatch);
340
+ * - duplicate input `kind`s and duplicate `partType`s within a batch;
341
+ * - field bindings on the driver-reserved `kind` / `partType` header keys.
342
+ * @param outputs - The assembled output descriptor table.
343
+ * @param inputs - The assembled input descriptor table.
344
+ */
345
+ const validateTables = <TInput, TOutput>(
346
+ outputs: readonly OutputDescriptor<TOutput>[],
347
+ inputs: readonly InputDescriptor<TInput>[],
348
+ ): void => {
349
+ const chunkTypes = new Map<string, string>();
350
+ const wireKinds = new Map<string, string>();
351
+ for (const descriptor of outputs) {
352
+ if (descriptor.construct === 'event') {
353
+ const owner = `output event '${descriptor.type}'`;
354
+ reserve(chunkTypes, descriptor.type, owner);
355
+ reserve(wireKinds, descriptor.type, owner);
356
+ rejectReservedFieldKeys(descriptor.fields, owner);
357
+ } else {
358
+ const owner = `output stream '${descriptor.kind}'`;
359
+ reserve(wireKinds, descriptor.kind, owner);
360
+ for (const phase of [descriptor.start, descriptor.delta, descriptor.end]) {
361
+ reserve(chunkTypes, phase, owner);
362
+ }
363
+ rejectReservedFieldKeys(descriptor.fields, owner);
364
+ }
365
+ }
366
+
367
+ const inputKinds = new Map<string, string>();
368
+ for (const descriptor of inputs) {
369
+ const owner = `input ${descriptor.construct} '${descriptor.kind}'`;
370
+ reserve(inputKinds, descriptor.kind, owner);
371
+ if (descriptor.construct === 'event') {
372
+ rejectReservedFieldKeys(descriptor.fields, owner);
373
+ } else {
374
+ const partTypes = new Map<string, string>();
375
+ for (const part of descriptor.parts) {
376
+ const partOwner = `${owner} part '${part.partType}'`;
377
+ reserve(partTypes, part.partType, partOwner);
378
+ rejectReservedFieldKeys(part.fields, partOwner);
379
+ }
380
+ }
381
+ }
382
+ };
383
+
384
+ // ---------------------------------------------------------------------------
385
+ // Factory
386
+ // ---------------------------------------------------------------------------
387
+
388
+ /**
389
+ * Assemble a fully-formed {@link Codec} from a codec's parts. Curried on the
390
+ * input/output unions so `TProjection` / `TMessage` infer from `config.reducer`
391
+ * — a caller writes `defineCodec<TInput, TOutput>()({ ... })` and never spells
392
+ * out the projection or message types.
393
+ * @template TInput - The codec's input union.
394
+ * @template TOutput - The codec's output union.
395
+ * @returns A function taking the codec's parts and returning the assembled codec.
396
+ */
397
+ export const defineCodec =
398
+ <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent>() =>
399
+ <TProjection, TMessage>(
400
+ config: DefineCodecConfig<TInput, TOutput, TProjection, TMessage>,
401
+ ): DefinedCodec<TInput, TOutput, TProjection, TMessage> => {
402
+ const { reducer, decodeLifecycle } = config;
403
+ // Build the direction-scoped builders, hand them to the codec's table
404
+ // functions, and collect the descriptor arrays the drivers consume.
405
+ const outputs = config.output(outputBuilder<TOutput>());
406
+ const inputs = config.input(inputBuilder<TInput>());
407
+ validateTables(outputs, inputs);
408
+ // The descriptor drivers are pure functions of the (fixed) tables — build
409
+ // them once here and share them across every encoder/decoder instance.
410
+ const outputEncoder = createOutputDescriptorEncoder(outputs, EVENT_AI_OUTPUT);
411
+ const inputEncoder = createInputDescriptorEncoder(inputs, EVENT_AI_INPUT);
412
+ const outputDecoder = createOutputDescriptorDecoder(outputs);
413
+ const inputDecoder = createInputDescriptorDecoder(inputs);
414
+ return {
415
+ // adapterTag is optional on Codec; only set it when supplied so a codec
416
+ // can opt out of Ably-Agent registration.
417
+ ...(config.adapterTag === undefined ? {} : { adapterTag: config.adapterTag }),
418
+ init: reducer.init,
419
+ fold: reducer.fold,
420
+ getMessages: reducer.getMessages,
421
+ createEncoder: (writer, options = {}) => new DefaultCodecEncoder(writer, options, outputEncoder, inputEncoder),
422
+ createDecoder: () =>
423
+ new DefaultCodecDecoder<TInput, TOutput>(
424
+ // The lifecycle policy (and its tracker) stays per-decoder: each
425
+ // decoder instance gets independent per-run phase state. No options
426
+ // thread through: Codec.createDecoder takes none, so accepting any
427
+ // here would be unreachable surface.
428
+ createDecoderCore(buildHooks(outputDecoder, inputDecoder, decodeLifecycle?.()), {}),
429
+ ),
430
+ ...wellKnownInputs<TInput>(),
431
+ };
432
+ };