@ably/ai-transport 0.0.1
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/LICENSE +176 -0
- package/README.md +426 -0
- package/dist/ably-ai-transport.js +1388 -0
- package/dist/ably-ai-transport.js.map +1 -0
- package/dist/ably-ai-transport.umd.cjs +2 -0
- package/dist/ably-ai-transport.umd.cjs.map +1 -0
- package/dist/constants.d.ts +50 -0
- package/dist/core/codec/decoder.d.ts +62 -0
- package/dist/core/codec/encoder.d.ts +56 -0
- package/dist/core/codec/index.d.ts +8 -0
- package/dist/core/codec/lifecycle-tracker.d.ts +74 -0
- package/dist/core/codec/types.d.ts +188 -0
- package/dist/core/transport/client-transport.d.ts +10 -0
- package/dist/core/transport/conversation-tree.d.ts +9 -0
- package/dist/core/transport/decode-history.d.ts +41 -0
- package/dist/core/transport/headers.d.ts +26 -0
- package/dist/core/transport/index.d.ts +4 -0
- package/dist/core/transport/pipe-stream.d.ts +16 -0
- package/dist/core/transport/server-transport.d.ts +7 -0
- package/dist/core/transport/stream-router.d.ts +19 -0
- package/dist/core/transport/turn-manager.d.ts +34 -0
- package/dist/core/transport/types.d.ts +407 -0
- package/dist/errors.d.ts +46 -0
- package/dist/event-emitter.d.ts +65 -0
- package/dist/index.d.ts +11 -0
- package/dist/logger.d.ts +103 -0
- package/dist/react/ably-ai-transport-react.js +823 -0
- package/dist/react/ably-ai-transport-react.js.map +1 -0
- package/dist/react/ably-ai-transport-react.umd.cjs +2 -0
- package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -0
- package/dist/react/index.d.ts +11 -0
- package/dist/react/use-ably-messages.d.ts +18 -0
- package/dist/react/use-active-turns.d.ts +8 -0
- package/dist/react/use-client-transport.d.ts +7 -0
- package/dist/react/use-conversation-tree.d.ts +20 -0
- package/dist/react/use-edit.d.ts +7 -0
- package/dist/react/use-history.d.ts +19 -0
- package/dist/react/use-messages.d.ts +7 -0
- package/dist/react/use-regenerate.d.ts +7 -0
- package/dist/react/use-send.d.ts +7 -0
- package/dist/utils.d.ts +127 -0
- package/dist/vercel/ably-ai-transport-vercel.js +2331 -0
- package/dist/vercel/ably-ai-transport-vercel.js.map +1 -0
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs +2 -0
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -0
- package/dist/vercel/codec/accumulator.d.ts +21 -0
- package/dist/vercel/codec/decoder.d.ts +22 -0
- package/dist/vercel/codec/encoder.d.ts +41 -0
- package/dist/vercel/codec/index.d.ts +22 -0
- package/dist/vercel/index.d.ts +3 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +2082 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +2 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -0
- package/dist/vercel/react/index.d.ts +3 -0
- package/dist/vercel/react/use-chat-transport.d.ts +29 -0
- package/dist/vercel/react/use-message-sync.d.ts +19 -0
- package/dist/vercel/transport/chat-transport.d.ts +118 -0
- package/dist/vercel/transport/index.d.ts +36 -0
- package/package.json +123 -0
- package/react/README.md +3 -0
- package/react/index.d.ts +1 -0
- package/react/index.js +1 -0
- package/react/index.umd.cjs +1 -0
- package/src/constants.ts +98 -0
- package/src/core/codec/decoder.ts +402 -0
- package/src/core/codec/encoder.ts +470 -0
- package/src/core/codec/index.ts +28 -0
- package/src/core/codec/lifecycle-tracker.ts +140 -0
- package/src/core/codec/types.ts +249 -0
- package/src/core/transport/client-transport.ts +959 -0
- package/src/core/transport/conversation-tree.ts +434 -0
- package/src/core/transport/decode-history.ts +337 -0
- package/src/core/transport/headers.ts +46 -0
- package/src/core/transport/index.ts +34 -0
- package/src/core/transport/pipe-stream.ts +95 -0
- package/src/core/transport/server-transport.ts +458 -0
- package/src/core/transport/stream-router.ts +118 -0
- package/src/core/transport/turn-manager.ts +147 -0
- package/src/core/transport/types.ts +533 -0
- package/src/errors.ts +58 -0
- package/src/event-emitter.ts +103 -0
- package/src/index.ts +89 -0
- package/src/logger.ts +241 -0
- package/src/react/index.ts +11 -0
- package/src/react/use-ably-messages.ts +37 -0
- package/src/react/use-active-turns.ts +61 -0
- package/src/react/use-client-transport.ts +37 -0
- package/src/react/use-conversation-tree.ts +71 -0
- package/src/react/use-edit.ts +24 -0
- package/src/react/use-history.ts +111 -0
- package/src/react/use-messages.ts +32 -0
- package/src/react/use-regenerate.ts +24 -0
- package/src/react/use-send.ts +25 -0
- package/src/react/vite.config.ts +32 -0
- package/src/tsconfig.json +25 -0
- package/src/utils.ts +230 -0
- package/src/vercel/codec/accumulator.ts +603 -0
- package/src/vercel/codec/decoder.ts +615 -0
- package/src/vercel/codec/encoder.ts +396 -0
- package/src/vercel/codec/index.ts +37 -0
- package/src/vercel/index.ts +12 -0
- package/src/vercel/react/index.ts +4 -0
- package/src/vercel/react/use-chat-transport.ts +60 -0
- package/src/vercel/react/use-message-sync.ts +34 -0
- package/src/vercel/react/vite.config.ts +33 -0
- package/src/vercel/transport/chat-transport.ts +278 -0
- package/src/vercel/transport/index.ts +56 -0
- package/src/vercel/vite.config.ts +33 -0
- package/src/vite.config.ts +31 -0
- package/vercel/README.md +3 -0
- package/vercel/index.d.ts +1 -0
- package/vercel/index.js +1 -0
- package/vercel/index.umd.cjs +1 -0
- package/vercel/react/README.md +3 -0
- package/vercel/react/index.d.ts +1 -0
- package/vercel/react/index.js +1 -0
- package/vercel/react/index.umd.cjs +1 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decoder core — action dispatch and serial tracking machinery.
|
|
3
|
+
*
|
|
4
|
+
* Handles the Ably message action patterns (create, append, update, delete)
|
|
5
|
+
* and delegates to domain-specific hooks for event building and discrete
|
|
6
|
+
* event decoding.
|
|
7
|
+
*
|
|
8
|
+
* Domain decoders call `createDecoderCore(hooks, options)` and provide hooks
|
|
9
|
+
* for stream classification, event building, and discrete decoding.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type * as Ably from 'ably';
|
|
13
|
+
|
|
14
|
+
import { HEADER_MSG_ID, HEADER_STATUS, HEADER_STREAM, HEADER_STREAM_ID } from '../../constants.js';
|
|
15
|
+
import type { Logger } from '../../logger.js';
|
|
16
|
+
import { getHeaders } from '../../utils.js';
|
|
17
|
+
import type { DecoderOutput, MessagePayload, StreamTrackerState } from './types.js';
|
|
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
|
+
];
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Options
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/** Options for creating a decoder core. */
|
|
33
|
+
export interface DecoderCoreOptions {
|
|
34
|
+
/** Called when a tracked stream is replaced (non-prefix update). Receives the tracker with updated state. */
|
|
35
|
+
onStreamUpdate?: (tracker: StreamTrackerState) => void;
|
|
36
|
+
/** Called when a message is deleted. Receives the serial and tracker (if one exists). */
|
|
37
|
+
onStreamDelete?: (serial: string, tracker: StreamTrackerState | undefined) => void;
|
|
38
|
+
/** Logger instance for diagnostic output. */
|
|
39
|
+
logger?: Logger;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Domain hooks
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** Hooks that a domain codec provides to the decoder core for stream classification and event building. */
|
|
47
|
+
export interface DecoderCoreHooks<TEvent, TMessage> {
|
|
48
|
+
/**
|
|
49
|
+
* Build domain events emitted when a new stream starts. May return multiple
|
|
50
|
+
* events (e.g. a start event and a start-step event).
|
|
51
|
+
*/
|
|
52
|
+
buildStartEvents(tracker: StreamTrackerState): DecoderOutput<TEvent, TMessage>[];
|
|
53
|
+
|
|
54
|
+
/** Build domain events for a text delta received on a stream. */
|
|
55
|
+
buildDeltaEvents(tracker: StreamTrackerState, delta: string): DecoderOutput<TEvent, TMessage>[];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build domain events emitted when a stream finishes (x-ably-status:finished).
|
|
59
|
+
* Not called for aborted streams. The closing headers may differ from
|
|
60
|
+
* tracker.headers if the closing append carried updated headers.
|
|
61
|
+
*/
|
|
62
|
+
buildEndEvents(
|
|
63
|
+
tracker: StreamTrackerState,
|
|
64
|
+
closingHeaders: Record<string, string>,
|
|
65
|
+
): DecoderOutput<TEvent, TMessage>[];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decode a discrete message (message.create where x-ably-stream is "false",
|
|
69
|
+
* or a non-streamable first-contact update). Handles user messages, lifecycle
|
|
70
|
+
* events, tool lifecycle, data-*, etc.
|
|
71
|
+
*/
|
|
72
|
+
decodeDiscrete(input: MessagePayload): DecoderOutput<TEvent, TMessage>[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Interface
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/** The decoder core returned by {@link createDecoderCore}. */
|
|
80
|
+
export interface DecoderCore<TEvent, TMessage> {
|
|
81
|
+
/** Decode a single Ably message into zero or more domain outputs. */
|
|
82
|
+
decode(message: Ably.InboundMessage): DecoderOutput<TEvent, TMessage>[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Default implementation
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
// Spec: AIT-CD7
|
|
90
|
+
class DefaultDecoderCore<TEvent, TMessage> implements DecoderCore<TEvent, TMessage> {
|
|
91
|
+
private readonly _hooks: DecoderCoreHooks<TEvent, TMessage>;
|
|
92
|
+
private readonly _logger: Logger | undefined;
|
|
93
|
+
private readonly _onStreamUpdate: ((tracker: StreamTrackerState) => void) | undefined;
|
|
94
|
+
private readonly _onStreamDelete: ((serial: string, tracker: StreamTrackerState | undefined) => void) | undefined;
|
|
95
|
+
private readonly _serialState = new Map<string, StreamTrackerState>();
|
|
96
|
+
|
|
97
|
+
constructor(hooks: DecoderCoreHooks<TEvent, TMessage>, options: DecoderCoreOptions = {}) {
|
|
98
|
+
this._hooks = hooks;
|
|
99
|
+
this._onStreamUpdate = options.onStreamUpdate;
|
|
100
|
+
this._onStreamDelete = options.onStreamDelete;
|
|
101
|
+
this._logger = options.logger?.withContext({ component: 'DecoderCore' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
decode(message: Ably.InboundMessage): DecoderOutput<TEvent, TMessage>[] {
|
|
105
|
+
const action = message.action;
|
|
106
|
+
|
|
107
|
+
this._logger?.trace('DefaultDecoderCore.decode();', { action, serial: message.serial, name: message.name });
|
|
108
|
+
|
|
109
|
+
let outputs: DecoderOutput<TEvent, TMessage>[];
|
|
110
|
+
|
|
111
|
+
switch (action) {
|
|
112
|
+
// Spec: AIT-CD7a
|
|
113
|
+
case 'message.create': {
|
|
114
|
+
const payload = this._toPayload(message);
|
|
115
|
+
|
|
116
|
+
outputs =
|
|
117
|
+
payload.headers?.[HEADER_STREAM] === 'true'
|
|
118
|
+
? this._decodeStreamedCreate(payload, message.serial)
|
|
119
|
+
: this._hooks.decodeDiscrete(payload);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case 'message.append': {
|
|
124
|
+
outputs = this._decodeAppend(message);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'message.update': {
|
|
129
|
+
outputs = this._decodeUpdate(message);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'message.delete': {
|
|
134
|
+
outputs = this._decodeDelete(message);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
default: {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
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
|
+
}
|
|
155
|
+
|
|
156
|
+
// -------------------------------------------------------------------------
|
|
157
|
+
// Private: extract MessagePayload
|
|
158
|
+
// -------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
private _toPayload(message: Ably.InboundMessage): MessagePayload {
|
|
161
|
+
return {
|
|
162
|
+
name: message.name ?? '',
|
|
163
|
+
// CAST: Ably SDK types `data` as `any`; cast to unknown is the safe boundary type.
|
|
164
|
+
data: message.data as unknown,
|
|
165
|
+
headers: getHeaders(message),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract string data from an Ably message, for stream accumulation paths.
|
|
171
|
+
* @param message - The Ably message to extract string data from.
|
|
172
|
+
* @returns The string data, or empty string if data is not a string.
|
|
173
|
+
*/
|
|
174
|
+
private _stringData(message: Ably.InboundMessage): string {
|
|
175
|
+
return typeof message.data === 'string' ? message.data : '';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// -------------------------------------------------------------------------
|
|
179
|
+
// Private: safe callback invocation
|
|
180
|
+
// -------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
private _invokeOnStreamUpdate(tracker: StreamTrackerState): void {
|
|
183
|
+
if (!this._onStreamUpdate) return;
|
|
184
|
+
try {
|
|
185
|
+
this._onStreamUpdate(tracker);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this._logger?.error('DefaultDecoderCore._invokeOnStreamUpdate(); callback threw', { error });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private _invokeOnStreamDelete(serial: string, tracker: StreamTrackerState | undefined): void {
|
|
192
|
+
if (!this._onStreamDelete) return;
|
|
193
|
+
try {
|
|
194
|
+
this._onStreamDelete(serial, tracker);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this._logger?.error('DefaultDecoderCore._invokeOnStreamDelete(); callback threw', { error });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// -------------------------------------------------------------------------
|
|
201
|
+
// Private: streamed message create
|
|
202
|
+
// -------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
private _decodeStreamedCreate(
|
|
205
|
+
payload: MessagePayload,
|
|
206
|
+
serial: string | undefined,
|
|
207
|
+
): DecoderOutput<TEvent, TMessage>[] {
|
|
208
|
+
if (!serial) return [];
|
|
209
|
+
|
|
210
|
+
const streamId = payload.headers?.[HEADER_STREAM_ID] ?? '';
|
|
211
|
+
const h = payload.headers ?? {};
|
|
212
|
+
|
|
213
|
+
const tracker: StreamTrackerState = {
|
|
214
|
+
name: payload.name,
|
|
215
|
+
streamId,
|
|
216
|
+
accumulated: '',
|
|
217
|
+
headers: { ...h },
|
|
218
|
+
closed: false,
|
|
219
|
+
};
|
|
220
|
+
this._serialState.set(serial, tracker);
|
|
221
|
+
|
|
222
|
+
this._logger?.debug('DefaultDecoderCore._decodeStreamedCreate(); new stream', {
|
|
223
|
+
name: payload.name,
|
|
224
|
+
streamId,
|
|
225
|
+
serial,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return this._hooks.buildStartEvents(tracker);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// -------------------------------------------------------------------------
|
|
232
|
+
// Private: append handling
|
|
233
|
+
// -------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
// Spec: AIT-CD8
|
|
236
|
+
private _decodeAppend(message: Ably.InboundMessage): DecoderOutput<TEvent, TMessage>[] {
|
|
237
|
+
const serial = message.serial;
|
|
238
|
+
if (!serial) return [];
|
|
239
|
+
|
|
240
|
+
const tracker = this._serialState.get(serial);
|
|
241
|
+
if (!tracker) {
|
|
242
|
+
// Unknown serial on append — treat as first-contact update
|
|
243
|
+
return this._decodeUpdate(message);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const h = getHeaders(message);
|
|
247
|
+
const delta = typeof message.data === 'string' ? message.data : '';
|
|
248
|
+
const status = h[HEADER_STATUS];
|
|
249
|
+
const outputs: DecoderOutput<TEvent, TMessage>[] = [];
|
|
250
|
+
|
|
251
|
+
if (delta.length > 0) {
|
|
252
|
+
tracker.accumulated += delta;
|
|
253
|
+
outputs.push(...this._hooks.buildDeltaEvents(tracker, delta));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (status === 'finished' && !tracker.closed) {
|
|
257
|
+
tracker.closed = true;
|
|
258
|
+
outputs.push(...this._hooks.buildEndEvents(tracker, h));
|
|
259
|
+
this._logger?.debug('DefaultDecoderCore._decodeAppend(); stream finished', { streamId: tracker.streamId });
|
|
260
|
+
} else if (status === 'aborted' && !tracker.closed) {
|
|
261
|
+
tracker.closed = true;
|
|
262
|
+
this._logger?.debug('DefaultDecoderCore._decodeAppend(); stream aborted', { streamId: tracker.streamId });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return outputs;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// -------------------------------------------------------------------------
|
|
269
|
+
// Private: update handling (first-contact, prefix-match, replacement)
|
|
270
|
+
// -------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
// Spec: AIT-CD9
|
|
273
|
+
private _decodeUpdate(message: Ably.InboundMessage): DecoderOutput<TEvent, TMessage>[] {
|
|
274
|
+
const serial = message.serial;
|
|
275
|
+
if (!serial) return [];
|
|
276
|
+
|
|
277
|
+
const payload = this._toPayload(message);
|
|
278
|
+
const h = payload.headers ?? {};
|
|
279
|
+
const isStreamed = h[HEADER_STREAM] === 'true';
|
|
280
|
+
const status = h[HEADER_STATUS];
|
|
281
|
+
|
|
282
|
+
const tracker = this._serialState.get(serial);
|
|
283
|
+
|
|
284
|
+
if (!tracker) {
|
|
285
|
+
return this._decodeFirstContact(payload, isStreamed, status, serial);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Updates to tracked streams use string data for prefix-match accumulation
|
|
289
|
+
const data = this._stringData(message);
|
|
290
|
+
|
|
291
|
+
// --- Tracker exists: prefix-match or replacement ---
|
|
292
|
+
if (data.startsWith(tracker.accumulated)) {
|
|
293
|
+
const delta = data.slice(tracker.accumulated.length);
|
|
294
|
+
const outputs: DecoderOutput<TEvent, TMessage>[] = [];
|
|
295
|
+
|
|
296
|
+
if (delta.length > 0) {
|
|
297
|
+
tracker.accumulated = data;
|
|
298
|
+
outputs.push(...this._hooks.buildDeltaEvents(tracker, delta));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (status === 'finished' && !tracker.closed) {
|
|
302
|
+
tracker.closed = true;
|
|
303
|
+
outputs.push(...this._hooks.buildEndEvents(tracker, h));
|
|
304
|
+
} else if (status === 'aborted' && !tracker.closed) {
|
|
305
|
+
tracker.closed = true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return outputs;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- Replacement (NOT a prefix match) ---
|
|
312
|
+
tracker.accumulated = data;
|
|
313
|
+
tracker.headers = { ...h };
|
|
314
|
+
|
|
315
|
+
this._invokeOnStreamUpdate(tracker);
|
|
316
|
+
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private _decodeFirstContact(
|
|
321
|
+
payload: MessagePayload,
|
|
322
|
+
isStreamed: boolean,
|
|
323
|
+
status: string | undefined,
|
|
324
|
+
serial: string,
|
|
325
|
+
): DecoderOutput<TEvent, TMessage>[] {
|
|
326
|
+
// Non-streamed messages are discrete
|
|
327
|
+
if (!isStreamed) {
|
|
328
|
+
return this._hooks.decodeDiscrete(payload);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const streamId = payload.headers?.[HEADER_STREAM_ID] ?? '';
|
|
332
|
+
const h = payload.headers ?? {};
|
|
333
|
+
const data = typeof payload.data === 'string' ? payload.data : '';
|
|
334
|
+
|
|
335
|
+
this._logger?.debug('DefaultDecoderCore._decodeFirstContact(); first-contact stream', {
|
|
336
|
+
name: payload.name,
|
|
337
|
+
streamId,
|
|
338
|
+
serial,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Create tracker
|
|
342
|
+
const newTracker: StreamTrackerState = {
|
|
343
|
+
name: payload.name,
|
|
344
|
+
streamId,
|
|
345
|
+
accumulated: data,
|
|
346
|
+
headers: { ...h },
|
|
347
|
+
closed: status === 'finished' || status === 'aborted',
|
|
348
|
+
};
|
|
349
|
+
this._serialState.set(serial, newTracker);
|
|
350
|
+
|
|
351
|
+
// Emit start + delta (if any) + end (if finished)
|
|
352
|
+
const outputs = this._hooks.buildStartEvents(newTracker);
|
|
353
|
+
|
|
354
|
+
if (data.length > 0) {
|
|
355
|
+
outputs.push(...this._hooks.buildDeltaEvents(newTracker, data));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (status === 'finished') {
|
|
359
|
+
outputs.push(...this._hooks.buildEndEvents(newTracker, h));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return outputs;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// -------------------------------------------------------------------------
|
|
366
|
+
// Private: delete handling
|
|
367
|
+
// -------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
// Spec: AIT-CD10
|
|
370
|
+
private _decodeDelete(message: Ably.InboundMessage): DecoderOutput<TEvent, TMessage>[] {
|
|
371
|
+
const serial = message.serial;
|
|
372
|
+
if (!serial) return [];
|
|
373
|
+
|
|
374
|
+
const tracker = this._serialState.get(serial);
|
|
375
|
+
|
|
376
|
+
this._invokeOnStreamDelete(serial, tracker);
|
|
377
|
+
|
|
378
|
+
if (tracker) {
|
|
379
|
+
tracker.accumulated = '';
|
|
380
|
+
tracker.closed = true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this._logger?.debug('DefaultDecoderCore._decodeDelete();', { serial });
|
|
384
|
+
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Factory
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Create a decoder core with the given domain hooks.
|
|
395
|
+
* @param hooks - Domain-specific hooks for stream classification, event building, and discrete decoding.
|
|
396
|
+
* @param options - Decoder configuration (callbacks, logger).
|
|
397
|
+
* @returns A new {@link DecoderCore} instance.
|
|
398
|
+
*/
|
|
399
|
+
export const createDecoderCore = <TEvent, TMessage>(
|
|
400
|
+
hooks: DecoderCoreHooks<TEvent, TMessage>,
|
|
401
|
+
options: DecoderCoreOptions = {},
|
|
402
|
+
): DecoderCore<TEvent, TMessage> => new DefaultDecoderCore(hooks, options);
|