@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Encoder core — message append lifecycle machinery.
|
|
3
3
|
*
|
|
4
|
-
* Provides Ably primitives (publish, append, close,
|
|
4
|
+
* Provides Ably primitives (publish, append, close, cancel, flush) that
|
|
5
5
|
* domain-specific encoders wire their event types to.
|
|
6
6
|
*
|
|
7
7
|
* Domain encoders call `createEncoderCore(writer, options)` and use the
|
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
import * as Ably from 'ably';
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
HEADER_CODEC_MESSAGE_ID,
|
|
16
|
+
HEADER_DISCRETE,
|
|
17
|
+
HEADER_STATUS,
|
|
18
|
+
HEADER_STREAM,
|
|
19
|
+
HEADER_STREAM_ID,
|
|
20
|
+
} from '../../constants.js';
|
|
15
21
|
import { ErrorCode } from '../../errors.js';
|
|
16
22
|
import type { Logger } from '../../logger.js';
|
|
17
23
|
import { mergeHeaders } from '../../utils.js';
|
|
@@ -36,8 +42,22 @@ interface StreamState {
|
|
|
36
42
|
name: string;
|
|
37
43
|
streamId: string;
|
|
38
44
|
accumulated: string;
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
/** Transport-tier headers repeated on every append (`extras.ai.transport`). */
|
|
46
|
+
persistentTransport: Record<string, string>;
|
|
47
|
+
/** Codec-tier headers repeated on every append (`extras.ai.codec`). */
|
|
48
|
+
persistentCodec: Record<string, string>;
|
|
49
|
+
cancelled: boolean;
|
|
50
|
+
/** Set by `closeStream` — a completed stream must never receive a cancelled terminal. */
|
|
51
|
+
completed: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The SDK's `extras.ai` namespace as written to the wire: a `transport` tier
|
|
56
|
+
* (always present on SDK-published messages) and an optional `codec` tier.
|
|
57
|
+
*/
|
|
58
|
+
interface AiExtras {
|
|
59
|
+
transport: Record<string, string>;
|
|
60
|
+
codec?: Record<string, string>;
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
interface PendingAppend {
|
|
@@ -57,32 +77,29 @@ export interface EncoderCore {
|
|
|
57
77
|
/** Publish multiple discrete messages atomically in a single channel publish. */
|
|
58
78
|
publishDiscreteBatch(payloads: MessagePayload[], opts?: WriteOptions): Promise<Ably.PublishResult>;
|
|
59
79
|
|
|
60
|
-
/** Start a streamed message with
|
|
80
|
+
/** Start a streamed message with status:streaming. */
|
|
61
81
|
startStream(streamId: string, payload: StreamPayload, opts?: WriteOptions): Promise<void>;
|
|
62
82
|
|
|
63
83
|
/**
|
|
64
84
|
* Append data to an in-flight streamed message. Fire-and-forget: errors are
|
|
65
|
-
* collected internally and surfaced by {@link closeStream}
|
|
85
|
+
* collected internally and surfaced by {@link closeStream},
|
|
86
|
+
* {@link cancelAllStreams} or {@link close}.
|
|
87
|
+
* @throws {Ably.ErrorInfo} InvalidArgument if there is no active stream for `streamId` or the core is closed.
|
|
66
88
|
*/
|
|
67
89
|
appendStream(streamId: string, data: string): void;
|
|
68
90
|
|
|
69
91
|
/**
|
|
70
|
-
* Close a streamed message with
|
|
92
|
+
* Close a streamed message with status:complete. Flushes all pending
|
|
71
93
|
* appends for recovery before returning. Repeats persistent and payload headers.
|
|
94
|
+
* @throws {Ably.ErrorInfo} InvalidArgument if there is no active stream for `streamId`, or the encoder has been closed; EncoderRecoveryFailed if a failed append cannot be recovered during the flush.
|
|
72
95
|
*/
|
|
73
96
|
closeStream(streamId: string, payload: StreamPayload): Promise<void>;
|
|
74
97
|
|
|
75
98
|
/**
|
|
76
|
-
*
|
|
77
|
-
* pending appends for recovery before returning.
|
|
78
|
-
*/
|
|
79
|
-
abortStream(streamId: string, opts?: WriteOptions): Promise<void>;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Abort all in-progress streams (x-ably-status:aborted) and flush all
|
|
99
|
+
* Cancel all in-progress streams (status:cancelled) and flush all
|
|
83
100
|
* pending appends for recovery before returning.
|
|
84
101
|
*/
|
|
85
|
-
|
|
102
|
+
cancelAllStreams(opts?: WriteOptions): Promise<void>;
|
|
86
103
|
|
|
87
104
|
/** Flush + clear trackers. Idempotent. */
|
|
88
105
|
close(): Promise<void>;
|
|
@@ -95,7 +112,6 @@ export interface EncoderCore {
|
|
|
95
112
|
// Spec: AIT-CD1
|
|
96
113
|
class DefaultEncoderCore implements EncoderCore {
|
|
97
114
|
private readonly _writer: ChannelWriter;
|
|
98
|
-
private readonly _defaultClientId: string | undefined;
|
|
99
115
|
private readonly _defaultExtras: Extras | undefined;
|
|
100
116
|
private readonly _onMessageHook: (message: Ably.Message) => void;
|
|
101
117
|
private readonly _logger: Logger | undefined;
|
|
@@ -106,7 +122,6 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
106
122
|
|
|
107
123
|
constructor(writer: ChannelWriter, options: EncoderCoreOptions = {}) {
|
|
108
124
|
this._writer = writer;
|
|
109
|
-
this._defaultClientId = options.clientId;
|
|
110
125
|
this._defaultExtras = options.extras;
|
|
111
126
|
this._onMessageHook =
|
|
112
127
|
options.onMessage ??
|
|
@@ -128,14 +143,7 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
128
143
|
async publishDiscreteBatch(payloads: MessagePayload[], opts?: WriteOptions): Promise<Ably.PublishResult> {
|
|
129
144
|
this._assertNotClosed();
|
|
130
145
|
this._logger?.trace('DefaultEncoderCore.publishDiscreteBatch();', { count: payloads.length });
|
|
131
|
-
const msgs = payloads.map((p) => this._buildDiscreteMessage(p, opts));
|
|
132
|
-
// Mark batch-published payloads as discrete message parts (from writeMessages).
|
|
133
|
-
// The decoder relies on this header to distinguish message parts from lifecycle
|
|
134
|
-
// events that also happen to be discrete (x-ably-stream: false).
|
|
135
|
-
for (const msg of msgs) {
|
|
136
|
-
// CAST: extras is built by _buildDiscreteMessage with a known { headers } shape.
|
|
137
|
-
(msg.extras as { headers: Record<string, string> }).headers[HEADER_DISCRETE] = 'true';
|
|
138
|
-
}
|
|
146
|
+
const msgs = payloads.map((p) => this._buildDiscreteMessage(p, opts, true));
|
|
139
147
|
return this._writer.publish(msgs);
|
|
140
148
|
}
|
|
141
149
|
|
|
@@ -144,17 +152,16 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
144
152
|
this._assertNotClosed();
|
|
145
153
|
this._logger?.trace('DefaultEncoderCore.startStream();', { name: payload.name, streamId });
|
|
146
154
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
const transport = this._buildTransport(payload.transportHeaders, opts);
|
|
156
|
+
transport[HEADER_STREAM] = 'true';
|
|
157
|
+
transport[HEADER_STATUS] = 'streaming';
|
|
158
|
+
transport[HEADER_STREAM_ID] = streamId;
|
|
159
|
+
const codec = payload.codecHeaders ?? {};
|
|
151
160
|
|
|
152
|
-
const clientId = this._resolveClientId(opts);
|
|
153
161
|
const msg: Ably.Message = {
|
|
154
162
|
name: payload.name,
|
|
155
163
|
data: payload.data,
|
|
156
|
-
extras: {
|
|
157
|
-
...(clientId ? { clientId } : {}),
|
|
164
|
+
extras: { ai: this._aiExtras(transport, codec) },
|
|
158
165
|
};
|
|
159
166
|
|
|
160
167
|
this._invokeOnMessage(msg);
|
|
@@ -175,8 +182,10 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
175
182
|
name: payload.name,
|
|
176
183
|
streamId,
|
|
177
184
|
accumulated: payload.data,
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
persistentTransport: transport,
|
|
186
|
+
persistentCodec: codec,
|
|
187
|
+
cancelled: false,
|
|
188
|
+
completed: false,
|
|
180
189
|
});
|
|
181
190
|
|
|
182
191
|
this._logger?.debug('DefaultEncoderCore.startStream(); stream started', {
|
|
@@ -204,7 +213,7 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
204
213
|
const appendMsg: Ably.Message = {
|
|
205
214
|
serial: tracker.serial,
|
|
206
215
|
data,
|
|
207
|
-
extras: {
|
|
216
|
+
extras: { ai: this._aiExtras({ ...tracker.persistentTransport }, { ...tracker.persistentCodec }) },
|
|
208
217
|
};
|
|
209
218
|
|
|
210
219
|
this._invokeOnMessage(appendMsg);
|
|
@@ -228,14 +237,17 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
228
237
|
|
|
229
238
|
// Accumulate closing data so recovery has the full content
|
|
230
239
|
tracker.accumulated += payload.data;
|
|
240
|
+
// Mark completed so a later cancelAllStreams (e.g. pipeStream terminating
|
|
241
|
+
// streams left open by an agent self-abort) skips this stream.
|
|
242
|
+
tracker.completed = true;
|
|
231
243
|
|
|
232
|
-
const
|
|
233
|
-
|
|
244
|
+
const { transport, codec } = this._buildClosing(tracker, payload);
|
|
245
|
+
transport[HEADER_STATUS] = 'complete';
|
|
234
246
|
|
|
235
247
|
const msg: Ably.Message = {
|
|
236
248
|
serial: tracker.serial,
|
|
237
249
|
data: payload.data,
|
|
238
|
-
extras: {
|
|
250
|
+
extras: { ai: this._aiExtras(transport, codec) },
|
|
239
251
|
};
|
|
240
252
|
|
|
241
253
|
this._invokeOnMessage(msg);
|
|
@@ -247,55 +259,25 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
247
259
|
this._logger?.debug('DefaultEncoderCore.closeStream(); stream closed', { streamId });
|
|
248
260
|
}
|
|
249
261
|
|
|
250
|
-
// Spec: AIT-CD5, AIT-
|
|
251
|
-
async
|
|
262
|
+
// Spec: AIT-CD5, AIT-CD5a
|
|
263
|
+
async cancelAllStreams(opts?: WriteOptions): Promise<void> {
|
|
252
264
|
this._assertNotClosed();
|
|
253
|
-
this._logger?.trace('DefaultEncoderCore.
|
|
254
|
-
|
|
255
|
-
const tracker = this._trackers.get(streamId);
|
|
256
|
-
if (!tracker) {
|
|
257
|
-
throw new Ably.ErrorInfo(
|
|
258
|
-
`unable to abort stream; no active stream for streamId '${streamId}'`,
|
|
259
|
-
ErrorCode.InvalidArgument,
|
|
260
|
-
400,
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
tracker.aborted = true;
|
|
265
|
-
|
|
266
|
-
const allHeaders = this._buildClosingHeaders(tracker, {}, opts);
|
|
267
|
-
allHeaders[HEADER_STATUS] = 'aborted';
|
|
268
|
-
|
|
269
|
-
const msg: Ably.Message = {
|
|
270
|
-
serial: tracker.serial,
|
|
271
|
-
data: '',
|
|
272
|
-
extras: { headers: allHeaders },
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
this._invokeOnMessage(msg);
|
|
276
|
-
const p = this._writer.appendMessage(msg);
|
|
277
|
-
this._pending.push({ promise: p, streamId });
|
|
278
|
-
|
|
279
|
-
await this._flushPending();
|
|
280
|
-
|
|
281
|
-
this._logger?.debug('DefaultEncoderCore.abortStream(); stream aborted', { streamId });
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Spec: AIT-CD5a
|
|
285
|
-
async abortAllStreams(opts?: WriteOptions): Promise<void> {
|
|
286
|
-
this._assertNotClosed();
|
|
287
|
-
this._logger?.trace('DefaultEncoderCore.abortAllStreams();', { streamCount: this._trackers.size });
|
|
265
|
+
this._logger?.trace('DefaultEncoderCore.cancelAllStreams();', { streamCount: this._trackers.size });
|
|
288
266
|
|
|
289
267
|
for (const tracker of this._trackers.values()) {
|
|
290
|
-
|
|
268
|
+
// Idempotent and complete-safe: a stream already cancelled must not be
|
|
269
|
+
// re-appended on a repeat call, and a stream that closed with
|
|
270
|
+
// status:complete must never receive a cancelled terminal.
|
|
271
|
+
if (tracker.cancelled || tracker.completed) continue;
|
|
272
|
+
tracker.cancelled = true;
|
|
291
273
|
|
|
292
|
-
const
|
|
293
|
-
|
|
274
|
+
const { transport, codec } = this._buildClosing(tracker, undefined, opts);
|
|
275
|
+
transport[HEADER_STATUS] = 'cancelled';
|
|
294
276
|
|
|
295
277
|
const msg: Ably.Message = {
|
|
296
278
|
serial: tracker.serial,
|
|
297
279
|
data: '',
|
|
298
|
-
extras: {
|
|
280
|
+
extras: { ai: this._aiExtras(transport, codec) },
|
|
299
281
|
};
|
|
300
282
|
|
|
301
283
|
this._invokeOnMessage(msg);
|
|
@@ -354,11 +336,16 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
354
336
|
const tracker = this._trackers.get(streamId);
|
|
355
337
|
if (!tracker) continue;
|
|
356
338
|
|
|
357
|
-
const recoveryStatus = tracker.
|
|
339
|
+
const recoveryStatus = tracker.cancelled ? 'cancelled' : 'complete';
|
|
358
340
|
const msg: Ably.Message = {
|
|
359
341
|
serial: tracker.serial,
|
|
360
342
|
data: tracker.accumulated,
|
|
361
|
-
extras: {
|
|
343
|
+
extras: {
|
|
344
|
+
ai: this._aiExtras(
|
|
345
|
+
{ ...tracker.persistentTransport, [HEADER_STATUS]: recoveryStatus },
|
|
346
|
+
{ ...tracker.persistentCodec },
|
|
347
|
+
),
|
|
348
|
+
},
|
|
362
349
|
};
|
|
363
350
|
|
|
364
351
|
try {
|
|
@@ -411,32 +398,53 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
411
398
|
}
|
|
412
399
|
}
|
|
413
400
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Build the transport-tier header record for a message: caller-configured
|
|
403
|
+
* transport headers (default extras + per-write overrides) layered with any
|
|
404
|
+
* transport headers the codec payload stamps directly, plus the message-id.
|
|
405
|
+
* @param payloadTransport - Transport headers carried on the codec payload.
|
|
406
|
+
* @param opts - Optional per-write overrides.
|
|
407
|
+
* @returns The transport-tier headers record (`extras.ai.transport`).
|
|
408
|
+
*/
|
|
409
|
+
private _buildTransport(
|
|
410
|
+
payloadTransport: Record<string, string> | undefined,
|
|
411
|
+
opts?: WriteOptions,
|
|
412
|
+
): Record<string, string> {
|
|
419
413
|
const callerHeaders = mergeHeaders(this._defaultExtras?.headers, opts?.extras?.headers);
|
|
420
|
-
const
|
|
414
|
+
const transport = { ...callerHeaders, ...payloadTransport };
|
|
421
415
|
if (opts?.messageId !== undefined) {
|
|
422
|
-
|
|
416
|
+
transport[HEADER_CODEC_MESSAGE_ID] = opts.messageId;
|
|
423
417
|
}
|
|
424
|
-
return
|
|
418
|
+
return transport;
|
|
425
419
|
}
|
|
426
420
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
421
|
+
/**
|
|
422
|
+
* Assemble the `extras.ai` namespace from its two tiers, omitting the codec
|
|
423
|
+
* tier when empty.
|
|
424
|
+
* @param transport - Transport-tier headers (always present on SDK messages).
|
|
425
|
+
* @param codec - Codec-tier headers; omitted from the wire when empty.
|
|
426
|
+
* @returns The `extras.ai` object.
|
|
427
|
+
*/
|
|
428
|
+
private _aiExtras(transport: Record<string, string>, codec: Record<string, string>): AiExtras {
|
|
429
|
+
return Object.keys(codec).length > 0 ? { transport, codec } : { transport };
|
|
430
|
+
}
|
|
431
431
|
|
|
432
|
+
private _buildDiscreteMessage(payload: MessagePayload, opts?: WriteOptions, discrete = false): Ably.Message {
|
|
433
|
+
const transport = this._buildTransport(payload.transportHeaders, opts);
|
|
434
|
+
transport[HEADER_STREAM] = 'false';
|
|
435
|
+
if (discrete) {
|
|
436
|
+
// Mark batch-published payloads as discrete message parts (from writeMessages).
|
|
437
|
+
// The decoder relies on this header to distinguish message parts from lifecycle
|
|
438
|
+
// events that also happen to be discrete (stream: false).
|
|
439
|
+
transport[HEADER_DISCRETE] = 'true';
|
|
440
|
+
}
|
|
432
441
|
const msg: Ably.Message = {
|
|
433
442
|
name: payload.name,
|
|
434
443
|
data: payload.data,
|
|
435
444
|
extras: {
|
|
436
|
-
|
|
445
|
+
ai: this._aiExtras(transport, payload.codecHeaders ?? {}),
|
|
437
446
|
...(payload.ephemeral ? { ephemeral: true } : {}),
|
|
438
447
|
},
|
|
439
|
-
...(clientId ? { clientId } : {}),
|
|
440
448
|
};
|
|
441
449
|
|
|
442
450
|
this._invokeOnMessage(msg);
|
|
@@ -444,24 +452,23 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
444
452
|
}
|
|
445
453
|
|
|
446
454
|
/**
|
|
447
|
-
* Build
|
|
448
|
-
* persistent headers (Ably replaces the entire extras object on append).
|
|
455
|
+
* Build both header tiers for a closing append. Closing appends must repeat
|
|
456
|
+
* ALL persistent headers (Ably replaces the entire extras object on append).
|
|
449
457
|
* Then layer caller and codec overrides.
|
|
450
458
|
* @param tracker - The stream tracker with persistent headers.
|
|
451
|
-
* @param
|
|
459
|
+
* @param payload - The closing stream payload (codec + transport headers).
|
|
452
460
|
* @param opts - Optional per-write overrides.
|
|
453
|
-
* @returns
|
|
461
|
+
* @returns The two tiers for the closing append.
|
|
454
462
|
*/
|
|
455
|
-
private
|
|
463
|
+
private _buildClosing(
|
|
456
464
|
tracker: StreamState,
|
|
457
|
-
|
|
465
|
+
payload: StreamPayload | undefined,
|
|
458
466
|
opts?: WriteOptions,
|
|
459
|
-
): Record<string, string> {
|
|
460
|
-
const h = { ...tracker.persistentHeaders };
|
|
467
|
+
): { transport: Record<string, string>; codec: Record<string, string> } {
|
|
461
468
|
const callerHeaders = mergeHeaders(this._defaultExtras?.headers, opts?.extras?.headers);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return
|
|
469
|
+
const transport = { ...tracker.persistentTransport, ...callerHeaders, ...payload?.transportHeaders };
|
|
470
|
+
const codec = { ...tracker.persistentCodec, ...payload?.codecHeaders };
|
|
471
|
+
return { transport, codec };
|
|
465
472
|
}
|
|
466
473
|
}
|
|
467
474
|
|
|
@@ -472,7 +479,7 @@ class DefaultEncoderCore implements EncoderCore {
|
|
|
472
479
|
/**
|
|
473
480
|
* Create an encoder core bound to the given channel writer.
|
|
474
481
|
* @param writer - The channel writer to publish messages through.
|
|
475
|
-
* @param options - Encoder configuration (
|
|
482
|
+
* @param options - Encoder configuration (extras, hooks, logger).
|
|
476
483
|
* @returns A new {@link EncoderCore} instance.
|
|
477
484
|
*/
|
|
478
485
|
export const createEncoderCore = (writer: ChannelWriter, options: EncoderCoreOptions = {}): EncoderCore =>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared header-field bag helpers.
|
|
3
|
+
*
|
|
4
|
+
* The wire dispatch discriminator (`kind`) plus the symmetric field↔headers
|
|
5
|
+
* write and read used by the descriptor drivers. Centralised so encode and
|
|
6
|
+
* decode operate on the same record shape and the dispatch key has one home —
|
|
7
|
+
* and so the input drivers can reuse the same primitives as the output drivers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { HeaderField } from './fields.js';
|
|
11
|
+
import type { OutputDescriptor, OutputEventDescriptor } from './output-descriptors.js';
|
|
12
|
+
|
|
13
|
+
/** The codec header carrying the SDK-controlled dispatch kind / stream family id. */
|
|
14
|
+
export const KIND_HEADER = 'kind';
|
|
15
|
+
|
|
16
|
+
/** The sentinel suffix marking a descriptor literal as a wildcard family. */
|
|
17
|
+
const WILDCARD_SUFFIX = '-*';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Derive a wildcard dispatch predicate from a descriptor literal: a literal
|
|
21
|
+
* ending in `-*` matches any value sharing its prefix, so the literal and its
|
|
22
|
+
* predicate can never disagree. Returns `undefined` for an exact literal.
|
|
23
|
+
* Shared by the output event builder and the input part builder so the `-*`
|
|
24
|
+
* sentinel rule lives in one place, next to the {@link partFor} that consumes it.
|
|
25
|
+
* @param literal - The declared descriptor literal (`type` / `partType`).
|
|
26
|
+
* @returns A prefix-match predicate for a wildcard literal, else `undefined`.
|
|
27
|
+
*/
|
|
28
|
+
export const wildcardMatcher = (literal: string): ((value: string) => boolean) | undefined =>
|
|
29
|
+
literal.endsWith(WILDCARD_SUFFIX) ? (value: string): boolean => value.startsWith(literal.slice(0, -1)) : undefined;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The codec header carrying a batch part's sub-discriminator. A batch stamps it
|
|
33
|
+
* on every exploded part on encode; the decoder reads it back to resolve the
|
|
34
|
+
* matching part descriptor. Centralised so the key has one home across the
|
|
35
|
+
* input encode and decode drivers and cannot drift between them.
|
|
36
|
+
*/
|
|
37
|
+
export const PART_TYPE_HEADER = 'partType';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read the value at a declared field key off a source object.
|
|
41
|
+
* @param source - The object to index (a chunk, or a lensed sub-object such as a payload).
|
|
42
|
+
* @param key - The declared field key.
|
|
43
|
+
* @returns The value at `key`, typed `unknown`.
|
|
44
|
+
*/
|
|
45
|
+
// CAST: a descriptor indexes a source object's props by a declared key. The
|
|
46
|
+
// source's indexed type isn't statically known here, but a descriptor only ever
|
|
47
|
+
// runs against the member it matches, so the value has the field's type at runtime.
|
|
48
|
+
export const prop = (source: object, key: string): unknown => (source as Record<string, unknown>)[key];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a codec-headers record from a source object through declared fields,
|
|
52
|
+
* seeded with the dispatch `kind`. Each field writes the value at its key on
|
|
53
|
+
* `source`; an optional `keys` subset restricts which fields are written.
|
|
54
|
+
* @param fields - The declared header fields.
|
|
55
|
+
* @param kindValue - The dispatch kind / stream family id to seed under {@link KIND_HEADER}.
|
|
56
|
+
* @param source - The object to read field values from (a chunk, or a lensed payload).
|
|
57
|
+
* @param keys - Optional subset of field keys to write; omit to write all.
|
|
58
|
+
* @returns The codec-headers record.
|
|
59
|
+
*/
|
|
60
|
+
export const writeFields = (
|
|
61
|
+
fields: readonly HeaderField<unknown>[],
|
|
62
|
+
kindValue: string,
|
|
63
|
+
source: object,
|
|
64
|
+
keys?: readonly string[],
|
|
65
|
+
): Record<string, string> => {
|
|
66
|
+
const rec: Record<string, string> = { [KIND_HEADER]: kindValue };
|
|
67
|
+
for (const field of fields) {
|
|
68
|
+
if (keys && !keys.includes(field.key)) continue;
|
|
69
|
+
field.write(rec, prop(source, field.key));
|
|
70
|
+
}
|
|
71
|
+
return rec;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read declared fields out of a codec-headers record into a bag keyed by field key.
|
|
76
|
+
* A field that reads `undefined` (absent, with no default) contributes no key — the
|
|
77
|
+
* bag carries only the values that are actually present.
|
|
78
|
+
* @param fields - The declared header fields.
|
|
79
|
+
* @param headers - The inbound codec-tier headers.
|
|
80
|
+
* @returns A bag of the present field values, keyed by each field's key.
|
|
81
|
+
*/
|
|
82
|
+
/** The structural slice of a part descriptor {@link partFor} dispatches on. */
|
|
83
|
+
interface PartDispatch {
|
|
84
|
+
/** The exact `partType` literal, or the `-*` wildcard literal for a family. */
|
|
85
|
+
partType: string;
|
|
86
|
+
/** Wildcard dispatch predicate; absent for an exact part. */
|
|
87
|
+
match?: (partType: string) => boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the part descriptor for a `partType`: an exact non-wildcard match,
|
|
92
|
+
* else a wildcard whose derived predicate accepts it. Wildcards are excluded
|
|
93
|
+
* from the exact pass — only their predicate may route to them. Shared by the
|
|
94
|
+
* input encode and decode drivers.
|
|
95
|
+
* @param parts - The batch's part descriptor sub-table.
|
|
96
|
+
* @param partType - The `partType` to resolve (from `partTypeOf` on encode, the wire header on decode).
|
|
97
|
+
* @returns The matching part descriptor, or undefined when none matches.
|
|
98
|
+
*/
|
|
99
|
+
export const partFor = <P extends PartDispatch>(parts: readonly P[], partType: string): P | undefined =>
|
|
100
|
+
parts.find((part) => !part.match && part.partType === partType) ?? parts.find((part) => part.match?.(partType));
|
|
101
|
+
|
|
102
|
+
/** An output descriptor set's event descriptors, split for dispatch. */
|
|
103
|
+
export interface OutputEventDispatch<U> {
|
|
104
|
+
/** Exact (non-wildcard) event descriptors, keyed by `type`. */
|
|
105
|
+
discreteByType: Map<string, OutputEventDescriptor<U>>;
|
|
106
|
+
/** Wildcard event descriptors, dispatched by their `match` predicate. */
|
|
107
|
+
wildcards: OutputEventDescriptor<U>[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Partition an output descriptor set's `event` descriptors into an exact-type
|
|
112
|
+
* map and a wildcard list. Stream descriptors are skipped — each driver indexes
|
|
113
|
+
* those by its own key (phase on encode, kind on decode). Shared by the output
|
|
114
|
+
* encode and decode drivers so the exact-vs-wildcard split has one home.
|
|
115
|
+
* @template U - The codec's event union.
|
|
116
|
+
* @param descriptors - The full descriptor set (events + streamed families).
|
|
117
|
+
* @returns The event descriptors split into {@link OutputEventDispatch}.
|
|
118
|
+
*/
|
|
119
|
+
export const partitionOutputEvents = <U extends { type: string }>(
|
|
120
|
+
descriptors: readonly OutputDescriptor<U>[],
|
|
121
|
+
): OutputEventDispatch<U> => {
|
|
122
|
+
const discreteByType = new Map<string, OutputEventDescriptor<U>>();
|
|
123
|
+
const wildcards: OutputEventDescriptor<U>[] = [];
|
|
124
|
+
for (const descriptor of descriptors) {
|
|
125
|
+
if (descriptor.construct !== 'event') continue;
|
|
126
|
+
if (descriptor.match) wildcards.push(descriptor);
|
|
127
|
+
else discreteByType.set(descriptor.type, descriptor);
|
|
128
|
+
}
|
|
129
|
+
return { discreteByType, wildcards };
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const readFields = (
|
|
133
|
+
fields: readonly HeaderField<unknown>[],
|
|
134
|
+
headers: Record<string, string>,
|
|
135
|
+
): Record<string, unknown> => {
|
|
136
|
+
const bag: Record<string, unknown> = {};
|
|
137
|
+
for (const field of fields) {
|
|
138
|
+
const value = field.read(headers);
|
|
139
|
+
if (value !== undefined) bag[field.key] = value;
|
|
140
|
+
}
|
|
141
|
+
return bag;
|
|
142
|
+
};
|