@ably/ai-transport 0.2.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 (166) hide show
  1. package/README.md +10 -19
  2. package/dist/ably-ai-transport.js +1790 -1091
  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 +2 -2
  7. package/dist/core/agent.d.ts +20 -5
  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 +4 -1
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +2 -7
  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 -1
  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/output-descriptor-decoder.d.ts +29 -0
  20. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  21. package/dist/core/codec/output-descriptors.d.ts +237 -0
  22. package/dist/core/codec/types.d.ts +95 -36
  23. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  24. package/dist/core/transport/agent-view.d.ts +296 -0
  25. package/dist/core/transport/decode-fold.d.ts +40 -32
  26. package/dist/core/transport/headers.d.ts +30 -1
  27. package/dist/core/transport/index.d.ts +1 -1
  28. package/dist/core/transport/invocation.d.ts +1 -1
  29. package/dist/core/transport/load-history-pages.d.ts +71 -0
  30. package/dist/core/transport/load-history.d.ts +21 -16
  31. package/dist/core/transport/run-manager.d.ts +9 -11
  32. package/dist/core/transport/session-support.d.ts +55 -0
  33. package/dist/core/transport/tree.d.ts +165 -15
  34. package/dist/core/transport/types/agent.d.ts +120 -98
  35. package/dist/core/transport/types/client.d.ts +45 -12
  36. package/dist/core/transport/types/tree.d.ts +52 -10
  37. package/dist/core/transport/types/view.d.ts +55 -28
  38. package/dist/core/transport/view.d.ts +176 -58
  39. package/dist/core/transport/wire-log.d.ts +102 -0
  40. package/dist/errors.d.ts +10 -4
  41. package/dist/index.d.ts +6 -5
  42. package/dist/react/ably-ai-transport-react.js +784 -415
  43. package/dist/react/ably-ai-transport-react.js.map +1 -1
  44. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  45. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  46. package/dist/react/contexts/client-session-context.d.ts +2 -1
  47. package/dist/react/contexts/client-session-provider.d.ts +3 -0
  48. package/dist/react/index.d.ts +2 -1
  49. package/dist/react/internal/skipped-session.d.ts +8 -0
  50. package/dist/react/use-view.d.ts +3 -3
  51. package/dist/utils.d.ts +22 -54
  52. package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
  53. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  55. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  56. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  57. package/dist/vercel/codec/events.d.ts +1 -2
  58. package/dist/vercel/codec/fields.d.ts +44 -0
  59. package/dist/vercel/codec/fold-content.d.ts +16 -0
  60. package/dist/vercel/codec/fold-data.d.ts +16 -0
  61. package/dist/vercel/codec/fold-input.d.ts +67 -0
  62. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  63. package/dist/vercel/codec/fold-text.d.ts +16 -0
  64. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  65. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  66. package/dist/vercel/codec/index.d.ts +5 -30
  67. package/dist/vercel/codec/inputs.d.ts +11 -0
  68. package/dist/vercel/codec/outputs.d.ts +11 -0
  69. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  70. package/dist/vercel/codec/reducer.d.ts +20 -102
  71. package/dist/vercel/codec/tool-transitions.d.ts +0 -6
  72. package/dist/vercel/codec/wire-data.d.ts +34 -0
  73. package/dist/vercel/index.d.ts +1 -0
  74. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
  75. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  76. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
  77. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  78. package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
  79. package/dist/vercel/run-end-reason.d.ts +66 -11
  80. package/dist/vercel/tool-part.d.ts +21 -0
  81. package/dist/vercel/transport/chat-transport.d.ts +0 -2
  82. package/dist/vercel/transport/index.d.ts +1 -1
  83. package/dist/vercel/transport/run-output-stream.d.ts +6 -8
  84. package/dist/version.d.ts +1 -1
  85. package/package.json +2 -2
  86. package/src/constants.ts +2 -2
  87. package/src/core/agent.ts +43 -19
  88. package/src/core/channel-options.ts +89 -0
  89. package/src/core/codec/codec-event.ts +27 -0
  90. package/src/core/codec/decoder.ts +145 -21
  91. package/src/core/codec/define-codec.ts +432 -0
  92. package/src/core/codec/encoder.ts +13 -54
  93. package/src/core/codec/field-bag.ts +142 -0
  94. package/src/core/codec/fields.ts +193 -0
  95. package/src/core/codec/index.ts +43 -0
  96. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  97. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  98. package/src/core/codec/input-descriptors.ts +373 -0
  99. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  100. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  101. package/src/core/codec/output-descriptors.ts +307 -0
  102. package/src/core/codec/types.ts +99 -36
  103. package/src/core/codec/well-known-inputs.ts +96 -0
  104. package/src/core/transport/agent-session.ts +330 -589
  105. package/src/core/transport/agent-view.ts +738 -0
  106. package/src/core/transport/client-session.ts +74 -69
  107. package/src/core/transport/decode-fold.ts +57 -47
  108. package/src/core/transport/headers.ts +57 -4
  109. package/src/core/transport/index.ts +2 -1
  110. package/src/core/transport/invocation.ts +1 -1
  111. package/src/core/transport/load-history-pages.ts +220 -0
  112. package/src/core/transport/load-history.ts +63 -61
  113. package/src/core/transport/pipe-stream.ts +10 -1
  114. package/src/core/transport/run-manager.ts +25 -31
  115. package/src/core/transport/session-support.ts +96 -0
  116. package/src/core/transport/tree.ts +414 -47
  117. package/src/core/transport/types/agent.ts +129 -102
  118. package/src/core/transport/types/client.ts +49 -13
  119. package/src/core/transport/types/tree.ts +61 -12
  120. package/src/core/transport/types/view.ts +57 -28
  121. package/src/core/transport/view.ts +520 -172
  122. package/src/core/transport/wire-log.ts +189 -0
  123. package/src/errors.ts +10 -3
  124. package/src/index.ts +44 -11
  125. package/src/react/contexts/client-session-context.ts +1 -1
  126. package/src/react/contexts/client-session-provider.tsx +38 -2
  127. package/src/react/index.ts +2 -1
  128. package/src/react/internal/skipped-session.ts +62 -0
  129. package/src/react/use-client-session.ts +7 -30
  130. package/src/react/use-view.ts +3 -3
  131. package/src/utils.ts +31 -97
  132. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  133. package/src/vercel/codec/events.ts +1 -3
  134. package/src/vercel/codec/fields.ts +58 -0
  135. package/src/vercel/codec/fold-content.ts +54 -0
  136. package/src/vercel/codec/fold-data.ts +46 -0
  137. package/src/vercel/codec/fold-input.ts +255 -0
  138. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  139. package/src/vercel/codec/fold-text.ts +55 -0
  140. package/src/vercel/codec/fold-tool-input.ts +86 -0
  141. package/src/vercel/codec/fold-tool-output.ts +79 -0
  142. package/src/vercel/codec/index.ts +23 -63
  143. package/src/vercel/codec/inputs.ts +116 -0
  144. package/src/vercel/codec/outputs.ts +207 -0
  145. package/src/vercel/codec/reducer-state.ts +169 -0
  146. package/src/vercel/codec/reducer.ts +52 -838
  147. package/src/vercel/codec/tool-transitions.ts +1 -12
  148. package/src/vercel/codec/wire-data.ts +64 -0
  149. package/src/vercel/index.ts +1 -0
  150. package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
  151. package/src/vercel/react/use-chat-transport.ts +8 -28
  152. package/src/vercel/react/use-message-sync.ts +5 -10
  153. package/src/vercel/run-end-reason.ts +95 -16
  154. package/src/vercel/tool-part.ts +25 -0
  155. package/src/vercel/transport/chat-transport.ts +10 -22
  156. package/src/vercel/transport/index.ts +1 -1
  157. package/src/vercel/transport/run-output-stream.ts +7 -8
  158. package/src/version.ts +1 -1
  159. package/dist/core/transport/branch-chain.d.ts +0 -43
  160. package/dist/core/transport/load-conversation.d.ts +0 -128
  161. package/dist/vercel/codec/decoder.d.ts +0 -9
  162. package/dist/vercel/codec/encoder.d.ts +0 -11
  163. package/src/core/transport/branch-chain.ts +0 -58
  164. package/src/core/transport/load-conversation.ts +0 -355
  165. package/src/vercel/codec/decoder.ts +0 -696
  166. package/src/vercel/codec/encoder.ts +0 -548
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Declarative input descriptors — the single source of truth for a codec's
3
+ * `ai-input` wire mapping, the input-side sibling of {@link import('./output-descriptors.js')}.
4
+ *
5
+ * Inputs come in two cardinalities: a single domain input ↔ one wire message
6
+ * (the `event` construct), and a single domain message ↔ many atomic wire events
7
+ * (the `batch` construct — the input sibling of the output `stream`). Both are
8
+ * declared once per codec and consumed by the generic input encode/decode
9
+ * drivers, so adding an input is one descriptor entry rather than a pair of
10
+ * hand-synchronised switch arms.
11
+ *
12
+ * Authoring is cast-free: the {@link inputBuilder} factory hands the codec a
13
+ * `{ event, batch }` pair curried on the codec's input union, so every `data` /
14
+ * `fields` / `parts` callback receives the exact narrowed member. The descriptors
15
+ * are then erased to a heterogeneous {@link InputDescriptor} via a single
16
+ * documented cast at each constructor boundary — never in author code.
17
+ */
18
+
19
+ import type * as Ably from 'ably';
20
+
21
+ import { wildcardMatcher } from './field-bag.js';
22
+ import type { DataCodec, FieldFor, HeaderField } from './fields.js';
23
+ import type { MessagePayload, WriteOptions } from './types.js';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Type helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /** Resolve the input union member a `kind` literal selects. */
30
+ export type ResolveInput<U extends { kind: string }, K extends U['kind']> = Extract<U, { kind: K }>;
31
+
32
+ /**
33
+ * The payload an input `event`'s `fields` / `data` operate on. Inputs nest their
34
+ * domain data under `payload` (the `{ kind, codecMessageId, payload }` envelope of
35
+ * the well-known input variants), so a single event's spec is authored against the
36
+ * payload, and the driver wraps/unwraps the envelope. A member with no `payload`
37
+ * (a `wireOnly` signal) resolves to `never` — such an event declares no `fields` /
38
+ * `data`, so the payload type is never used.
39
+ * @template C - The narrowed input member.
40
+ */
41
+ export type PayloadOf<C> = C extends { payload: infer P } ? P : never;
42
+
43
+ /**
44
+ * Resolve the part union member a `partType` literal selects, mirroring the
45
+ * output {@link import('./output-descriptors.js').ResolveType} curry one level down.
46
+ * An exact match wins; a wildcard literal (`data-*`) resolves to the template
47
+ * member (`data-${string}`).
48
+ * @template P - The part union.
49
+ * @template T - The selected `partType` literal (or a `*-*` wildcard).
50
+ */
51
+ export type ResolvePart<P extends { type: string }, T extends string> =
52
+ Extract<P, { type: T }> extends never
53
+ ? T extends `${infer Pre}-*`
54
+ ? Extract<P, { type: `${Pre}-${string}` }>
55
+ : never
56
+ : Extract<P, { type: T }>;
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Author-facing specs (narrowed)
60
+ // ---------------------------------------------------------------------------
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Input driver core surface + contexts
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * The encoder-core view the input encode driver receives: discrete publishes
68
+ * only — inputs never stream. The concrete {@link EncoderCore} satisfies this
69
+ * structurally.
70
+ */
71
+ export interface InputEncoderCore {
72
+ /** Publish a single discrete message. */
73
+ publishDiscrete(payload: MessagePayload, opts?: WriteOptions): Promise<Ably.PublishResult>;
74
+ /** Publish multiple discrete messages atomically (the batch fan-out). */
75
+ publishDiscreteBatch(payloads: MessagePayload[], opts?: WriteOptions): Promise<Ably.PublishResult>;
76
+ }
77
+
78
+ /** Per-write context passed to the input encode driver. */
79
+ export interface InputEncodeContext {
80
+ /** Per-write overrides (the wire codec-message-id is stamped here by the client session). */
81
+ opts: WriteOptions | undefined;
82
+ }
83
+
84
+ /** Per-message context the input decode driver receives for one inbound `ai-input` message. */
85
+ export interface InputDecodeContext {
86
+ /** The codec `kind` header value (the input descriptor's dispatch key). */
87
+ codecKind: string;
88
+ /** The inbound message data. */
89
+ data: unknown;
90
+ /** The inbound codec-tier headers. */
91
+ codecHeaders: Record<string, string>;
92
+ /** The inbound transport-tier headers (role, codec-message-id, discrete marker). */
93
+ transportHeaders: Record<string, string>;
94
+ }
95
+
96
+ /**
97
+ * The spec the input `event` construct accepts for member `C`. A member with
98
+ * no `payload` has nothing for `fields` / `data` to lens onto, so it may only
99
+ * be declared `wireOnly` or escape-hatched; the driver also rejects a
100
+ * payload-less encode at runtime.
101
+ * @template C - The narrowed input member.
102
+ */
103
+ export type InputEventSpecFor<C> = [PayloadOf<C>] extends [never]
104
+ ? Pick<InputEventSpec<C>, 'wireOnly'>
105
+ : InputEventSpec<C>;
106
+
107
+ /**
108
+ * A single-event input descriptor spec, narrowed to input member `C`. `fields`
109
+ * and `data` operate on the member's {@link PayloadOf payload}; the driver wraps
110
+ * the `{ kind, codecMessageId, payload }` envelope on decode and unwraps it on
111
+ * encode. A `wireOnly` event carries no payload (kind only).
112
+ * @template C - The narrowed input member.
113
+ */
114
+ export interface InputEventSpec<C> {
115
+ /**
116
+ * Declared header fields over the member's payload, written on encode and
117
+ * read on decode. Each field's key names both the wire header and the
118
+ * payload property it carries (see {@link FieldFor}). Omit for none.
119
+ */
120
+ fields?: readonly FieldFor<PayloadOf<C>>[];
121
+ /** Wire `data` codec over the payload. Omit when the input carries no data (`data: ''`). */
122
+ data?: DataCodec<PayloadOf<C>>;
123
+ /** Wire-only signal: encode stamps only the `kind` header (empty data, no fields); decode yields `[]`. */
124
+ wireOnly?: boolean;
125
+ }
126
+
127
+ /**
128
+ * A per-part wire mapping inside a {@link BatchSpec}, narrowed to part member `Q`.
129
+ * `fields` and `data` operate on the selected part; the batch driver fans the
130
+ * domain message out into one wire event per part and reassembles them in the
131
+ * reducer (merge by codec-message-id).
132
+ * @template Q - The narrowed part member.
133
+ */
134
+ export interface PartSpec<Q> {
135
+ /**
136
+ * Declared header fields for this part, written on encode and read on
137
+ * decode. Each field's key names both the wire header and the part property
138
+ * it carries (see {@link FieldFor}). Omit for none.
139
+ */
140
+ fields?: readonly FieldFor<Q>[];
141
+ /** Wire `data` codec over the part. Omit when the part carries no data. */
142
+ data?: DataCodec<Q>;
143
+ }
144
+
145
+ /**
146
+ * The curried part sub-builder a {@link BatchSpec.parts} function receives.
147
+ * Mirrors the {@link inputBuilder} `event` curry one level down — and the
148
+ * output builder's wildcard idiom: `p(partType, spec)` narrows `spec` to the
149
+ * part member the literal selects, and a `-*` literal (e.g. `data-*`) declares
150
+ * a wildcard family whose dispatch predicate is derived from the literal's
151
+ * prefix, narrowing `spec` to the template member. Both forms are cast-free in
152
+ * author code.
153
+ * @template P - The part union.
154
+ */
155
+ export type PartBuilder<P extends { type: string }> = <T extends P['type'] | `${string}-*`>(
156
+ partType: T,
157
+ spec: PartSpec<ResolvePart<P, T>>,
158
+ ) => PartDescriptor;
159
+
160
+ /**
161
+ * Per-message wire headers a {@link BatchSpec.messageHeaders} stamps on every
162
+ * part of one batch. These carry metadata that belongs to the whole message
163
+ * rather than an individual part (e.g. the message id as a codec header, the
164
+ * sender role as a transport header), so the decode side can reconstruct the
165
+ * shared message envelope from any single part. Both tiers are optional.
166
+ */
167
+ export interface BatchMessageHeaders {
168
+ /** Codec-tier headers stamped on every part (e.g. a per-message id). */
169
+ codecHeaders?: Record<string, string>;
170
+ /** Transport-tier headers stamped on every part (e.g. the sender role). */
171
+ transportHeaders?: Record<string, string>;
172
+ }
173
+
174
+ /**
175
+ * The context a {@link BatchSpec.assemble} receives alongside one decoded part:
176
+ * the inbound header tiers of the wire event the part was decoded from. A batch
177
+ * fans out into N independent wire events, so each part arrives carrying the
178
+ * shared per-message headers ({@link BatchMessageHeaders}); `assemble` reads them
179
+ * to rebuild the message envelope (id, role, …) around its one part.
180
+ */
181
+ export interface BatchAssembleContext {
182
+ /** The inbound codec-tier headers (carries the per-message codec headers). */
183
+ codecHeaders: Record<string, string>;
184
+ /** The inbound transport-tier headers (carries the per-message transport headers). */
185
+ transportHeaders: Record<string, string>;
186
+ }
187
+
188
+ /**
189
+ * A multi-part input descriptor spec: one domain message decomposed into many
190
+ * atomic wire events sharing the input member's `kind` and codec-message-id, each
191
+ * carrying a `partType` sub-discriminator. The part union `P` is inferred from
192
+ * `explode`'s return type and threaded into `parts`'s curried `p` and `assemble`,
193
+ * so all three are cast-free in author code.
194
+ * @template C - The narrowed input member (the message-bearing input).
195
+ * @template P - The part union the message explodes into.
196
+ */
197
+ export interface BatchSpec<C, P extends { type: string }> {
198
+ /** ENCODE: decompose the domain message into its parts. */
199
+ explode: (input: C) => readonly P[];
200
+ /** The `partType` sub-discriminator read off each part on encode. */
201
+ partTypeOf: (part: P) => string;
202
+ /** Declarative per-part wire mapping (a sub-table built via the curried `p`). */
203
+ parts: (p: PartBuilder<P>) => readonly PartDescriptor[];
204
+ /**
205
+ * ENCODE: per-message headers stamped on every part (the driver merges them
206
+ * onto each part's headers, including the ≥1-event fallback). Use for metadata
207
+ * shared by the whole message — e.g. a message-id codec header and a role
208
+ * transport header. Omit when parts carry no shared per-message metadata.
209
+ */
210
+ messageHeaders?: (input: C) => BatchMessageHeaders;
211
+ /**
212
+ * DECODE: shape one decoded wire part into a one-part input (the reducer merges
213
+ * parts by codec-message-id). `ctx` exposes the inbound header tiers so the
214
+ * shared per-message metadata stamped by `messageHeaders` can be read back.
215
+ * The driver stamps only `kind`; the per-message identity rides the
216
+ * transport header and is recovered through `ctx` when needed.
217
+ */
218
+ assemble: (part: P, ctx: BatchAssembleContext) => Omit<C, 'kind'>;
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Erased descriptors (heterogeneous array elements)
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /** A single-event input descriptor erased to the codec's input union `U`. */
226
+ export interface InputEventDescriptor {
227
+ /** Discriminator. */
228
+ construct: 'event';
229
+ /** The wire `kind` this input dispatches on. */
230
+ kind: string;
231
+ /** Declared header fields (read/written against the member's payload). */
232
+ fields: readonly HeaderField<unknown>[];
233
+ /** Wire `data` codec, if any. */
234
+ data?: DataCodec<unknown>;
235
+ /** Wire-only signal flag. */
236
+ wireOnly: boolean;
237
+ }
238
+
239
+ /** An erased per-part wire mapping within a {@link BatchDescriptor}. */
240
+ export interface PartDescriptor {
241
+ /** The exact `partType` this part encodes as (the wildcard sentinel for a family). */
242
+ partType: string;
243
+ /** Decode-dispatch predicate for a wildcard part; absent for an exact part. */
244
+ match?: (partType: string) => boolean;
245
+ /** Declared header fields for this part. */
246
+ fields: readonly HeaderField<unknown>[];
247
+ /** Wire `data` codec over the part, if any. */
248
+ data?: DataCodec<unknown>;
249
+ }
250
+
251
+ /** A multi-part (batch) input descriptor erased to the codec's input union `U`. */
252
+ export interface BatchDescriptor<U> {
253
+ /** Discriminator. */
254
+ construct: 'batch';
255
+ /** The wire `kind` every part of this batch shares. */
256
+ kind: string;
257
+ /** Decompose the domain input into its parts. */
258
+ explode: (input: U) => readonly unknown[];
259
+ /** Read the `partType` sub-discriminator off a part. */
260
+ partTypeOf: (part: unknown) => string;
261
+ /** The per-part wire mappings. */
262
+ parts: readonly PartDescriptor[];
263
+ /** Build the per-message headers stamped on every part, if any. */
264
+ messageHeaders?: (input: U) => BatchMessageHeaders;
265
+ /** Shape one decoded part into a one-part input (sans the driver-stamped `kind`). */
266
+ assemble: (part: unknown, ctx: BatchAssembleContext) => Omit<U, 'kind'>;
267
+ }
268
+
269
+ /** An erased input descriptor — a single event or a multi-part batch. */
270
+ export type InputDescriptor<U> = InputEventDescriptor | BatchDescriptor<U>;
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Builder factory
274
+ // ---------------------------------------------------------------------------
275
+
276
+ /**
277
+ * The direction-scoped input builder `defineCodec` injects into the `input`
278
+ * config function — `event` and `batch`, both curried on the codec's input union
279
+ * so author entries narrow cast-free.
280
+ * @template U - The codec's input union.
281
+ */
282
+ export interface InputBuilder<U extends { kind: string }> {
283
+ /**
284
+ * Declare a single-event input. Narrows `spec` to the member `kind` selects;
285
+ * `fields` / `data` operate on that member's payload.
286
+ * @param kind - The input member's `kind` literal (the wire dispatch key).
287
+ * @param spec - The narrowed input spec. Omit for a bare-`kind` input.
288
+ * @returns An erased {@link InputDescriptor}.
289
+ */
290
+ event: <K extends U['kind']>(kind: K, spec?: InputEventSpecFor<ResolveInput<U, K>>) => InputDescriptor<U>;
291
+ /**
292
+ * Declare a multi-part (batch) input. Narrows the spec to the message-bearing
293
+ * member `kind` selects; `explode`'s return type fixes the part union `P`, which
294
+ * threads into `parts`'s curried `p` and `assemble` cast-free.
295
+ * @param kind - The input member's `kind` literal (the shared wire dispatch key).
296
+ * @param spec - The narrowed batch spec.
297
+ * @returns An erased {@link InputDescriptor}.
298
+ */
299
+ batch: <K extends U['kind'], P extends { type: string }>(
300
+ kind: K,
301
+ spec: BatchSpec<ResolveInput<U, K>, P>,
302
+ ) => InputDescriptor<U>;
303
+ }
304
+
305
+ /**
306
+ * Build the curried `{ event, batch }` input builder for a codec's input union.
307
+ * `defineCodec` calls this once and hands the result to the `input` config
308
+ * function; mirrors the output side's {@link import('./output-descriptors.js').outputBuilder}.
309
+ * @template U - The codec's input union.
310
+ * @returns The direction-scoped {@link InputBuilder}.
311
+ */
312
+ export const inputBuilder = <U extends { kind: string }>(): InputBuilder<U> => {
313
+ // The internal part sub-builder reads only the structural `fields`/`data` off the
314
+ // spec; the narrowed part type is an author-facing concern, erased here.
315
+ interface ErasedPartSpec {
316
+ fields?: readonly HeaderField<unknown>[];
317
+ data?: DataCodec<unknown>;
318
+ }
319
+ const part = (partType: string, spec: ErasedPartSpec): PartDescriptor => {
320
+ // A `-*` literal declares a wildcard family; the dispatch predicate is
321
+ // derived from the literal so the two can never disagree (see wildcardMatcher).
322
+ const match = wildcardMatcher(partType);
323
+ return {
324
+ partType,
325
+ ...(match ? { match } : {}),
326
+ fields: spec.fields ?? [],
327
+ data: spec.data,
328
+ };
329
+ };
330
+ // CAST: the part sub-builder is exposed to authors narrowed (PartBuilder<P>) so
331
+ // each `p(partType, spec)` narrows its spec to the selected part. Internally it
332
+ // reads only the structural `fields`/`data`, so the narrowed specs erase to the
333
+ // structural `ErasedPartSpec` at this boundary; a descriptor's part callbacks
334
+ // only ever run against the part their literal/predicate matched.
335
+ const p = part as unknown as PartBuilder<{ type: string }>;
336
+
337
+ return {
338
+ event: (kind, spec) => {
339
+ // CAST: the author-facing spec is conditional (a payload-less member may
340
+ // only declare wireOnly / escape hatches); both branches erase to one
341
+ // structural bag here, and the impl only reads optional properties off it.
342
+ const bag = spec as InputEventSpec<{ kind: string; payload: unknown }> | undefined;
343
+ // CAST: `spec` is narrowed to the member `kind` selects; the descriptor erases
344
+ // that to the codec's union `U` so heterogeneous descriptors share one array
345
+ // type. The drivers only ever invoke a descriptor's callbacks with the matching
346
+ // member, so the erasure is sound.
347
+ return {
348
+ construct: 'event',
349
+ kind,
350
+ fields: bag?.fields ?? [],
351
+ data: bag?.data,
352
+ wireOnly: bag?.wireOnly ?? false,
353
+ } as unknown as InputDescriptor<U>;
354
+ },
355
+ batch: (kind, spec) => {
356
+ // CAST: `p` is the single structural sub-builder; the author's `parts`
357
+ // function is typed to the narrowed `PartBuilder<P>`, so we hand it `p`
358
+ // through the same erasure the part specs already cross.
359
+ const parts = (spec.parts as (b: PartBuilder<{ type: string }>) => readonly PartDescriptor[])(p);
360
+ // CAST: see `event` — the narrowed batch spec (with its part-union-typed
361
+ // `explode`/`partTypeOf`/`assemble`) erases to `BatchDescriptor<U>`.
362
+ return {
363
+ construct: 'batch',
364
+ kind,
365
+ explode: spec.explode,
366
+ partTypeOf: spec.partTypeOf,
367
+ parts,
368
+ messageHeaders: spec.messageHeaders,
369
+ assemble: spec.assemble,
370
+ } as unknown as InputDescriptor<U>;
371
+ },
372
+ };
373
+ };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Generic output decode driver over a descriptor set.
3
+ *
4
+ * Rebuilds events from the wire: discrete messages dispatch on the codec `kind`
5
+ * header, streamed families rebuild start/delta/end chunks from the stream
6
+ * tracker. Escape-hatch `decode`/`decodeEnd`/`decodeDiscrete` functions take
7
+ * over where a pure field rebuild can't express the mapping.
8
+ *
9
+ * This driver is pure chunk reconstruction — it carries no decode-time side
10
+ * effects (e.g. a codec's stream-join lifecycle repair). Those belong in the
11
+ * codec's hook layer wrapping this driver.
12
+ */
13
+
14
+ import { stripUndefined } from '../../utils.js';
15
+ import { KIND_HEADER, partitionOutputEvents, readFields } from './field-bag.js';
16
+ import type {
17
+ OutputDecodeContext,
18
+ OutputDescriptor,
19
+ OutputEventDescriptor,
20
+ OutputStreamDescriptor,
21
+ } from './output-descriptors.js';
22
+ import type { StreamTrackerState } from './types.js';
23
+
24
+ /**
25
+ * The reconstructed chunk's domain discriminator field — the codec model's own
26
+ * `type` discriminator, per `CodecOutputEvent.type`. Distinct
27
+ * from {@link KIND_HEADER}: this is the rebuilt object's property, never the
28
+ * wire dispatch key.
29
+ */
30
+ const TYPE_FIELD = 'type';
31
+
32
+ /** Decodes wire messages of union `U` from a descriptor set. */
33
+ export interface OutputDescriptorDecoder<U> {
34
+ /** Rebuild the chunk(s) emitted when a stream starts. */
35
+ buildStart(tracker: StreamTrackerState): U[];
36
+ /** Rebuild the chunk(s) for a stream delta. */
37
+ buildDelta(tracker: StreamTrackerState, delta: string): U[];
38
+ /** Rebuild the chunk(s) emitted when a stream completes. */
39
+ buildEnd(tracker: StreamTrackerState, closingCodecHeaders: Record<string, string>): U[];
40
+ /**
41
+ * Decode a discrete message by its codec `kind`.
42
+ * @param codecKind - The codec `kind` header value (the dispatch key).
43
+ * @param codecHeaders - The inbound codec-tier headers.
44
+ * @param transportHeaders - The inbound transport-tier headers.
45
+ * @param data - The inbound message data.
46
+ * @returns The decoded events (empty if no descriptor matches).
47
+ */
48
+ decodeDiscrete(
49
+ codecKind: string,
50
+ codecHeaders: Record<string, string>,
51
+ transportHeaders: Record<string, string>,
52
+ data: unknown,
53
+ ): U[];
54
+ }
55
+
56
+ /**
57
+ * Build an output decode driver for a descriptor set.
58
+ * @template U - The codec's event union.
59
+ * @param descriptors - The descriptor set (events + streamed families).
60
+ * @returns An {@link OutputDescriptorDecoder} that reconstructs events from the wire.
61
+ */
62
+ export const createOutputDescriptorDecoder = <U extends { type: string }>(
63
+ descriptors: readonly OutputDescriptor<U>[],
64
+ ): OutputDescriptorDecoder<U> => {
65
+ const { discreteByType, wildcards } = partitionOutputEvents(descriptors);
66
+ const streamByKind = new Map<string, OutputStreamDescriptor<U>>();
67
+
68
+ for (const descriptor of descriptors) {
69
+ if (descriptor.construct === 'stream') {
70
+ streamByKind.set(descriptor.kind, descriptor);
71
+ }
72
+ }
73
+
74
+ // CAST: the rebuild seam — `bag` is assembled from the descriptor's declared
75
+ // fields and data codec, so it conforms to the matched member by construction.
76
+ // `typeValue` is the descriptor identity, written to the chunk's domain `type`
77
+ // field (not the wire `kind` header).
78
+ const rebuild = (typeValue: string, bag: Record<string, unknown>): U => {
79
+ bag[TYPE_FIELD] = typeValue;
80
+ return stripUndefined(bag) as unknown as U;
81
+ };
82
+
83
+ const decodeEvent = (descriptor: OutputEventDescriptor<U>, codecKind: string, ctx: OutputDecodeContext): U[] => {
84
+ const bag = readFields(descriptor.fields, ctx.codecHeaders);
85
+ if (descriptor.data) Object.assign(bag, descriptor.data.decode(ctx.data));
86
+ return [rebuild(codecKind, bag)];
87
+ };
88
+
89
+ // Resolve the stream family from the tracker's `kind` header. An unrecognized
90
+ // family yields no descriptor, and the build* hooks return no events —
91
+ // unreachable in practice, since a tracker only exists because the encoder
92
+ // started a stream stamping a known family id.
93
+ const familyOf = (tracker: StreamTrackerState): OutputStreamDescriptor<U> | undefined =>
94
+ streamByKind.get(tracker.codecHeaders[KIND_HEADER] ?? '');
95
+
96
+ return {
97
+ buildStart: (tracker) => {
98
+ const desc = familyOf(tracker);
99
+ if (!desc) return [];
100
+ const bag = readFields(desc.fields, tracker.codecHeaders);
101
+ bag[desc.idField] = tracker.streamId;
102
+ return [rebuild(desc.start, bag)];
103
+ },
104
+
105
+ buildDelta: (tracker, delta) => {
106
+ const desc = familyOf(tracker);
107
+ if (!desc) return [];
108
+ const bag: Record<string, unknown> = { [desc.idField]: tracker.streamId, [desc.deltaField]: delta };
109
+ return [rebuild(desc.delta, bag)];
110
+ },
111
+
112
+ buildEnd: (tracker, closingCodecHeaders) => {
113
+ const desc = familyOf(tracker);
114
+ if (!desc) return [];
115
+ if (desc.decodeEnd) {
116
+ return desc.decodeEnd({
117
+ streamId: tracker.streamId,
118
+ accumulated: tracker.accumulated,
119
+ codecHeaders: tracker.codecHeaders,
120
+ closingCodecHeaders,
121
+ });
122
+ }
123
+ const bag = readFields(desc.fields, closingCodecHeaders);
124
+ bag[desc.idField] = tracker.streamId;
125
+ return [rebuild(desc.end, bag)];
126
+ },
127
+
128
+ decodeDiscrete: (codecKind, codecHeaders, transportHeaders, data) => {
129
+ const ctx: OutputDecodeContext = { codecKind, codecHeaders, transportHeaders, data };
130
+ const evt = discreteByType.get(codecKind);
131
+ if (evt) return decodeEvent(evt, codecKind, ctx);
132
+ const streamDesc = streamByKind.get(codecKind);
133
+ if (streamDesc?.decodeDiscrete) return streamDesc.decodeDiscrete(ctx);
134
+ const wildcard = wildcards.find((w) => w.match?.(codecKind));
135
+ if (wildcard) return decodeEvent(wildcard, codecKind, ctx);
136
+ return [];
137
+ },
138
+ };
139
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Generic output encode driver over a descriptor set.
3
+ *
4
+ * Builds a chunk→descriptor registry once, then routes each event: discrete
5
+ * descriptors publish a single message, streamed families drive
6
+ * start/append/close, and escape-hatch `encode` functions take over entirely.
7
+ * Headers are always built through the descriptor's declared fields (the `h`
8
+ * builder), so the imperative paths can't drift from the declarative ones.
9
+ */
10
+
11
+ import * as Ably from 'ably';
12
+
13
+ import { ErrorCode } from '../../errors.js';
14
+ import type { EncoderCore } from './encoder.js';
15
+ import { partitionOutputEvents, prop, writeFields } from './field-bag.js';
16
+ import type { HeaderBuilder, OutputDescriptor, OutputStreamDescriptor } from './output-descriptors.js';
17
+ import type { WriteOptions } from './types.js';
18
+
19
+ /** Per-write output encode context threaded from the encoder. */
20
+ export interface OutputEncodeContext {
21
+ /** The encoder's configured fallback message id, if any. */
22
+ messageId: string | undefined;
23
+ /** Per-write overrides. */
24
+ opts: WriteOptions | undefined;
25
+ }
26
+
27
+ /** Encodes events of union `U` to channel operations via a descriptor set. */
28
+ export interface OutputDescriptorEncoder<U> {
29
+ /**
30
+ * Encode one event through its descriptor.
31
+ * @param chunk - The event to encode.
32
+ * @param core - The encoder core to publish/stream through.
33
+ * @param ctx - Per-write context (fallback message id, write options).
34
+ * @returns A promise resolving when the publish/stream operation completes.
35
+ */
36
+ encode(chunk: U, core: EncoderCore, ctx: OutputEncodeContext): Promise<void>;
37
+ }
38
+
39
+ /**
40
+ * Build an output encode driver for a descriptor set bound to a wire message name.
41
+ * @template U - The codec's event union.
42
+ * @param descriptors - The descriptor set (events + streamed families).
43
+ * @param wireName - The Ably message name for this direction (`ai-output` / `ai-input`).
44
+ * @returns An {@link OutputDescriptorEncoder} routing each event through its descriptor.
45
+ */
46
+ export const createOutputDescriptorEncoder = <U extends { type: string }>(
47
+ descriptors: readonly OutputDescriptor<U>[],
48
+ wireName: string,
49
+ ): OutputDescriptorEncoder<U> => {
50
+ const { discreteByType, wildcards } = partitionOutputEvents(descriptors);
51
+ const streamByPhase = new Map<string, { descriptor: OutputStreamDescriptor<U>; phase: 'start' | 'delta' | 'end' }>();
52
+
53
+ for (const descriptor of descriptors) {
54
+ if (descriptor.construct === 'stream') {
55
+ streamByPhase.set(descriptor.start, { descriptor, phase: 'start' });
56
+ streamByPhase.set(descriptor.delta, { descriptor, phase: 'delta' });
57
+ streamByPhase.set(descriptor.end, { descriptor, phase: 'end' });
58
+ }
59
+ }
60
+
61
+ return {
62
+ encode: async (chunk, core, ctx) => {
63
+ const { type } = chunk;
64
+
65
+ const streamEntry = streamByPhase.get(type);
66
+ if (streamEntry) {
67
+ const { descriptor, phase } = streamEntry;
68
+ const h: HeaderBuilder<U> = (c, keys) => writeFields(descriptor.fields, descriptor.kind, c, keys);
69
+ // CAST: idField/deltaField are string-valued chunk keys by construction.
70
+ const streamId = prop(chunk, descriptor.idField) as string;
71
+ if (phase === 'start') {
72
+ await core.startStream(streamId, { name: wireName, data: '', codecHeaders: h(chunk) }, ctx.opts);
73
+ } else if (phase === 'delta') {
74
+ core.appendStream(streamId, prop(chunk, descriptor.deltaField) as string);
75
+ } else if (descriptor.onEnd) {
76
+ await descriptor.onEnd(chunk, core, { h, name: wireName, messageId: ctx.messageId, opts: ctx.opts });
77
+ } else {
78
+ await core.closeStream(streamId, { name: wireName, data: '', codecHeaders: h(chunk) });
79
+ }
80
+ return;
81
+ }
82
+
83
+ const descriptor = discreteByType.get(type) ?? wildcards.find((w) => w.match?.(type));
84
+ if (!descriptor) {
85
+ throw new Ably.ErrorInfo(`unable to publish; unsupported event type '${type}'`, ErrorCode.InvalidArgument, 400);
86
+ }
87
+
88
+ const h: HeaderBuilder<U> = (c, keys) => writeFields(descriptor.fields, c.type, c, keys);
89
+ if (descriptor.encode) {
90
+ await descriptor.encode(chunk, core, { h, name: wireName, messageId: ctx.messageId, opts: ctx.opts });
91
+ return;
92
+ }
93
+
94
+ const data = descriptor.data ? descriptor.data.encode(chunk) : '';
95
+ await core.publishDiscrete(
96
+ { name: wireName, data, codecHeaders: h(chunk), ephemeral: descriptor.ephemeral?.(chunk) },
97
+ ctx.opts,
98
+ );
99
+ },
100
+ };
101
+ };