@ably/ai-transport 0.0.1 → 0.2.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 +114 -116
- package/dist/ably-ai-transport.js +1743 -961
- 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 +117 -39
- package/dist/core/agent.d.ts +29 -0
- package/dist/core/codec/decoder.d.ts +20 -23
- package/dist/core/codec/encoder.d.ts +11 -8
- package/dist/core/codec/index.d.ts +1 -2
- package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
- package/dist/core/codec/types.d.ts +410 -101
- package/dist/core/transport/agent-session.d.ts +10 -0
- package/dist/core/transport/branch-chain.d.ts +43 -0
- package/dist/core/transport/client-session.d.ts +13 -0
- package/dist/core/transport/decode-fold.d.ts +47 -0
- package/dist/core/transport/headers.d.ts +97 -17
- package/dist/core/transport/index.d.ts +5 -3
- 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-conversation.d.ts +128 -0
- package/dist/core/transport/load-history.d.ts +39 -0
- package/dist/core/transport/pipe-stream.d.ts +9 -8
- package/dist/core/transport/run-manager.d.ts +78 -0
- package/dist/core/transport/tree.d.ts +435 -0
- package/dist/core/transport/types/agent.d.ts +353 -0
- package/dist/core/transport/types/client.d.ts +168 -0
- package/dist/core/transport/types/shared.d.ts +24 -0
- package/dist/core/transport/types/tree.d.ts +315 -0
- package/dist/core/transport/types/view.d.ts +222 -0
- package/dist/core/transport/types.d.ts +13 -402
- package/dist/core/transport/view.d.ts +354 -0
- package/dist/errors.d.ts +37 -9
- package/dist/index.d.ts +6 -6
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +1164 -645
- 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 +36 -0
- package/dist/react/contexts/client-session-provider.d.ts +53 -0
- package/dist/react/create-session-hooks.d.ts +116 -0
- package/dist/react/index.d.ts +16 -10
- package/dist/react/internal/use-resolved-session.d.ts +36 -0
- package/dist/react/use-ably-messages.d.ts +20 -11
- package/dist/react/use-client-session.d.ts +81 -0
- package/dist/react/use-create-view.d.ts +23 -0
- package/dist/react/use-tree.d.ts +35 -0
- package/dist/react/use-view.d.ts +110 -0
- package/dist/utils.d.ts +32 -23
- package/dist/vercel/ably-ai-transport-vercel.js +2748 -1625
- 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/decoder.d.ts +5 -18
- package/dist/vercel/codec/encoder.d.ts +6 -36
- package/dist/vercel/codec/events.d.ts +51 -0
- package/dist/vercel/codec/index.d.ts +24 -12
- package/dist/vercel/codec/reducer.d.ts +144 -0
- package/dist/vercel/codec/tool-transitions.d.ts +50 -0
- package/dist/vercel/index.d.ts +4 -2
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +10298 -1410
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +70 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +33 -0
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +96 -0
- package/dist/vercel/react/index.d.ts +4 -0
- package/dist/vercel/react/use-chat-transport.d.ts +66 -21
- package/dist/vercel/react/use-message-sync.d.ts +31 -12
- package/dist/vercel/run-end-reason.d.ts +29 -0
- package/dist/vercel/transport/chat-transport.d.ts +71 -30
- package/dist/vercel/transport/index.d.ts +25 -18
- package/dist/vercel/transport/run-output-stream.d.ts +56 -0
- package/dist/version.d.ts +2 -0
- package/package.json +47 -34
- package/src/constants.ts +126 -47
- package/src/core/agent.ts +68 -0
- package/src/core/codec/decoder.ts +71 -98
- package/src/core/codec/encoder.ts +115 -58
- package/src/core/codec/index.ts +13 -6
- package/src/core/codec/lifecycle-tracker.ts +10 -9
- package/src/core/codec/types.ts +438 -106
- package/src/core/transport/agent-session.ts +1344 -0
- package/src/core/transport/branch-chain.ts +58 -0
- package/src/core/transport/client-session.ts +775 -0
- package/src/core/transport/decode-fold.ts +91 -0
- package/src/core/transport/headers.ts +182 -19
- package/src/core/transport/index.ts +29 -22
- package/src/core/transport/internal/bounded-map.ts +27 -0
- package/src/core/transport/invocation.ts +98 -0
- package/src/core/transport/load-conversation.ts +355 -0
- package/src/core/transport/load-history.ts +269 -0
- package/src/core/transport/pipe-stream.ts +58 -40
- package/src/core/transport/run-manager.ts +249 -0
- package/src/core/transport/tree.ts +1167 -0
- package/src/core/transport/types/agent.ts +407 -0
- package/src/core/transport/types/client.ts +211 -0
- package/src/core/transport/types/shared.ts +27 -0
- package/src/core/transport/types/tree.ts +344 -0
- package/src/core/transport/types/view.ts +259 -0
- package/src/core/transport/types.ts +13 -527
- package/src/core/transport/view.ts +1271 -0
- package/src/errors.ts +42 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +55 -39
- package/src/logger.ts +14 -1
- package/src/react/contexts/client-session-context.ts +41 -0
- package/src/react/contexts/client-session-provider.tsx +186 -0
- package/src/react/create-session-hooks.ts +141 -0
- package/src/react/index.ts +27 -10
- package/src/react/internal/use-resolved-session.ts +63 -0
- package/src/react/use-ably-messages.ts +47 -19
- package/src/react/use-client-session.ts +201 -0
- package/src/react/use-create-view.ts +72 -0
- package/src/react/use-tree.ts +84 -0
- package/src/react/use-view.ts +275 -0
- package/src/react/vite.config.ts +4 -1
- package/src/utils.ts +63 -45
- package/src/vercel/codec/decoder.ts +336 -255
- package/src/vercel/codec/encoder.ts +348 -196
- package/src/vercel/codec/events.ts +87 -0
- package/src/vercel/codec/index.ts +59 -14
- package/src/vercel/codec/reducer.ts +977 -0
- package/src/vercel/codec/tool-transitions.ts +122 -0
- package/src/vercel/index.ts +7 -3
- package/src/vercel/react/contexts/chat-transport-context.ts +41 -0
- package/src/vercel/react/contexts/chat-transport-provider.tsx +150 -0
- package/src/vercel/react/index.ts +13 -1
- package/src/vercel/react/use-chat-transport.ts +162 -42
- package/src/vercel/react/use-message-sync.ts +121 -22
- package/src/vercel/react/vite.config.ts +4 -2
- package/src/vercel/run-end-reason.ts +78 -0
- package/src/vercel/transport/chat-transport.ts +553 -113
- package/src/vercel/transport/index.ts +40 -28
- package/src/vercel/transport/run-output-stream.ts +170 -0
- package/src/version.ts +2 -0
- package/dist/core/transport/client-transport.d.ts +0 -10
- package/dist/core/transport/conversation-tree.d.ts +0 -9
- package/dist/core/transport/decode-history.d.ts +0 -41
- package/dist/core/transport/server-transport.d.ts +0 -7
- package/dist/core/transport/stream-router.d.ts +0 -19
- package/dist/core/transport/turn-manager.d.ts +0 -34
- package/dist/react/use-active-turns.d.ts +0 -8
- package/dist/react/use-client-transport.d.ts +0 -7
- package/dist/react/use-conversation-tree.d.ts +0 -20
- package/dist/react/use-edit.d.ts +0 -7
- package/dist/react/use-history.d.ts +0 -19
- package/dist/react/use-messages.d.ts +0 -7
- package/dist/react/use-regenerate.d.ts +0 -7
- package/dist/react/use-send.d.ts +0 -7
- package/dist/vercel/codec/accumulator.d.ts +0 -21
- package/src/core/transport/client-transport.ts +0 -959
- package/src/core/transport/conversation-tree.ts +0 -434
- package/src/core/transport/decode-history.ts +0 -337
- package/src/core/transport/server-transport.ts +0 -458
- package/src/core/transport/stream-router.ts +0 -118
- package/src/core/transport/turn-manager.ts +0 -147
- package/src/react/use-active-turns.ts +0 -61
- package/src/react/use-client-transport.ts +0 -37
- package/src/react/use-conversation-tree.ts +0 -71
- package/src/react/use-edit.ts +0 -24
- package/src/react/use-history.ts +0 -111
- package/src/react/use-messages.ts +0 -32
- package/src/react/use-regenerate.ts +0 -24
- package/src/react/use-send.ts +0 -25
- package/src/vercel/codec/accumulator.ts +0 -603
|
@@ -1,458 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core server-side transport, parameterized by codec.
|
|
3
|
-
*
|
|
4
|
-
* Composes TurnManager and pipeStream to handle the full server-side turn
|
|
5
|
-
* lifecycle. Cancel message routing is handled directly by the transport's
|
|
6
|
-
* single channel subscription — no separate cancel manager needed.
|
|
7
|
-
*
|
|
8
|
-
* The transport exposes a single factory method — `newTurn()` — which returns
|
|
9
|
-
* a Turn object with explicit lifecycle methods: start(), addMessages(),
|
|
10
|
-
* streamResponse(), and end().
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import * as Ably from 'ably';
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
EVENT_CANCEL,
|
|
17
|
-
HEADER_CANCEL_ALL,
|
|
18
|
-
HEADER_CANCEL_CLIENT_ID,
|
|
19
|
-
HEADER_CANCEL_OWN,
|
|
20
|
-
HEADER_CANCEL_TURN_ID,
|
|
21
|
-
HEADER_MSG_ID,
|
|
22
|
-
} from '../../constants.js';
|
|
23
|
-
import { ErrorCode } from '../../errors.js';
|
|
24
|
-
import type { Logger } from '../../logger.js';
|
|
25
|
-
import { getHeaders, mergeHeaders } from '../../utils.js';
|
|
26
|
-
import { buildTransportHeaders } from './headers.js';
|
|
27
|
-
import { pipeStream } from './pipe-stream.js';
|
|
28
|
-
import type { TurnManager } from './turn-manager.js';
|
|
29
|
-
import { createTurnManager } from './turn-manager.js';
|
|
30
|
-
import type {
|
|
31
|
-
AddMessageOptions,
|
|
32
|
-
AddMessagesResult,
|
|
33
|
-
CancelFilter,
|
|
34
|
-
CancelRequest,
|
|
35
|
-
MessageWithHeaders,
|
|
36
|
-
NewTurnOptions,
|
|
37
|
-
ServerTransport,
|
|
38
|
-
ServerTransportOptions,
|
|
39
|
-
StreamResponseOptions,
|
|
40
|
-
StreamResult,
|
|
41
|
-
Turn,
|
|
42
|
-
TurnEndReason,
|
|
43
|
-
} from './types.js';
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// Internal turn record for cancel routing
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
interface RegisteredTurn {
|
|
50
|
-
turnId: string;
|
|
51
|
-
clientId: string;
|
|
52
|
-
controller: AbortController;
|
|
53
|
-
onCancel?: (request: CancelRequest) => Promise<boolean>;
|
|
54
|
-
onError?: (error: Ably.ErrorInfo) => void;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// Implementation
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
|
|
61
|
-
// Spec: AIT-ST1
|
|
62
|
-
class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent, TMessage> {
|
|
63
|
-
private readonly _channel: Ably.RealtimeChannel;
|
|
64
|
-
private readonly _codec: ServerTransportOptions<TEvent, TMessage>['codec'];
|
|
65
|
-
private readonly _logger: Logger | undefined;
|
|
66
|
-
private readonly _onError: ((error: Ably.ErrorInfo) => void) | undefined;
|
|
67
|
-
private readonly _turnManager: TurnManager;
|
|
68
|
-
private readonly _registeredTurns = new Map<string, RegisteredTurn>();
|
|
69
|
-
private readonly _channelListener: (msg: Ably.InboundMessage) => void;
|
|
70
|
-
private readonly _attachPromise: Promise<void>;
|
|
71
|
-
|
|
72
|
-
constructor(options: ServerTransportOptions<TEvent, TMessage>) {
|
|
73
|
-
this._channel = options.channel;
|
|
74
|
-
this._codec = options.codec;
|
|
75
|
-
this._logger = options.logger?.withContext({ component: 'ServerTransport' });
|
|
76
|
-
this._onError = options.onError;
|
|
77
|
-
this._turnManager = createTurnManager(this._channel, this._logger);
|
|
78
|
-
|
|
79
|
-
this._channelListener = (msg: Ably.InboundMessage) => {
|
|
80
|
-
this._handleChannelMessage(msg);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Spec: AIT-ST2
|
|
84
|
-
// Subscribe before attach (RTL7g) — ensures no messages are missed.
|
|
85
|
-
this._attachPromise = this._channel.subscribe(EVENT_CANCEL, this._channelListener).then(
|
|
86
|
-
/* eslint-disable @typescript-eslint/no-empty-function -- discard subscription handle */
|
|
87
|
-
() => {},
|
|
88
|
-
/* eslint-enable @typescript-eslint/no-empty-function */
|
|
89
|
-
(error: unknown) => {
|
|
90
|
-
const errInfo = new Ably.ErrorInfo(
|
|
91
|
-
`unable to subscribe to cancel messages; ${error instanceof Error ? error.message : String(error)}`,
|
|
92
|
-
ErrorCode.TransportSubscriptionError,
|
|
93
|
-
500,
|
|
94
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
95
|
-
);
|
|
96
|
-
this._logger?.error('DefaultServerTransport(); subscribe failed');
|
|
97
|
-
this._onError?.(errInfo);
|
|
98
|
-
},
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
this._logger?.debug('DefaultServerTransport(); transport created');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// -------------------------------------------------------------------------
|
|
105
|
-
// Public API
|
|
106
|
-
// -------------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
// Spec: AIT-ST3
|
|
109
|
-
newTurn(turnOpts: NewTurnOptions<TEvent>): Turn<TEvent, TMessage> {
|
|
110
|
-
this._logger?.trace('DefaultServerTransport.newTurn();', { turnId: turnOpts.turnId });
|
|
111
|
-
return this._createTurn(turnOpts);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Spec: AIT-ST11
|
|
115
|
-
close(): void {
|
|
116
|
-
this._logger?.trace('DefaultServerTransport.close();');
|
|
117
|
-
this._channel.unsubscribe(EVENT_CANCEL, this._channelListener);
|
|
118
|
-
for (const reg of this._registeredTurns.values()) {
|
|
119
|
-
reg.controller.abort();
|
|
120
|
-
}
|
|
121
|
-
this._registeredTurns.clear();
|
|
122
|
-
this._turnManager.close();
|
|
123
|
-
this._logger?.debug('DefaultServerTransport.close(); transport closed');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// -------------------------------------------------------------------------
|
|
127
|
-
// Cancel message routing
|
|
128
|
-
// -------------------------------------------------------------------------
|
|
129
|
-
|
|
130
|
-
private _resolveFilter(filter: CancelFilter, senderClientId?: string): string[] {
|
|
131
|
-
const turnIds = [...this._registeredTurns.keys()];
|
|
132
|
-
|
|
133
|
-
if (filter.all) return turnIds;
|
|
134
|
-
if (filter.own && senderClientId) {
|
|
135
|
-
return turnIds.filter((id) => this._registeredTurns.get(id)?.clientId === senderClientId);
|
|
136
|
-
}
|
|
137
|
-
if (filter.clientId) {
|
|
138
|
-
return turnIds.filter((id) => this._registeredTurns.get(id)?.clientId === filter.clientId);
|
|
139
|
-
}
|
|
140
|
-
if (filter.turnId && this._registeredTurns.has(filter.turnId)) {
|
|
141
|
-
return [filter.turnId];
|
|
142
|
-
}
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Spec: AIT-ST8, AIT-ST9
|
|
147
|
-
private async _handleCancelMessage(msg: Ably.InboundMessage): Promise<void> {
|
|
148
|
-
const headers = getHeaders(msg);
|
|
149
|
-
|
|
150
|
-
const filter: CancelFilter = {};
|
|
151
|
-
if (headers[HEADER_CANCEL_TURN_ID]) {
|
|
152
|
-
filter.turnId = headers[HEADER_CANCEL_TURN_ID];
|
|
153
|
-
} else if (headers[HEADER_CANCEL_OWN] === 'true') {
|
|
154
|
-
filter.own = true;
|
|
155
|
-
} else if (headers[HEADER_CANCEL_CLIENT_ID]) {
|
|
156
|
-
filter.clientId = headers[HEADER_CANCEL_CLIENT_ID];
|
|
157
|
-
} else if (headers[HEADER_CANCEL_ALL] === 'true') {
|
|
158
|
-
filter.all = true;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const matchedTurnIds = this._resolveFilter(filter, msg.clientId);
|
|
162
|
-
if (matchedTurnIds.length === 0) return;
|
|
163
|
-
|
|
164
|
-
this._logger?.debug('DefaultServerTransport._handleCancelMessage(); matched turns', {
|
|
165
|
-
matchedTurnIds,
|
|
166
|
-
filter,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const owners = new Map<string, string>();
|
|
170
|
-
for (const tid of matchedTurnIds) {
|
|
171
|
-
const reg = this._registeredTurns.get(tid);
|
|
172
|
-
owners.set(tid, reg?.clientId ?? '');
|
|
173
|
-
}
|
|
174
|
-
const request: CancelRequest = { message: msg, filter, matchedTurnIds, turnOwners: owners };
|
|
175
|
-
|
|
176
|
-
for (const turnId of matchedTurnIds) {
|
|
177
|
-
const reg = this._registeredTurns.get(turnId);
|
|
178
|
-
if (!reg) continue;
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
if (reg.onCancel) {
|
|
182
|
-
const allowed = await reg.onCancel(request);
|
|
183
|
-
if (!allowed) {
|
|
184
|
-
this._logger?.debug('DefaultServerTransport._handleCancelMessage(); cancel rejected by onCancel', {
|
|
185
|
-
turnId,
|
|
186
|
-
});
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
reg.controller.abort();
|
|
191
|
-
this._logger?.debug('DefaultServerTransport._handleCancelMessage(); turn aborted', { turnId });
|
|
192
|
-
} catch (error) {
|
|
193
|
-
// A throwing onCancel handler must not prevent other turns from being cancelled.
|
|
194
|
-
const errInfo = new Ably.ErrorInfo(
|
|
195
|
-
`unable to process cancel for turn ${turnId}; onCancel handler threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
196
|
-
ErrorCode.CancelListenerError,
|
|
197
|
-
500,
|
|
198
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
199
|
-
);
|
|
200
|
-
this._logger?.error('DefaultServerTransport._handleCancelMessage(); onCancel threw', { turnId });
|
|
201
|
-
(reg.onError ?? this._onError)?.(errInfo);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// -------------------------------------------------------------------------
|
|
207
|
-
// Channel subscription handler
|
|
208
|
-
// -------------------------------------------------------------------------
|
|
209
|
-
|
|
210
|
-
private _handleChannelMessage(msg: Ably.InboundMessage): void {
|
|
211
|
-
try {
|
|
212
|
-
if (msg.name === EVENT_CANCEL) {
|
|
213
|
-
// Fire-and-forget async handler — errors are caught internally.
|
|
214
|
-
this._handleCancelMessage(msg).catch((error: unknown) => {
|
|
215
|
-
const errInfo = new Ably.ErrorInfo(
|
|
216
|
-
`unable to route cancel message; ${error instanceof Error ? error.message : String(error)}`,
|
|
217
|
-
ErrorCode.CancelListenerError,
|
|
218
|
-
500,
|
|
219
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
220
|
-
);
|
|
221
|
-
this._logger?.error('DefaultServerTransport._handleChannelMessage(); cancel routing error');
|
|
222
|
-
this._onError?.(errInfo);
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
} catch (error) {
|
|
226
|
-
const errInfo = new Ably.ErrorInfo(
|
|
227
|
-
`unable to process channel message; ${error instanceof Error ? error.message : String(error)}`,
|
|
228
|
-
ErrorCode.TransportSubscriptionError,
|
|
229
|
-
500,
|
|
230
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
231
|
-
);
|
|
232
|
-
this._logger?.error('DefaultServerTransport._handleChannelMessage(); subscription error');
|
|
233
|
-
this._onError?.(errInfo);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// -------------------------------------------------------------------------
|
|
238
|
-
// Turn creation
|
|
239
|
-
// -------------------------------------------------------------------------
|
|
240
|
-
|
|
241
|
-
private _createTurn(turnOpts: NewTurnOptions<TEvent>): Turn<TEvent, TMessage> {
|
|
242
|
-
const {
|
|
243
|
-
turnId,
|
|
244
|
-
clientId: turnClientId,
|
|
245
|
-
onMessage,
|
|
246
|
-
onAbort,
|
|
247
|
-
onCancel,
|
|
248
|
-
onError: turnOnError,
|
|
249
|
-
parent: turnParent,
|
|
250
|
-
forkOf: turnForkOf,
|
|
251
|
-
} = turnOpts;
|
|
252
|
-
|
|
253
|
-
const controller = new AbortController();
|
|
254
|
-
let started = false;
|
|
255
|
-
let ended = false;
|
|
256
|
-
|
|
257
|
-
// Register immediately so early cancels can fire the abort signal.
|
|
258
|
-
const registration: RegisteredTurn = {
|
|
259
|
-
turnId,
|
|
260
|
-
clientId: turnClientId ?? '',
|
|
261
|
-
controller,
|
|
262
|
-
onCancel,
|
|
263
|
-
onError: turnOnError,
|
|
264
|
-
};
|
|
265
|
-
this._registeredTurns.set(turnId, registration);
|
|
266
|
-
|
|
267
|
-
// Capture instance members as locals so arrow functions close over them
|
|
268
|
-
// without needing `this` (avoids unicorn/no-this-assignment).
|
|
269
|
-
const logger = this._logger;
|
|
270
|
-
const turnManager = this._turnManager;
|
|
271
|
-
const attachPromise = this._attachPromise;
|
|
272
|
-
const codec = this._codec;
|
|
273
|
-
const channel = this._channel;
|
|
274
|
-
const registeredTurns = this._registeredTurns;
|
|
275
|
-
|
|
276
|
-
const turn: Turn<TEvent, TMessage> = {
|
|
277
|
-
get turnId() {
|
|
278
|
-
return turnId;
|
|
279
|
-
},
|
|
280
|
-
get abortSignal() {
|
|
281
|
-
return controller.signal;
|
|
282
|
-
},
|
|
283
|
-
|
|
284
|
-
// Spec: AIT-ST4
|
|
285
|
-
start: async (): Promise<void> => {
|
|
286
|
-
logger?.trace('Turn.start();', { turnId });
|
|
287
|
-
|
|
288
|
-
if (controller.signal.aborted) {
|
|
289
|
-
throw new Ably.ErrorInfo(
|
|
290
|
-
`unable to start turn; turn ${turnId} was cancelled before start()`,
|
|
291
|
-
ErrorCode.InvalidArgument,
|
|
292
|
-
400,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
if (started) return;
|
|
296
|
-
started = true;
|
|
297
|
-
|
|
298
|
-
try {
|
|
299
|
-
await turnManager.startTurn(turnId, turnClientId, controller);
|
|
300
|
-
} catch (error) {
|
|
301
|
-
const errInfo = new Ably.ErrorInfo(
|
|
302
|
-
`unable to publish turn-start for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
|
|
303
|
-
ErrorCode.TurnLifecycleError,
|
|
304
|
-
500,
|
|
305
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
306
|
-
);
|
|
307
|
-
logger?.error('Turn.start(); failed to publish turn-start', { turnId });
|
|
308
|
-
turnOnError?.(errInfo);
|
|
309
|
-
throw errInfo;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
logger?.debug('Turn.start(); turn started', { turnId });
|
|
313
|
-
},
|
|
314
|
-
|
|
315
|
-
// Spec: AIT-ST5
|
|
316
|
-
addMessages: async (
|
|
317
|
-
inputs: MessageWithHeaders<TMessage>[],
|
|
318
|
-
opts?: AddMessageOptions,
|
|
319
|
-
): Promise<AddMessagesResult> => {
|
|
320
|
-
logger?.trace('Turn.addMessages();', { turnId, count: inputs.length });
|
|
321
|
-
|
|
322
|
-
if (!started) {
|
|
323
|
-
throw new Ably.ErrorInfo(
|
|
324
|
-
`unable to add messages; start() must be called before addMessages() (turn ${turnId})`,
|
|
325
|
-
ErrorCode.InvalidArgument,
|
|
326
|
-
400,
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
await attachPromise;
|
|
330
|
-
|
|
331
|
-
const msgIds: string[] = [];
|
|
332
|
-
|
|
333
|
-
for (const input of inputs) {
|
|
334
|
-
const msgId = crypto.randomUUID();
|
|
335
|
-
|
|
336
|
-
// Transport headers are the defaults; per-message headers from the
|
|
337
|
-
// client override them. This lets the client's x-ably-msg-id pass
|
|
338
|
-
// through for optimistic reconciliation with client inserts.
|
|
339
|
-
const headers = mergeHeaders(
|
|
340
|
-
buildTransportHeaders({
|
|
341
|
-
role: 'user',
|
|
342
|
-
turnId,
|
|
343
|
-
msgId,
|
|
344
|
-
turnClientId: opts?.clientId,
|
|
345
|
-
// Per-operation options override turn-level defaults
|
|
346
|
-
parent: opts?.parent === undefined ? (turnParent ?? undefined) : (opts.parent ?? undefined),
|
|
347
|
-
forkOf: opts?.forkOf ?? turnForkOf,
|
|
348
|
-
}),
|
|
349
|
-
input.headers,
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
const encoder = codec.createEncoder(channel, {
|
|
353
|
-
extras: { headers },
|
|
354
|
-
onMessage,
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
await encoder.writeMessages([input.message], opts?.clientId ? { clientId: opts.clientId } : undefined);
|
|
358
|
-
|
|
359
|
-
// Capture the effective msg-id after input.headers may have overridden it.
|
|
360
|
-
msgIds.push(headers[HEADER_MSG_ID] ?? msgId);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
logger?.debug('Turn.addMessages(); messages published', { turnId, count: inputs.length });
|
|
364
|
-
return { msgIds };
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
// Spec: AIT-ST6
|
|
368
|
-
streamResponse: async (
|
|
369
|
-
stream: ReadableStream<TEvent>,
|
|
370
|
-
streamOpts?: StreamResponseOptions,
|
|
371
|
-
): Promise<StreamResult> => {
|
|
372
|
-
logger?.trace('Turn.streamResponse();', { turnId });
|
|
373
|
-
|
|
374
|
-
if (!started) {
|
|
375
|
-
throw new Ably.ErrorInfo(
|
|
376
|
-
`unable to stream response; start() must be called before streamResponse() (turn ${turnId})`,
|
|
377
|
-
ErrorCode.InvalidArgument,
|
|
378
|
-
400,
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
await attachPromise;
|
|
382
|
-
|
|
383
|
-
const signal = turnManager.getSignal(turnId);
|
|
384
|
-
const turnOwnerClientId = turnManager.getClientId(turnId);
|
|
385
|
-
|
|
386
|
-
// Per-operation parent overrides the turn-level default.
|
|
387
|
-
const assistantParent =
|
|
388
|
-
streamOpts?.parent === undefined ? (turnParent ?? undefined) : (streamOpts.parent ?? undefined);
|
|
389
|
-
|
|
390
|
-
const defaultHeaders = buildTransportHeaders({
|
|
391
|
-
role: 'assistant',
|
|
392
|
-
turnId,
|
|
393
|
-
msgId: crypto.randomUUID(),
|
|
394
|
-
turnClientId: turnOwnerClientId,
|
|
395
|
-
parent: assistantParent,
|
|
396
|
-
forkOf: streamOpts?.forkOf ?? turnForkOf,
|
|
397
|
-
});
|
|
398
|
-
const encoder = codec.createEncoder(channel, {
|
|
399
|
-
extras: { headers: defaultHeaders },
|
|
400
|
-
onMessage,
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
const result = await pipeStream(stream, encoder, signal, onAbort, logger);
|
|
404
|
-
|
|
405
|
-
logger?.debug('Turn.streamResponse(); stream finished', { turnId, reason: result.reason });
|
|
406
|
-
return result;
|
|
407
|
-
},
|
|
408
|
-
|
|
409
|
-
// Spec: AIT-ST7
|
|
410
|
-
end: async (reason: TurnEndReason): Promise<void> => {
|
|
411
|
-
logger?.trace('Turn.end();', { turnId, reason });
|
|
412
|
-
|
|
413
|
-
if (!started) {
|
|
414
|
-
throw new Ably.ErrorInfo(
|
|
415
|
-
`unable to end turn; start() must be called before end() (turn ${turnId})`,
|
|
416
|
-
ErrorCode.InvalidArgument,
|
|
417
|
-
400,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
if (ended) return;
|
|
421
|
-
ended = true;
|
|
422
|
-
|
|
423
|
-
try {
|
|
424
|
-
await turnManager.endTurn(turnId, reason);
|
|
425
|
-
} catch (error) {
|
|
426
|
-
const errInfo = new Ably.ErrorInfo(
|
|
427
|
-
`unable to publish turn-end for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
|
|
428
|
-
ErrorCode.TurnLifecycleError,
|
|
429
|
-
500,
|
|
430
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
431
|
-
);
|
|
432
|
-
logger?.error('Turn.end(); failed to publish turn-end', { turnId });
|
|
433
|
-
turnOnError?.(errInfo);
|
|
434
|
-
throw errInfo;
|
|
435
|
-
} finally {
|
|
436
|
-
registeredTurns.delete(turnId);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
logger?.debug('Turn.end(); turn ended', { turnId, reason });
|
|
440
|
-
},
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
return turn;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// ---------------------------------------------------------------------------
|
|
448
|
-
// Factory
|
|
449
|
-
// ---------------------------------------------------------------------------
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Create a server transport bound to the given channel and codec.
|
|
453
|
-
* @param options - Transport configuration.
|
|
454
|
-
* @returns A new {@link ServerTransport} instance.
|
|
455
|
-
*/
|
|
456
|
-
export const createServerTransport = <TEvent, TMessage>(
|
|
457
|
-
options: ServerTransportOptions<TEvent, TMessage>,
|
|
458
|
-
): ServerTransport<TEvent, TMessage> => new DefaultServerTransport(options);
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client-side stream routing.
|
|
3
|
-
*
|
|
4
|
-
* Maintains a map of turnId to ReadableStreamController. Routes decoded events
|
|
5
|
-
* to the correct stream. Closes streams on terminal events or explicit close.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as Ably from 'ably';
|
|
9
|
-
|
|
10
|
-
import { ErrorCode } from '../../errors.js';
|
|
11
|
-
import type { Logger } from '../../logger.js';
|
|
12
|
-
import type { TurnEntry } from './types.js';
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Interface
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
/** Routes decoded events to the correct turn's ReadableStream. */
|
|
19
|
-
export interface StreamRouter<TEvent> {
|
|
20
|
-
/** Register a new stream for a turnId. Returns the ReadableStream the consumer reads from. */
|
|
21
|
-
createStream(turnId: string): ReadableStream<TEvent>;
|
|
22
|
-
/** Close the stream for a turnId. Returns true if a stream was closed. */
|
|
23
|
-
closeStream(turnId: string): boolean;
|
|
24
|
-
/** Enqueue an event to the correct stream. Returns true if routed successfully. */
|
|
25
|
-
route(turnId: string, event: TEvent): boolean;
|
|
26
|
-
/** Whether a specific turnId has an active stream. */
|
|
27
|
-
has(turnId: string): boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Implementation
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
// Spec: AIT-CT14
|
|
35
|
-
class DefaultStreamRouter<TEvent> implements StreamRouter<TEvent> {
|
|
36
|
-
private readonly _turns = new Map<string, TurnEntry<TEvent>>();
|
|
37
|
-
private readonly _isTerminal: (event: TEvent) => boolean;
|
|
38
|
-
private readonly _logger: Logger;
|
|
39
|
-
|
|
40
|
-
constructor(isTerminal: (event: TEvent) => boolean, logger: Logger) {
|
|
41
|
-
this._isTerminal = isTerminal;
|
|
42
|
-
this._logger = logger;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
createStream(turnId: string): ReadableStream<TEvent> {
|
|
46
|
-
this._logger.trace('StreamRouter.createStream();', { turnId });
|
|
47
|
-
|
|
48
|
-
// Build stream+controller together. ReadableStream's start() runs synchronously
|
|
49
|
-
// per spec, so the controller is captured before the constructor returns.
|
|
50
|
-
const entry: { controller?: ReadableStreamDefaultController<TEvent> } = {};
|
|
51
|
-
const stream = new ReadableStream<TEvent>({
|
|
52
|
-
start(controller) {
|
|
53
|
-
entry.controller = controller;
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
if (!entry.controller) {
|
|
57
|
-
throw new Ably.ErrorInfo(
|
|
58
|
-
'unable to create stream; ReadableStream start() was not called synchronously',
|
|
59
|
-
ErrorCode.TransportSubscriptionError,
|
|
60
|
-
500,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
this._turns.set(turnId, { controller: entry.controller, turnId });
|
|
64
|
-
return stream;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Spec: AIT-CT14b
|
|
68
|
-
closeStream(turnId: string): boolean {
|
|
69
|
-
const turn = this._turns.get(turnId);
|
|
70
|
-
if (!turn) return false;
|
|
71
|
-
|
|
72
|
-
this._logger.debug('StreamRouter.closeStream(); closing stream', { turnId });
|
|
73
|
-
try {
|
|
74
|
-
turn.controller.close();
|
|
75
|
-
} catch {
|
|
76
|
-
/* already closed */
|
|
77
|
-
}
|
|
78
|
-
this._turns.delete(turnId);
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Spec: AIT-CT14a
|
|
83
|
-
route(turnId: string, event: TEvent): boolean {
|
|
84
|
-
const turn = this._turns.get(turnId);
|
|
85
|
-
if (!turn) return false;
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
turn.controller.enqueue(event);
|
|
89
|
-
} catch {
|
|
90
|
-
this._turns.delete(turnId);
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (this._isTerminal(event)) {
|
|
95
|
-
this.closeStream(turnId);
|
|
96
|
-
}
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
has(turnId: string): boolean {
|
|
101
|
-
return this._turns.has(turnId);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
// Factory
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Create a StreamRouter that routes decoded events to per-turn ReadableStreams.
|
|
111
|
-
* @param isTerminal - Predicate that returns true for events that close the stream.
|
|
112
|
-
* @param logger - Logger for diagnostic output.
|
|
113
|
-
* @returns A new {@link StreamRouter} instance.
|
|
114
|
-
*/
|
|
115
|
-
export const createStreamRouter = <TEvent>(
|
|
116
|
-
isTerminal: (event: TEvent) => boolean,
|
|
117
|
-
logger: Logger,
|
|
118
|
-
): StreamRouter<TEvent> => new DefaultStreamRouter(isTerminal, logger);
|