@ai-sdk/harness 1.0.163 → 1.0.165
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 +23 -0
- package/README.md +4 -24
- package/dist/agent/index.js.map +1 -1
- package/dist/utils/index.d.ts +106 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +280 -61
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -5
- package/src/utils/bridge-endpoint.ts +106 -0
- package/src/utils/bridge-web-socket.ts +286 -0
- package/src/utils/index.ts +11 -1
- package/src/utils/sandbox-channel.ts +11 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.165",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@ai-sdk/provider": "4.0.
|
|
48
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
49
|
-
"ai": "7.0.
|
|
47
|
+
"@ai-sdk/provider": "4.0.34",
|
|
48
|
+
"@ai-sdk/provider-utils": "5.0.69",
|
|
49
|
+
"ai": "7.0.154"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"ws": "^8.21.0",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@ai-sdk/otel": "1.0.
|
|
61
|
+
"@ai-sdk/otel": "1.0.154",
|
|
62
62
|
"@opentelemetry/sdk-trace-base": "2.7.1",
|
|
63
63
|
"@types/node": "22.19.19",
|
|
64
64
|
"@types/ws": "^8.5.13",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
|
+
import { HarnessCapabilityUnsupportedError } from '../errors/harness-capability-unsupported-error';
|
|
3
|
+
import type {
|
|
4
|
+
HarnessV1MintBridgeTokenCallback,
|
|
5
|
+
HarnessV1NetworkSandboxSession,
|
|
6
|
+
HarnessV1PortEndpoint,
|
|
7
|
+
} from '../v1';
|
|
8
|
+
import { withBridgeToken } from './bridge-token';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates the bridge connection settings against the sandbox session before
|
|
12
|
+
* any bridge work starts.
|
|
13
|
+
*
|
|
14
|
+
* Basic sandbox sessions cannot expose ports, so they need both an explicit
|
|
15
|
+
* `port` and `portEndpoint` setting. A custom `mintBridgeToken` callback needs
|
|
16
|
+
* a sandbox id to mint the token for.
|
|
17
|
+
*/
|
|
18
|
+
export function validateBridgeSandboxSettings({
|
|
19
|
+
sandboxSession,
|
|
20
|
+
sandboxId,
|
|
21
|
+
portOverride,
|
|
22
|
+
portEndpointOverride,
|
|
23
|
+
mintBridgeToken,
|
|
24
|
+
}: {
|
|
25
|
+
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
26
|
+
sandboxId: string | undefined;
|
|
27
|
+
portOverride: number | undefined;
|
|
28
|
+
portEndpointOverride: HarnessV1PortEndpoint | undefined;
|
|
29
|
+
mintBridgeToken: HarnessV1MintBridgeTokenCallback | undefined;
|
|
30
|
+
}): void {
|
|
31
|
+
if (!('getPortEndpoint' in sandboxSession)) {
|
|
32
|
+
if (portOverride == null) {
|
|
33
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
34
|
+
message:
|
|
35
|
+
'The harness requires an explicit `port` setting when using a basic sandbox session.',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (portEndpointOverride == null) {
|
|
39
|
+
throw createMissingPortEndpointError();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (mintBridgeToken != null && sandboxId == null) {
|
|
43
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
44
|
+
message:
|
|
45
|
+
'The harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id.',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the port the bridge listens on inside the sandbox: the `port`
|
|
52
|
+
* setting if present, otherwise the first port the sandbox exposes.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveBridgePort({
|
|
55
|
+
sandboxSession,
|
|
56
|
+
portOverride,
|
|
57
|
+
}: {
|
|
58
|
+
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
59
|
+
portOverride: number | undefined;
|
|
60
|
+
}): number {
|
|
61
|
+
if (portOverride !== undefined) return portOverride;
|
|
62
|
+
if ('ports' in sandboxSession && sandboxSession.ports.length > 0) {
|
|
63
|
+
return sandboxSession.ports[0];
|
|
64
|
+
}
|
|
65
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
66
|
+
message:
|
|
67
|
+
'The harness needs a TCP port exposed by the sandbox. ' +
|
|
68
|
+
'Create the sandbox with `ports: [<port>]` or configure the harness `port` setting.',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolves the host-reachable WebSocket endpoint for a bridge port and
|
|
74
|
+
* authenticates it with the bridge token.
|
|
75
|
+
*
|
|
76
|
+
* A `portEndpoint` setting takes precedence and is used as-is regardless of
|
|
77
|
+
* `port`, so it must point at the port the bridge actually listens on.
|
|
78
|
+
*/
|
|
79
|
+
export async function resolveBridgeEndpoint({
|
|
80
|
+
sandboxSession,
|
|
81
|
+
port,
|
|
82
|
+
portEndpointOverride,
|
|
83
|
+
token,
|
|
84
|
+
}: {
|
|
85
|
+
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
86
|
+
port: number;
|
|
87
|
+
portEndpointOverride: HarnessV1PortEndpoint | undefined;
|
|
88
|
+
token: string;
|
|
89
|
+
}): Promise<HarnessV1PortEndpoint> {
|
|
90
|
+
let endpoint: HarnessV1PortEndpoint;
|
|
91
|
+
if (portEndpointOverride != null) {
|
|
92
|
+
endpoint = portEndpointOverride;
|
|
93
|
+
} else if ('getPortEndpoint' in sandboxSession) {
|
|
94
|
+
endpoint = await sandboxSession.getPortEndpoint({ port, protocol: 'ws' });
|
|
95
|
+
} else {
|
|
96
|
+
throw createMissingPortEndpointError();
|
|
97
|
+
}
|
|
98
|
+
return withBridgeToken({ endpoint, token });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createMissingPortEndpointError(): HarnessCapabilityUnsupportedError {
|
|
102
|
+
return new HarnessCapabilityUnsupportedError({
|
|
103
|
+
message:
|
|
104
|
+
'The harness requires an explicit `portEndpoint` setting when using a basic sandbox session.',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { safeParseJSON } from '@ai-sdk/provider-utils';
|
|
2
|
+
import type { WebSocket } from 'ws';
|
|
3
|
+
import type { z } from 'zod/v4';
|
|
4
|
+
import { HarnessError } from '../errors/harness-error';
|
|
5
|
+
import type { HarnessV1PortEndpoint } from '../v1';
|
|
6
|
+
import { harnessV1BridgeHelloSchema } from '../v1/harness-v1-bridge-protocol';
|
|
7
|
+
import type { SandboxChannelConnectOptions } from './sandbox-channel';
|
|
8
|
+
import { sleep } from './sleep';
|
|
9
|
+
|
|
10
|
+
export type BridgeHello = z.infer<typeof harnessV1BridgeHelloSchema>;
|
|
11
|
+
|
|
12
|
+
export interface CreateBridgeWebSocketConnectOptions {
|
|
13
|
+
/** Bridge endpoint, already authenticated with the bridge token. */
|
|
14
|
+
endpoint: HarnessV1PortEndpoint;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `WebSocket` class from the `ws` package. Passed in by the adapter because
|
|
18
|
+
* `ws` is an optional peer dependency of `@ai-sdk/harness`.
|
|
19
|
+
*/
|
|
20
|
+
webSocketConstructor: typeof WebSocket;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Retry budget in milliseconds for the channel's initial open (connect
|
|
24
|
+
* reason `'open'`). Omit for a single attempt. Reconnects always make a
|
|
25
|
+
* single attempt, because the channel retries them itself.
|
|
26
|
+
*/
|
|
27
|
+
openRetryTimeoutMs?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Abort signal that additionally cancels the channel's initial open, e.g.
|
|
31
|
+
* the abort signal of the harness start call. Not applied to reconnects.
|
|
32
|
+
*/
|
|
33
|
+
startAbortSignal?: AbortSignal;
|
|
34
|
+
|
|
35
|
+
/** Called with the `bridge-hello` frame after every successful connect. */
|
|
36
|
+
onHello?: (hello: BridgeHello) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const OPEN_TIMEOUT_MS = 10_000;
|
|
40
|
+
const HELLO_TIMEOUT_MS = 5_000;
|
|
41
|
+
const UNAUTHORIZED_CLOSE_CODE = 1008;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates the `connect` function for a bridge `SandboxChannel`.
|
|
45
|
+
*
|
|
46
|
+
* A connection only counts as established once the socket is open and the
|
|
47
|
+
* bridge has sent `bridge-hello`. Some sandbox runtimes complete the host-side
|
|
48
|
+
* WebSocket upgrade before the connection reaches the bridge process, and
|
|
49
|
+
* frames sent in that gap are dropped. The bridge sends `bridge-hello` the
|
|
50
|
+
* instant it accepts a connection, and sends nothing else until the host sends
|
|
51
|
+
* `start` or `resume`, so no frame is lost between the handshake and the
|
|
52
|
+
* channel wiring the socket.
|
|
53
|
+
*/
|
|
54
|
+
export function createBridgeWebSocketConnect({
|
|
55
|
+
endpoint,
|
|
56
|
+
webSocketConstructor,
|
|
57
|
+
openRetryTimeoutMs,
|
|
58
|
+
startAbortSignal,
|
|
59
|
+
onHello,
|
|
60
|
+
}: CreateBridgeWebSocketConnectOptions): (
|
|
61
|
+
options: SandboxChannelConnectOptions,
|
|
62
|
+
) => Promise<WebSocket> {
|
|
63
|
+
return async ({ abortSignal, reason }) => {
|
|
64
|
+
const isInitialOpen = reason === 'open';
|
|
65
|
+
const connectAbortSignal =
|
|
66
|
+
isInitialOpen && startAbortSignal != null
|
|
67
|
+
? AbortSignal.any([abortSignal, startAbortSignal])
|
|
68
|
+
: abortSignal;
|
|
69
|
+
|
|
70
|
+
const { ws, hello } =
|
|
71
|
+
isInitialOpen && openRetryTimeoutMs != null
|
|
72
|
+
? await openBridgeWebSocketWithRetry({
|
|
73
|
+
endpoint,
|
|
74
|
+
webSocketConstructor,
|
|
75
|
+
abortSignal: connectAbortSignal,
|
|
76
|
+
retryTimeoutMs: openRetryTimeoutMs,
|
|
77
|
+
})
|
|
78
|
+
: await openBridgeWebSocket({
|
|
79
|
+
endpoint,
|
|
80
|
+
webSocketConstructor,
|
|
81
|
+
abortSignal: connectAbortSignal,
|
|
82
|
+
openTimeoutMs: OPEN_TIMEOUT_MS,
|
|
83
|
+
getHelloTimeoutMs: () => HELLO_TIMEOUT_MS,
|
|
84
|
+
});
|
|
85
|
+
onHello?.(hello);
|
|
86
|
+
return ws;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class BridgeWebSocketUnauthorizedError extends Error {}
|
|
91
|
+
|
|
92
|
+
async function openBridgeWebSocketWithRetry({
|
|
93
|
+
endpoint,
|
|
94
|
+
webSocketConstructor,
|
|
95
|
+
abortSignal,
|
|
96
|
+
retryTimeoutMs,
|
|
97
|
+
}: {
|
|
98
|
+
endpoint: HarnessV1PortEndpoint;
|
|
99
|
+
webSocketConstructor: typeof WebSocket;
|
|
100
|
+
abortSignal: AbortSignal;
|
|
101
|
+
retryTimeoutMs: number;
|
|
102
|
+
}): Promise<{ ws: WebSocket; hello: BridgeHello }> {
|
|
103
|
+
const deadline = Date.now() + retryTimeoutMs;
|
|
104
|
+
let attempt = 0;
|
|
105
|
+
let lastError: unknown;
|
|
106
|
+
|
|
107
|
+
while (!abortSignal.aborted && Date.now() < deadline) {
|
|
108
|
+
attempt++;
|
|
109
|
+
try {
|
|
110
|
+
const remaining = Math.max(1, deadline - Date.now());
|
|
111
|
+
return await openBridgeWebSocket({
|
|
112
|
+
endpoint,
|
|
113
|
+
webSocketConstructor,
|
|
114
|
+
abortSignal,
|
|
115
|
+
openTimeoutMs: Math.min(OPEN_TIMEOUT_MS, remaining),
|
|
116
|
+
getHelloTimeoutMs: () =>
|
|
117
|
+
Math.min(HELLO_TIMEOUT_MS, Math.max(1, deadline - Date.now())),
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (abortSignal.aborted) throw getAbortReason({ abortSignal });
|
|
121
|
+
if (error instanceof BridgeWebSocketUnauthorizedError) throw error;
|
|
122
|
+
lastError = error;
|
|
123
|
+
const remaining = deadline - Date.now();
|
|
124
|
+
if (remaining <= 0) break;
|
|
125
|
+
await sleep({
|
|
126
|
+
ms: Math.min(250 * attempt, 1_000, remaining),
|
|
127
|
+
abortSignal,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (abortSignal.aborted) throw getAbortReason({ abortSignal });
|
|
133
|
+
|
|
134
|
+
throw new HarnessError({
|
|
135
|
+
message: `The harness bridge did not complete the WebSocket handshake within ${retryTimeoutMs}ms after ${attempt} attempt(s).`,
|
|
136
|
+
cause: lastError,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function openBridgeWebSocket({
|
|
141
|
+
endpoint,
|
|
142
|
+
webSocketConstructor,
|
|
143
|
+
abortSignal,
|
|
144
|
+
openTimeoutMs,
|
|
145
|
+
getHelloTimeoutMs,
|
|
146
|
+
}: {
|
|
147
|
+
endpoint: HarnessV1PortEndpoint;
|
|
148
|
+
webSocketConstructor: typeof WebSocket;
|
|
149
|
+
abortSignal: AbortSignal;
|
|
150
|
+
openTimeoutMs: number;
|
|
151
|
+
getHelloTimeoutMs: () => number;
|
|
152
|
+
}): Promise<{ ws: WebSocket; hello: BridgeHello }> {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
if (abortSignal.aborted) {
|
|
155
|
+
reject(getAbortReason({ abortSignal }));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const ws = new webSocketConstructor(endpoint.url, {
|
|
160
|
+
headers: endpoint.headers == null ? undefined : { ...endpoint.headers },
|
|
161
|
+
});
|
|
162
|
+
let opened = false;
|
|
163
|
+
let hello: BridgeHello | undefined;
|
|
164
|
+
let settled = false;
|
|
165
|
+
let openTimer: ReturnType<typeof setTimeout> | undefined;
|
|
166
|
+
let helloTimer: ReturnType<typeof setTimeout> | undefined;
|
|
167
|
+
|
|
168
|
+
const cleanup = () => {
|
|
169
|
+
clearTimeout(openTimer);
|
|
170
|
+
clearTimeout(helloTimer);
|
|
171
|
+
ws.off('open', onOpen);
|
|
172
|
+
ws.off('message', onMessage);
|
|
173
|
+
ws.off('close', onClose);
|
|
174
|
+
ws.off('error', onError);
|
|
175
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
176
|
+
};
|
|
177
|
+
const fail = (error: unknown) => {
|
|
178
|
+
if (settled) return;
|
|
179
|
+
settled = true;
|
|
180
|
+
cleanup();
|
|
181
|
+
/*
|
|
182
|
+
* Terminating a socket that is still connecting makes `ws` emit an
|
|
183
|
+
* `error` event. Keep a no-op listener so that event cannot surface as
|
|
184
|
+
* an unhandled error and the original failure reason is preserved.
|
|
185
|
+
*/
|
|
186
|
+
ws.on('error', () => {});
|
|
187
|
+
try {
|
|
188
|
+
ws.terminate();
|
|
189
|
+
} catch {
|
|
190
|
+
try {
|
|
191
|
+
ws.close();
|
|
192
|
+
} catch {
|
|
193
|
+
// best-effort
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
reject(error);
|
|
197
|
+
};
|
|
198
|
+
const tryResolve = () => {
|
|
199
|
+
if (settled || !opened || hello == null) return;
|
|
200
|
+
settled = true;
|
|
201
|
+
cleanup();
|
|
202
|
+
resolve({ ws, hello });
|
|
203
|
+
};
|
|
204
|
+
const onOpen = () => {
|
|
205
|
+
opened = true;
|
|
206
|
+
clearTimeout(openTimer);
|
|
207
|
+
const helloTimeoutMs = getHelloTimeoutMs();
|
|
208
|
+
helloTimer = setTimeout(
|
|
209
|
+
() =>
|
|
210
|
+
fail(
|
|
211
|
+
new Error(
|
|
212
|
+
`The harness bridge did not send bridge-hello within ${helloTimeoutMs}ms.`,
|
|
213
|
+
),
|
|
214
|
+
),
|
|
215
|
+
helloTimeoutMs,
|
|
216
|
+
);
|
|
217
|
+
unrefTimer(helloTimer);
|
|
218
|
+
tryResolve();
|
|
219
|
+
};
|
|
220
|
+
const onMessage = (raw: unknown) => {
|
|
221
|
+
void (async () => {
|
|
222
|
+
const parsed = await safeParseJSON({
|
|
223
|
+
text: webSocketMessageToString(raw),
|
|
224
|
+
schema: harnessV1BridgeHelloSchema,
|
|
225
|
+
});
|
|
226
|
+
if (!parsed.success || settled) return;
|
|
227
|
+
hello = parsed.value;
|
|
228
|
+
tryResolve();
|
|
229
|
+
})();
|
|
230
|
+
};
|
|
231
|
+
const onClose = (code: number) => {
|
|
232
|
+
fail(
|
|
233
|
+
code === UNAUTHORIZED_CLOSE_CODE
|
|
234
|
+
? new BridgeWebSocketUnauthorizedError(
|
|
235
|
+
`The harness bridge rejected the WebSocket connection as unauthorized (code ${code}).`,
|
|
236
|
+
)
|
|
237
|
+
: new Error(
|
|
238
|
+
`The harness bridge closed the WebSocket (code ${code}) before sending bridge-hello.`,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
const onError = (error: Error) => fail(error);
|
|
243
|
+
const onAbort = () => fail(getAbortReason({ abortSignal }));
|
|
244
|
+
|
|
245
|
+
ws.on('open', onOpen);
|
|
246
|
+
ws.on('message', onMessage);
|
|
247
|
+
ws.on('close', onClose);
|
|
248
|
+
ws.on('error', onError);
|
|
249
|
+
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
250
|
+
openTimer = setTimeout(
|
|
251
|
+
() =>
|
|
252
|
+
fail(
|
|
253
|
+
new Error(
|
|
254
|
+
`The harness bridge WebSocket did not open within ${openTimeoutMs}ms.`,
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
openTimeoutMs,
|
|
258
|
+
);
|
|
259
|
+
unrefTimer(openTimer);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getAbortReason({
|
|
264
|
+
abortSignal,
|
|
265
|
+
}: {
|
|
266
|
+
abortSignal: AbortSignal;
|
|
267
|
+
}): unknown {
|
|
268
|
+
return abortSignal.reason ?? new Error('WebSocket connection aborted');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
|
|
272
|
+
(timer as { unref?: () => void }).unref?.();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function webSocketMessageToString(raw: unknown): string {
|
|
276
|
+
if (typeof raw === 'string') return raw;
|
|
277
|
+
if (Buffer.isBuffer(raw)) return raw.toString('utf8');
|
|
278
|
+
if (Array.isArray(raw)) return Buffer.concat(raw).toString('utf8');
|
|
279
|
+
if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
|
|
280
|
+
if (ArrayBuffer.isView(raw)) {
|
|
281
|
+
return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString(
|
|
282
|
+
'utf8',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return String(raw);
|
|
286
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -58,7 +58,17 @@ export {
|
|
|
58
58
|
type WaitForBridgeReadyOptions,
|
|
59
59
|
type WaitForBridgeReadyResult,
|
|
60
60
|
} from './bridge-ready';
|
|
61
|
-
export { createBridgeToken
|
|
61
|
+
export { createBridgeToken } from './bridge-token';
|
|
62
|
+
export {
|
|
63
|
+
resolveBridgeEndpoint,
|
|
64
|
+
resolveBridgePort,
|
|
65
|
+
validateBridgeSandboxSettings,
|
|
66
|
+
} from './bridge-endpoint';
|
|
67
|
+
export {
|
|
68
|
+
createBridgeWebSocketConnect,
|
|
69
|
+
type BridgeHello,
|
|
70
|
+
type CreateBridgeWebSocketConnectOptions,
|
|
71
|
+
} from './bridge-web-socket';
|
|
62
72
|
export { createReadBridgeAsset } from './bridge-asset';
|
|
63
73
|
export {
|
|
64
74
|
createBridgeErrorHandler,
|
|
@@ -36,6 +36,13 @@ export interface SandboxChannelReconnectOptions {
|
|
|
36
36
|
|
|
37
37
|
export interface SandboxChannelConnectOptions {
|
|
38
38
|
readonly abortSignal: AbortSignal;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Why the channel is connecting: `'open'` for the initial
|
|
42
|
+
* {@link SandboxChannel.open} call, `'reconnect'` for an attempt to recover
|
|
43
|
+
* from a transient disconnect.
|
|
44
|
+
*/
|
|
45
|
+
readonly reason: 'open' | 'reconnect';
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
export interface SandboxChannelOptions<TOut> {
|
|
@@ -296,6 +303,7 @@ export class SandboxChannel<
|
|
|
296
303
|
try {
|
|
297
304
|
const ws = await this.connect({
|
|
298
305
|
abortSignal: abortController.signal,
|
|
306
|
+
reason: 'open',
|
|
299
307
|
});
|
|
300
308
|
this.wire(ws);
|
|
301
309
|
this.ws = ws;
|
|
@@ -528,6 +536,7 @@ export class SandboxChannel<
|
|
|
528
536
|
try {
|
|
529
537
|
const ws = await this.connect({
|
|
530
538
|
abortSignal: abortController.signal,
|
|
539
|
+
reason: 'reconnect',
|
|
531
540
|
});
|
|
532
541
|
if (this.terminal || this.closing) {
|
|
533
542
|
closeWebSocket({ ws });
|
|
@@ -590,10 +599,11 @@ export class SandboxChannel<
|
|
|
590
599
|
|
|
591
600
|
private connect({
|
|
592
601
|
abortSignal,
|
|
602
|
+
reason,
|
|
593
603
|
}: SandboxChannelConnectOptions): Promise<WebSocket> {
|
|
594
604
|
return awaitWebSocketConnection({
|
|
595
605
|
connection: Promise.resolve().then(() =>
|
|
596
|
-
this.connectThunk({ abortSignal }),
|
|
606
|
+
this.connectThunk({ abortSignal, reason }),
|
|
597
607
|
),
|
|
598
608
|
abortSignal,
|
|
599
609
|
});
|