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