ai 7.0.134 → 7.0.136
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/CHANGELOG.md +24 -0
- package/dist/index.d.ts +234 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +616 -57
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +6 -2
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -1
- package/docs/03-agents/04-loop-control.mdx +2 -0
- package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +2 -2
- package/docs/03-ai-sdk-core/17-mcp-events.mdx +6 -2
- package/docs/03-ai-sdk-core/25-settings.mdx +4 -2
- package/docs/03-ai-sdk-core/30-embeddings.mdx +27 -0
- package/docs/03-ai-sdk-core/32-decisions.mdx +3 -2
- package/docs/04-ai-sdk-ui/21-transport.mdx +73 -0
- package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +2 -2
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +2 -2
- package/docs/07-reference/01-ai-sdk-core/05-embed.mdx +14 -0
- package/docs/07-reference/01-ai-sdk-core/06-embed-many.mdx +14 -0
- package/docs/07-reference/01-ai-sdk-core/16-tool-loop-agent.mdx +5 -5
- package/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx +2 -2
- package/docs/07-reference/01-ai-sdk-core/24-mcp-events.mdx +123 -5
- package/docs/07-reference/02-ai-sdk-ui/51-websocket-chat-transport.mdx +393 -0
- package/docs/07-reference/02-ai-sdk-ui/index.mdx +6 -0
- package/package.json +12 -12
- package/src/agent/default-stop-condition.ts +33 -0
- package/src/agent/tool-loop-agent.ts +2 -2
- package/src/embed/embed-events.ts +6 -0
- package/src/embed/embed-many.ts +18 -0
- package/src/embed/embed.ts +16 -0
- package/src/embed/validate-embedding-dimensions.ts +14 -0
- package/src/generate-text/stream-text.ts +31 -24
- package/src/prompt/request-options.ts +2 -2
- package/src/ui/chat-transport.ts +16 -0
- package/src/ui/chat.ts +123 -34
- package/src/ui/index.ts +13 -0
- package/src/ui/process-ui-message-stream.ts +6 -2
- package/src/ui/websocket-chat-transport.ts +987 -0
- package/src/util/serial-job-executor.ts +22 -5
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateId,
|
|
3
|
+
getWebSocketConstructor,
|
|
4
|
+
normalizeHeaders,
|
|
5
|
+
readWebSocketMessageText,
|
|
6
|
+
resolve,
|
|
7
|
+
safeParseJSON,
|
|
8
|
+
safeValidateTypes,
|
|
9
|
+
toWebSocketUrl,
|
|
10
|
+
waitForWebSocketBufferDrain,
|
|
11
|
+
type Resolvable,
|
|
12
|
+
type WebSocketConstructor,
|
|
13
|
+
type WebSocketLike,
|
|
14
|
+
} from '@ai-sdk/provider-utils';
|
|
15
|
+
import {
|
|
16
|
+
uiMessageChunkSchema,
|
|
17
|
+
type UIMessageChunk,
|
|
18
|
+
} from '../ui-message-stream/ui-message-chunks';
|
|
19
|
+
import type { ChatTransport } from './chat-transport';
|
|
20
|
+
import { safeValidateUIMessages } from './validate-ui-messages';
|
|
21
|
+
import type { UIMessage } from './ui-messages';
|
|
22
|
+
|
|
23
|
+
export type WebSocketChatTransportSendRequest<
|
|
24
|
+
UI_MESSAGE extends UIMessage = UIMessage,
|
|
25
|
+
> = {
|
|
26
|
+
type: 'send';
|
|
27
|
+
requestId: string;
|
|
28
|
+
id: string;
|
|
29
|
+
trigger: 'submit-message' | 'regenerate-message';
|
|
30
|
+
messageId: string | undefined;
|
|
31
|
+
messages: UI_MESSAGE[];
|
|
32
|
+
headers: Record<string, string>;
|
|
33
|
+
body: object;
|
|
34
|
+
metadata: unknown;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type WebSocketChatTransportResumeRequest = {
|
|
38
|
+
type: 'resume';
|
|
39
|
+
requestId: string;
|
|
40
|
+
id: string;
|
|
41
|
+
/** Omitted by this transport to request a complete UI message replay. */
|
|
42
|
+
lastSequence: number | undefined;
|
|
43
|
+
headers: Record<string, string>;
|
|
44
|
+
body: object;
|
|
45
|
+
metadata: unknown;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type WebSocketChatTransportAbortRequest = {
|
|
49
|
+
type: 'abort';
|
|
50
|
+
requestId: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A client-to-server frame used by `WebSocketChatTransport`.
|
|
55
|
+
*/
|
|
56
|
+
export type WebSocketChatTransportRequest<
|
|
57
|
+
UI_MESSAGE extends UIMessage = UIMessage,
|
|
58
|
+
> =
|
|
59
|
+
| WebSocketChatTransportSendRequest<UI_MESSAGE>
|
|
60
|
+
| WebSocketChatTransportResumeRequest
|
|
61
|
+
| WebSocketChatTransportAbortRequest;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A server-to-client frame used by `WebSocketChatTransport`.
|
|
65
|
+
*/
|
|
66
|
+
export type WebSocketChatTransportResponse =
|
|
67
|
+
| { type: 'start'; requestId: string }
|
|
68
|
+
| {
|
|
69
|
+
type: 'chunk';
|
|
70
|
+
requestId: string;
|
|
71
|
+
sequence: number;
|
|
72
|
+
chunk: UIMessageChunk;
|
|
73
|
+
}
|
|
74
|
+
| { type: 'end'; requestId: string }
|
|
75
|
+
| { type: 'error'; requestId: string; errorText?: string }
|
|
76
|
+
| { type: 'no-active'; requestId: string };
|
|
77
|
+
|
|
78
|
+
export type SafeValidateWebSocketChatTransportRequestResult<
|
|
79
|
+
UI_MESSAGE extends UIMessage,
|
|
80
|
+
> =
|
|
81
|
+
| {
|
|
82
|
+
success: true;
|
|
83
|
+
data: WebSocketChatTransportRequest<UI_MESSAGE>;
|
|
84
|
+
}
|
|
85
|
+
| {
|
|
86
|
+
success: false;
|
|
87
|
+
error: Error;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type PrepareWebSocketChatTransportSendMessagesRequest<
|
|
91
|
+
UI_MESSAGE extends UIMessage,
|
|
92
|
+
> = (options: {
|
|
93
|
+
id: string;
|
|
94
|
+
messages: UI_MESSAGE[];
|
|
95
|
+
requestMetadata: unknown;
|
|
96
|
+
body: object;
|
|
97
|
+
headers: Record<string, string>;
|
|
98
|
+
trigger: 'submit-message' | 'regenerate-message';
|
|
99
|
+
messageId: string | undefined;
|
|
100
|
+
}) =>
|
|
101
|
+
| {
|
|
102
|
+
messages?: UI_MESSAGE[];
|
|
103
|
+
body?: object;
|
|
104
|
+
headers?: HeadersInit;
|
|
105
|
+
}
|
|
106
|
+
| PromiseLike<{
|
|
107
|
+
messages?: UI_MESSAGE[];
|
|
108
|
+
body?: object;
|
|
109
|
+
headers?: HeadersInit;
|
|
110
|
+
}>;
|
|
111
|
+
|
|
112
|
+
export type PrepareWebSocketChatTransportReconnectToStreamRequest = (options: {
|
|
113
|
+
id: string;
|
|
114
|
+
requestMetadata: unknown;
|
|
115
|
+
body: object;
|
|
116
|
+
headers: Record<string, string>;
|
|
117
|
+
}) =>
|
|
118
|
+
| {
|
|
119
|
+
body?: object;
|
|
120
|
+
headers?: HeadersInit;
|
|
121
|
+
}
|
|
122
|
+
| PromiseLike<{
|
|
123
|
+
body?: object;
|
|
124
|
+
headers?: HeadersInit;
|
|
125
|
+
}>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Options for `WebSocketChatTransport`.
|
|
129
|
+
*/
|
|
130
|
+
export type WebSocketChatTransportInitOptions<UI_MESSAGE extends UIMessage> = {
|
|
131
|
+
/**
|
|
132
|
+
* WebSocket endpoint URL. HTTP(S) URLs are converted to WS(S), and relative
|
|
133
|
+
* URLs are resolved against the current browser location.
|
|
134
|
+
*/
|
|
135
|
+
url: string;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Optional WebSocket subprotocols used during the connection handshake.
|
|
139
|
+
*/
|
|
140
|
+
protocols?: string | string[];
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Query parameters appended when a new connection is opened.
|
|
144
|
+
*/
|
|
145
|
+
params?: Resolvable<Record<string, string>>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Headers included in each application-level request frame.
|
|
149
|
+
*
|
|
150
|
+
* Browser WebSocket APIs cannot set handshake headers. Use cookies, query
|
|
151
|
+
* parameters, or subprotocols for handshake authentication.
|
|
152
|
+
*/
|
|
153
|
+
headers?: Resolvable<Record<string, string> | Headers>;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extra application data included in each send and resume request.
|
|
157
|
+
*/
|
|
158
|
+
body?: Resolvable<object>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Custom WebSocket implementation, for example the `ws` package in Node.js.
|
|
162
|
+
*/
|
|
163
|
+
webSocket?: WebSocketConstructor;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Customizes send request frames before they are transmitted.
|
|
167
|
+
*/
|
|
168
|
+
prepareSendMessagesRequest?: PrepareWebSocketChatTransportSendMessagesRequest<UI_MESSAGE>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Customizes resume request frames before they are transmitted.
|
|
172
|
+
*/
|
|
173
|
+
prepareReconnectToStreamRequest?: PrepareWebSocketChatTransportReconnectToStreamRequest;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
type ConnectionState = {
|
|
177
|
+
promise: Promise<WebSocketLike>;
|
|
178
|
+
resolve: (socket: WebSocketLike) => void;
|
|
179
|
+
reject: (error: Error) => void;
|
|
180
|
+
socket?: WebSocketLike;
|
|
181
|
+
settled: boolean;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type ActiveRequest = {
|
|
185
|
+
requestId: string;
|
|
186
|
+
kind: 'send' | 'resume';
|
|
187
|
+
lastSequence: number;
|
|
188
|
+
stream: ReadableStream<UIMessageChunk>;
|
|
189
|
+
controller: ReadableStreamDefaultController<UIMessageChunk>;
|
|
190
|
+
responseSettled: boolean;
|
|
191
|
+
resolveResponse?: (stream: ReadableStream<UIMessageChunk> | null) => void;
|
|
192
|
+
rejectResponse?: (error: Error) => void;
|
|
193
|
+
abortSignal?: AbortSignal;
|
|
194
|
+
abortListener?: () => void;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const WEBSOCKET_OPEN_STATE = 1;
|
|
198
|
+
|
|
199
|
+
function createAbortError(signal?: AbortSignal): Error {
|
|
200
|
+
const reason = signal?.reason;
|
|
201
|
+
if (reason instanceof Error) {
|
|
202
|
+
return reason;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const error = new Error('The WebSocket chat request was aborted.');
|
|
206
|
+
error.name = 'AbortError';
|
|
207
|
+
return error;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function asError(error: unknown): Error {
|
|
211
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
215
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
219
|
+
return (
|
|
220
|
+
isRecord(value) &&
|
|
221
|
+
Object.values(value).every(item => typeof item === 'string')
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isSequence(value: unknown): value is number {
|
|
226
|
+
return Number.isSafeInteger(value) && (value as number) >= 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Validates an untrusted client-to-server WebSocket chat frame.
|
|
231
|
+
*/
|
|
232
|
+
export async function safeValidateWebSocketChatTransportRequest<
|
|
233
|
+
UI_MESSAGE extends UIMessage = UIMessage,
|
|
234
|
+
>({
|
|
235
|
+
value,
|
|
236
|
+
}: {
|
|
237
|
+
value: unknown;
|
|
238
|
+
}): Promise<SafeValidateWebSocketChatTransportRequestResult<UI_MESSAGE>> {
|
|
239
|
+
try {
|
|
240
|
+
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
241
|
+
throw new Error('Invalid WebSocket chat request frame.');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (value.type === 'abort') {
|
|
245
|
+
if (typeof value.requestId !== 'string') {
|
|
246
|
+
throw new Error('Invalid WebSocket chat abort request.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
success: true,
|
|
251
|
+
data: value as WebSocketChatTransportAbortRequest,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
typeof value.requestId !== 'string' ||
|
|
257
|
+
typeof value.id !== 'string' ||
|
|
258
|
+
!isStringRecord(value.headers) ||
|
|
259
|
+
!isRecord(value.body)
|
|
260
|
+
) {
|
|
261
|
+
throw new Error('Invalid WebSocket chat request frame.');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (value.type === 'resume') {
|
|
265
|
+
if (value.lastSequence !== undefined && !isSequence(value.lastSequence)) {
|
|
266
|
+
throw new Error('Invalid WebSocket chat resume sequence.');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
success: true,
|
|
271
|
+
data: value as WebSocketChatTransportResumeRequest,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (
|
|
276
|
+
value.type !== 'send' ||
|
|
277
|
+
!['submit-message', 'regenerate-message'].includes(
|
|
278
|
+
value.trigger as string,
|
|
279
|
+
) ||
|
|
280
|
+
(value.messageId !== undefined && typeof value.messageId !== 'string')
|
|
281
|
+
) {
|
|
282
|
+
throw new Error('Invalid WebSocket chat send request.');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const messages = await safeValidateUIMessages<UI_MESSAGE>({
|
|
286
|
+
messages: value.messages,
|
|
287
|
+
});
|
|
288
|
+
if (!messages.success) {
|
|
289
|
+
throw new Error('Invalid UI messages in WebSocket chat request.', {
|
|
290
|
+
cause: messages.error,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
success: true,
|
|
296
|
+
data: {
|
|
297
|
+
...(value as Omit<
|
|
298
|
+
WebSocketChatTransportSendRequest<UI_MESSAGE>,
|
|
299
|
+
'messages'
|
|
300
|
+
>),
|
|
301
|
+
messages: messages.data,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
error: asError(error),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* A `ChatTransport` that multiplexes correlated UI message streams over one
|
|
314
|
+
* persistent WebSocket connection.
|
|
315
|
+
*
|
|
316
|
+
* The server must implement the protocol represented by
|
|
317
|
+
* `WebSocketChatTransportRequest` and `WebSocketChatTransportResponse`.
|
|
318
|
+
*/
|
|
319
|
+
export class WebSocketChatTransport<
|
|
320
|
+
UI_MESSAGE extends UIMessage,
|
|
321
|
+
> implements ChatTransport<UI_MESSAGE> {
|
|
322
|
+
readonly resumeStreamIsReplay = true;
|
|
323
|
+
|
|
324
|
+
private readonly url: string;
|
|
325
|
+
private readonly protocols?: string | string[];
|
|
326
|
+
private readonly params?: Resolvable<Record<string, string>>;
|
|
327
|
+
private readonly headers?: Resolvable<Record<string, string> | Headers>;
|
|
328
|
+
private readonly body?: Resolvable<object>;
|
|
329
|
+
private readonly webSocket?: WebSocketConstructor;
|
|
330
|
+
private readonly prepareSendMessagesRequest?: PrepareWebSocketChatTransportSendMessagesRequest<UI_MESSAGE>;
|
|
331
|
+
private readonly prepareReconnectToStreamRequest?: PrepareWebSocketChatTransportReconnectToStreamRequest;
|
|
332
|
+
|
|
333
|
+
private connection?: ConnectionState;
|
|
334
|
+
private readonly activeRequests = new Map<string, ActiveRequest>();
|
|
335
|
+
private sendQueue: Promise<void> = Promise.resolve();
|
|
336
|
+
|
|
337
|
+
constructor(options: WebSocketChatTransportInitOptions<UI_MESSAGE>) {
|
|
338
|
+
this.url = options.url;
|
|
339
|
+
this.protocols = options.protocols;
|
|
340
|
+
this.params = options.params;
|
|
341
|
+
this.headers = options.headers;
|
|
342
|
+
this.body = options.body;
|
|
343
|
+
this.webSocket = options.webSocket;
|
|
344
|
+
this.prepareSendMessagesRequest = options.prepareSendMessagesRequest;
|
|
345
|
+
this.prepareReconnectToStreamRequest =
|
|
346
|
+
options.prepareReconnectToStreamRequest;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async sendMessages({
|
|
350
|
+
abortSignal,
|
|
351
|
+
...options
|
|
352
|
+
}: Parameters<ChatTransport<UI_MESSAGE>['sendMessages']>[0]): Promise<
|
|
353
|
+
ReadableStream<UIMessageChunk>
|
|
354
|
+
> {
|
|
355
|
+
this.throwIfAborted(abortSignal);
|
|
356
|
+
|
|
357
|
+
const [resolvedHeaders, resolvedBody] = await Promise.all([
|
|
358
|
+
resolve(this.headers),
|
|
359
|
+
resolve(this.body),
|
|
360
|
+
]);
|
|
361
|
+
this.throwIfAborted(abortSignal);
|
|
362
|
+
|
|
363
|
+
const headers = {
|
|
364
|
+
...normalizeHeaders(resolvedHeaders),
|
|
365
|
+
...normalizeHeaders(options.headers),
|
|
366
|
+
};
|
|
367
|
+
const body = { ...resolvedBody, ...options.body };
|
|
368
|
+
|
|
369
|
+
const preparedRequest = await this.prepareSendMessagesRequest?.({
|
|
370
|
+
id: options.chatId,
|
|
371
|
+
messages: options.messages,
|
|
372
|
+
requestMetadata: options.metadata,
|
|
373
|
+
body,
|
|
374
|
+
headers,
|
|
375
|
+
trigger: options.trigger,
|
|
376
|
+
messageId: options.messageId,
|
|
377
|
+
});
|
|
378
|
+
this.throwIfAborted(abortSignal);
|
|
379
|
+
|
|
380
|
+
const socket = await this.getSocket(abortSignal);
|
|
381
|
+
const requestId = generateId();
|
|
382
|
+
const request = this.createActiveRequest({
|
|
383
|
+
requestId,
|
|
384
|
+
kind: 'send',
|
|
385
|
+
lastSequence: -1,
|
|
386
|
+
abortSignal,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const frame: WebSocketChatTransportSendRequest<UI_MESSAGE> = {
|
|
390
|
+
type: 'send',
|
|
391
|
+
requestId,
|
|
392
|
+
id: options.chatId,
|
|
393
|
+
trigger: options.trigger,
|
|
394
|
+
messageId: options.messageId,
|
|
395
|
+
messages: preparedRequest?.messages ?? options.messages,
|
|
396
|
+
headers:
|
|
397
|
+
preparedRequest?.headers == null
|
|
398
|
+
? headers
|
|
399
|
+
: normalizeHeaders(preparedRequest.headers),
|
|
400
|
+
body: preparedRequest?.body ?? body,
|
|
401
|
+
metadata: options.metadata,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
await this.sendFrame(socket, frame, abortSignal);
|
|
406
|
+
return request.stream;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
const requestError = asError(error);
|
|
409
|
+
this.failRequest(request, requestError);
|
|
410
|
+
throw requestError;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async reconnectToStream(
|
|
415
|
+
options: Parameters<ChatTransport<UI_MESSAGE>['reconnectToStream']>[0],
|
|
416
|
+
): Promise<ReadableStream<UIMessageChunk> | null> {
|
|
417
|
+
this.throwIfAborted(options.abortSignal);
|
|
418
|
+
|
|
419
|
+
const [resolvedHeaders, resolvedBody] = await Promise.all([
|
|
420
|
+
resolve(this.headers),
|
|
421
|
+
resolve(this.body),
|
|
422
|
+
]);
|
|
423
|
+
this.throwIfAborted(options.abortSignal);
|
|
424
|
+
|
|
425
|
+
const headers = {
|
|
426
|
+
...normalizeHeaders(resolvedHeaders),
|
|
427
|
+
...normalizeHeaders(options.headers),
|
|
428
|
+
};
|
|
429
|
+
const body = { ...resolvedBody, ...options.body };
|
|
430
|
+
|
|
431
|
+
const preparedRequest = await this.prepareReconnectToStreamRequest?.({
|
|
432
|
+
id: options.chatId,
|
|
433
|
+
requestMetadata: options.metadata,
|
|
434
|
+
body,
|
|
435
|
+
headers,
|
|
436
|
+
});
|
|
437
|
+
this.throwIfAborted(options.abortSignal);
|
|
438
|
+
|
|
439
|
+
const socket = await this.getSocket(options.abortSignal);
|
|
440
|
+
const requestId = generateId();
|
|
441
|
+
|
|
442
|
+
let resolveResponse:
|
|
443
|
+
| ((stream: ReadableStream<UIMessageChunk> | null) => void)
|
|
444
|
+
| undefined;
|
|
445
|
+
let rejectResponse: ((error: Error) => void) | undefined;
|
|
446
|
+
const responsePromise = new Promise<ReadableStream<UIMessageChunk> | null>(
|
|
447
|
+
(resolve, reject) => {
|
|
448
|
+
resolveResponse = resolve;
|
|
449
|
+
rejectResponse = reject;
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
// The request can be aborted between registration and transmission. Mark
|
|
453
|
+
// the promise as handled immediately while preserving its rejection for
|
|
454
|
+
// the caller that receives it below.
|
|
455
|
+
void responsePromise.catch(() => {});
|
|
456
|
+
|
|
457
|
+
const request = this.createActiveRequest({
|
|
458
|
+
requestId,
|
|
459
|
+
kind: 'resume',
|
|
460
|
+
// Chat reconstructs a resumed message with a fresh stream parser. Replay
|
|
461
|
+
// from the beginning so text, reasoning, and tool deltas have their
|
|
462
|
+
// prerequisite chunks, even when this transport received them before.
|
|
463
|
+
lastSequence: -1,
|
|
464
|
+
abortSignal: options.abortSignal,
|
|
465
|
+
resolveResponse,
|
|
466
|
+
rejectResponse,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const frame: WebSocketChatTransportResumeRequest = {
|
|
470
|
+
type: 'resume',
|
|
471
|
+
requestId,
|
|
472
|
+
id: options.chatId,
|
|
473
|
+
lastSequence: undefined,
|
|
474
|
+
headers:
|
|
475
|
+
preparedRequest?.headers == null
|
|
476
|
+
? headers
|
|
477
|
+
: normalizeHeaders(preparedRequest.headers),
|
|
478
|
+
body: preparedRequest?.body ?? body,
|
|
479
|
+
metadata: options.metadata,
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
await this.sendFrame(socket, frame, options.abortSignal);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
const requestError = asError(error);
|
|
486
|
+
request.responseSettled = true;
|
|
487
|
+
this.failRequest(request, requestError);
|
|
488
|
+
throw requestError;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return responsePromise;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Closes the current connection and errors any active response streams.
|
|
496
|
+
* A later request opens a new connection.
|
|
497
|
+
*/
|
|
498
|
+
close(): void {
|
|
499
|
+
const error = new Error('WebSocket chat transport was closed.');
|
|
500
|
+
const connection = this.connection;
|
|
501
|
+
this.connection = undefined;
|
|
502
|
+
this.failAllRequests(error);
|
|
503
|
+
|
|
504
|
+
if (connection != null) {
|
|
505
|
+
this.rejectConnection(connection, error);
|
|
506
|
+
try {
|
|
507
|
+
connection.socket?.close(1000, 'Client closed transport');
|
|
508
|
+
} catch {
|
|
509
|
+
// The socket may already be closed.
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private throwIfAborted(abortSignal: AbortSignal | undefined): void {
|
|
515
|
+
if (abortSignal?.aborted) {
|
|
516
|
+
throw createAbortError(abortSignal);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private async getSocket(
|
|
521
|
+
abortSignal: AbortSignal | undefined,
|
|
522
|
+
): Promise<WebSocketLike> {
|
|
523
|
+
const existingSocket = this.connection?.socket;
|
|
524
|
+
if (existingSocket?.readyState === WEBSOCKET_OPEN_STATE) {
|
|
525
|
+
return existingSocket;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (this.connection == null) {
|
|
529
|
+
let resolveConnection!: (socket: WebSocketLike) => void;
|
|
530
|
+
let rejectConnection!: (error: Error) => void;
|
|
531
|
+
const promise = new Promise<WebSocketLike>((resolve, reject) => {
|
|
532
|
+
resolveConnection = resolve;
|
|
533
|
+
rejectConnection = reject;
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const connection: ConnectionState = {
|
|
537
|
+
promise,
|
|
538
|
+
resolve: resolveConnection,
|
|
539
|
+
reject: rejectConnection,
|
|
540
|
+
settled: false,
|
|
541
|
+
};
|
|
542
|
+
this.connection = connection;
|
|
543
|
+
void this.initializeConnection(connection);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const connection = this.connection;
|
|
547
|
+
if (connection == null) {
|
|
548
|
+
throw new Error('Failed to initialize WebSocket connection.');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return this.waitForConnection(connection.promise, abortSignal);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private async initializeConnection(
|
|
555
|
+
connection: ConnectionState,
|
|
556
|
+
): Promise<void> {
|
|
557
|
+
try {
|
|
558
|
+
const params = await resolve(this.params);
|
|
559
|
+
if (this.connection !== connection) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const url = this.buildUrl(params);
|
|
564
|
+
const WebSocketConstructor = getWebSocketConstructor(this.webSocket);
|
|
565
|
+
const socket = new WebSocketConstructor(url, this.protocols);
|
|
566
|
+
connection.socket = socket;
|
|
567
|
+
|
|
568
|
+
let messageTail: Promise<void> = Promise.resolve();
|
|
569
|
+
|
|
570
|
+
socket.onopen = () => {
|
|
571
|
+
if (this.connection !== connection) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
this.resolveConnection(connection, socket);
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
socket.onmessage = event => {
|
|
578
|
+
messageTail = messageTail
|
|
579
|
+
.then(() => readWebSocketMessageText(event.data))
|
|
580
|
+
.then(text => this.handleResponseFrame(connection, text))
|
|
581
|
+
.catch(error => {
|
|
582
|
+
this.terminateConnection(connection, asError(error));
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
socket.onerror = () => {
|
|
587
|
+
this.terminateConnection(
|
|
588
|
+
connection,
|
|
589
|
+
new TypeError('WebSocket chat network connection failed.'),
|
|
590
|
+
);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
socket.onclose = event => {
|
|
594
|
+
messageTail = messageTail
|
|
595
|
+
.then(() => {
|
|
596
|
+
if (this.connection !== connection) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const closeEvent = event as
|
|
601
|
+
| { code?: unknown; reason?: unknown }
|
|
602
|
+
| null
|
|
603
|
+
| undefined;
|
|
604
|
+
const code =
|
|
605
|
+
typeof closeEvent?.code === 'number'
|
|
606
|
+
? closeEvent.code
|
|
607
|
+
: undefined;
|
|
608
|
+
const reason =
|
|
609
|
+
typeof closeEvent?.reason === 'string' && closeEvent.reason !== ''
|
|
610
|
+
? `: ${closeEvent.reason}`
|
|
611
|
+
: '';
|
|
612
|
+
|
|
613
|
+
this.terminateConnection(
|
|
614
|
+
connection,
|
|
615
|
+
new TypeError(
|
|
616
|
+
`WebSocket chat network connection closed${
|
|
617
|
+
code == null ? '' : ` (code ${code}${reason})`
|
|
618
|
+
}.`,
|
|
619
|
+
),
|
|
620
|
+
false,
|
|
621
|
+
);
|
|
622
|
+
})
|
|
623
|
+
.catch(error => {
|
|
624
|
+
this.terminateConnection(connection, asError(error));
|
|
625
|
+
});
|
|
626
|
+
};
|
|
627
|
+
} catch (error) {
|
|
628
|
+
this.terminateConnection(connection, asError(error), false);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private buildUrl(params: Record<string, string> | undefined): URL {
|
|
633
|
+
let url: URL;
|
|
634
|
+
try {
|
|
635
|
+
const baseUrl =
|
|
636
|
+
typeof globalThis.location === 'undefined'
|
|
637
|
+
? undefined
|
|
638
|
+
: globalThis.location.href;
|
|
639
|
+
url = new URL(this.url, baseUrl);
|
|
640
|
+
url = toWebSocketUrl(url);
|
|
641
|
+
} catch {
|
|
642
|
+
throw new Error(
|
|
643
|
+
'Invalid WebSocket chat URL. Use an absolute URL outside the browser.',
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (!['ws:', 'wss:'].includes(url.protocol) || url.hostname === '') {
|
|
648
|
+
throw new Error('Invalid WebSocket chat URL. Expected ws:// or wss://.');
|
|
649
|
+
}
|
|
650
|
+
if (url.hash !== '') {
|
|
651
|
+
throw new Error('Invalid WebSocket chat URL. Fragments are not allowed.');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
655
|
+
url.searchParams.set(key, value);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return url;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private waitForConnection(
|
|
662
|
+
promise: Promise<WebSocketLike>,
|
|
663
|
+
abortSignal: AbortSignal | undefined,
|
|
664
|
+
): Promise<WebSocketLike> {
|
|
665
|
+
if (abortSignal == null) {
|
|
666
|
+
return promise;
|
|
667
|
+
}
|
|
668
|
+
if (abortSignal.aborted) {
|
|
669
|
+
return Promise.reject(createAbortError(abortSignal));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return new Promise<WebSocketLike>((resolve, reject) => {
|
|
673
|
+
const onAbort = () => {
|
|
674
|
+
reject(createAbortError(abortSignal));
|
|
675
|
+
};
|
|
676
|
+
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
677
|
+
|
|
678
|
+
void promise.then(
|
|
679
|
+
socket => {
|
|
680
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
681
|
+
resolve(socket);
|
|
682
|
+
},
|
|
683
|
+
error => {
|
|
684
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
685
|
+
reject(error);
|
|
686
|
+
},
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
private resolveConnection(
|
|
692
|
+
connection: ConnectionState,
|
|
693
|
+
socket: WebSocketLike,
|
|
694
|
+
): void {
|
|
695
|
+
if (connection.settled) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
connection.settled = true;
|
|
699
|
+
connection.resolve(socket);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private rejectConnection(connection: ConnectionState, error: Error): void {
|
|
703
|
+
if (connection.settled) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
connection.settled = true;
|
|
707
|
+
connection.reject(error);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private terminateConnection(
|
|
711
|
+
connection: ConnectionState,
|
|
712
|
+
error: Error,
|
|
713
|
+
closeSocket = true,
|
|
714
|
+
): void {
|
|
715
|
+
if (this.connection !== connection) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
this.connection = undefined;
|
|
720
|
+
this.rejectConnection(connection, error);
|
|
721
|
+
this.failAllRequests(error);
|
|
722
|
+
|
|
723
|
+
if (closeSocket) {
|
|
724
|
+
try {
|
|
725
|
+
connection.socket?.close(1011, 'WebSocket chat transport error');
|
|
726
|
+
} catch {
|
|
727
|
+
// The socket may already be closed.
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private createActiveRequest({
|
|
733
|
+
requestId,
|
|
734
|
+
kind,
|
|
735
|
+
lastSequence,
|
|
736
|
+
abortSignal,
|
|
737
|
+
resolveResponse,
|
|
738
|
+
rejectResponse,
|
|
739
|
+
}: {
|
|
740
|
+
requestId: string;
|
|
741
|
+
kind: ActiveRequest['kind'];
|
|
742
|
+
lastSequence: number;
|
|
743
|
+
abortSignal: AbortSignal | undefined;
|
|
744
|
+
resolveResponse?: ActiveRequest['resolveResponse'];
|
|
745
|
+
rejectResponse?: ActiveRequest['rejectResponse'];
|
|
746
|
+
}): ActiveRequest {
|
|
747
|
+
let controller!: ReadableStreamDefaultController<UIMessageChunk>;
|
|
748
|
+
const stream = new ReadableStream<UIMessageChunk>({
|
|
749
|
+
start(streamController) {
|
|
750
|
+
controller = streamController;
|
|
751
|
+
},
|
|
752
|
+
cancel: () => {
|
|
753
|
+
this.abortRequest(requestId, false);
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const request: ActiveRequest = {
|
|
758
|
+
requestId,
|
|
759
|
+
kind,
|
|
760
|
+
lastSequence,
|
|
761
|
+
stream,
|
|
762
|
+
controller,
|
|
763
|
+
responseSettled: false,
|
|
764
|
+
resolveResponse,
|
|
765
|
+
rejectResponse,
|
|
766
|
+
abortSignal,
|
|
767
|
+
};
|
|
768
|
+
this.activeRequests.set(requestId, request);
|
|
769
|
+
|
|
770
|
+
if (abortSignal != null) {
|
|
771
|
+
const abortListener = () => {
|
|
772
|
+
this.abortRequest(requestId, true);
|
|
773
|
+
};
|
|
774
|
+
request.abortListener = abortListener;
|
|
775
|
+
abortSignal.addEventListener('abort', abortListener, { once: true });
|
|
776
|
+
if (abortSignal.aborted) {
|
|
777
|
+
abortListener();
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return request;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private abortRequest(requestId: string, closeStream: boolean): void {
|
|
785
|
+
const request = this.activeRequests.get(requestId);
|
|
786
|
+
if (request == null) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const error = createAbortError(request.abortSignal);
|
|
791
|
+
if (request.kind === 'resume' && !request.responseSettled) {
|
|
792
|
+
request.responseSettled = true;
|
|
793
|
+
request.rejectResponse?.(error);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
this.cleanupRequest(request);
|
|
797
|
+
if (closeStream) {
|
|
798
|
+
try {
|
|
799
|
+
request.controller.close();
|
|
800
|
+
} catch {
|
|
801
|
+
// The stream may already be closed or canceled.
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const socket = this.connection?.socket;
|
|
806
|
+
if (socket?.readyState === WEBSOCKET_OPEN_STATE) {
|
|
807
|
+
void this.sendFrame(socket, {
|
|
808
|
+
type: 'abort',
|
|
809
|
+
requestId,
|
|
810
|
+
} satisfies WebSocketChatTransportAbortRequest).catch(() => {
|
|
811
|
+
// Aborting is best-effort once the local stream has been closed.
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
private cleanupRequest(request: ActiveRequest): void {
|
|
817
|
+
this.activeRequests.delete(request.requestId);
|
|
818
|
+
if (request.abortListener != null) {
|
|
819
|
+
request.abortSignal?.removeEventListener('abort', request.abortListener);
|
|
820
|
+
request.abortListener = undefined;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private failRequest(request: ActiveRequest, error: Error): void {
|
|
825
|
+
if (!this.activeRequests.has(request.requestId)) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (request.kind === 'resume' && !request.responseSettled) {
|
|
830
|
+
request.responseSettled = true;
|
|
831
|
+
request.rejectResponse?.(error);
|
|
832
|
+
}
|
|
833
|
+
this.cleanupRequest(request);
|
|
834
|
+
try {
|
|
835
|
+
request.controller.error(error);
|
|
836
|
+
} catch {
|
|
837
|
+
// The stream may already be closed or canceled.
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
private failAllRequests(error: Error): void {
|
|
842
|
+
for (const request of [...this.activeRequests.values()]) {
|
|
843
|
+
this.failRequest(request, error);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
private resolveResumeStream(request: ActiveRequest): void {
|
|
848
|
+
if (request.kind !== 'resume' || request.responseSettled) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
request.responseSettled = true;
|
|
852
|
+
request.resolveResponse?.(request.stream);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
private async validateResponseChunk(value: unknown): Promise<UIMessageChunk> {
|
|
856
|
+
const chunk = await safeValidateTypes<UIMessageChunk>({
|
|
857
|
+
value,
|
|
858
|
+
schema: uiMessageChunkSchema,
|
|
859
|
+
});
|
|
860
|
+
if (!chunk.success) {
|
|
861
|
+
throw new Error('Invalid UI message chunk in WebSocket response.');
|
|
862
|
+
}
|
|
863
|
+
return chunk.value;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private async handleResponseFrame(
|
|
867
|
+
connection: ConnectionState,
|
|
868
|
+
text: string,
|
|
869
|
+
): Promise<void> {
|
|
870
|
+
if (this.connection !== connection) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const parsed = await safeParseJSON({ text });
|
|
875
|
+
if (!parsed.success || !isRecord(parsed.value)) {
|
|
876
|
+
throw new Error('Invalid WebSocket chat response frame.');
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const type = parsed.value.type;
|
|
880
|
+
const requestId = parsed.value.requestId;
|
|
881
|
+
if (typeof type !== 'string' || typeof requestId !== 'string') {
|
|
882
|
+
throw new Error('Invalid WebSocket chat response frame.');
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const request = this.activeRequests.get(requestId);
|
|
886
|
+
if (request == null) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
switch (type) {
|
|
891
|
+
case 'start': {
|
|
892
|
+
this.resolveResumeStream(request);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
case 'chunk': {
|
|
897
|
+
if (!isSequence(parsed.value.sequence)) {
|
|
898
|
+
throw new Error('Invalid WebSocket chat chunk sequence.');
|
|
899
|
+
}
|
|
900
|
+
if (parsed.value.sequence <= request.lastSequence) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (parsed.value.sequence !== request.lastSequence + 1) {
|
|
904
|
+
throw new Error('Out-of-order WebSocket chat chunk sequence.');
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const chunk = await this.validateResponseChunk(parsed.value.chunk);
|
|
908
|
+
|
|
909
|
+
// Validation is asynchronous, so the request may have been aborted or
|
|
910
|
+
// canceled while it was in progress. Ignore the stale frame instead of
|
|
911
|
+
// touching a closed controller and terminating the shared connection.
|
|
912
|
+
if (this.activeRequests.get(requestId) !== request) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
this.resolveResumeStream(request);
|
|
917
|
+
try {
|
|
918
|
+
request.controller.enqueue(chunk);
|
|
919
|
+
} catch {
|
|
920
|
+
// The stream may already be closed or canceled.
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
request.lastSequence = parsed.value.sequence;
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
case 'end': {
|
|
928
|
+
this.resolveResumeStream(request);
|
|
929
|
+
this.cleanupRequest(request);
|
|
930
|
+
request.controller.close();
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
case 'no-active': {
|
|
935
|
+
if (request.kind === 'resume' && !request.responseSettled) {
|
|
936
|
+
request.responseSettled = true;
|
|
937
|
+
request.resolveResponse?.(null);
|
|
938
|
+
}
|
|
939
|
+
this.cleanupRequest(request);
|
|
940
|
+
request.controller.close();
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
case 'error': {
|
|
945
|
+
if (
|
|
946
|
+
parsed.value.errorText !== undefined &&
|
|
947
|
+
typeof parsed.value.errorText !== 'string'
|
|
948
|
+
) {
|
|
949
|
+
throw new Error('Invalid WebSocket chat error response.');
|
|
950
|
+
}
|
|
951
|
+
this.failRequest(
|
|
952
|
+
request,
|
|
953
|
+
new Error(
|
|
954
|
+
parsed.value.errorText ??
|
|
955
|
+
'WebSocket chat server returned an error.',
|
|
956
|
+
),
|
|
957
|
+
);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
default:
|
|
962
|
+
throw new Error(`Unknown WebSocket chat response type: ${type}.`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private async sendFrame(
|
|
967
|
+
socket: WebSocketLike,
|
|
968
|
+
frame: WebSocketChatTransportRequest<UI_MESSAGE>,
|
|
969
|
+
abortSignal?: AbortSignal,
|
|
970
|
+
): Promise<void> {
|
|
971
|
+
const send = this.sendQueue.then(async () => {
|
|
972
|
+
await waitForWebSocketBufferDrain(socket, { abortSignal });
|
|
973
|
+
this.throwIfAborted(abortSignal);
|
|
974
|
+
|
|
975
|
+
if (
|
|
976
|
+
this.connection?.socket !== socket ||
|
|
977
|
+
socket.readyState !== WEBSOCKET_OPEN_STATE
|
|
978
|
+
) {
|
|
979
|
+
throw new TypeError('WebSocket chat network connection is not open.');
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
socket.send(JSON.stringify(frame));
|
|
983
|
+
});
|
|
984
|
+
this.sendQueue = send.catch(() => {});
|
|
985
|
+
return send;
|
|
986
|
+
}
|
|
987
|
+
}
|