@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,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toCodecEvents` — tag a decoded message's events with their wire direction.
|
|
3
|
+
*
|
|
4
|
+
* A decoded message is already split into inputs and outputs by the decoder
|
|
5
|
+
* (driven by the Ably message name — the authoritative direction signal). This
|
|
6
|
+
* helper folds that split into the ordered {@link CodecEvent} stream the reducer
|
|
7
|
+
* consumes, so the direction is carried explicitly rather than re-inferred from
|
|
8
|
+
* each event's shape. Inputs are tagged before outputs, preserving the wire
|
|
9
|
+
* order within a single message (a message is single-direction, so the relative
|
|
10
|
+
* order of the two groups is immaterial).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { CodecEvent, CodecInputEvent, CodecOutputEvent, DecodedMessage } from './types.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tag a decoded message's events with their wire direction.
|
|
17
|
+
* @template TInput - The codec's input union.
|
|
18
|
+
* @template TOutput - The codec's output union.
|
|
19
|
+
* @param decoded - The decoder's input/output split for one inbound message.
|
|
20
|
+
* @returns The events as a direction-tagged {@link CodecEvent} list, inputs first.
|
|
21
|
+
*/
|
|
22
|
+
export const toCodecEvents = <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent>(
|
|
23
|
+
decoded: DecodedMessage<TInput, TOutput>,
|
|
24
|
+
): CodecEvent<TInput, TOutput>[] => [
|
|
25
|
+
...decoded.inputs.map((event): CodecEvent<TInput, TOutput> => ({ direction: 'input', event })),
|
|
26
|
+
...decoded.outputs.map((event): CodecEvent<TInput, TOutput> => ({ direction: 'output', event })),
|
|
27
|
+
];
|
|
@@ -3,27 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles the Ably message action patterns (create, append, update, delete)
|
|
5
5
|
* and delegates to domain-specific hooks for event building and discrete
|
|
6
|
-
* event decoding.
|
|
6
|
+
* event decoding. Stream trackers are version-guarded: a delivery whose
|
|
7
|
+
* `Message.version.serial` the tracker has already incorporated decodes to
|
|
8
|
+
* nothing, so the same decoder instance can serve both the live
|
|
9
|
+
* subscription and history hydration without double-decoding.
|
|
7
10
|
*
|
|
8
11
|
* Domain decoders call `createDecoderCore(hooks, options)` and provide hooks
|
|
9
|
-
* for stream classification, event building, and discrete decoding.
|
|
12
|
+
* for stream classification, event building, and discrete decoding. Hooks
|
|
13
|
+
* return a flat `TEvent[]` — no event-vs-message union. Per-message routing
|
|
14
|
+
* concerns (`codec-message-id`) are handled by the SDK via `ReducerMeta`, not
|
|
15
|
+
* here.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
import type * as Ably from 'ably';
|
|
13
19
|
|
|
14
|
-
import {
|
|
20
|
+
import { HEADER_STATUS, HEADER_STREAM, HEADER_STREAM_ID } from '../../constants.js';
|
|
15
21
|
import type { Logger } from '../../logger.js';
|
|
16
|
-
import {
|
|
17
|
-
import type {
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Wrap a domain event as a single-element decoder output array.
|
|
21
|
-
* @param event - The domain event to wrap.
|
|
22
|
-
* @returns A single-element array containing the event as a decoder output.
|
|
23
|
-
*/
|
|
24
|
-
export const eventOutput = <TEvent, TMessage>(event: TEvent): DecoderOutput<TEvent, TMessage>[] => [
|
|
25
|
-
{ kind: 'event', event },
|
|
26
|
-
];
|
|
22
|
+
import { getCodecHeaders, getTransportHeaders } from '../../utils.js';
|
|
23
|
+
import type { MessagePayload, StreamTrackerState } from './types.js';
|
|
27
24
|
|
|
28
25
|
// ---------------------------------------------------------------------------
|
|
29
26
|
// Options
|
|
@@ -44,32 +41,29 @@ export interface DecoderCoreOptions {
|
|
|
44
41
|
// ---------------------------------------------------------------------------
|
|
45
42
|
|
|
46
43
|
/** Hooks that a domain codec provides to the decoder core for stream classification and event building. */
|
|
47
|
-
export interface DecoderCoreHooks<TEvent
|
|
44
|
+
export interface DecoderCoreHooks<TEvent> {
|
|
48
45
|
/**
|
|
49
46
|
* Build domain events emitted when a new stream starts. May return multiple
|
|
50
47
|
* events (e.g. a start event and a start-step event).
|
|
51
48
|
*/
|
|
52
|
-
buildStartEvents(tracker: StreamTrackerState):
|
|
49
|
+
buildStartEvents(tracker: StreamTrackerState): TEvent[];
|
|
53
50
|
|
|
54
51
|
/** Build domain events for a text delta received on a stream. */
|
|
55
|
-
buildDeltaEvents(tracker: StreamTrackerState, delta: string):
|
|
52
|
+
buildDeltaEvents(tracker: StreamTrackerState, delta: string): TEvent[];
|
|
56
53
|
|
|
57
54
|
/**
|
|
58
|
-
* Build domain events emitted when a stream
|
|
59
|
-
* Not called for
|
|
60
|
-
* tracker.
|
|
55
|
+
* Build domain events emitted when a stream completes (status:complete).
|
|
56
|
+
* Not called for cancelled streams. The closing codec headers may differ
|
|
57
|
+
* from tracker.codecHeaders if the closing append carried updated headers.
|
|
61
58
|
*/
|
|
62
|
-
buildEndEvents(
|
|
63
|
-
tracker: StreamTrackerState,
|
|
64
|
-
closingHeaders: Record<string, string>,
|
|
65
|
-
): DecoderOutput<TEvent, TMessage>[];
|
|
59
|
+
buildEndEvents(tracker: StreamTrackerState, closingCodecHeaders: Record<string, string>): TEvent[];
|
|
66
60
|
|
|
67
61
|
/**
|
|
68
|
-
* Decode a discrete message (message.create
|
|
69
|
-
* or a non-streamable first-contact update). Handles user messages,
|
|
70
|
-
*
|
|
62
|
+
* Decode a discrete message (a `message.create` whose stream header is not
|
|
63
|
+
* "true", or a non-streamable first-contact update). Handles user messages,
|
|
64
|
+
* tool lifecycle, data-*, etc.
|
|
71
65
|
*/
|
|
72
|
-
decodeDiscrete(input: MessagePayload):
|
|
66
|
+
decodeDiscrete(input: MessagePayload): TEvent[];
|
|
73
67
|
}
|
|
74
68
|
|
|
75
69
|
// ---------------------------------------------------------------------------
|
|
@@ -77,9 +71,9 @@ export interface DecoderCoreHooks<TEvent, TMessage> {
|
|
|
77
71
|
// ---------------------------------------------------------------------------
|
|
78
72
|
|
|
79
73
|
/** The decoder core returned by {@link createDecoderCore}. */
|
|
80
|
-
export interface DecoderCore<TEvent
|
|
81
|
-
/** Decode a single Ably message into zero or more domain
|
|
82
|
-
decode(message: Ably.InboundMessage):
|
|
74
|
+
export interface DecoderCore<TEvent> {
|
|
75
|
+
/** Decode a single Ably message into zero or more domain TEvents. */
|
|
76
|
+
decode(message: Ably.InboundMessage): TEvent[];
|
|
83
77
|
}
|
|
84
78
|
|
|
85
79
|
// ---------------------------------------------------------------------------
|
|
@@ -87,70 +81,50 @@ export interface DecoderCore<TEvent, TMessage> {
|
|
|
87
81
|
// ---------------------------------------------------------------------------
|
|
88
82
|
|
|
89
83
|
// Spec: AIT-CD7
|
|
90
|
-
class DefaultDecoderCore<TEvent
|
|
91
|
-
private readonly _hooks: DecoderCoreHooks<TEvent
|
|
84
|
+
class DefaultDecoderCore<TEvent> implements DecoderCore<TEvent> {
|
|
85
|
+
private readonly _hooks: DecoderCoreHooks<TEvent>;
|
|
92
86
|
private readonly _logger: Logger | undefined;
|
|
93
87
|
private readonly _onStreamUpdate: ((tracker: StreamTrackerState) => void) | undefined;
|
|
94
88
|
private readonly _onStreamDelete: ((serial: string, tracker: StreamTrackerState | undefined) => void) | undefined;
|
|
95
89
|
private readonly _serialState = new Map<string, StreamTrackerState>();
|
|
96
90
|
|
|
97
|
-
constructor(hooks: DecoderCoreHooks<TEvent
|
|
91
|
+
constructor(hooks: DecoderCoreHooks<TEvent>, options: DecoderCoreOptions = {}) {
|
|
98
92
|
this._hooks = hooks;
|
|
99
93
|
this._onStreamUpdate = options.onStreamUpdate;
|
|
100
94
|
this._onStreamDelete = options.onStreamDelete;
|
|
101
95
|
this._logger = options.logger?.withContext({ component: 'DecoderCore' });
|
|
102
96
|
}
|
|
103
97
|
|
|
104
|
-
decode(message: Ably.InboundMessage):
|
|
98
|
+
decode(message: Ably.InboundMessage): TEvent[] {
|
|
105
99
|
const action = message.action;
|
|
106
100
|
|
|
107
101
|
this._logger?.trace('DefaultDecoderCore.decode();', { action, serial: message.serial, name: message.name });
|
|
108
102
|
|
|
109
|
-
let outputs: DecoderOutput<TEvent, TMessage>[];
|
|
110
|
-
|
|
111
103
|
switch (action) {
|
|
112
104
|
// Spec: AIT-CD7a
|
|
113
105
|
case 'message.create': {
|
|
114
106
|
const payload = this._toPayload(message);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
payload
|
|
118
|
-
? this._decodeStreamedCreate(payload, message.serial)
|
|
119
|
-
: this._hooks.decodeDiscrete(payload);
|
|
120
|
-
break;
|
|
107
|
+
return payload.transportHeaders?.[HEADER_STREAM] === 'true'
|
|
108
|
+
? this._decodeStreamedCreate(payload, message.serial, message.version.serial)
|
|
109
|
+
: this._hooks.decodeDiscrete(payload);
|
|
121
110
|
}
|
|
122
111
|
|
|
123
112
|
case 'message.append': {
|
|
124
|
-
|
|
125
|
-
break;
|
|
113
|
+
return this._decodeAppend(message);
|
|
126
114
|
}
|
|
127
115
|
|
|
128
116
|
case 'message.update': {
|
|
129
|
-
|
|
130
|
-
break;
|
|
117
|
+
return this._decodeUpdate(message);
|
|
131
118
|
}
|
|
132
119
|
|
|
133
120
|
case 'message.delete': {
|
|
134
|
-
|
|
135
|
-
break;
|
|
121
|
+
return this._decodeDelete(message);
|
|
136
122
|
}
|
|
137
123
|
|
|
138
124
|
default: {
|
|
139
125
|
return [];
|
|
140
126
|
}
|
|
141
127
|
}
|
|
142
|
-
|
|
143
|
-
// Tag all event outputs with the message ID from x-ably-msg-id for accumulator correlation.
|
|
144
|
-
const messageId = getHeaders(message)[HEADER_MSG_ID];
|
|
145
|
-
if (messageId) {
|
|
146
|
-
for (const output of outputs) {
|
|
147
|
-
if (output.kind === 'event') {
|
|
148
|
-
output.messageId = messageId;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return outputs;
|
|
154
128
|
}
|
|
155
129
|
|
|
156
130
|
// -------------------------------------------------------------------------
|
|
@@ -162,7 +136,8 @@ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessa
|
|
|
162
136
|
name: message.name ?? '',
|
|
163
137
|
// CAST: Ably SDK types `data` as `any`; cast to unknown is the safe boundary type.
|
|
164
138
|
data: message.data as unknown,
|
|
165
|
-
|
|
139
|
+
transportHeaders: getTransportHeaders(message),
|
|
140
|
+
codecHeaders: getCodecHeaders(message),
|
|
166
141
|
};
|
|
167
142
|
}
|
|
168
143
|
|
|
@@ -197,6 +172,99 @@ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessa
|
|
|
197
172
|
}
|
|
198
173
|
}
|
|
199
174
|
|
|
175
|
+
// -------------------------------------------------------------------------
|
|
176
|
+
// Private: version guard
|
|
177
|
+
// -------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Whether a delivery is already incorporated into (or out of contract for)
|
|
181
|
+
* an existing tracker, and so must decode to nothing. Covers two cases:
|
|
182
|
+
*
|
|
183
|
+
* - The delivery carries a `version.serial` at or below the tracker's —
|
|
184
|
+
* the mutation it describes is already incorporated (a history aggregate
|
|
185
|
+
* covered by live deltas, a resume retransmission, a whole-wire replay).
|
|
186
|
+
* - The tracker is closed — the stream has ended and its accumulated text
|
|
187
|
+
* has been dropped, so nothing further can fold into it. In-contract
|
|
188
|
+
* replays are already covered by the version check; this catches
|
|
189
|
+
* out-of-contract version-less deliveries for an ended stream.
|
|
190
|
+
*
|
|
191
|
+
* A version-bearing delivery that passes advances the tracker's version.
|
|
192
|
+
* @param method - Calling method name, for log messages.
|
|
193
|
+
* @param serial - The message serial (the tracker's key).
|
|
194
|
+
* @param tracker - The existing tracker for the serial.
|
|
195
|
+
* @param version - The delivery's `Message.version.serial`, if present.
|
|
196
|
+
* @returns True when the delivery must decode to nothing.
|
|
197
|
+
*/
|
|
198
|
+
private _alreadyIncorporated(
|
|
199
|
+
method: string,
|
|
200
|
+
serial: string,
|
|
201
|
+
tracker: StreamTrackerState,
|
|
202
|
+
version: string | undefined,
|
|
203
|
+
): boolean {
|
|
204
|
+
if (version !== undefined && version <= tracker.version) {
|
|
205
|
+
this._logger?.debug(`DefaultDecoderCore.${method}(); delivery already incorporated`, {
|
|
206
|
+
serial,
|
|
207
|
+
version,
|
|
208
|
+
trackerVersion: tracker.version,
|
|
209
|
+
});
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (tracker.closed) {
|
|
213
|
+
this._logger?.debug(`DefaultDecoderCore.${method}(); stream closed, dropping delivery`, { serial, version });
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (version !== undefined) tracker.version = version;
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Close a tracker, dropping its accumulated text. What remains is a
|
|
222
|
+
* `{version, closed}` tombstone: enough to recognise covered replays and
|
|
223
|
+
* out-of-contract post-close deliveries, without retaining the stream's
|
|
224
|
+
* full content for the decoder's lifetime.
|
|
225
|
+
* @param tracker - The tracker to close.
|
|
226
|
+
*/
|
|
227
|
+
private _closeTracker(tracker: StreamTrackerState): void {
|
|
228
|
+
tracker.closed = true;
|
|
229
|
+
tracker.accumulated = '';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// -------------------------------------------------------------------------
|
|
233
|
+
// Private: terminal-status transition
|
|
234
|
+
// -------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Apply a stream's terminal status (complete / cancelled) to a tracker. On
|
|
238
|
+
* `complete` it emits end events (read before the tracker is closed) and
|
|
239
|
+
* then closes the tracker; on `cancelled` it closes silently. Both the
|
|
240
|
+
* append and prefix-match update paths funnel through here so they can't
|
|
241
|
+
* diverge. Covered replays and post-close deliveries are filtered upstream
|
|
242
|
+
* by `_alreadyIncorporated`, so no closed-once guard is needed here.
|
|
243
|
+
* Returns whether a terminal transition fired (so callers can log it).
|
|
244
|
+
* @param tracker - The stream tracker to close.
|
|
245
|
+
* @param status - The status header value from the message (may be undefined).
|
|
246
|
+
* @param closingCodecHeaders - Codec headers from the closing message, passed to buildEndEvents.
|
|
247
|
+
* @param outputs - The output array end events are pushed into.
|
|
248
|
+
* @returns True when this call closed the tracker; false otherwise.
|
|
249
|
+
*/
|
|
250
|
+
private _applyTerminalStatus(
|
|
251
|
+
tracker: StreamTrackerState,
|
|
252
|
+
status: string | undefined,
|
|
253
|
+
closingCodecHeaders: Record<string, string>,
|
|
254
|
+
outputs: TEvent[],
|
|
255
|
+
): boolean {
|
|
256
|
+
if (status === 'complete') {
|
|
257
|
+
outputs.push(...this._hooks.buildEndEvents(tracker, closingCodecHeaders));
|
|
258
|
+
this._closeTracker(tracker);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (status === 'cancelled') {
|
|
262
|
+
this._closeTracker(tracker);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
200
268
|
// -------------------------------------------------------------------------
|
|
201
269
|
// Private: streamed message create
|
|
202
270
|
// -------------------------------------------------------------------------
|
|
@@ -204,17 +272,29 @@ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessa
|
|
|
204
272
|
private _decodeStreamedCreate(
|
|
205
273
|
payload: MessagePayload,
|
|
206
274
|
serial: string | undefined,
|
|
207
|
-
|
|
275
|
+
version: string | undefined,
|
|
276
|
+
): TEvent[] {
|
|
208
277
|
if (!serial) return [];
|
|
209
278
|
|
|
210
|
-
const
|
|
211
|
-
|
|
279
|
+
const existing = this._serialState.get(serial);
|
|
280
|
+
if (existing) {
|
|
281
|
+
// A create is the message's first version, so a tracker for this serial
|
|
282
|
+
// has already incorporated it (resume retransmission, whole-wire replay).
|
|
283
|
+
this._logger?.debug('DefaultDecoderCore._decodeStreamedCreate(); duplicate create for tracked stream', {
|
|
284
|
+
serial,
|
|
285
|
+
});
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const streamId = payload.transportHeaders?.[HEADER_STREAM_ID] ?? '';
|
|
212
290
|
|
|
213
291
|
const tracker: StreamTrackerState = {
|
|
214
292
|
name: payload.name,
|
|
215
293
|
streamId,
|
|
216
294
|
accumulated: '',
|
|
217
|
-
|
|
295
|
+
codecHeaders: { ...payload.codecHeaders },
|
|
296
|
+
transportHeaders: { ...payload.transportHeaders },
|
|
297
|
+
version: version ?? serial,
|
|
218
298
|
closed: false,
|
|
219
299
|
};
|
|
220
300
|
this._serialState.set(serial, tracker);
|
|
@@ -233,33 +313,42 @@ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessa
|
|
|
233
313
|
// -------------------------------------------------------------------------
|
|
234
314
|
|
|
235
315
|
// Spec: AIT-CD8
|
|
236
|
-
private _decodeAppend(message: Ably.InboundMessage):
|
|
316
|
+
private _decodeAppend(message: Ably.InboundMessage): TEvent[] {
|
|
237
317
|
const serial = message.serial;
|
|
238
318
|
if (!serial) return [];
|
|
239
319
|
|
|
240
320
|
const tracker = this._serialState.get(serial);
|
|
241
321
|
if (!tracker) {
|
|
242
|
-
//
|
|
322
|
+
// Out of contract: the platform converts the first post-attach append
|
|
323
|
+
// of an in-flight message into a full-contents update, so an append
|
|
324
|
+
// should never be a stream's first contact. Keep the first-contact
|
|
325
|
+
// heuristic as a defensive fallback.
|
|
326
|
+
this._logger?.warn('DefaultDecoderCore._decodeAppend(); append with no tracker, treating as first contact', {
|
|
327
|
+
serial,
|
|
328
|
+
});
|
|
243
329
|
return this._decodeUpdate(message);
|
|
244
330
|
}
|
|
245
331
|
|
|
246
|
-
|
|
332
|
+
if (this._alreadyIncorporated('_decodeAppend', serial, tracker, message.version.serial)) return [];
|
|
333
|
+
|
|
334
|
+
const transport = getTransportHeaders(message);
|
|
335
|
+
const closingCodec = getCodecHeaders(message);
|
|
247
336
|
const delta = typeof message.data === 'string' ? message.data : '';
|
|
248
|
-
const status =
|
|
249
|
-
const outputs:
|
|
337
|
+
const status = transport[HEADER_STATUS];
|
|
338
|
+
const outputs: TEvent[] = [];
|
|
250
339
|
|
|
251
340
|
if (delta.length > 0) {
|
|
252
341
|
tracker.accumulated += delta;
|
|
253
342
|
outputs.push(...this._hooks.buildDeltaEvents(tracker, delta));
|
|
254
343
|
}
|
|
255
344
|
|
|
256
|
-
if (status
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
345
|
+
if (this._applyTerminalStatus(tracker, status, closingCodec, outputs)) {
|
|
346
|
+
this._logger?.debug(
|
|
347
|
+
`DefaultDecoderCore._decodeAppend(); stream ${status === 'complete' ? 'complete' : 'cancelled'}`,
|
|
348
|
+
{
|
|
349
|
+
streamId: tracker.streamId,
|
|
350
|
+
},
|
|
351
|
+
);
|
|
263
352
|
}
|
|
264
353
|
|
|
265
354
|
return outputs;
|
|
@@ -270,47 +359,46 @@ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessa
|
|
|
270
359
|
// -------------------------------------------------------------------------
|
|
271
360
|
|
|
272
361
|
// Spec: AIT-CD9
|
|
273
|
-
private _decodeUpdate(message: Ably.InboundMessage):
|
|
362
|
+
private _decodeUpdate(message: Ably.InboundMessage): TEvent[] {
|
|
274
363
|
const serial = message.serial;
|
|
275
364
|
if (!serial) return [];
|
|
276
365
|
|
|
277
366
|
const payload = this._toPayload(message);
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
const
|
|
367
|
+
const transport = payload.transportHeaders ?? {};
|
|
368
|
+
const codec = payload.codecHeaders ?? {};
|
|
369
|
+
const isStreamed = transport[HEADER_STREAM] === 'true';
|
|
370
|
+
const status = transport[HEADER_STATUS];
|
|
281
371
|
|
|
282
372
|
const tracker = this._serialState.get(serial);
|
|
283
373
|
|
|
284
374
|
if (!tracker) {
|
|
285
|
-
return this._decodeFirstContact(payload, isStreamed, status, serial);
|
|
375
|
+
return this._decodeFirstContact(payload, isStreamed, status, serial, message.version.serial);
|
|
286
376
|
}
|
|
287
377
|
|
|
378
|
+
if (this._alreadyIncorporated('_decodeUpdate', serial, tracker, message.version.serial)) return [];
|
|
379
|
+
|
|
288
380
|
// Updates to tracked streams use string data for prefix-match accumulation
|
|
289
381
|
const data = this._stringData(message);
|
|
290
382
|
|
|
291
383
|
// --- Tracker exists: prefix-match or replacement ---
|
|
292
384
|
if (data.startsWith(tracker.accumulated)) {
|
|
293
385
|
const delta = data.slice(tracker.accumulated.length);
|
|
294
|
-
const outputs:
|
|
386
|
+
const outputs: TEvent[] = [];
|
|
295
387
|
|
|
296
388
|
if (delta.length > 0) {
|
|
297
389
|
tracker.accumulated = data;
|
|
298
390
|
outputs.push(...this._hooks.buildDeltaEvents(tracker, delta));
|
|
299
391
|
}
|
|
300
392
|
|
|
301
|
-
|
|
302
|
-
tracker.closed = true;
|
|
303
|
-
outputs.push(...this._hooks.buildEndEvents(tracker, h));
|
|
304
|
-
} else if (status === 'aborted' && !tracker.closed) {
|
|
305
|
-
tracker.closed = true;
|
|
306
|
-
}
|
|
393
|
+
this._applyTerminalStatus(tracker, status, codec, outputs);
|
|
307
394
|
|
|
308
395
|
return outputs;
|
|
309
396
|
}
|
|
310
397
|
|
|
311
398
|
// --- Replacement (NOT a prefix match) ---
|
|
312
399
|
tracker.accumulated = data;
|
|
313
|
-
tracker.
|
|
400
|
+
tracker.codecHeaders = { ...codec };
|
|
401
|
+
tracker.transportHeaders = { ...transport };
|
|
314
402
|
|
|
315
403
|
this._invokeOnStreamUpdate(tracker);
|
|
316
404
|
|
|
@@ -322,14 +410,15 @@ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessa
|
|
|
322
410
|
isStreamed: boolean,
|
|
323
411
|
status: string | undefined,
|
|
324
412
|
serial: string,
|
|
325
|
-
|
|
413
|
+
version: string | undefined,
|
|
414
|
+
): TEvent[] {
|
|
326
415
|
// Non-streamed messages are discrete
|
|
327
416
|
if (!isStreamed) {
|
|
328
417
|
return this._hooks.decodeDiscrete(payload);
|
|
329
418
|
}
|
|
330
419
|
|
|
331
|
-
const streamId = payload.
|
|
332
|
-
const
|
|
420
|
+
const streamId = payload.transportHeaders?.[HEADER_STREAM_ID] ?? '';
|
|
421
|
+
const codec = payload.codecHeaders ?? {};
|
|
333
422
|
const data = typeof payload.data === 'string' ? payload.data : '';
|
|
334
423
|
|
|
335
424
|
this._logger?.debug('DefaultDecoderCore._decodeFirstContact(); first-contact stream', {
|
|
@@ -343,20 +432,26 @@ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessa
|
|
|
343
432
|
name: payload.name,
|
|
344
433
|
streamId,
|
|
345
434
|
accumulated: data,
|
|
346
|
-
|
|
347
|
-
|
|
435
|
+
codecHeaders: { ...codec },
|
|
436
|
+
transportHeaders: { ...payload.transportHeaders },
|
|
437
|
+
version: version ?? serial,
|
|
438
|
+
closed: false,
|
|
348
439
|
};
|
|
349
440
|
this._serialState.set(serial, newTracker);
|
|
350
441
|
|
|
351
|
-
// Emit start + delta (if any) + end (if
|
|
442
|
+
// Emit start + delta (if any) + end (if complete)
|
|
352
443
|
const outputs = this._hooks.buildStartEvents(newTracker);
|
|
353
444
|
|
|
354
445
|
if (data.length > 0) {
|
|
355
446
|
outputs.push(...this._hooks.buildDeltaEvents(newTracker, data));
|
|
356
447
|
}
|
|
357
448
|
|
|
358
|
-
if (status === '
|
|
359
|
-
outputs.push(...this._hooks.buildEndEvents(newTracker,
|
|
449
|
+
if (status === 'complete') {
|
|
450
|
+
outputs.push(...this._hooks.buildEndEvents(newTracker, codec));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (status === 'complete' || status === 'cancelled') {
|
|
454
|
+
this._closeTracker(newTracker);
|
|
360
455
|
}
|
|
361
456
|
|
|
362
457
|
return outputs;
|
|
@@ -367,7 +462,7 @@ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessa
|
|
|
367
462
|
// -------------------------------------------------------------------------
|
|
368
463
|
|
|
369
464
|
// Spec: AIT-CD10
|
|
370
|
-
private _decodeDelete(message: Ably.InboundMessage):
|
|
465
|
+
private _decodeDelete(message: Ably.InboundMessage): TEvent[] {
|
|
371
466
|
const serial = message.serial;
|
|
372
467
|
if (!serial) return [];
|
|
373
468
|
|
|
@@ -376,8 +471,10 @@ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessa
|
|
|
376
471
|
this._invokeOnStreamDelete(serial, tracker);
|
|
377
472
|
|
|
378
473
|
if (tracker) {
|
|
379
|
-
tracker
|
|
380
|
-
|
|
474
|
+
// No need to advance the tracker's version here: `_closeTracker` leaves a
|
|
475
|
+
// closed tombstone, and `_alreadyIncorporated`'s closed check drops every
|
|
476
|
+
// later delivery regardless of version.
|
|
477
|
+
this._closeTracker(tracker);
|
|
381
478
|
}
|
|
382
479
|
|
|
383
480
|
this._logger?.debug('DefaultDecoderCore._decodeDelete();', { serial });
|
|
@@ -396,7 +493,7 @@ class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessa
|
|
|
396
493
|
* @param options - Decoder configuration (callbacks, logger).
|
|
397
494
|
* @returns A new {@link DecoderCore} instance.
|
|
398
495
|
*/
|
|
399
|
-
export const createDecoderCore = <TEvent
|
|
400
|
-
hooks: DecoderCoreHooks<TEvent
|
|
496
|
+
export const createDecoderCore = <TEvent>(
|
|
497
|
+
hooks: DecoderCoreHooks<TEvent>,
|
|
401
498
|
options: DecoderCoreOptions = {},
|
|
402
|
-
): DecoderCore<TEvent
|
|
499
|
+
): DecoderCore<TEvent> => new DefaultDecoderCore(hooks, options);
|