@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,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
|
+
};
|
|
@@ -47,6 +47,8 @@ interface StreamState {
|
|
|
47
47
|
/** Codec-tier headers repeated on every append (`extras.ai.codec`). */
|
|
48
48
|
persistentCodec: Record<string, string>;
|
|
49
49
|
cancelled: boolean;
|
|
50
|
+
/** Set by `closeStream` — a completed stream must never receive a cancelled terminal. */
|
|
51
|
+
completed: boolean;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -80,7 +82,7 @@ export interface EncoderCore {
|
|
|
80
82
|
|
|
81
83
|
/**
|
|
82
84
|
* Append data to an in-flight streamed message. Fire-and-forget: errors are
|
|
83
|
-
* collected internally and surfaced by {@link closeStream},
|
|
85
|
+
* collected internally and surfaced by {@link closeStream},
|
|
84
86
|
* {@link cancelAllStreams} or {@link close}.
|
|
85
87
|
* @throws {Ably.ErrorInfo} InvalidArgument if there is no active stream for `streamId` or the core is closed.
|
|
86
88
|
*/
|
|
@@ -93,12 +95,6 @@ export interface EncoderCore {
|
|
|
93
95
|
*/
|
|
94
96
|
closeStream(streamId: string, payload: StreamPayload): Promise<void>;
|
|
95
97
|
|
|
96
|
-
/**
|
|
97
|
-
* Cancel a single in-progress stream (status:cancelled) and flush all
|
|
98
|
-
* pending appends for recovery before returning.
|
|
99
|
-
*/
|
|
100
|
-
cancelStream(streamId: string, opts?: WriteOptions): Promise<void>;
|
|
101
|
-
|
|
102
98
|
/**
|
|
103
99
|
* Cancel all in-progress streams (status:cancelled) and flush all
|
|
104
100
|
* pending appends for recovery before returning.
|
|
@@ -116,7 +112,6 @@ export interface EncoderCore {
|
|
|
116
112
|
// Spec: AIT-CD1
|
|
117
113
|
class DefaultEncoderCore implements EncoderCore {
|
|
118
114
|
private readonly _writer: ChannelWriter;
|
|
119
|
-
private readonly _defaultClientId: string | undefined;
|
|
120
115
|
private readonly _defaultExtras: Extras | undefined;
|
|
121
116
|
private readonly _onMessageHook: (message: Ably.Message) => void;
|
|
122
117
|
private readonly _logger: Logger | undefined;
|
|
@@ -127,7 +122,6 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
127
122
|
|
|
128
123
|
constructor(writer: ChannelWriter, options: EncoderCoreOptions = {}) {
|
|
129
124
|
this._writer = writer;
|
|
130
|
-
this._defaultClientId = options.clientId;
|
|
131
125
|
this._defaultExtras = options.extras;
|
|
132
126
|
this._onMessageHook =
|
|
133
127
|
options.onMessage ??
|
|
@@ -164,12 +158,10 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
164
158
|
transport[HEADER_STREAM_ID] = streamId;
|
|
165
159
|
const codec = payload.codecHeaders ?? {};
|
|
166
160
|
|
|
167
|
-
const clientId = this._resolveClientId(opts);
|
|
168
161
|
const msg: Ably.Message = {
|
|
169
162
|
name: payload.name,
|
|
170
163
|
data: payload.data,
|
|
171
164
|
extras: { ai: this._aiExtras(transport, codec) },
|
|
172
|
-
...(clientId ? { clientId } : {}),
|
|
173
165
|
};
|
|
174
166
|
|
|
175
167
|
this._invokeOnMessage(msg);
|
|
@@ -193,6 +185,7 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
193
185
|
persistentTransport: transport,
|
|
194
186
|
persistentCodec: codec,
|
|
195
187
|
cancelled: false,
|
|
188
|
+
completed: false,
|
|
196
189
|
});
|
|
197
190
|
|
|
198
191
|
this._logger?.debug('DefaultEncoderCore.startStream(); stream started', {
|
|
@@ -244,6 +237,9 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
244
237
|
|
|
245
238
|
// Accumulate closing data so recovery has the full content
|
|
246
239
|
tracker.accumulated += payload.data;
|
|
240
|
+
// Mark completed so a later cancelAllStreams (e.g. pipeStream terminating
|
|
241
|
+
// streams left open by an agent self-abort) skips this stream.
|
|
242
|
+
tracker.completed = true;
|
|
247
243
|
|
|
248
244
|
const { transport, codec } = this._buildClosing(tracker, payload);
|
|
249
245
|
transport[HEADER_STATUS] = 'complete';
|
|
@@ -263,46 +259,16 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
263
259
|
this._logger?.debug('DefaultEncoderCore.closeStream(); stream closed', { streamId });
|
|
264
260
|
}
|
|
265
261
|
|
|
266
|
-
// Spec: AIT-CD5, AIT-
|
|
267
|
-
async cancelStream(streamId: string, opts?: WriteOptions): Promise<void> {
|
|
268
|
-
this._assertNotClosed();
|
|
269
|
-
this._logger?.trace('DefaultEncoderCore.cancelStream();', { streamId });
|
|
270
|
-
|
|
271
|
-
const tracker = this._trackers.get(streamId);
|
|
272
|
-
if (!tracker) {
|
|
273
|
-
throw new Ably.ErrorInfo(
|
|
274
|
-
`unable to cancel stream; no active stream for streamId '${streamId}'`,
|
|
275
|
-
ErrorCode.InvalidArgument,
|
|
276
|
-
400,
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
tracker.cancelled = true;
|
|
281
|
-
|
|
282
|
-
const { transport, codec } = this._buildClosing(tracker, undefined, opts);
|
|
283
|
-
transport[HEADER_STATUS] = 'cancelled';
|
|
284
|
-
|
|
285
|
-
const msg: Ably.Message = {
|
|
286
|
-
serial: tracker.serial,
|
|
287
|
-
data: '',
|
|
288
|
-
extras: { ai: this._aiExtras(transport, codec) },
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
this._invokeOnMessage(msg);
|
|
292
|
-
const p = this._writer.appendMessage(msg);
|
|
293
|
-
this._pending.push({ promise: p, streamId });
|
|
294
|
-
|
|
295
|
-
await this._flushPending();
|
|
296
|
-
|
|
297
|
-
this._logger?.debug('DefaultEncoderCore.cancelStream(); stream cancelled', { streamId });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Spec: AIT-CD5a
|
|
262
|
+
// Spec: AIT-CD5, AIT-CD5a
|
|
301
263
|
async cancelAllStreams(opts?: WriteOptions): Promise<void> {
|
|
302
264
|
this._assertNotClosed();
|
|
303
265
|
this._logger?.trace('DefaultEncoderCore.cancelAllStreams();', { streamCount: this._trackers.size });
|
|
304
266
|
|
|
305
267
|
for (const tracker of this._trackers.values()) {
|
|
268
|
+
// Idempotent and complete-safe: a stream already cancelled must not be
|
|
269
|
+
// re-appended on a repeat call, and a stream that closed with
|
|
270
|
+
// status:complete must never receive a cancelled terminal.
|
|
271
|
+
if (tracker.cancelled || tracker.completed) continue;
|
|
306
272
|
tracker.cancelled = true;
|
|
307
273
|
|
|
308
274
|
const { transport, codec } = this._buildClosing(tracker, undefined, opts);
|
|
@@ -432,10 +398,6 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
432
398
|
}
|
|
433
399
|
}
|
|
434
400
|
|
|
435
|
-
private _resolveClientId(opts?: WriteOptions): string | undefined {
|
|
436
|
-
return opts?.clientId ?? this._defaultClientId;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
401
|
/**
|
|
440
402
|
* Build the transport-tier header record for a message: caller-configured
|
|
441
403
|
* transport headers (default extras + per-write overrides) layered with any
|
|
@@ -476,8 +438,6 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
476
438
|
// events that also happen to be discrete (stream: false).
|
|
477
439
|
transport[HEADER_DISCRETE] = 'true';
|
|
478
440
|
}
|
|
479
|
-
const clientId = this._resolveClientId(opts);
|
|
480
|
-
|
|
481
441
|
const msg: Ably.Message = {
|
|
482
442
|
name: payload.name,
|
|
483
443
|
data: payload.data,
|
|
@@ -485,7 +445,6 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
485
445
|
ai: this._aiExtras(transport, payload.codecHeaders ?? {}),
|
|
486
446
|
...(payload.ephemeral ? { ephemeral: true } : {}),
|
|
487
447
|
},
|
|
488
|
-
...(clientId ? { clientId } : {}),
|
|
489
448
|
};
|
|
490
449
|
|
|
491
450
|
this._invokeOnMessage(msg);
|
|
@@ -520,7 +479,7 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
520
479
|
/**
|
|
521
480
|
* Create an encoder core bound to the given channel writer.
|
|
522
481
|
* @param writer - The channel writer to publish messages through.
|
|
523
|
-
* @param options - Encoder configuration (
|
|
482
|
+
* @param options - Encoder configuration (extras, hooks, logger).
|
|
524
483
|
* @returns A new {@link EncoderCore} instance.
|
|
525
484
|
*/
|
|
526
485
|
export const createEncoderCore = (writer: ChannelWriter, options: EncoderCoreOptions = {}): EncoderCore =>
|