@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,612 +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
|
-
} from '../../constants.js';
|
|
22
|
-
import { ErrorCode } from '../../errors.js';
|
|
23
|
-
import type { Logger } from '../../logger.js';
|
|
24
|
-
import { getHeaders, mergeHeaders } from '../../utils.js';
|
|
25
|
-
import { buildTransportHeaders } from './headers.js';
|
|
26
|
-
import { pipeStream } from './pipe-stream.js';
|
|
27
|
-
import type { TurnManager } from './turn-manager.js';
|
|
28
|
-
import { createTurnManager } from './turn-manager.js';
|
|
29
|
-
import type {
|
|
30
|
-
AddMessageOptions,
|
|
31
|
-
AddMessagesResult,
|
|
32
|
-
CancelFilter,
|
|
33
|
-
CancelRequest,
|
|
34
|
-
EventsNode,
|
|
35
|
-
MessageNode,
|
|
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
|
-
/** Composite signal that fires when either the internal controller or the external signal aborts. */
|
|
54
|
-
signal: AbortSignal;
|
|
55
|
-
onCancel?: (request: CancelRequest) => Promise<boolean>;
|
|
56
|
-
onError?: (error: Ably.ErrorInfo) => void;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Internal state machines
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
enum ServerTransportState {
|
|
64
|
-
READY = 'ready',
|
|
65
|
-
CLOSED = 'closed',
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
enum TurnState {
|
|
69
|
-
INITIALIZED = 'initialized',
|
|
70
|
-
STARTED = 'started',
|
|
71
|
-
ENDED = 'ended',
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// Implementation
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
// Spec: AIT-ST1
|
|
79
|
-
class DefaultServerTransport<TEvent, TMessage> implements ServerTransport<TEvent, TMessage> {
|
|
80
|
-
private readonly _channel: Ably.RealtimeChannel;
|
|
81
|
-
private readonly _codec: ServerTransportOptions<TEvent, TMessage>['codec'];
|
|
82
|
-
private readonly _logger: Logger | undefined;
|
|
83
|
-
private readonly _onError: ((error: Ably.ErrorInfo) => void) | undefined;
|
|
84
|
-
private readonly _turnManager: TurnManager;
|
|
85
|
-
private readonly _registeredTurns = new Map<string, RegisteredTurn>();
|
|
86
|
-
private readonly _channelListener: (msg: Ably.InboundMessage) => void;
|
|
87
|
-
private readonly _attachPromise: Promise<void>;
|
|
88
|
-
|
|
89
|
-
private _state = ServerTransportState.READY;
|
|
90
|
-
private _hasAttachedOnce: boolean;
|
|
91
|
-
private readonly _onChannelStateChange: Ably.channelEventCallback;
|
|
92
|
-
|
|
93
|
-
constructor(options: ServerTransportOptions<TEvent, TMessage>) {
|
|
94
|
-
this._channel = options.channel;
|
|
95
|
-
this._codec = options.codec;
|
|
96
|
-
this._logger = options.logger?.withContext({ component: 'ServerTransport' });
|
|
97
|
-
this._onError = options.onError;
|
|
98
|
-
this._turnManager = createTurnManager(this._channel, this._logger);
|
|
99
|
-
|
|
100
|
-
this._channelListener = (msg: Ably.InboundMessage) => {
|
|
101
|
-
this._handleChannelMessage(msg);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Spec: AIT-ST2
|
|
105
|
-
// Subscribe before attach (RTL7g) — ensures no messages are missed.
|
|
106
|
-
this._attachPromise = this._channel.subscribe(EVENT_CANCEL, this._channelListener).then(
|
|
107
|
-
/* eslint-disable @typescript-eslint/no-empty-function -- discard subscription handle */
|
|
108
|
-
() => {},
|
|
109
|
-
/* eslint-enable @typescript-eslint/no-empty-function */
|
|
110
|
-
(error: unknown) => {
|
|
111
|
-
const errInfo = new Ably.ErrorInfo(
|
|
112
|
-
`unable to subscribe to cancel messages; ${error instanceof Error ? error.message : String(error)}`,
|
|
113
|
-
ErrorCode.TransportSubscriptionError,
|
|
114
|
-
500,
|
|
115
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
116
|
-
);
|
|
117
|
-
this._logger?.error('DefaultServerTransport(); subscribe failed');
|
|
118
|
-
this._onError?.(errInfo);
|
|
119
|
-
},
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
// Spec: AIT-ST12, AIT-ST12a
|
|
123
|
-
// Listen for channel state changes that break message continuity. The
|
|
124
|
-
// server only consumes cancel messages from the channel, so losing one
|
|
125
|
-
// is survivable — but the developer needs to know so they can decide
|
|
126
|
-
// whether to abort in-flight work. _hasAttachedOnce is seeded from the
|
|
127
|
-
// channel's current state so pre-attached channels are handled correctly;
|
|
128
|
-
// it distinguishes the initial attach from a genuine discontinuity.
|
|
129
|
-
this._hasAttachedOnce = this._channel.state === 'attached';
|
|
130
|
-
this._onChannelStateChange = (stateChange: Ably.ChannelStateChange) => {
|
|
131
|
-
this._handleChannelStateChange(stateChange);
|
|
132
|
-
};
|
|
133
|
-
this._channel.on(this._onChannelStateChange);
|
|
134
|
-
|
|
135
|
-
this._logger?.debug('DefaultServerTransport(); transport created');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// -------------------------------------------------------------------------
|
|
139
|
-
// Public API
|
|
140
|
-
// -------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
// Spec: AIT-ST3
|
|
143
|
-
newTurn(turnOpts: NewTurnOptions<TEvent>): Turn<TEvent, TMessage> {
|
|
144
|
-
this._logger?.trace('DefaultServerTransport.newTurn();', { turnId: turnOpts.turnId });
|
|
145
|
-
return this._createTurn(turnOpts);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Spec: AIT-ST11
|
|
149
|
-
close(): void {
|
|
150
|
-
if (this._state === ServerTransportState.CLOSED) return;
|
|
151
|
-
this._state = ServerTransportState.CLOSED;
|
|
152
|
-
this._logger?.trace('DefaultServerTransport.close();');
|
|
153
|
-
this._channel.unsubscribe(EVENT_CANCEL, this._channelListener);
|
|
154
|
-
this._channel.off(this._onChannelStateChange);
|
|
155
|
-
for (const reg of this._registeredTurns.values()) {
|
|
156
|
-
reg.controller.abort();
|
|
157
|
-
}
|
|
158
|
-
this._registeredTurns.clear();
|
|
159
|
-
this._turnManager.close();
|
|
160
|
-
this._logger?.debug('DefaultServerTransport.close(); transport closed');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// -------------------------------------------------------------------------
|
|
164
|
-
// Cancel message routing
|
|
165
|
-
// -------------------------------------------------------------------------
|
|
166
|
-
|
|
167
|
-
private _resolveFilter(filter: CancelFilter, senderClientId?: string): string[] {
|
|
168
|
-
const turnIds = [...this._registeredTurns.keys()];
|
|
169
|
-
|
|
170
|
-
if (filter.all) return turnIds;
|
|
171
|
-
if (filter.own && senderClientId) {
|
|
172
|
-
return turnIds.filter((id) => this._registeredTurns.get(id)?.clientId === senderClientId);
|
|
173
|
-
}
|
|
174
|
-
if (filter.clientId) {
|
|
175
|
-
return turnIds.filter((id) => this._registeredTurns.get(id)?.clientId === filter.clientId);
|
|
176
|
-
}
|
|
177
|
-
if (filter.turnId && this._registeredTurns.has(filter.turnId)) {
|
|
178
|
-
return [filter.turnId];
|
|
179
|
-
}
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Spec: AIT-ST8, AIT-ST8a, AIT-ST8b, AIT-ST8c, AIT-ST8d, AIT-ST9, AIT-ST9a
|
|
184
|
-
private async _handleCancelMessage(msg: Ably.InboundMessage): Promise<void> {
|
|
185
|
-
const headers = getHeaders(msg);
|
|
186
|
-
|
|
187
|
-
// Spec: AIT-ST8a, AIT-ST8b, AIT-ST8c, AIT-ST8d
|
|
188
|
-
const filter: CancelFilter = {};
|
|
189
|
-
if (headers[HEADER_CANCEL_TURN_ID]) {
|
|
190
|
-
filter.turnId = headers[HEADER_CANCEL_TURN_ID];
|
|
191
|
-
} else if (headers[HEADER_CANCEL_OWN] === 'true') {
|
|
192
|
-
filter.own = true;
|
|
193
|
-
} else if (headers[HEADER_CANCEL_CLIENT_ID]) {
|
|
194
|
-
filter.clientId = headers[HEADER_CANCEL_CLIENT_ID];
|
|
195
|
-
} else if (headers[HEADER_CANCEL_ALL] === 'true') {
|
|
196
|
-
filter.all = true;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const matchedTurnIds = this._resolveFilter(filter, msg.clientId);
|
|
200
|
-
if (matchedTurnIds.length === 0) return;
|
|
201
|
-
|
|
202
|
-
this._logger?.debug('DefaultServerTransport._handleCancelMessage(); matched turns', {
|
|
203
|
-
matchedTurnIds,
|
|
204
|
-
filter,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const owners = new Map<string, string>();
|
|
208
|
-
for (const tid of matchedTurnIds) {
|
|
209
|
-
const reg = this._registeredTurns.get(tid);
|
|
210
|
-
owners.set(tid, reg?.clientId ?? '');
|
|
211
|
-
}
|
|
212
|
-
const request: CancelRequest = { message: msg, filter, matchedTurnIds, turnOwners: owners };
|
|
213
|
-
|
|
214
|
-
for (const turnId of matchedTurnIds) {
|
|
215
|
-
const reg = this._registeredTurns.get(turnId);
|
|
216
|
-
if (!reg) continue;
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
if (reg.onCancel) {
|
|
220
|
-
const allowed = await reg.onCancel(request);
|
|
221
|
-
if (!allowed) {
|
|
222
|
-
this._logger?.debug('DefaultServerTransport._handleCancelMessage(); cancel rejected by onCancel', {
|
|
223
|
-
turnId,
|
|
224
|
-
});
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
reg.controller.abort();
|
|
229
|
-
this._logger?.debug('DefaultServerTransport._handleCancelMessage(); turn aborted', { turnId });
|
|
230
|
-
} catch (error) {
|
|
231
|
-
// A throwing onCancel handler must not prevent other turns from being cancelled.
|
|
232
|
-
const errInfo = new Ably.ErrorInfo(
|
|
233
|
-
`unable to process cancel for turn ${turnId}; onCancel handler threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
234
|
-
ErrorCode.CancelListenerError,
|
|
235
|
-
500,
|
|
236
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
237
|
-
);
|
|
238
|
-
this._logger?.error('DefaultServerTransport._handleCancelMessage(); onCancel threw', { turnId });
|
|
239
|
-
(reg.onError ?? this._onError)?.(errInfo);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// -------------------------------------------------------------------------
|
|
245
|
-
// Channel state change handler
|
|
246
|
-
// -------------------------------------------------------------------------
|
|
247
|
-
|
|
248
|
-
// Spec: AIT-ST12, AIT-ST12a
|
|
249
|
-
private _handleChannelStateChange(stateChange: Ably.ChannelStateChange): void {
|
|
250
|
-
if (this._state === ServerTransportState.CLOSED) return;
|
|
251
|
-
|
|
252
|
-
const { current, resumed } = stateChange;
|
|
253
|
-
|
|
254
|
-
// Track the initial attach so we don't treat it as a discontinuity
|
|
255
|
-
if (current === 'attached' && !this._hasAttachedOnce) {
|
|
256
|
-
this._hasAttachedOnce = true;
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Continuity-breaking states:
|
|
261
|
-
// - FAILED, SUSPENDED, DETACHED: no more messages expected (or gap)
|
|
262
|
-
// - ATTACHED with resumed: false (UPDATE): messages were lost
|
|
263
|
-
const continuityLost =
|
|
264
|
-
current === 'failed' || current === 'suspended' || current === 'detached' || (current === 'attached' && !resumed);
|
|
265
|
-
|
|
266
|
-
if (!continuityLost) return;
|
|
267
|
-
|
|
268
|
-
this._logger?.error('DefaultServerTransport._handleChannelStateChange(); channel continuity lost', {
|
|
269
|
-
current,
|
|
270
|
-
resumed,
|
|
271
|
-
previous: stateChange.previous,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const err = new Ably.ErrorInfo(
|
|
275
|
-
`unable to deliver cancel messages; channel continuity lost (${current}${current === 'attached' ? ', resumed: false' : ''})`,
|
|
276
|
-
ErrorCode.ChannelContinuityLost,
|
|
277
|
-
500,
|
|
278
|
-
stateChange.reason,
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
// Transport-level notification only: continuity loss is not scoped to any
|
|
282
|
-
// turn. Per-turn onError handlers are reserved for errors from that turn's
|
|
283
|
-
// own operations (publish failures, encoder errors). Developers that need
|
|
284
|
-
// per-turn reaction can iterate active turns from the transport handler.
|
|
285
|
-
this._onError?.(err);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// -------------------------------------------------------------------------
|
|
289
|
-
// Channel subscription handler
|
|
290
|
-
// -------------------------------------------------------------------------
|
|
291
|
-
|
|
292
|
-
private _handleChannelMessage(msg: Ably.InboundMessage): void {
|
|
293
|
-
try {
|
|
294
|
-
if (msg.name === EVENT_CANCEL) {
|
|
295
|
-
// Fire-and-forget async handler — errors are caught internally.
|
|
296
|
-
this._handleCancelMessage(msg).catch((error: unknown) => {
|
|
297
|
-
const errInfo = new Ably.ErrorInfo(
|
|
298
|
-
`unable to route cancel message; ${error instanceof Error ? error.message : String(error)}`,
|
|
299
|
-
ErrorCode.CancelListenerError,
|
|
300
|
-
500,
|
|
301
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
302
|
-
);
|
|
303
|
-
this._logger?.error('DefaultServerTransport._handleChannelMessage(); cancel routing error');
|
|
304
|
-
this._onError?.(errInfo);
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
} catch (error) {
|
|
308
|
-
const errInfo = new Ably.ErrorInfo(
|
|
309
|
-
`unable to process channel message; ${error instanceof Error ? error.message : String(error)}`,
|
|
310
|
-
ErrorCode.TransportSubscriptionError,
|
|
311
|
-
500,
|
|
312
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
313
|
-
);
|
|
314
|
-
this._logger?.error('DefaultServerTransport._handleChannelMessage(); subscription error');
|
|
315
|
-
this._onError?.(errInfo);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// -------------------------------------------------------------------------
|
|
320
|
-
// Turn creation
|
|
321
|
-
// -------------------------------------------------------------------------
|
|
322
|
-
|
|
323
|
-
private _createTurn(turnOpts: NewTurnOptions<TEvent>): Turn<TEvent, TMessage> {
|
|
324
|
-
const {
|
|
325
|
-
turnId,
|
|
326
|
-
clientId: turnClientId,
|
|
327
|
-
onMessage,
|
|
328
|
-
onAbort,
|
|
329
|
-
onCancel,
|
|
330
|
-
onError: turnOnError,
|
|
331
|
-
parent: turnParent,
|
|
332
|
-
forkOf: turnForkOf,
|
|
333
|
-
signal: externalSignal,
|
|
334
|
-
} = turnOpts;
|
|
335
|
-
|
|
336
|
-
const controller = new AbortController();
|
|
337
|
-
let state = TurnState.INITIALIZED;
|
|
338
|
-
|
|
339
|
-
// Compose the internal controller signal with the external signal (e.g.
|
|
340
|
-
// req.signal) so platform-level cancellation (request cancellation, function
|
|
341
|
-
// timeout) aborts the turn through the same path as Ably cancel messages.
|
|
342
|
-
const signal = externalSignal ? AbortSignal.any([controller.signal, externalSignal]) : controller.signal;
|
|
343
|
-
|
|
344
|
-
// Spec: AIT-ST3a — register immediately so early cancels can fire the abort signal.
|
|
345
|
-
const registration: RegisteredTurn = {
|
|
346
|
-
turnId,
|
|
347
|
-
clientId: turnClientId ?? '',
|
|
348
|
-
controller,
|
|
349
|
-
signal,
|
|
350
|
-
onCancel,
|
|
351
|
-
onError: turnOnError,
|
|
352
|
-
};
|
|
353
|
-
this._registeredTurns.set(turnId, registration);
|
|
354
|
-
|
|
355
|
-
// Capture instance members as locals so arrow functions close over them
|
|
356
|
-
// without needing `this` (avoids unicorn/no-this-assignment).
|
|
357
|
-
const logger = this._logger;
|
|
358
|
-
const turnManager = this._turnManager;
|
|
359
|
-
const attachPromise = this._attachPromise;
|
|
360
|
-
const codec = this._codec;
|
|
361
|
-
const channel = this._channel;
|
|
362
|
-
const registeredTurns = this._registeredTurns;
|
|
363
|
-
|
|
364
|
-
const turn: Turn<TEvent, TMessage> = {
|
|
365
|
-
get turnId() {
|
|
366
|
-
return turnId;
|
|
367
|
-
},
|
|
368
|
-
get abortSignal() {
|
|
369
|
-
return signal;
|
|
370
|
-
},
|
|
371
|
-
|
|
372
|
-
// Spec: AIT-ST4, AIT-ST4a, AIT-ST4b
|
|
373
|
-
start: async (): Promise<void> => {
|
|
374
|
-
logger?.trace('Turn.start();', { turnId });
|
|
375
|
-
|
|
376
|
-
// Spec: AIT-ST4a
|
|
377
|
-
if (signal.aborted) {
|
|
378
|
-
throw new Ably.ErrorInfo(
|
|
379
|
-
`unable to start turn; turn ${turnId} was cancelled before start()`,
|
|
380
|
-
ErrorCode.InvalidArgument,
|
|
381
|
-
400,
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
if (state !== TurnState.INITIALIZED) return;
|
|
385
|
-
state = TurnState.STARTED;
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
await turnManager.startTurn(turnId, turnClientId, controller, {
|
|
389
|
-
parent: turnParent,
|
|
390
|
-
forkOf: turnForkOf,
|
|
391
|
-
});
|
|
392
|
-
} catch (error) {
|
|
393
|
-
const errInfo = new Ably.ErrorInfo(
|
|
394
|
-
`unable to publish turn-start for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
|
|
395
|
-
ErrorCode.TurnLifecycleError,
|
|
396
|
-
500,
|
|
397
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
398
|
-
);
|
|
399
|
-
logger?.error('Turn.start(); failed to publish turn-start', { turnId });
|
|
400
|
-
throw errInfo;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
logger?.debug('Turn.start(); turn started', { turnId });
|
|
404
|
-
},
|
|
405
|
-
|
|
406
|
-
// Spec: AIT-ST5, AIT-ST5a, AIT-ST5b, AIT-ST5c
|
|
407
|
-
addMessages: async (nodes: MessageNode<TMessage>[], opts?: AddMessageOptions): Promise<AddMessagesResult> => {
|
|
408
|
-
logger?.trace('Turn.addMessages();', { turnId, count: nodes.length });
|
|
409
|
-
|
|
410
|
-
if (state === TurnState.INITIALIZED) {
|
|
411
|
-
throw new Ably.ErrorInfo(
|
|
412
|
-
`unable to add messages; start() must be called before addMessages() (turn ${turnId})`,
|
|
413
|
-
ErrorCode.InvalidArgument,
|
|
414
|
-
400,
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
await attachPromise;
|
|
418
|
-
|
|
419
|
-
const msgIds: string[] = [];
|
|
420
|
-
|
|
421
|
-
try {
|
|
422
|
-
for (const node of nodes) {
|
|
423
|
-
// Build transport headers from the node's typed fields, then merge
|
|
424
|
-
// any extra headers from the node (e.g. domain-specific headers).
|
|
425
|
-
const headers = mergeHeaders(
|
|
426
|
-
buildTransportHeaders({
|
|
427
|
-
role: 'user',
|
|
428
|
-
turnId,
|
|
429
|
-
msgId: node.msgId,
|
|
430
|
-
turnClientId: opts?.clientId,
|
|
431
|
-
parent: node.parentId ?? turnParent,
|
|
432
|
-
forkOf: node.forkOf ?? turnForkOf,
|
|
433
|
-
}),
|
|
434
|
-
node.headers,
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
const encoder = codec.createEncoder(channel, {
|
|
438
|
-
extras: { headers },
|
|
439
|
-
onMessage,
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
await encoder.writeMessages([node.message], opts?.clientId ? { clientId: opts.clientId } : undefined);
|
|
443
|
-
|
|
444
|
-
msgIds.push(node.msgId);
|
|
445
|
-
}
|
|
446
|
-
} catch (error) {
|
|
447
|
-
const errInfo = new Ably.ErrorInfo(
|
|
448
|
-
`unable to publish messages for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
|
|
449
|
-
ErrorCode.TurnLifecycleError,
|
|
450
|
-
500,
|
|
451
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
452
|
-
);
|
|
453
|
-
logger?.error('Turn.addMessages(); publish failed', { turnId });
|
|
454
|
-
throw errInfo;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
logger?.debug('Turn.addMessages(); messages published', { turnId, count: nodes.length });
|
|
458
|
-
return { msgIds };
|
|
459
|
-
},
|
|
460
|
-
|
|
461
|
-
// Spec: AIT-ST5c
|
|
462
|
-
addEvents: async (nodes: EventsNode<TEvent>[]): Promise<void> => {
|
|
463
|
-
logger?.trace('Turn.addEvents();', { turnId, count: nodes.length });
|
|
464
|
-
|
|
465
|
-
if (state === TurnState.INITIALIZED) {
|
|
466
|
-
throw new Ably.ErrorInfo(
|
|
467
|
-
`unable to add events; start() must be called before addEvents() (turn ${turnId})`,
|
|
468
|
-
ErrorCode.InvalidArgument,
|
|
469
|
-
400,
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
await attachPromise;
|
|
473
|
-
|
|
474
|
-
const turnOwnerClientId = turnManager.getClientId(turnId);
|
|
475
|
-
|
|
476
|
-
try {
|
|
477
|
-
for (const node of nodes) {
|
|
478
|
-
const headers = buildTransportHeaders({
|
|
479
|
-
role: 'assistant',
|
|
480
|
-
turnId,
|
|
481
|
-
msgId: node.msgId,
|
|
482
|
-
turnClientId: turnOwnerClientId,
|
|
483
|
-
amend: node.msgId,
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
const encoder = codec.createEncoder(channel, {
|
|
487
|
-
extras: { headers },
|
|
488
|
-
onMessage,
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
for (const event of node.events) {
|
|
492
|
-
await encoder.writeEvent(event);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
await encoder.close();
|
|
496
|
-
}
|
|
497
|
-
} catch (error) {
|
|
498
|
-
const errInfo = new Ably.ErrorInfo(
|
|
499
|
-
`unable to publish events for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
|
|
500
|
-
ErrorCode.TurnLifecycleError,
|
|
501
|
-
500,
|
|
502
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
503
|
-
);
|
|
504
|
-
logger?.error('Turn.addEvents(); publish failed', { turnId });
|
|
505
|
-
throw errInfo;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
logger?.debug('Turn.addEvents(); events published', { turnId, count: nodes.length });
|
|
509
|
-
},
|
|
510
|
-
|
|
511
|
-
// Spec: AIT-ST6, AIT-ST6a, AIT-ST6b, AIT-ST6b1, AIT-ST6b2, AIT-ST6b3, AIT-ST6b4, AIT-ST6c
|
|
512
|
-
streamResponse: async (
|
|
513
|
-
stream: ReadableStream<TEvent>,
|
|
514
|
-
streamOpts?: StreamResponseOptions<TEvent>,
|
|
515
|
-
): Promise<StreamResult> => {
|
|
516
|
-
logger?.trace('Turn.streamResponse();', { turnId });
|
|
517
|
-
|
|
518
|
-
if (state === TurnState.INITIALIZED) {
|
|
519
|
-
throw new Ably.ErrorInfo(
|
|
520
|
-
`unable to stream response; start() must be called before streamResponse() (turn ${turnId})`,
|
|
521
|
-
ErrorCode.InvalidArgument,
|
|
522
|
-
400,
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
await attachPromise;
|
|
526
|
-
|
|
527
|
-
const turnOwnerClientId = turnManager.getClientId(turnId);
|
|
528
|
-
|
|
529
|
-
// Per-operation parent overrides the turn-level default.
|
|
530
|
-
const assistantParent = streamOpts?.parent === undefined ? turnParent : streamOpts.parent;
|
|
531
|
-
|
|
532
|
-
const msgId = crypto.randomUUID();
|
|
533
|
-
const defaultHeaders = buildTransportHeaders({
|
|
534
|
-
role: 'assistant',
|
|
535
|
-
turnId,
|
|
536
|
-
msgId,
|
|
537
|
-
turnClientId: turnOwnerClientId,
|
|
538
|
-
parent: assistantParent,
|
|
539
|
-
forkOf: streamOpts?.forkOf ?? turnForkOf,
|
|
540
|
-
});
|
|
541
|
-
const encoder = codec.createEncoder(channel, {
|
|
542
|
-
extras: { headers: defaultHeaders },
|
|
543
|
-
onMessage,
|
|
544
|
-
messageId: msgId,
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
const result = await pipeStream(stream, encoder, signal, onAbort, streamOpts?.resolveWriteOptions, logger);
|
|
548
|
-
|
|
549
|
-
if (result.error) {
|
|
550
|
-
const errInfo = new Ably.ErrorInfo(
|
|
551
|
-
`unable to stream response for turn ${turnId}; ${result.error.message}`,
|
|
552
|
-
ErrorCode.StreamError,
|
|
553
|
-
500,
|
|
554
|
-
result.error instanceof Ably.ErrorInfo ? result.error : undefined,
|
|
555
|
-
);
|
|
556
|
-
logger?.error('Turn.streamResponse(); stream error', { turnId });
|
|
557
|
-
turnOnError?.(errInfo);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
logger?.debug('Turn.streamResponse(); stream finished', { turnId, reason: result.reason });
|
|
561
|
-
return result;
|
|
562
|
-
},
|
|
563
|
-
|
|
564
|
-
// Spec: AIT-ST7, AIT-ST7a, AIT-ST7b
|
|
565
|
-
end: async (reason: TurnEndReason): Promise<void> => {
|
|
566
|
-
logger?.trace('Turn.end();', { turnId, reason });
|
|
567
|
-
|
|
568
|
-
if (state === TurnState.INITIALIZED) {
|
|
569
|
-
throw new Ably.ErrorInfo(
|
|
570
|
-
`unable to end turn; start() must be called before end() (turn ${turnId})`,
|
|
571
|
-
ErrorCode.InvalidArgument,
|
|
572
|
-
400,
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
if (state === TurnState.ENDED) return;
|
|
576
|
-
state = TurnState.ENDED;
|
|
577
|
-
|
|
578
|
-
try {
|
|
579
|
-
await turnManager.endTurn(turnId, reason);
|
|
580
|
-
} catch (error) {
|
|
581
|
-
const errInfo = new Ably.ErrorInfo(
|
|
582
|
-
`unable to publish turn-end for turn ${turnId}; ${error instanceof Error ? error.message : String(error)}`,
|
|
583
|
-
ErrorCode.TurnLifecycleError,
|
|
584
|
-
500,
|
|
585
|
-
error instanceof Ably.ErrorInfo ? error : undefined,
|
|
586
|
-
);
|
|
587
|
-
logger?.error('Turn.end(); failed to publish turn-end', { turnId });
|
|
588
|
-
throw errInfo;
|
|
589
|
-
} finally {
|
|
590
|
-
registeredTurns.delete(turnId);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
logger?.debug('Turn.end(); turn ended', { turnId, reason });
|
|
594
|
-
},
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
return turn;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// ---------------------------------------------------------------------------
|
|
602
|
-
// Factory
|
|
603
|
-
// ---------------------------------------------------------------------------
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Create a server transport bound to the given channel and codec.
|
|
607
|
-
* @param options - Transport configuration.
|
|
608
|
-
* @returns A new {@link ServerTransport} instance.
|
|
609
|
-
*/
|
|
610
|
-
export const createServerTransport = <TEvent, TMessage>(
|
|
611
|
-
options: ServerTransportOptions<TEvent, TMessage>,
|
|
612
|
-
): ServerTransport<TEvent, TMessage> => new DefaultServerTransport(options);
|