@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.
- package/README.md +10 -19
- package/dist/ably-ai-transport.js +1790 -1091
- package/dist/ably-ai-transport.js.map +1 -1
- package/dist/ably-ai-transport.umd.cjs +1 -1
- package/dist/ably-ai-transport.umd.cjs.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/core/agent.d.ts +20 -5
- package/dist/core/channel-options.d.ts +57 -0
- package/dist/core/codec/codec-event.d.ts +9 -0
- package/dist/core/codec/decoder.d.ts +4 -1
- package/dist/core/codec/define-codec.d.ts +100 -0
- package/dist/core/codec/encoder.d.ts +2 -7
- package/dist/core/codec/field-bag.d.ts +85 -0
- package/dist/core/codec/fields.d.ts +141 -0
- package/dist/core/codec/index.d.ts +8 -1
- package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
- package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
- package/dist/core/codec/input-descriptors.d.ts +281 -0
- package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
- package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
- package/dist/core/codec/output-descriptors.d.ts +237 -0
- package/dist/core/codec/types.d.ts +95 -36
- package/dist/core/codec/well-known-inputs.d.ts +52 -0
- package/dist/core/transport/agent-view.d.ts +296 -0
- package/dist/core/transport/decode-fold.d.ts +40 -32
- package/dist/core/transport/headers.d.ts +30 -1
- package/dist/core/transport/index.d.ts +1 -1
- package/dist/core/transport/invocation.d.ts +1 -1
- package/dist/core/transport/load-history-pages.d.ts +71 -0
- package/dist/core/transport/load-history.d.ts +21 -16
- package/dist/core/transport/run-manager.d.ts +9 -11
- package/dist/core/transport/session-support.d.ts +55 -0
- package/dist/core/transport/tree.d.ts +165 -15
- package/dist/core/transport/types/agent.d.ts +120 -98
- package/dist/core/transport/types/client.d.ts +45 -12
- package/dist/core/transport/types/tree.d.ts +52 -10
- package/dist/core/transport/types/view.d.ts +55 -28
- package/dist/core/transport/view.d.ts +176 -58
- package/dist/core/transport/wire-log.d.ts +102 -0
- package/dist/errors.d.ts +10 -4
- package/dist/index.d.ts +6 -5
- package/dist/react/ably-ai-transport-react.js +784 -415
- package/dist/react/ably-ai-transport-react.js.map +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
- package/dist/react/contexts/client-session-context.d.ts +2 -1
- package/dist/react/contexts/client-session-provider.d.ts +3 -0
- package/dist/react/index.d.ts +2 -1
- package/dist/react/internal/skipped-session.d.ts +8 -0
- package/dist/react/use-view.d.ts +3 -3
- package/dist/utils.d.ts +22 -54
- package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
- package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
- package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
- package/dist/vercel/codec/events.d.ts +1 -2
- package/dist/vercel/codec/fields.d.ts +44 -0
- package/dist/vercel/codec/fold-content.d.ts +16 -0
- package/dist/vercel/codec/fold-data.d.ts +16 -0
- package/dist/vercel/codec/fold-input.d.ts +67 -0
- package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
- package/dist/vercel/codec/fold-text.d.ts +16 -0
- package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
- package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
- package/dist/vercel/codec/index.d.ts +5 -30
- package/dist/vercel/codec/inputs.d.ts +11 -0
- package/dist/vercel/codec/outputs.d.ts +11 -0
- package/dist/vercel/codec/reducer-state.d.ts +121 -0
- package/dist/vercel/codec/reducer.d.ts +20 -102
- package/dist/vercel/codec/tool-transitions.d.ts +0 -6
- package/dist/vercel/codec/wire-data.d.ts +34 -0
- package/dist/vercel/index.d.ts +1 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
- package/dist/vercel/run-end-reason.d.ts +66 -11
- package/dist/vercel/tool-part.d.ts +21 -0
- package/dist/vercel/transport/chat-transport.d.ts +0 -2
- package/dist/vercel/transport/index.d.ts +1 -1
- package/dist/vercel/transport/run-output-stream.d.ts +6 -8
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/constants.ts +2 -2
- package/src/core/agent.ts +43 -19
- package/src/core/channel-options.ts +89 -0
- package/src/core/codec/codec-event.ts +27 -0
- package/src/core/codec/decoder.ts +145 -21
- package/src/core/codec/define-codec.ts +432 -0
- package/src/core/codec/encoder.ts +13 -54
- package/src/core/codec/field-bag.ts +142 -0
- package/src/core/codec/fields.ts +193 -0
- package/src/core/codec/index.ts +43 -0
- package/src/core/codec/input-descriptor-decoder.ts +97 -0
- package/src/core/codec/input-descriptor-encoder.ts +150 -0
- package/src/core/codec/input-descriptors.ts +373 -0
- package/src/core/codec/output-descriptor-decoder.ts +139 -0
- package/src/core/codec/output-descriptor-encoder.ts +101 -0
- package/src/core/codec/output-descriptors.ts +307 -0
- package/src/core/codec/types.ts +99 -36
- package/src/core/codec/well-known-inputs.ts +96 -0
- package/src/core/transport/agent-session.ts +330 -589
- package/src/core/transport/agent-view.ts +738 -0
- package/src/core/transport/client-session.ts +74 -69
- package/src/core/transport/decode-fold.ts +57 -47
- package/src/core/transport/headers.ts +57 -4
- package/src/core/transport/index.ts +2 -1
- package/src/core/transport/invocation.ts +1 -1
- package/src/core/transport/load-history-pages.ts +220 -0
- package/src/core/transport/load-history.ts +63 -61
- package/src/core/transport/pipe-stream.ts +10 -1
- package/src/core/transport/run-manager.ts +25 -31
- package/src/core/transport/session-support.ts +96 -0
- package/src/core/transport/tree.ts +414 -47
- package/src/core/transport/types/agent.ts +129 -102
- package/src/core/transport/types/client.ts +49 -13
- package/src/core/transport/types/tree.ts +61 -12
- package/src/core/transport/types/view.ts +57 -28
- package/src/core/transport/view.ts +520 -172
- package/src/core/transport/wire-log.ts +189 -0
- package/src/errors.ts +10 -3
- package/src/index.ts +44 -11
- package/src/react/contexts/client-session-context.ts +1 -1
- package/src/react/contexts/client-session-provider.tsx +38 -2
- package/src/react/index.ts +2 -1
- package/src/react/internal/skipped-session.ts +62 -0
- package/src/react/use-client-session.ts +7 -30
- package/src/react/use-view.ts +3 -3
- package/src/utils.ts +31 -97
- package/src/vercel/codec/decode-lifecycle.ts +70 -0
- package/src/vercel/codec/events.ts +1 -3
- package/src/vercel/codec/fields.ts +58 -0
- package/src/vercel/codec/fold-content.ts +54 -0
- package/src/vercel/codec/fold-data.ts +46 -0
- package/src/vercel/codec/fold-input.ts +255 -0
- package/src/vercel/codec/fold-lifecycle.ts +85 -0
- package/src/vercel/codec/fold-text.ts +55 -0
- package/src/vercel/codec/fold-tool-input.ts +86 -0
- package/src/vercel/codec/fold-tool-output.ts +79 -0
- package/src/vercel/codec/index.ts +23 -63
- package/src/vercel/codec/inputs.ts +116 -0
- package/src/vercel/codec/outputs.ts +207 -0
- package/src/vercel/codec/reducer-state.ts +169 -0
- package/src/vercel/codec/reducer.ts +52 -838
- package/src/vercel/codec/tool-transitions.ts +1 -12
- package/src/vercel/codec/wire-data.ts +64 -0
- package/src/vercel/index.ts +1 -0
- package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
- package/src/vercel/react/use-chat-transport.ts +8 -28
- package/src/vercel/react/use-message-sync.ts +5 -10
- package/src/vercel/run-end-reason.ts +95 -16
- package/src/vercel/tool-part.ts +25 -0
- package/src/vercel/transport/chat-transport.ts +10 -22
- package/src/vercel/transport/index.ts +1 -1
- package/src/vercel/transport/run-output-stream.ts +7 -8
- package/src/version.ts +1 -1
- package/dist/core/transport/branch-chain.d.ts +0 -43
- package/dist/core/transport/load-conversation.d.ts +0 -128
- package/dist/vercel/codec/decoder.d.ts +0 -9
- package/dist/vercel/codec/encoder.d.ts +0 -11
- package/src/core/transport/branch-chain.ts +0 -58
- package/src/core/transport/load-conversation.ts +0 -355
- package/src/vercel/codec/decoder.ts +0 -696
- package/src/vercel/codec/encoder.ts +0 -548
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed header-field bindings.
|
|
3
|
+
*
|
|
4
|
+
* A {@link HeaderField} binds a codec header key to its value type **once**,
|
|
5
|
+
* and exposes a symmetric {@link HeaderField.read | read} / {@link
|
|
6
|
+
* HeaderField.write | write} pair over a raw `Record<string, string>` headers
|
|
7
|
+
* record. Because a single binding drives both the encode and decode side, the
|
|
8
|
+
* header key and its value type stay in lockstep across directions — a key
|
|
9
|
+
* cannot be misspelled on one side and silently read as absent on the other.
|
|
10
|
+
*
|
|
11
|
+
* This is deliberately **not** a schema library: it is a thin bidirectional
|
|
12
|
+
* string (de)serializer over the headers record. The SDK ships no hard runtime
|
|
13
|
+
* dependencies, and a schema approach would force re-declaring peer-SDK-owned
|
|
14
|
+
* types. The four constructors cover every header value shape the codecs use:
|
|
15
|
+
*
|
|
16
|
+
* - {@link strField} — string values, optional default.
|
|
17
|
+
* - {@link boolField} — `"true"`/`"false"` booleans, optional default.
|
|
18
|
+
* - {@link jsonField} — JSON-serialized structured values.
|
|
19
|
+
* - {@link enumField} — string values validated against an allow-list with a
|
|
20
|
+
* fallback (e.g. a finish reason).
|
|
21
|
+
*
|
|
22
|
+
* Passing a default to `strField`/`boolField` makes the field **total**: its
|
|
23
|
+
* `read` returns `V` rather than `V | undefined`, for required headers that
|
|
24
|
+
* should always decode to a concrete value.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { parseBool, parseJson } from '../../utils.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A header key bound to its value type, with symmetric read/write over a raw
|
|
31
|
+
* headers record. Created via {@link strField}, {@link boolField}, {@link
|
|
32
|
+
* jsonField}, or {@link enumField}.
|
|
33
|
+
*
|
|
34
|
+
* The `key` plays a dual role in descriptor `fields` tables: it is the wire
|
|
35
|
+
* header key AND the property name the drivers read off the source object on
|
|
36
|
+
* encode and write back into the rebuilt object on decode. {@link FieldFor}
|
|
37
|
+
* enforces this — a declared field's key must name a real property of the
|
|
38
|
+
* member it lenses onto.
|
|
39
|
+
* @template V - The decoded value type this field reads and writes.
|
|
40
|
+
* @template K - The header key literal (preserved so {@link FieldFor} can match it against the member's property names).
|
|
41
|
+
*/
|
|
42
|
+
export interface HeaderField<V, K extends string = string> {
|
|
43
|
+
/** The raw header key this field reads from and writes to — also the source/rebuilt property name in descriptor tables. */
|
|
44
|
+
readonly key: K;
|
|
45
|
+
/**
|
|
46
|
+
* Read and decode this field's value from a headers record.
|
|
47
|
+
* @param headers - The raw codec headers record to read from.
|
|
48
|
+
* @returns The decoded value. For defaulted/validated fields this is total
|
|
49
|
+
* (the default/fallback is returned when the header is absent or invalid);
|
|
50
|
+
* otherwise `undefined` when the header is absent.
|
|
51
|
+
*/
|
|
52
|
+
read(headers: Record<string, string>): V;
|
|
53
|
+
/**
|
|
54
|
+
* Encode and write this field's value into a headers record, mutating it in
|
|
55
|
+
* place. `undefined` (and `null`, for JSON values), and values whose runtime
|
|
56
|
+
* type doesn't match the field, are skipped — the key is left unset rather
|
|
57
|
+
* than written. The parameter is `unknown` (not `V`) so a field keeps `V` in
|
|
58
|
+
* a single covariant position (`read`); this lets heterogeneous fields share a
|
|
59
|
+
* `HeaderField<unknown>[]` array, which the descriptor drivers rely on. At a
|
|
60
|
+
* typed call site the caller still passes a `V`.
|
|
61
|
+
* @param headers - The headers record to mutate.
|
|
62
|
+
* @param value - The value to encode and set.
|
|
63
|
+
*/
|
|
64
|
+
write(headers: Record<string, string>, value: unknown): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Symmetric codec for a descriptor's wire `data`. Many wire payloads are object
|
|
69
|
+
* envelopes a decode reads several chunk props out of (e.g. `{ errorText, input }`),
|
|
70
|
+
* so a single field can't model them. `encode` produces the wire data from the
|
|
71
|
+
* chunk; `decode` returns the chunk props the envelope contributes, merged into
|
|
72
|
+
* the rebuilt chunk by the driver.
|
|
73
|
+
* @template C - The narrowed chunk member.
|
|
74
|
+
*/
|
|
75
|
+
export interface DataCodec<C> {
|
|
76
|
+
/** Produce the wire `data` from the chunk. */
|
|
77
|
+
encode: (chunk: C) => unknown;
|
|
78
|
+
/**
|
|
79
|
+
* Extract the chunk props this envelope contributes from the wire `data`.
|
|
80
|
+
* Undefined-valued props are stripped when the driver rebuilds the object —
|
|
81
|
+
* every rebuild seam (output chunk, input payload, batch part) applies the
|
|
82
|
+
* same rule, since absent and undefined are indistinguishable on the wire.
|
|
83
|
+
*/
|
|
84
|
+
decode: (data: unknown) => Partial<C>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The header fields a descriptor may declare against member `C`. For each
|
|
89
|
+
* string-keyed property of `C`, a field is acceptable when its key IS that
|
|
90
|
+
* property name and its value type can hold the property. A mistyped key or a
|
|
91
|
+
* wrong-typed field (e.g. a `boolField` on a string property) is a compile
|
|
92
|
+
* error instead of a silently absent header.
|
|
93
|
+
* @template C - The member (chunk, payload, or part) the fields lens onto.
|
|
94
|
+
*/
|
|
95
|
+
export type FieldFor<C> = {
|
|
96
|
+
[K in keyof C & string]-?: HeaderField<C[K] | undefined, K>;
|
|
97
|
+
}[keyof C & string];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Bind a string-valued header field.
|
|
101
|
+
* @param key - The header key (and source property name in descriptor tables).
|
|
102
|
+
* @returns A field whose `read` yields `string | undefined` (absent → `undefined`).
|
|
103
|
+
*/
|
|
104
|
+
export function strField<K extends string>(key: K): HeaderField<string | undefined, K>;
|
|
105
|
+
/**
|
|
106
|
+
* Bind a string-valued header field with a default, making it total.
|
|
107
|
+
* @param key - The header key (and source property name in descriptor tables).
|
|
108
|
+
* @param fallback - Value returned by `read` when the header is absent.
|
|
109
|
+
* @returns A field whose `read` yields `string` (absent → `fallback`).
|
|
110
|
+
*/
|
|
111
|
+
export function strField<K extends string>(key: K, fallback: string): HeaderField<string, K>;
|
|
112
|
+
export function strField<K extends string>(key: K, fallback?: string): HeaderField<string | undefined, K> {
|
|
113
|
+
return {
|
|
114
|
+
key,
|
|
115
|
+
read: (headers) => headers[key] ?? fallback,
|
|
116
|
+
write: (headers, value) => {
|
|
117
|
+
if (typeof value === 'string') headers[key] = value;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Bind a boolean-valued header field, serialized as `"true"`/`"false"`.
|
|
124
|
+
* @param key - The header key (and source property name in descriptor tables).
|
|
125
|
+
* @returns A field whose `read` yields `boolean | undefined` (absent → `undefined`).
|
|
126
|
+
*/
|
|
127
|
+
export function boolField<K extends string>(key: K): HeaderField<boolean | undefined, K>;
|
|
128
|
+
/**
|
|
129
|
+
* Bind a boolean-valued header field with a default, making it total.
|
|
130
|
+
* @param key - The header key (and source property name in descriptor tables).
|
|
131
|
+
* @param fallback - Value returned by `read` when the header is absent.
|
|
132
|
+
* @returns A field whose `read` yields `boolean` (absent → `fallback`).
|
|
133
|
+
*/
|
|
134
|
+
export function boolField<K extends string>(key: K, fallback: boolean): HeaderField<boolean, K>;
|
|
135
|
+
export function boolField<K extends string>(key: K, fallback?: boolean): HeaderField<boolean | undefined, K> {
|
|
136
|
+
return {
|
|
137
|
+
key,
|
|
138
|
+
read: (headers) => parseBool(headers[key]) ?? fallback,
|
|
139
|
+
write: (headers, value) => {
|
|
140
|
+
if (typeof value === 'boolean') headers[key] = String(value);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Bind a JSON-serialized header field. The value is written with
|
|
147
|
+
* `JSON.stringify` and read back with `JSON.parse`; malformed JSON reads as
|
|
148
|
+
* `undefined`. The decoded shape is a trust boundary — the caller asserts it
|
|
149
|
+
* via the `V` type parameter.
|
|
150
|
+
* @template V - The expected decoded shape of the JSON value.
|
|
151
|
+
* @template K - The header key literal. Inferred when `V` is omitted; pass it explicitly alongside `V` when the field participates in a typed descriptor `fields` table.
|
|
152
|
+
* @param key - The header key (and source property name in descriptor tables).
|
|
153
|
+
* @returns A field whose `read` yields `V | undefined` (absent or malformed → `undefined`).
|
|
154
|
+
*/
|
|
155
|
+
export const jsonField = <V, K extends string = string>(key: K): HeaderField<V | undefined, K> => ({
|
|
156
|
+
key,
|
|
157
|
+
// CAST: header values are wire data parsed via JSON.parse — a trust
|
|
158
|
+
// boundary. The caller declares the expected shape through `V`; malformed
|
|
159
|
+
// JSON reads back as `undefined` (parseJson swallows the parse error).
|
|
160
|
+
read: (headers) => parseJson(headers[key]) as V | undefined,
|
|
161
|
+
write: (headers, value) => {
|
|
162
|
+
// Skip undefined and null so an absent value leaves the key unset rather
|
|
163
|
+
// than serializing to "null".
|
|
164
|
+
if (value !== undefined && value !== null) headers[key] = JSON.stringify(value);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Bind a string-valued header field validated against a fixed allow-list,
|
|
170
|
+
* falling back to a given value when the header is absent or unrecognized. Use
|
|
171
|
+
* for headers with a small closed set of valid values (e.g. a finish reason).
|
|
172
|
+
* @template T - The union of allowed string literals, inferred from `allowed`.
|
|
173
|
+
* @template K - The header key literal, inferred from `key`.
|
|
174
|
+
* @param key - The header key (and source property name in descriptor tables).
|
|
175
|
+
* @param allowed - The exhaustive list of valid values.
|
|
176
|
+
* @param fallback - Value returned by `read` when the header is absent or not in `allowed`.
|
|
177
|
+
* @returns A total field whose `read` yields one of the allowed literals.
|
|
178
|
+
*/
|
|
179
|
+
export const enumField = <const T extends string, K extends string>(
|
|
180
|
+
key: K,
|
|
181
|
+
allowed: readonly T[],
|
|
182
|
+
fallback: NoInfer<T>,
|
|
183
|
+
): HeaderField<T, K> => ({
|
|
184
|
+
key,
|
|
185
|
+
read: (headers) => {
|
|
186
|
+
const raw = headers[key];
|
|
187
|
+
// find returns the matched literal (typed T) or undefined — no cast needed.
|
|
188
|
+
return allowed.find((candidate) => candidate === raw) ?? fallback;
|
|
189
|
+
},
|
|
190
|
+
write: (headers, value) => {
|
|
191
|
+
if (typeof value === 'string') headers[key] = value;
|
|
192
|
+
},
|
|
193
|
+
});
|
package/src/core/codec/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type {
|
|
2
2
|
ChannelWriter,
|
|
3
3
|
Codec,
|
|
4
|
+
CodecEvent,
|
|
4
5
|
CodecInputEvent,
|
|
5
6
|
CodecMessage,
|
|
6
7
|
CodecOutputEvent,
|
|
@@ -33,3 +34,45 @@ export { createDecoderCore } from './decoder.js';
|
|
|
33
34
|
// Lifecycle tracker
|
|
34
35
|
export type { LifecycleTracker, PhaseConfig } from './lifecycle-tracker.js';
|
|
35
36
|
export { createLifecycleTracker } from './lifecycle-tracker.js';
|
|
37
|
+
|
|
38
|
+
// Typed header-field bindings
|
|
39
|
+
export type { DataCodec, FieldFor, HeaderField } from './fields.js';
|
|
40
|
+
export { boolField, enumField, jsonField, strField } from './fields.js';
|
|
41
|
+
|
|
42
|
+
// Well-known input factories (merged into every codec by defineCodec)
|
|
43
|
+
export type { WellKnownInputFactories } from './well-known-inputs.js';
|
|
44
|
+
|
|
45
|
+
// Output descriptor authoring surface
|
|
46
|
+
export type {
|
|
47
|
+
EscapeHatchCore,
|
|
48
|
+
HeaderBuilder,
|
|
49
|
+
OutputDecodeContext,
|
|
50
|
+
OutputDescriptor,
|
|
51
|
+
OutputEncodeHatchContext,
|
|
52
|
+
OutputEventSpec,
|
|
53
|
+
OutputStreamEndContext,
|
|
54
|
+
OutputStreamSpec,
|
|
55
|
+
} from './output-descriptors.js';
|
|
56
|
+
|
|
57
|
+
// Input descriptor authoring surface
|
|
58
|
+
export type {
|
|
59
|
+
BatchAssembleContext,
|
|
60
|
+
BatchMessageHeaders,
|
|
61
|
+
BatchSpec,
|
|
62
|
+
InputDescriptor,
|
|
63
|
+
InputEventSpec,
|
|
64
|
+
PartBuilder,
|
|
65
|
+
PartSpec,
|
|
66
|
+
} from './input-descriptors.js';
|
|
67
|
+
|
|
68
|
+
// Codec composition factory
|
|
69
|
+
export type {
|
|
70
|
+
CodecReducer,
|
|
71
|
+
DefineCodecConfig,
|
|
72
|
+
DefinedCodec,
|
|
73
|
+
InputBuilder,
|
|
74
|
+
LifecycleDiscreteContext,
|
|
75
|
+
LifecyclePolicy,
|
|
76
|
+
OutputBuilder,
|
|
77
|
+
} from './define-codec.js';
|
|
78
|
+
export { defineCodec } from './define-codec.js';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic input decode driver over an input descriptor set — the input-side
|
|
3
|
+
* sibling of {@link import('./output-descriptor-decoder.js')}.
|
|
4
|
+
*
|
|
5
|
+
* Rebuilds inputs from one inbound `ai-input` message, dispatching on the codec
|
|
6
|
+
* `kind` header. A single `event` rebuilds its field bag (and `data`) and wraps
|
|
7
|
+
* it into the `{ kind, codecMessageId, payload }` envelope; `wireOnly` events
|
|
8
|
+
* decode to `[]`. A
|
|
9
|
+
* `batch` reads the `partType` sub-discriminator, rebuilds the part via its
|
|
10
|
+
* sub-table, `assemble`s it into a one-part input, and the driver stamps the
|
|
11
|
+
* `kind` plus the codec-message-id reconstructed from the transport header.
|
|
12
|
+
*
|
|
13
|
+
* Returns bare `TInput[]`, never `CodecEvent[]` — direction tagging is
|
|
14
|
+
* core-owned, downstream at the decode→fold seam.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { HEADER_CODEC_MESSAGE_ID } from '../../constants.js';
|
|
18
|
+
import { stripUndefined } from '../../utils.js';
|
|
19
|
+
import { PART_TYPE_HEADER, partFor, readFields } from './field-bag.js';
|
|
20
|
+
import type {
|
|
21
|
+
BatchDescriptor,
|
|
22
|
+
InputDecodeContext,
|
|
23
|
+
InputDescriptor,
|
|
24
|
+
InputEventDescriptor,
|
|
25
|
+
} from './input-descriptors.js';
|
|
26
|
+
|
|
27
|
+
/** Decodes inbound `ai-input` messages of union `U` from an input descriptor set. */
|
|
28
|
+
export interface InputDescriptorDecoder<U> {
|
|
29
|
+
/**
|
|
30
|
+
* Rebuild zero or more inputs from one inbound `ai-input` message.
|
|
31
|
+
* @param ctx - The inbound message context (codec kind, data, header tiers).
|
|
32
|
+
* @returns The decoded inputs (empty when no descriptor matches or the input is wire-only).
|
|
33
|
+
*/
|
|
34
|
+
decode(ctx: InputDecodeContext): U[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build an input decode driver for an input descriptor set.
|
|
39
|
+
* @template U - The codec's input union.
|
|
40
|
+
* @param descriptors - The input descriptor set (events + batches).
|
|
41
|
+
* @returns An {@link InputDescriptorDecoder} that reconstructs inputs from the wire.
|
|
42
|
+
*/
|
|
43
|
+
export const createInputDescriptorDecoder = <U extends { kind: string }>(
|
|
44
|
+
descriptors: readonly InputDescriptor<U>[],
|
|
45
|
+
): InputDescriptorDecoder<U> => {
|
|
46
|
+
const byKind = new Map<string, InputDescriptor<U>>();
|
|
47
|
+
for (const descriptor of descriptors) byKind.set(descriptor.kind, descriptor);
|
|
48
|
+
|
|
49
|
+
const decodeEvent = (descriptor: InputEventDescriptor, ctx: InputDecodeContext): U[] => {
|
|
50
|
+
if (descriptor.wireOnly) return [];
|
|
51
|
+
|
|
52
|
+
const bag = readFields(descriptor.fields, ctx.codecHeaders);
|
|
53
|
+
if (descriptor.data) Object.assign(bag, descriptor.data.decode(ctx.data));
|
|
54
|
+
|
|
55
|
+
const codecMessageId = ctx.transportHeaders[HEADER_CODEC_MESSAGE_ID] ?? '';
|
|
56
|
+
// The payload bag is stripped of undefined-valued props — the same rule
|
|
57
|
+
// every rebuild seam applies to its innermost bag (absent and undefined
|
|
58
|
+
// are indistinguishable on the wire). The envelope keys are always defined.
|
|
59
|
+
// CAST: the rebuild seam — `bag` is assembled from the descriptor's declared
|
|
60
|
+
// fields and data codec onto the payload, so the `{ kind, codecMessageId, payload }`
|
|
61
|
+
// envelope conforms to the matched member by construction.
|
|
62
|
+
return [{ kind: descriptor.kind, codecMessageId, payload: stripUndefined(bag) } as unknown as U];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const decodeBatch = (descriptor: BatchDescriptor<U>, ctx: InputDecodeContext): U[] => {
|
|
66
|
+
const partType = ctx.codecHeaders[PART_TYPE_HEADER] ?? '';
|
|
67
|
+
const partDesc = partFor(descriptor.parts, partType);
|
|
68
|
+
if (!partDesc) return [];
|
|
69
|
+
|
|
70
|
+
const bag = readFields(partDesc.fields, ctx.codecHeaders);
|
|
71
|
+
if (partDesc.data) Object.assign(bag, partDesc.data.decode(ctx.data));
|
|
72
|
+
bag.type = partType;
|
|
73
|
+
|
|
74
|
+
// `assemble` takes the erased part (`unknown`); `bag` is the part rebuilt from
|
|
75
|
+
// its declared fields/data plus the `partType` written to the domain `type`
|
|
76
|
+
// field. The header tiers carry the per-message metadata (id, role, …) the
|
|
77
|
+
// batch stamped on every part, so `assemble` can rebuild the message envelope.
|
|
78
|
+
const partial = descriptor.assemble(stripUndefined(bag), {
|
|
79
|
+
codecHeaders: ctx.codecHeaders,
|
|
80
|
+
transportHeaders: ctx.transportHeaders,
|
|
81
|
+
});
|
|
82
|
+
// CAST: the driver stamps the shared `kind` onto the assembled one-part input; together
|
|
83
|
+
// they complete the matched member. A batch creates a new message (not addressed by a
|
|
84
|
+
// codec-message-id, unlike single `event`s), so none is stamped — the per-message
|
|
85
|
+
// identity rides the transport header and is recovered by `assemble` when needed.
|
|
86
|
+
return [{ kind: descriptor.kind, ...partial } as unknown as U];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
decode: (ctx) => {
|
|
91
|
+
const descriptor = byKind.get(ctx.codecKind);
|
|
92
|
+
if (!descriptor) return [];
|
|
93
|
+
if (descriptor.construct === 'event') return decodeEvent(descriptor, ctx);
|
|
94
|
+
return decodeBatch(descriptor, ctx);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic input encode driver over an input descriptor set — the input-side
|
|
3
|
+
* sibling of {@link import('./output-descriptor-encoder.js')}.
|
|
4
|
+
*
|
|
5
|
+
* Builds a `kind`→descriptor registry once, then routes each input: a single
|
|
6
|
+
* `event` publishes one discrete message (fields/data lensed onto the member's
|
|
7
|
+
* `payload`, or kind-only when `wireOnly`); a `batch` explodes the domain
|
|
8
|
+
* message into one wire event per part and publishes them atomically, with a
|
|
9
|
+
* built-in ≥1-event guarantee so the codec-message-id and role survive an empty
|
|
10
|
+
* decomposition. Headers are always built through the descriptor's declared
|
|
11
|
+
* fields ({@link writeFields}), so the imperative paths can't drift.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as Ably from 'ably';
|
|
15
|
+
|
|
16
|
+
import { ErrorCode } from '../../errors.js';
|
|
17
|
+
import { KIND_HEADER, PART_TYPE_HEADER, partFor, prop, writeFields } from './field-bag.js';
|
|
18
|
+
import type {
|
|
19
|
+
BatchDescriptor,
|
|
20
|
+
BatchMessageHeaders,
|
|
21
|
+
InputDescriptor,
|
|
22
|
+
InputEncodeContext,
|
|
23
|
+
InputEncoderCore,
|
|
24
|
+
InputEventDescriptor,
|
|
25
|
+
} from './input-descriptors.js';
|
|
26
|
+
import type { MessagePayload } from './types.js';
|
|
27
|
+
|
|
28
|
+
/** Encodes inputs of union `U` to channel operations via an input descriptor set. */
|
|
29
|
+
export interface InputDescriptorEncoder<U> {
|
|
30
|
+
/**
|
|
31
|
+
* Encode one input through its descriptor.
|
|
32
|
+
* @param input - The input to encode.
|
|
33
|
+
* @param core - The input encoder core to publish through.
|
|
34
|
+
* @param ctx - Per-write context (write options, carrying the codec-message-id).
|
|
35
|
+
* @returns A promise resolving when the publish operation completes.
|
|
36
|
+
*/
|
|
37
|
+
encode(input: U, core: InputEncoderCore, ctx: InputEncodeContext): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Layer the batch's per-message transport headers onto a part payload, if any.
|
|
41
|
+
const withMessageTransport = (payload: MessagePayload, message: BatchMessageHeaders | undefined): MessagePayload =>
|
|
42
|
+
message?.transportHeaders === undefined
|
|
43
|
+
? payload
|
|
44
|
+
: { ...payload, transportHeaders: { ...payload.transportHeaders, ...message.transportHeaders } };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build an input encode driver for an input descriptor set bound to a wire name.
|
|
48
|
+
* @template U - The codec's input union.
|
|
49
|
+
* @param descriptors - The input descriptor set (events + batches).
|
|
50
|
+
* @param wireName - The Ably message name for the input direction (`ai-input`).
|
|
51
|
+
* @returns An {@link InputDescriptorEncoder} routing each input through its descriptor.
|
|
52
|
+
*/
|
|
53
|
+
export const createInputDescriptorEncoder = <U extends { kind: string }>(
|
|
54
|
+
descriptors: readonly InputDescriptor<U>[],
|
|
55
|
+
wireName: string,
|
|
56
|
+
): InputDescriptorEncoder<U> => {
|
|
57
|
+
const byKind = new Map<string, InputDescriptor<U>>();
|
|
58
|
+
for (const descriptor of descriptors) byKind.set(descriptor.kind, descriptor);
|
|
59
|
+
|
|
60
|
+
const encodeEvent = async (
|
|
61
|
+
descriptor: InputEventDescriptor,
|
|
62
|
+
input: U,
|
|
63
|
+
core: InputEncoderCore,
|
|
64
|
+
ctx: InputEncodeContext,
|
|
65
|
+
): Promise<void> => {
|
|
66
|
+
if (descriptor.wireOnly) {
|
|
67
|
+
// Kind only: no fields, no data — the parent/target ride transport headers.
|
|
68
|
+
await core.publishDiscrete(
|
|
69
|
+
{ name: wireName, data: '', codecHeaders: { [KIND_HEADER]: descriptor.kind } },
|
|
70
|
+
ctx.opts,
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// A non-wireOnly input nests its domain data under `payload` — fields and
|
|
75
|
+
// data are authored against it. Fail fast on a payload-less member instead
|
|
76
|
+
// of silently publishing empty data the decoder can't rebuild from.
|
|
77
|
+
const source = prop(input, 'payload');
|
|
78
|
+
if (typeof source !== 'object' || source === null) {
|
|
79
|
+
throw new Ably.ErrorInfo(
|
|
80
|
+
`unable to encode input; event '${descriptor.kind}' carries no payload object — declare it wireOnly or use an encode escape hatch`,
|
|
81
|
+
ErrorCode.InvalidArgument,
|
|
82
|
+
400,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const codecHeaders = writeFields(descriptor.fields, descriptor.kind, source);
|
|
86
|
+
const data = descriptor.data ? descriptor.data.encode(source) : '';
|
|
87
|
+
await core.publishDiscrete({ name: wireName, data, codecHeaders }, ctx.opts);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const encodeBatch = async (
|
|
91
|
+
descriptor: BatchDescriptor<U>,
|
|
92
|
+
input: U,
|
|
93
|
+
core: InputEncoderCore,
|
|
94
|
+
ctx: InputEncodeContext,
|
|
95
|
+
): Promise<void> => {
|
|
96
|
+
// Per-message headers (e.g. message id, role) are stamped on every part so
|
|
97
|
+
// the decode side can reconstruct the shared message envelope from any one.
|
|
98
|
+
const message = descriptor.messageHeaders?.(input);
|
|
99
|
+
const payloads: MessagePayload[] = [];
|
|
100
|
+
for (const part of descriptor.explode(input)) {
|
|
101
|
+
const partType = descriptor.partTypeOf(part);
|
|
102
|
+
const partDesc = partFor(descriptor.parts, partType);
|
|
103
|
+
if (!partDesc) continue;
|
|
104
|
+
// CAST: a part is indexed by its declared fields; the part descriptor only
|
|
105
|
+
// runs against the part its predicate/literal matched, so the source has the
|
|
106
|
+
// field's type at runtime. The wire `partType` is the resolved part type.
|
|
107
|
+
const source = part as object;
|
|
108
|
+
const codecHeaders = {
|
|
109
|
+
...writeFields(partDesc.fields, descriptor.kind, source),
|
|
110
|
+
...message?.codecHeaders,
|
|
111
|
+
[PART_TYPE_HEADER]: partType,
|
|
112
|
+
};
|
|
113
|
+
const data = partDesc.data ? partDesc.data.encode(part) : '';
|
|
114
|
+
payloads.push(withMessageTransport({ name: wireName, data, codecHeaders }, message));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (payloads.length === 0) {
|
|
118
|
+
// ≥1-event guarantee: emit one bare part so the per-message headers (e.g.
|
|
119
|
+
// the message id and role) reach the wire even when no exploded part
|
|
120
|
+
// matched a descriptor. This fallback carries no partType, so the batch
|
|
121
|
+
// decode path yields no input for it — a codec that needs an empty
|
|
122
|
+
// message to round-trip must guarantee ≥1 encodable part in `explode`
|
|
123
|
+
// (e.g. by substituting a canonical empty part).
|
|
124
|
+
payloads.push(
|
|
125
|
+
withMessageTransport(
|
|
126
|
+
{ name: wireName, data: '', codecHeaders: { [KIND_HEADER]: descriptor.kind, ...message?.codecHeaders } },
|
|
127
|
+
message,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await core.publishDiscreteBatch(payloads, ctx.opts);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
encode: async (input, core, ctx) => {
|
|
137
|
+
const descriptor = byKind.get(input.kind);
|
|
138
|
+
if (!descriptor) {
|
|
139
|
+
throw new Ably.ErrorInfo(
|
|
140
|
+
`unable to publish; unsupported input kind '${input.kind}'`,
|
|
141
|
+
ErrorCode.InvalidArgument,
|
|
142
|
+
400,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
await (descriptor.construct === 'event'
|
|
146
|
+
? encodeEvent(descriptor, input, core, ctx)
|
|
147
|
+
: encodeBatch(descriptor, input, core, ctx));
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
};
|