@rynx-ai/daemon 0.1.0 → 0.1.9
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/dist/browser-cli-args.d.ts +20 -0
- package/dist/browser-cli-args.js +100 -0
- package/dist/browser-runner-session-context.d.ts +24 -0
- package/dist/browser-runner-session-context.js +113 -0
- package/dist/cdp-keyboard-input.d.ts +6 -0
- package/dist/cdp-keyboard-input.js +209 -0
- package/dist/channel-store.d.ts +0 -6
- package/dist/channel-store.js +2 -18
- package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
- package/dist/chrome-for-testing-release-resolver.js +214 -0
- package/dist/chrome-for-testing-store.d.ts +85 -0
- package/dist/chrome-for-testing-store.js +836 -0
- package/dist/cli.js +467 -30
- package/dist/control-client.d.ts +113 -0
- package/dist/control-client.js +792 -0
- package/dist/control-deps.js +37 -6
- package/dist/control-endpoint.d.ts +14 -0
- package/dist/control-endpoint.js +124 -0
- package/dist/control-link-store.d.ts +16 -0
- package/dist/control-link-store.js +53 -0
- package/dist/daemon-owner.d.ts +14 -0
- package/dist/daemon-owner.js +104 -0
- package/dist/daemon-server.d.ts +12 -2
- package/dist/daemon-server.js +559 -28
- package/dist/db.js +140 -10
- package/dist/desktop-browser-host-client.d.ts +34 -0
- package/dist/desktop-browser-host-client.js +413 -0
- package/dist/direct-runtime-authenticator.d.ts +13 -0
- package/dist/direct-runtime-authenticator.js +127 -0
- package/dist/direct-runtime-config.d.ts +20 -0
- package/dist/direct-runtime-config.js +101 -0
- package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
- package/dist/embedded-browser-profile-cleanup.js +338 -0
- package/dist/headless-browser-host.d.ts +14 -0
- package/dist/headless-browser-host.js +2156 -0
- package/dist/index-daemon.js +28 -5
- package/dist/installation-id.d.ts +33 -0
- package/dist/installation-id.js +692 -0
- package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
- package/dist/migrations/cleanup-legacy-sessions.js +10 -28
- package/dist/plugin-cli.d.ts +30 -0
- package/dist/plugin-cli.js +270 -0
- package/dist/plugin-host-rpc.d.ts +85 -0
- package/dist/plugin-host-rpc.js +878 -0
- package/dist/plugin-install-restart.d.ts +13 -0
- package/dist/plugin-install-restart.js +24 -0
- package/dist/plugin-installer.d.ts +36 -15
- package/dist/plugin-installer.js +420 -155
- package/dist/plugin-package.d.ts +38 -0
- package/dist/plugin-package.js +553 -0
- package/dist/plugin-runtime-control.d.ts +2 -0
- package/dist/plugin-runtime-control.js +37 -0
- package/dist/plugin-store.d.ts +37 -8
- package/dist/plugin-store.js +80 -15
- package/dist/plugin-supervisor.d.ts +160 -0
- package/dist/plugin-supervisor.js +1143 -0
- package/dist/pm2.d.ts +3 -1
- package/dist/pm2.js +55 -13
- package/dist/provider-cli-service.d.ts +46 -0
- package/dist/provider-cli-service.js +510 -0
- package/dist/registry.d.ts +6 -19
- package/dist/registry.js +22 -87
- package/dist/remote-runtime-access-store.d.ts +61 -0
- package/dist/remote-runtime-access-store.js +386 -0
- package/dist/remote-runtime-admin.d.ts +32 -0
- package/dist/remote-runtime-admin.js +107 -0
- package/dist/remote-runtime-connection-manager.d.ts +92 -0
- package/dist/remote-runtime-connection-manager.js +1409 -0
- package/dist/remote-runtime-credential-store.d.ts +45 -0
- package/dist/remote-runtime-credential-store.js +639 -0
- package/dist/remote-runtime-target-control.d.ts +47 -0
- package/dist/remote-runtime-target-control.js +433 -0
- package/dist/remote-runtime-target-store.d.ts +53 -0
- package/dist/remote-runtime-target-store.js +307 -0
- package/dist/runtime-share-addresses.d.ts +21 -0
- package/dist/runtime-share-addresses.js +50 -0
- package/dist/session-browser-capability-store.d.ts +13 -0
- package/dist/session-browser-capability-store.js +58 -0
- package/dist/session-emulator-binding-store.d.ts +10 -0
- package/dist/session-emulator-binding-store.js +61 -0
- package/dist/session-launch-operations.d.ts +41 -0
- package/dist/session-launch-operations.js +124 -0
- package/dist/session-meta-store.d.ts +3 -2
- package/dist/session-meta-store.js +57 -7
- package/dist/setup.js +9 -4
- package/dist/skills-catalog.js +8 -4
- package/dist/update.d.ts +11 -2
- package/dist/update.js +0 -27
- package/package.json +18 -10
|
@@ -0,0 +1,1409 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
3
|
+
import { RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-emulator-surface";
|
|
4
|
+
import { assertRemoteRuntimeRpcMethodSupported, assertRemoteRuntimeSessionEventsSupported, assertRemoteRuntimeSessionTerminalSupported, parseRemoteRuntimeRpcRequest, REMOTE_RUNTIME_RPC_METHOD_METADATA, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
5
|
+
import { REMOTE_RUNTIME_CORE_PROTOCOL, } from "@rynx-ai/protocol/remote-runtime";
|
|
6
|
+
import { connectDirectRuntime, openDirectRuntimeBrowserSurface, DirectRuntimeCallTransportError, DirectRuntimeClientCapacityError, DirectRuntimeClientError, DirectRuntimeRpcError, DirectRuntimeSessionEventsError, DirectRuntimeSessionTerminalError, } from "@rynx-ai/remote-runtime-client";
|
|
7
|
+
import { SessionTerminalOpenError } from "@rynx-ai/server";
|
|
8
|
+
const LOCAL_SELECTOR = "local";
|
|
9
|
+
const TARGET_SELECTOR_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/;
|
|
10
|
+
const DEFAULT_IDLE_TTL_MS = 15_000;
|
|
11
|
+
const DEFAULT_MAX_IDLE_CONNECTIONS = 8;
|
|
12
|
+
const LOCAL_SESSION_MAX_QUEUED_EVENTS = 128;
|
|
13
|
+
const LOCAL_SESSION_MAX_QUEUED_EVENT_BYTES = 256 * 1024;
|
|
14
|
+
const MIN_CALL_TIMEOUT_MS = 100;
|
|
15
|
+
const MAX_CALL_TIMEOUT_MS = 120_000;
|
|
16
|
+
const LOCAL_SUBJECT = Object.freeze({
|
|
17
|
+
subjectId: "local-control-plane",
|
|
18
|
+
effectiveAuthorities: Object.freeze(["daemon.full"]),
|
|
19
|
+
});
|
|
20
|
+
export class RuntimeConnectionManagerError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
outcome;
|
|
23
|
+
constructor(code, message, options) {
|
|
24
|
+
super(message, options);
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.name = "RuntimeConnectionManagerError";
|
|
27
|
+
this.outcome = options?.outcome ?? (code === "outcome_unknown" ? "unknown" : undefined);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Daemon-owned Local/Direct Runtime resolver.
|
|
32
|
+
*
|
|
33
|
+
* Application callers receive only a logical target and a typed call surface;
|
|
34
|
+
* target routes and client credentials never cross this boundary. Direct
|
|
35
|
+
* connections are reused while healthy, but a failed call is never retried.
|
|
36
|
+
*/
|
|
37
|
+
export class DaemonRuntimeConnectionManager {
|
|
38
|
+
options;
|
|
39
|
+
client;
|
|
40
|
+
idleTtlMs;
|
|
41
|
+
maxIdleConnections;
|
|
42
|
+
slots = new Map();
|
|
43
|
+
generations = new Map();
|
|
44
|
+
localStatusSnapshot;
|
|
45
|
+
closed = false;
|
|
46
|
+
managerClosePromise;
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.options = options;
|
|
49
|
+
this.client = options.client ?? {
|
|
50
|
+
connect: (credential) => connectDirectRuntime(credential),
|
|
51
|
+
openBrowserSurface: (credential, frame, surfaceOptions) => openDirectRuntimeBrowserSurface(credential, frame, surfaceOptions),
|
|
52
|
+
};
|
|
53
|
+
this.idleTtlMs = nonNegativeInteger(options.idleTtlMs, DEFAULT_IDLE_TTL_MS, "idleTtlMs");
|
|
54
|
+
this.maxIdleConnections = nonNegativeInteger(options.maxIdleConnections, DEFAULT_MAX_IDLE_CONNECTIONS, "maxIdleConnections");
|
|
55
|
+
}
|
|
56
|
+
async acquire(selector, options = {}) {
|
|
57
|
+
this.assertOpen();
|
|
58
|
+
throwIfAborted(options.signal);
|
|
59
|
+
if (selector === LOCAL_SELECTOR)
|
|
60
|
+
return this.localLease();
|
|
61
|
+
if (!TARGET_SELECTOR_PATTERN.test(selector)) {
|
|
62
|
+
throw managerError("invalid_target", "Runtime target selector is invalid");
|
|
63
|
+
}
|
|
64
|
+
const target = this.options.targets.get(selector);
|
|
65
|
+
if (!target)
|
|
66
|
+
throw managerError("not_found", "Runtime target was not found");
|
|
67
|
+
const slot = this.slotFor(target);
|
|
68
|
+
slot.waiters += 1;
|
|
69
|
+
try {
|
|
70
|
+
await waitFor(slot.connecting, options.signal);
|
|
71
|
+
this.assertSlotCurrent(slot);
|
|
72
|
+
throwIfAborted(options.signal);
|
|
73
|
+
this.cancelIdleTimer(slot);
|
|
74
|
+
slot.refs += 1;
|
|
75
|
+
slot.lastUsedAt = Date.now();
|
|
76
|
+
return this.directLease(slot);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
slot.waiters = Math.max(0, slot.waiters - 1);
|
|
80
|
+
if (this.isSlotCurrent(slot) && slot.refs === 0 && slot.waiters === 0) {
|
|
81
|
+
this.scheduleIdleClose(slot);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Synchronously detaches the generation, then closes it asynchronously. */
|
|
86
|
+
invalidate(daemonId) {
|
|
87
|
+
if (daemonId === LOCAL_SELECTOR || daemonId === this.options.localDaemonId) {
|
|
88
|
+
return Promise.reject(managerError("invalid_target", "the local Runtime connection cannot be invalidated"));
|
|
89
|
+
}
|
|
90
|
+
this.bumpGeneration(daemonId);
|
|
91
|
+
const slot = this.slots.get(daemonId);
|
|
92
|
+
if (!slot)
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
this.slots.delete(daemonId);
|
|
95
|
+
this.cancelIdleTimer(slot);
|
|
96
|
+
return this.closeSlot(slot);
|
|
97
|
+
}
|
|
98
|
+
close() {
|
|
99
|
+
if (this.managerClosePromise)
|
|
100
|
+
return this.managerClosePromise;
|
|
101
|
+
this.closed = true;
|
|
102
|
+
const slots = [...this.slots.values()];
|
|
103
|
+
this.slots.clear();
|
|
104
|
+
for (const slot of slots) {
|
|
105
|
+
this.bumpGeneration(slot.daemonId);
|
|
106
|
+
this.cancelIdleTimer(slot);
|
|
107
|
+
}
|
|
108
|
+
this.managerClosePromise = closeAll(slots.map((slot) => this.closeSlot(slot)));
|
|
109
|
+
return this.managerClosePromise;
|
|
110
|
+
}
|
|
111
|
+
localLease() {
|
|
112
|
+
let released = false;
|
|
113
|
+
return {
|
|
114
|
+
target: {
|
|
115
|
+
selector: LOCAL_SELECTOR,
|
|
116
|
+
daemonId: this.options.localDaemonId,
|
|
117
|
+
},
|
|
118
|
+
call: async (method, params, callOptions = {}) => {
|
|
119
|
+
if (released)
|
|
120
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
121
|
+
this.assertOpen();
|
|
122
|
+
throwIfAborted(callOptions.signal);
|
|
123
|
+
return await this.callLocal(method, params, callOptions);
|
|
124
|
+
},
|
|
125
|
+
subscribeSessionEvents: async (sessionId, subscriptionOptions = {}) => {
|
|
126
|
+
if (released)
|
|
127
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
128
|
+
this.assertOpen();
|
|
129
|
+
throwIfAborted(subscriptionOptions.signal);
|
|
130
|
+
let status;
|
|
131
|
+
try {
|
|
132
|
+
status = this.localStatusSnapshot ?? await this.readLocalStatus(subscriptionOptions);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw managerPreflightError(error, "Local Runtime Session events capability preflight failed");
|
|
136
|
+
}
|
|
137
|
+
this.assertSessionEventsSupported(status, status.coreProtocolVersion);
|
|
138
|
+
const timeoutMs = parseCallTimeout(subscriptionOptions.timeoutMs);
|
|
139
|
+
const deadlineController = timeoutMs === undefined ? undefined : new AbortController();
|
|
140
|
+
const signal = combinedSignal(subscriptionOptions.signal, deadlineController?.signal);
|
|
141
|
+
const opening = Promise.resolve(this.options.dispatcher.openSessionEvents(sessionId, {
|
|
142
|
+
subject: LOCAL_SUBJECT,
|
|
143
|
+
...(signal ? { signal } : {}),
|
|
144
|
+
}));
|
|
145
|
+
let opened;
|
|
146
|
+
try {
|
|
147
|
+
opened = timeoutMs === undefined
|
|
148
|
+
? await opening
|
|
149
|
+
: await withDeadline(opening, timeoutMs, () => deadlineController.abort());
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error instanceof LocalRuntimeDeadlineError) {
|
|
153
|
+
throw managerError("deadline_exceeded", "Local Runtime Session subscription exceeded its ready deadline", error);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return boundedLocalSessionSubscription(opened);
|
|
158
|
+
},
|
|
159
|
+
openSessionTerminal: async (sessionId, terminalOptions) => {
|
|
160
|
+
if (released)
|
|
161
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
162
|
+
this.assertOpen();
|
|
163
|
+
throwIfAborted(terminalOptions.signal);
|
|
164
|
+
let status;
|
|
165
|
+
try {
|
|
166
|
+
status = this.localStatusSnapshot ?? await this.readLocalStatus(terminalOptions);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
throw managerPreflightError(error, "Local Runtime Session Terminal capability preflight failed");
|
|
170
|
+
}
|
|
171
|
+
this.assertSessionTerminalSupported(status, status.coreProtocolVersion);
|
|
172
|
+
const timeoutMs = parseCallTimeout(terminalOptions.timeoutMs);
|
|
173
|
+
const deadlineController = timeoutMs === undefined ? undefined : new AbortController();
|
|
174
|
+
const signal = combinedSignal(terminalOptions.signal, deadlineController?.signal);
|
|
175
|
+
const opening = this.options.dispatcher.openSessionTerminal(sessionId, {
|
|
176
|
+
role: terminalOptions.role,
|
|
177
|
+
cols: terminalOptions.cols,
|
|
178
|
+
rows: terminalOptions.rows,
|
|
179
|
+
}, { subject: LOCAL_SUBJECT, ...(signal ? { signal } : {}) });
|
|
180
|
+
try {
|
|
181
|
+
return timeoutMs === undefined
|
|
182
|
+
? await opening
|
|
183
|
+
: await withDeadline(opening, timeoutMs, () => deadlineController.abort());
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (error instanceof LocalRuntimeDeadlineError) {
|
|
187
|
+
void opening.then((opened) => opened.close()).catch(() => undefined);
|
|
188
|
+
throw managerError("deadline_exceeded", "Local Runtime Session Terminal exceeded its ready deadline", error);
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
openBrowserSurface: async (frame, surfaceOptions = {}) => {
|
|
194
|
+
if (released)
|
|
195
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
196
|
+
this.assertOpen();
|
|
197
|
+
throwIfAborted(surfaceOptions.signal);
|
|
198
|
+
const host = this.options.localBrowserSurface;
|
|
199
|
+
if (!host) {
|
|
200
|
+
throw managerError("incompatible", "Local Runtime Browser Surface is unavailable");
|
|
201
|
+
}
|
|
202
|
+
const timeoutMs = parseCallTimeout(surfaceOptions.timeoutMs);
|
|
203
|
+
const deadlineController = timeoutMs === undefined ? undefined : new AbortController();
|
|
204
|
+
const signal = combinedSignal(surfaceOptions.signal, deadlineController?.signal)
|
|
205
|
+
?? new AbortController().signal;
|
|
206
|
+
const opening = Promise.resolve(host.open(frame, {
|
|
207
|
+
peer: {
|
|
208
|
+
subject: LOCAL_SUBJECT,
|
|
209
|
+
clientId: "local-control-plane",
|
|
210
|
+
grantId: "local-control-plane",
|
|
211
|
+
},
|
|
212
|
+
signal,
|
|
213
|
+
}));
|
|
214
|
+
try {
|
|
215
|
+
const attachment = timeoutMs === undefined
|
|
216
|
+
? await opening
|
|
217
|
+
: await withDeadline(opening, timeoutMs, () => deadlineController.abort());
|
|
218
|
+
return localBrowserSurfaceConnection(attachment);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (error instanceof LocalRuntimeDeadlineError) {
|
|
222
|
+
void opening.then((attachment) => attachment.close("transport_error"))
|
|
223
|
+
.catch(() => undefined);
|
|
224
|
+
throw managerError("deadline_exceeded", "Local Runtime Browser Surface exceeded its ready deadline", error);
|
|
225
|
+
}
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
openEmulatorSurface: async (sessionId, bindingId, surfaceOptions = {}) => {
|
|
230
|
+
if (released)
|
|
231
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
232
|
+
this.assertOpen();
|
|
233
|
+
throwIfAborted(surfaceOptions.signal);
|
|
234
|
+
const host = this.options.localEmulatorSurface;
|
|
235
|
+
if (!host)
|
|
236
|
+
throw managerError("incompatible", "Local Runtime Emulator Surface is unavailable");
|
|
237
|
+
const timeoutMs = parseCallTimeout(surfaceOptions.timeoutMs);
|
|
238
|
+
const deadlineController = timeoutMs === undefined ? undefined : new AbortController();
|
|
239
|
+
const signal = combinedSignal(surfaceOptions.signal, deadlineController?.signal)
|
|
240
|
+
?? new AbortController().signal;
|
|
241
|
+
const opening = Promise.resolve(host.open({
|
|
242
|
+
type: "emulator.surface.open",
|
|
243
|
+
surfaceId: randomUUID(),
|
|
244
|
+
sessionId,
|
|
245
|
+
bindingId,
|
|
246
|
+
maximumFrameRate: surfaceOptions.maximumFrameRate ?? 3,
|
|
247
|
+
}, {
|
|
248
|
+
peer: {
|
|
249
|
+
subject: LOCAL_SUBJECT,
|
|
250
|
+
clientId: "local-control-plane",
|
|
251
|
+
grantId: "local-control-plane",
|
|
252
|
+
},
|
|
253
|
+
signal,
|
|
254
|
+
}));
|
|
255
|
+
try {
|
|
256
|
+
const attachment = timeoutMs === undefined
|
|
257
|
+
? await opening
|
|
258
|
+
: await withDeadline(opening, timeoutMs, () => deadlineController.abort());
|
|
259
|
+
return localEmulatorSurfaceConnection(attachment, surfaceOptions.maximumFrameRate ?? 3);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (error instanceof LocalRuntimeDeadlineError) {
|
|
263
|
+
void opening.then((attachment) => attachment.close()).catch(() => undefined);
|
|
264
|
+
throw managerError("deadline_exceeded", "Local Runtime Emulator Surface exceeded its ready deadline", error);
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
release: () => {
|
|
270
|
+
released = true;
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
async callLocal(method, params, callOptions) {
|
|
275
|
+
const request = parseRemoteRuntimeRpcRequest({
|
|
276
|
+
type: "rpc.request",
|
|
277
|
+
id: randomUUID(),
|
|
278
|
+
method,
|
|
279
|
+
params,
|
|
280
|
+
});
|
|
281
|
+
if (method !== "status.get") {
|
|
282
|
+
let status;
|
|
283
|
+
try {
|
|
284
|
+
status = this.localStatusSnapshot ?? await this.readLocalStatus(callOptions);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
throw managerPreflightError(error, `Local Runtime ${method} capability preflight failed`);
|
|
288
|
+
}
|
|
289
|
+
this.assertMethodSupported(method, status, status.coreProtocolVersion);
|
|
290
|
+
}
|
|
291
|
+
const result = await this.dispatchLocalRequest(method, request, callOptions);
|
|
292
|
+
if (method === "status.get") {
|
|
293
|
+
this.rememberLocalStatus(result);
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
async readLocalStatus(callOptions) {
|
|
298
|
+
if (this.localStatusSnapshot)
|
|
299
|
+
return this.localStatusSnapshot;
|
|
300
|
+
const request = parseRemoteRuntimeRpcRequest({
|
|
301
|
+
type: "rpc.request",
|
|
302
|
+
id: randomUUID(),
|
|
303
|
+
method: "status.get",
|
|
304
|
+
params: {},
|
|
305
|
+
});
|
|
306
|
+
const status = await this.dispatchLocalRequest("status.get", request, callOptions);
|
|
307
|
+
this.rememberLocalStatus(status);
|
|
308
|
+
return this.localStatusSnapshot;
|
|
309
|
+
}
|
|
310
|
+
async dispatchLocalRequest(method, request, callOptions) {
|
|
311
|
+
const timeoutMs = parseCallTimeout(callOptions.timeoutMs);
|
|
312
|
+
const deadlineController = timeoutMs === undefined ? undefined : new AbortController();
|
|
313
|
+
const signal = combinedSignal(callOptions.signal, deadlineController?.signal);
|
|
314
|
+
const dispatch = this.options.dispatcher.dispatch(request, {
|
|
315
|
+
subject: LOCAL_SUBJECT,
|
|
316
|
+
...(signal ? { signal } : {}),
|
|
317
|
+
});
|
|
318
|
+
let response;
|
|
319
|
+
try {
|
|
320
|
+
response = timeoutMs === undefined
|
|
321
|
+
? await dispatch
|
|
322
|
+
: await withDeadline(dispatch, timeoutMs, () => deadlineController.abort());
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
if (error instanceof LocalRuntimeDeadlineError) {
|
|
326
|
+
if (REMOTE_RUNTIME_RPC_METHOD_METADATA[method].retryPolicy === "never") {
|
|
327
|
+
throw managerError("outcome_unknown", `Local Runtime may have applied ${method} before its deadline`, error);
|
|
328
|
+
}
|
|
329
|
+
throw managerError("deadline_exceeded", `Local Runtime ${method} exceeded its deadline`, error);
|
|
330
|
+
}
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
if (response.type === "rpc.error") {
|
|
334
|
+
throw new DirectRuntimeRpcError(response.error.code, method, response.id, response.error.message);
|
|
335
|
+
}
|
|
336
|
+
return response.result;
|
|
337
|
+
}
|
|
338
|
+
rememberLocalStatus(status) {
|
|
339
|
+
if (status.daemonId !== this.options.localDaemonId) {
|
|
340
|
+
throw managerError("identity_mismatch", "Local Runtime status belongs to a different daemon");
|
|
341
|
+
}
|
|
342
|
+
this.localStatusSnapshot = {
|
|
343
|
+
...status,
|
|
344
|
+
semanticCapabilities: [...status.semanticCapabilities],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
assertMethodSupported(method, status, negotiatedCoreVersion) {
|
|
348
|
+
try {
|
|
349
|
+
assertRemoteRuntimeRpcMethodSupported(method, negotiatedCoreVersion, status.semanticCapabilities);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
throw managerError("incompatible", error instanceof Error ? error.message : `Remote Runtime method ${method} is unavailable`, error, "not_started");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
assertSessionEventsSupported(status, negotiatedCoreVersion) {
|
|
356
|
+
try {
|
|
357
|
+
assertRemoteRuntimeSessionEventsSupported(negotiatedCoreVersion, status.semanticCapabilities);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
throw managerError("incompatible", error instanceof Error ? error.message : "Remote Runtime Session events are unavailable", error, "not_started");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
assertSessionTerminalSupported(status, negotiatedCoreVersion) {
|
|
364
|
+
try {
|
|
365
|
+
assertRemoteRuntimeSessionTerminalSupported(negotiatedCoreVersion, status.semanticCapabilities);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
throw managerError("incompatible", error instanceof Error ? error.message : "Remote Runtime Session Terminal is unavailable", error, "not_started");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
directLease(slot) {
|
|
372
|
+
let released = false;
|
|
373
|
+
return {
|
|
374
|
+
target: {
|
|
375
|
+
selector: slot.daemonId,
|
|
376
|
+
daemonId: slot.daemonId,
|
|
377
|
+
},
|
|
378
|
+
call: async (method, params, callOptions = {}) => {
|
|
379
|
+
if (released)
|
|
380
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
381
|
+
this.assertOpen();
|
|
382
|
+
this.assertSlotCurrent(slot);
|
|
383
|
+
throwIfAborted(callOptions.signal);
|
|
384
|
+
const connection = slot.connection;
|
|
385
|
+
if (!connection)
|
|
386
|
+
throw managerError("closed", "Runtime connection is not available");
|
|
387
|
+
const status = slot.status;
|
|
388
|
+
if (!status)
|
|
389
|
+
throw managerError("closed", "Runtime status is not available");
|
|
390
|
+
this.assertMethodSupported(method, status, Math.min(status.coreProtocolVersion, REMOTE_RUNTIME_CORE_PROTOCOL.currentVersion));
|
|
391
|
+
try {
|
|
392
|
+
const result = callOptions.timeoutMs === undefined
|
|
393
|
+
? await connection.call(method, params)
|
|
394
|
+
: await connection.call(method, params, { timeoutMs: callOptions.timeoutMs });
|
|
395
|
+
throwIfAborted(callOptions.signal);
|
|
396
|
+
slot.lastUsedAt = Date.now();
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
if (isAbort(error))
|
|
401
|
+
throw error;
|
|
402
|
+
if (error instanceof DirectRuntimeClientCapacityError)
|
|
403
|
+
throw error;
|
|
404
|
+
if (error instanceof DirectRuntimeClientError && error.code === "incompatible") {
|
|
405
|
+
throw managerError("incompatible", error.message, error, "not_started");
|
|
406
|
+
}
|
|
407
|
+
if (error instanceof DirectRuntimeRpcError) {
|
|
408
|
+
// The client closes an authorization-revoked channel. Detach it
|
|
409
|
+
// immediately instead of handing the next caller a known-dead slot.
|
|
410
|
+
if (error.code === "unauthorized") {
|
|
411
|
+
await this.evict(slot).catch(() => undefined);
|
|
412
|
+
}
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
// The failed channel is already detached synchronously by evict(). A
|
|
416
|
+
// secondary socket-close error must not hide the original call's
|
|
417
|
+
// delivery semantics, especially outcome_unknown for mutations.
|
|
418
|
+
await this.evict(slot).catch(() => undefined);
|
|
419
|
+
if (error instanceof DirectRuntimeCallTransportError &&
|
|
420
|
+
error.outcome === "unknown" &&
|
|
421
|
+
REMOTE_RUNTIME_RPC_METHOD_METADATA[method].retryPolicy === "never") {
|
|
422
|
+
throw managerError("outcome_unknown", `Remote Runtime may have applied ${method} before the connection was lost`, error);
|
|
423
|
+
}
|
|
424
|
+
if (error instanceof DirectRuntimeCallTransportError &&
|
|
425
|
+
error.code === "deadline_exceeded") {
|
|
426
|
+
throw managerError("deadline_exceeded", `Remote Runtime ${method} exceeded its deadline`, error);
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
subscribeSessionEvents: async (sessionId, subscriptionOptions = {}) => {
|
|
432
|
+
if (released)
|
|
433
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
434
|
+
this.assertOpen();
|
|
435
|
+
this.assertSlotCurrent(slot);
|
|
436
|
+
throwIfAborted(subscriptionOptions.signal);
|
|
437
|
+
const connection = slot.connection;
|
|
438
|
+
if (!connection)
|
|
439
|
+
throw managerError("closed", "Runtime connection is not available");
|
|
440
|
+
const status = slot.status;
|
|
441
|
+
if (!status)
|
|
442
|
+
throw managerError("closed", "Runtime status is not available");
|
|
443
|
+
this.assertSessionEventsSupported(status, Math.min(status.coreProtocolVersion, REMOTE_RUNTIME_CORE_PROTOCOL.currentVersion));
|
|
444
|
+
const timeoutMs = parseCallTimeout(subscriptionOptions.timeoutMs);
|
|
445
|
+
const opening = connection.subscribeSessionEvents(sessionId, timeoutMs === undefined ? undefined : { timeoutMs });
|
|
446
|
+
let subscription;
|
|
447
|
+
try {
|
|
448
|
+
subscription = await waitFor(opening, subscriptionOptions.signal);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
if (isAbort(error)) {
|
|
452
|
+
void opening.then((opened) => opened.close()).catch(() => undefined);
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
if (!(error instanceof DirectRuntimeClientCapacityError) &&
|
|
456
|
+
shouldEvictAfterSubscriptionError(error)) {
|
|
457
|
+
await this.evict(slot).catch(() => undefined);
|
|
458
|
+
}
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
slot.lastUsedAt = Date.now();
|
|
462
|
+
return this.wrapDirectSessionSubscription(slot, subscription, subscriptionOptions.signal);
|
|
463
|
+
},
|
|
464
|
+
openSessionTerminal: async (sessionId, terminalOptions) => {
|
|
465
|
+
if (released)
|
|
466
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
467
|
+
this.assertOpen();
|
|
468
|
+
this.assertSlotCurrent(slot);
|
|
469
|
+
throwIfAborted(terminalOptions.signal);
|
|
470
|
+
const status = slot.status;
|
|
471
|
+
if (!status)
|
|
472
|
+
throw managerError("closed", "Runtime status is not available");
|
|
473
|
+
this.assertSessionTerminalSupported(status, Math.min(status.coreProtocolVersion, REMOTE_RUNTIME_CORE_PROTOCOL.currentVersion));
|
|
474
|
+
return await this.openDedicatedDirectTerminal(slot, sessionId, terminalOptions);
|
|
475
|
+
},
|
|
476
|
+
openBrowserSurface: async (frame, surfaceOptions = {}) => {
|
|
477
|
+
if (released)
|
|
478
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
479
|
+
this.assertOpen();
|
|
480
|
+
this.assertSlotCurrent(slot);
|
|
481
|
+
throwIfAborted(surfaceOptions.signal);
|
|
482
|
+
const status = slot.status;
|
|
483
|
+
if (!status)
|
|
484
|
+
throw managerError("closed", "Runtime status is not available");
|
|
485
|
+
if (!status.semanticCapabilities.includes(RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY)) {
|
|
486
|
+
throw managerError("incompatible", "Remote Runtime Browser Surface is unavailable", undefined, "not_started");
|
|
487
|
+
}
|
|
488
|
+
return await this.openDedicatedDirectBrowserSurface(slot, frame, surfaceOptions);
|
|
489
|
+
},
|
|
490
|
+
openEmulatorSurface: async (sessionId, bindingId, surfaceOptions = {}) => {
|
|
491
|
+
if (released)
|
|
492
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
493
|
+
this.assertOpen();
|
|
494
|
+
this.assertSlotCurrent(slot);
|
|
495
|
+
throwIfAborted(surfaceOptions.signal);
|
|
496
|
+
const status = slot.status;
|
|
497
|
+
if (!status)
|
|
498
|
+
throw managerError("closed", "Runtime status is not available");
|
|
499
|
+
if (!status.semanticCapabilities.includes(RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY)) {
|
|
500
|
+
throw managerError("incompatible", "Remote Runtime Emulator Surface is unavailable", undefined, "not_started");
|
|
501
|
+
}
|
|
502
|
+
return await this.openDedicatedDirectEmulatorSurface(slot, sessionId, bindingId, surfaceOptions);
|
|
503
|
+
},
|
|
504
|
+
release: () => {
|
|
505
|
+
if (released)
|
|
506
|
+
return;
|
|
507
|
+
released = true;
|
|
508
|
+
slot.refs = Math.max(0, slot.refs - 1);
|
|
509
|
+
slot.lastUsedAt = Date.now();
|
|
510
|
+
if (this.isSlotCurrent(slot) && slot.refs === 0 && slot.waiters === 0) {
|
|
511
|
+
this.scheduleIdleClose(slot);
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
wrapDirectSessionSubscription(slot, subscription, signal) {
|
|
517
|
+
let closed = false;
|
|
518
|
+
const onAbort = () => {
|
|
519
|
+
void close().catch(() => undefined);
|
|
520
|
+
};
|
|
521
|
+
const close = async () => {
|
|
522
|
+
if (closed)
|
|
523
|
+
return;
|
|
524
|
+
closed = true;
|
|
525
|
+
signal?.removeEventListener("abort", onAbort);
|
|
526
|
+
try {
|
|
527
|
+
await subscription.close();
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
if (shouldEvictAfterSubscriptionError(error)) {
|
|
531
|
+
await this.evict(slot).catch(() => undefined);
|
|
532
|
+
}
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
if (signal?.aborted)
|
|
537
|
+
void close().catch(() => undefined);
|
|
538
|
+
else
|
|
539
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
540
|
+
return {
|
|
541
|
+
sessionId: subscription.sessionId,
|
|
542
|
+
daemonInstanceId: subscription.daemonInstanceId,
|
|
543
|
+
[Symbol.asyncIterator]: () => {
|
|
544
|
+
const iterator = subscription[Symbol.asyncIterator]();
|
|
545
|
+
return {
|
|
546
|
+
next: async () => {
|
|
547
|
+
try {
|
|
548
|
+
const result = await iterator.next();
|
|
549
|
+
if (result.done)
|
|
550
|
+
signal?.removeEventListener("abort", onAbort);
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
signal?.removeEventListener("abort", onAbort);
|
|
555
|
+
if (shouldEvictAfterSubscriptionError(error)) {
|
|
556
|
+
await this.evict(slot).catch(() => undefined);
|
|
557
|
+
}
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
return: async () => {
|
|
562
|
+
await close();
|
|
563
|
+
return { done: true, value: undefined };
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
},
|
|
567
|
+
close,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async openDedicatedDirectTerminal(slot, sessionId, options) {
|
|
571
|
+
const timeoutMs = parseCallTimeout(options.timeoutMs);
|
|
572
|
+
let persisted;
|
|
573
|
+
try {
|
|
574
|
+
persisted = await this.options.credentials.load(slot.target.credentialRef);
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
throw managerError("authentication_failed", "Runtime target credential is unavailable", error);
|
|
578
|
+
}
|
|
579
|
+
if (persisted.daemonId !== slot.target.daemonId ||
|
|
580
|
+
persisted.identityPublicKey !== slot.target.identityPublicKey) {
|
|
581
|
+
throw managerError("identity_mismatch", "Runtime target metadata does not match its protected credential");
|
|
582
|
+
}
|
|
583
|
+
this.assertSlotCurrent(slot);
|
|
584
|
+
throwIfAborted(options.signal);
|
|
585
|
+
const credential = {
|
|
586
|
+
...persisted,
|
|
587
|
+
directEndpoint: slot.target.directRoute.endpoint,
|
|
588
|
+
};
|
|
589
|
+
const connecting = this.client.connect(credential);
|
|
590
|
+
let connection;
|
|
591
|
+
try {
|
|
592
|
+
connection = await waitFor(connecting, options.signal);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
if (isAbort(error)) {
|
|
596
|
+
void connecting.then((opened) => opened.close()).catch(() => undefined);
|
|
597
|
+
}
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
600
|
+
let opening;
|
|
601
|
+
try {
|
|
602
|
+
this.assertSlotCurrent(slot);
|
|
603
|
+
throwIfAborted(options.signal);
|
|
604
|
+
opening = connection.openSessionTerminal(sessionId, {
|
|
605
|
+
role: options.role,
|
|
606
|
+
cols: options.cols,
|
|
607
|
+
rows: options.rows,
|
|
608
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
609
|
+
});
|
|
610
|
+
const terminal = await waitFor(opening, options.signal);
|
|
611
|
+
this.assertSlotCurrent(slot);
|
|
612
|
+
throwIfAborted(options.signal);
|
|
613
|
+
slot.lastUsedAt = Date.now();
|
|
614
|
+
return this.trackDedicatedTerminal(slot, dedicatedDirectTerminal(connection, terminal, options.signal));
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
if (opening && isAbort(error)) {
|
|
618
|
+
void opening.then((terminal) => terminal.close()).catch(() => undefined);
|
|
619
|
+
}
|
|
620
|
+
await connection.close().catch(() => undefined);
|
|
621
|
+
if (error instanceof DirectRuntimeSessionTerminalError) {
|
|
622
|
+
throw new SessionTerminalOpenError(remoteTerminalOpenErrorCode(error.reason), error.message, { cause: error });
|
|
623
|
+
}
|
|
624
|
+
if (error instanceof DirectRuntimeClientCapacityError) {
|
|
625
|
+
throw new SessionTerminalOpenError("capacity", error.message, { cause: error });
|
|
626
|
+
}
|
|
627
|
+
if (error instanceof DirectRuntimeClientError && error.code === "incompatible") {
|
|
628
|
+
throw new SessionTerminalOpenError("method_not_found", error.message, { cause: error });
|
|
629
|
+
}
|
|
630
|
+
if (error instanceof DirectRuntimeClientError && error.code === "authentication_failed") {
|
|
631
|
+
throw new SessionTerminalOpenError("unauthorized", error.message, { cause: error });
|
|
632
|
+
}
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async openDedicatedDirectBrowserSurface(slot, frame, options) {
|
|
637
|
+
const timeoutMs = parseCallTimeout(options.timeoutMs);
|
|
638
|
+
let persisted;
|
|
639
|
+
try {
|
|
640
|
+
persisted = await this.options.credentials.load(slot.target.credentialRef);
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
throw managerError("authentication_failed", "Runtime target credential is unavailable", error);
|
|
644
|
+
}
|
|
645
|
+
if (persisted.daemonId !== slot.target.daemonId ||
|
|
646
|
+
persisted.identityPublicKey !== slot.target.identityPublicKey) {
|
|
647
|
+
throw managerError("identity_mismatch", "Runtime target metadata does not match its protected credential");
|
|
648
|
+
}
|
|
649
|
+
this.assertSlotCurrent(slot);
|
|
650
|
+
throwIfAborted(options.signal);
|
|
651
|
+
const credential = {
|
|
652
|
+
...persisted,
|
|
653
|
+
directEndpoint: slot.target.directRoute.endpoint,
|
|
654
|
+
};
|
|
655
|
+
const openBrowserSurface = this.client.openBrowserSurface
|
|
656
|
+
?? ((surfaceCredential, surfaceFrame, surfaceOptions) => openDirectRuntimeBrowserSurface(surfaceCredential, surfaceFrame, surfaceOptions));
|
|
657
|
+
const signal = combinedSignal(options.signal, slot.dedicatedAbortController.signal);
|
|
658
|
+
let resolveOpeningOperation;
|
|
659
|
+
const openingOperation = new Promise((resolve) => {
|
|
660
|
+
resolveOpeningOperation = resolve;
|
|
661
|
+
});
|
|
662
|
+
slot.pendingDedicatedOpenings.add(openingOperation);
|
|
663
|
+
let resolvedSurface;
|
|
664
|
+
let opening;
|
|
665
|
+
try {
|
|
666
|
+
throwIfAborted(signal);
|
|
667
|
+
opening = openBrowserSurface(credential, frame, {
|
|
668
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
669
|
+
...(signal ? { signal } : {}),
|
|
670
|
+
}).then((opened) => {
|
|
671
|
+
resolvedSurface = opened;
|
|
672
|
+
return opened;
|
|
673
|
+
});
|
|
674
|
+
const surface = await waitFor(opening, signal);
|
|
675
|
+
this.assertSlotCurrent(slot);
|
|
676
|
+
throwIfAborted(signal);
|
|
677
|
+
slot.lastUsedAt = Date.now();
|
|
678
|
+
return this.trackDedicatedBrowserSurface(slot, surface);
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
if (resolvedSurface) {
|
|
682
|
+
await resolvedSurface.close().catch(() => undefined);
|
|
683
|
+
}
|
|
684
|
+
else if (opening) {
|
|
685
|
+
// Cancellation may win while the dedicated connection is still
|
|
686
|
+
// opening. Ownership never reaches the caller in that case, so close
|
|
687
|
+
// the late result exactly once when it arrives.
|
|
688
|
+
const closeLate = opening.then((opened) => opened.close()).catch(() => undefined);
|
|
689
|
+
if (slot.dedicatedAbortController.signal.aborted)
|
|
690
|
+
await closeLate;
|
|
691
|
+
else
|
|
692
|
+
void closeLate;
|
|
693
|
+
}
|
|
694
|
+
if (slot.dedicatedAbortController.signal.aborted && isAbort(error)) {
|
|
695
|
+
throw managerError("closed", "Runtime connection was invalidated while opening Browser Surface");
|
|
696
|
+
}
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
699
|
+
finally {
|
|
700
|
+
slot.pendingDedicatedOpenings.delete(openingOperation);
|
|
701
|
+
resolveOpeningOperation();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async openDedicatedDirectEmulatorSurface(slot, sessionId, bindingId, options) {
|
|
705
|
+
const timeoutMs = parseCallTimeout(options.timeoutMs);
|
|
706
|
+
let persisted;
|
|
707
|
+
try {
|
|
708
|
+
persisted = await this.options.credentials.load(slot.target.credentialRef);
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
throw managerError("authentication_failed", "Runtime target credential is unavailable", error);
|
|
712
|
+
}
|
|
713
|
+
if (persisted.daemonId !== slot.target.daemonId ||
|
|
714
|
+
persisted.identityPublicKey !== slot.target.identityPublicKey)
|
|
715
|
+
throw managerError("identity_mismatch", "Runtime target metadata does not match its protected credential");
|
|
716
|
+
const credential = {
|
|
717
|
+
...persisted,
|
|
718
|
+
directEndpoint: slot.target.directRoute.endpoint,
|
|
719
|
+
};
|
|
720
|
+
const signal = combinedSignal(options.signal, slot.dedicatedAbortController.signal);
|
|
721
|
+
let resolveOpeningOperation;
|
|
722
|
+
const openingOperation = new Promise((resolve) => { resolveOpeningOperation = resolve; });
|
|
723
|
+
slot.pendingDedicatedOpenings.add(openingOperation);
|
|
724
|
+
let connection;
|
|
725
|
+
let connecting;
|
|
726
|
+
let surface;
|
|
727
|
+
let opening;
|
|
728
|
+
try {
|
|
729
|
+
throwIfAborted(signal);
|
|
730
|
+
connecting = this.client.connect(credential).then((opened) => {
|
|
731
|
+
connection = opened;
|
|
732
|
+
return opened;
|
|
733
|
+
});
|
|
734
|
+
connection = await waitFor(connecting, signal);
|
|
735
|
+
throwIfAborted(signal);
|
|
736
|
+
opening = connection.openEmulatorSurface(sessionId, bindingId, {
|
|
737
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
738
|
+
...(options.maximumFrameRate === undefined ? {} : { maximumFrameRate: options.maximumFrameRate }),
|
|
739
|
+
}).then((opened) => {
|
|
740
|
+
surface = opened;
|
|
741
|
+
return opened;
|
|
742
|
+
});
|
|
743
|
+
surface = await waitFor(opening, signal);
|
|
744
|
+
this.assertSlotCurrent(slot);
|
|
745
|
+
throwIfAborted(signal);
|
|
746
|
+
slot.lastUsedAt = Date.now();
|
|
747
|
+
return this.trackDedicatedEmulatorSurface(slot, connection, surface);
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
const closeSurface = surface
|
|
751
|
+
? surface.close().catch(() => undefined)
|
|
752
|
+
: opening?.then((opened) => opened.close()).catch(() => undefined);
|
|
753
|
+
const closeConnection = connection
|
|
754
|
+
? connection.close().catch(() => undefined)
|
|
755
|
+
: connecting?.then((opened) => opened.close()).catch(() => undefined);
|
|
756
|
+
if (slot.dedicatedAbortController.signal.aborted) {
|
|
757
|
+
await Promise.all([closeSurface, closeConnection]);
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
void Promise.all([closeSurface, closeConnection]);
|
|
761
|
+
}
|
|
762
|
+
if (slot.dedicatedAbortController.signal.aborted && isAbort(error)) {
|
|
763
|
+
throw managerError("closed", "Runtime connection was invalidated while opening Emulator Surface");
|
|
764
|
+
}
|
|
765
|
+
throw error;
|
|
766
|
+
}
|
|
767
|
+
finally {
|
|
768
|
+
slot.pendingDedicatedOpenings.delete(openingOperation);
|
|
769
|
+
resolveOpeningOperation();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
slotFor(target) {
|
|
773
|
+
const current = this.slots.get(target.daemonId);
|
|
774
|
+
if (current && current.generation === this.generation(target.daemonId))
|
|
775
|
+
return current;
|
|
776
|
+
const generation = this.generation(target.daemonId);
|
|
777
|
+
const slot = {
|
|
778
|
+
daemonId: target.daemonId,
|
|
779
|
+
generation,
|
|
780
|
+
target,
|
|
781
|
+
refs: 0,
|
|
782
|
+
waiters: 0,
|
|
783
|
+
lastUsedAt: Date.now(),
|
|
784
|
+
dedicatedClosers: new Set(),
|
|
785
|
+
dedicatedAbortController: new AbortController(),
|
|
786
|
+
pendingDedicatedOpenings: new Set(),
|
|
787
|
+
};
|
|
788
|
+
slot.connecting = this.openDirectConnection(slot);
|
|
789
|
+
this.slots.set(target.daemonId, slot);
|
|
790
|
+
void slot.connecting.then(() => {
|
|
791
|
+
if (this.isSlotCurrent(slot) && slot.refs === 0 && slot.waiters === 0) {
|
|
792
|
+
this.scheduleIdleClose(slot);
|
|
793
|
+
}
|
|
794
|
+
}, () => undefined);
|
|
795
|
+
return slot;
|
|
796
|
+
}
|
|
797
|
+
async openDirectConnection(slot) {
|
|
798
|
+
let persisted;
|
|
799
|
+
let connection;
|
|
800
|
+
try {
|
|
801
|
+
persisted = await this.options.credentials.load(slot.target.credentialRef);
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
if (this.isSlotCurrent(slot)) {
|
|
805
|
+
this.slots.delete(slot.daemonId);
|
|
806
|
+
this.bumpGeneration(slot.daemonId);
|
|
807
|
+
}
|
|
808
|
+
throw managerError("authentication_failed", "Runtime target credential is unavailable", error);
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
if (persisted.daemonId !== slot.target.daemonId ||
|
|
812
|
+
persisted.identityPublicKey !== slot.target.identityPublicKey) {
|
|
813
|
+
throw managerError("identity_mismatch", "Runtime target metadata does not match its protected credential");
|
|
814
|
+
}
|
|
815
|
+
const credential = {
|
|
816
|
+
...persisted,
|
|
817
|
+
directEndpoint: slot.target.directRoute.endpoint,
|
|
818
|
+
};
|
|
819
|
+
connection = await this.client.connect(credential);
|
|
820
|
+
slot.connection = connection;
|
|
821
|
+
const status = await connection.status();
|
|
822
|
+
if (status.daemonId !== slot.daemonId) {
|
|
823
|
+
throw managerError("identity_mismatch", "Runtime status belongs to a different daemon");
|
|
824
|
+
}
|
|
825
|
+
slot.status = {
|
|
826
|
+
...status,
|
|
827
|
+
semanticCapabilities: [...status.semanticCapabilities],
|
|
828
|
+
};
|
|
829
|
+
if (!this.isSlotCurrent(slot)) {
|
|
830
|
+
throw managerError("closed", "Runtime connection was invalidated while connecting");
|
|
831
|
+
}
|
|
832
|
+
return connection;
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
const slotIsCurrent = this.isSlotCurrent(slot);
|
|
836
|
+
// invalidate()/close() already owns closure for a detached generation.
|
|
837
|
+
// Close here only when authentication/status preflight itself failed.
|
|
838
|
+
if (connection && slotIsCurrent)
|
|
839
|
+
await connection.close().catch(() => undefined);
|
|
840
|
+
if (slotIsCurrent) {
|
|
841
|
+
this.slots.delete(slot.daemonId);
|
|
842
|
+
this.bumpGeneration(slot.daemonId);
|
|
843
|
+
}
|
|
844
|
+
if (error instanceof RuntimeConnectionManagerError)
|
|
845
|
+
throw error;
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async evict(slot) {
|
|
850
|
+
if (this.isSlotCurrent(slot)) {
|
|
851
|
+
this.slots.delete(slot.daemonId);
|
|
852
|
+
this.bumpGeneration(slot.daemonId);
|
|
853
|
+
}
|
|
854
|
+
this.cancelIdleTimer(slot);
|
|
855
|
+
await this.closeSlot(slot);
|
|
856
|
+
}
|
|
857
|
+
closeSlot(slot) {
|
|
858
|
+
if (slot.closePromise)
|
|
859
|
+
return slot.closePromise;
|
|
860
|
+
slot.closePromise = (async () => {
|
|
861
|
+
// Start closing generation-scoped dedicated transports immediately;
|
|
862
|
+
// do not let a still-connecting shared channel keep them authorized.
|
|
863
|
+
slot.dedicatedAbortController.abort();
|
|
864
|
+
const dedicatedClosing = [...slot.dedicatedClosers].map((close) => close());
|
|
865
|
+
const pendingDedicatedOpenings = [...slot.pendingDedicatedOpenings];
|
|
866
|
+
let connection = slot.connection;
|
|
867
|
+
if (!connection) {
|
|
868
|
+
try {
|
|
869
|
+
connection = await slot.connecting;
|
|
870
|
+
}
|
|
871
|
+
catch {
|
|
872
|
+
connection = slot.connection;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
await closeAll([
|
|
876
|
+
...dedicatedClosing,
|
|
877
|
+
...pendingDedicatedOpenings,
|
|
878
|
+
...(connection ? [connection.close()] : []),
|
|
879
|
+
]);
|
|
880
|
+
})();
|
|
881
|
+
return slot.closePromise;
|
|
882
|
+
}
|
|
883
|
+
trackDedicatedTerminal(slot, terminal) {
|
|
884
|
+
const close = this.registerDedicatedCloser(slot, () => terminal.close(), terminal.closed.then(() => undefined));
|
|
885
|
+
return {
|
|
886
|
+
sessionId: terminal.sessionId,
|
|
887
|
+
role: terminal.role,
|
|
888
|
+
closed: terminal.closed,
|
|
889
|
+
write: (data) => {
|
|
890
|
+
this.assertSlotCurrent(slot);
|
|
891
|
+
return terminal.write(data);
|
|
892
|
+
},
|
|
893
|
+
resize: (cols, rows) => {
|
|
894
|
+
this.assertSlotCurrent(slot);
|
|
895
|
+
return terminal.resize(cols, rows);
|
|
896
|
+
},
|
|
897
|
+
close,
|
|
898
|
+
[Symbol.asyncIterator]: () => {
|
|
899
|
+
const iterator = terminal[Symbol.asyncIterator]();
|
|
900
|
+
return {
|
|
901
|
+
next: () => {
|
|
902
|
+
this.assertSlotCurrent(slot);
|
|
903
|
+
return iterator.next();
|
|
904
|
+
},
|
|
905
|
+
return: async () => {
|
|
906
|
+
await close();
|
|
907
|
+
return { done: true, value: undefined };
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
},
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
trackDedicatedBrowserSurface(slot, surface) {
|
|
914
|
+
const close = this.registerDedicatedCloser(slot, () => surface.close(), surface.closed.then(() => undefined));
|
|
915
|
+
return {
|
|
916
|
+
ready: surface.ready,
|
|
917
|
+
send: async (frame) => {
|
|
918
|
+
this.assertSlotCurrent(slot);
|
|
919
|
+
await surface.send(frame);
|
|
920
|
+
},
|
|
921
|
+
close,
|
|
922
|
+
[Symbol.asyncIterator]: () => {
|
|
923
|
+
const iterator = surface[Symbol.asyncIterator]();
|
|
924
|
+
return {
|
|
925
|
+
next: async () => {
|
|
926
|
+
this.assertSlotCurrent(slot);
|
|
927
|
+
return await iterator.next();
|
|
928
|
+
},
|
|
929
|
+
return: async () => {
|
|
930
|
+
await close();
|
|
931
|
+
return { done: true, value: undefined };
|
|
932
|
+
},
|
|
933
|
+
};
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
trackDedicatedEmulatorSurface(slot, connection, surface) {
|
|
938
|
+
const close = this.registerDedicatedCloser(slot, async () => {
|
|
939
|
+
try {
|
|
940
|
+
await surface.close();
|
|
941
|
+
}
|
|
942
|
+
finally {
|
|
943
|
+
await connection.close();
|
|
944
|
+
}
|
|
945
|
+
}, surface.closed.then(() => connection.close(), () => connection.close()).then(() => undefined));
|
|
946
|
+
return {
|
|
947
|
+
ready: surface.ready,
|
|
948
|
+
close,
|
|
949
|
+
sendTouch: (point) => surface.sendTouch(point),
|
|
950
|
+
[Symbol.asyncIterator]: () => {
|
|
951
|
+
const iterator = surface[Symbol.asyncIterator]();
|
|
952
|
+
return {
|
|
953
|
+
next: async () => {
|
|
954
|
+
this.assertSlotCurrent(slot);
|
|
955
|
+
return await iterator.next();
|
|
956
|
+
},
|
|
957
|
+
return: async () => {
|
|
958
|
+
await close();
|
|
959
|
+
return { done: true, value: undefined };
|
|
960
|
+
},
|
|
961
|
+
};
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
registerDedicatedCloser(slot, closeResource, naturallyClosed) {
|
|
966
|
+
this.assertSlotCurrent(slot);
|
|
967
|
+
let closePromise;
|
|
968
|
+
const close = () => {
|
|
969
|
+
closePromise ??= Promise.resolve()
|
|
970
|
+
.then(closeResource)
|
|
971
|
+
.finally(() => slot.dedicatedClosers.delete(close));
|
|
972
|
+
return closePromise;
|
|
973
|
+
};
|
|
974
|
+
slot.dedicatedClosers.add(close);
|
|
975
|
+
if (naturallyClosed) {
|
|
976
|
+
void naturallyClosed.then(() => slot.dedicatedClosers.delete(close), () => slot.dedicatedClosers.delete(close));
|
|
977
|
+
}
|
|
978
|
+
return close;
|
|
979
|
+
}
|
|
980
|
+
scheduleIdleClose(slot) {
|
|
981
|
+
this.cancelIdleTimer(slot);
|
|
982
|
+
if (this.idleTtlMs === 0) {
|
|
983
|
+
void this.evict(slot).catch(() => undefined);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
slot.idleTimer = setTimeout(() => {
|
|
987
|
+
if (this.isSlotCurrent(slot) && slot.refs === 0 && slot.waiters === 0) {
|
|
988
|
+
void this.evict(slot).catch(() => undefined);
|
|
989
|
+
}
|
|
990
|
+
}, this.idleTtlMs);
|
|
991
|
+
slot.idleTimer.unref?.();
|
|
992
|
+
this.pruneIdleConnections();
|
|
993
|
+
}
|
|
994
|
+
pruneIdleConnections() {
|
|
995
|
+
const idle = [...this.slots.values()]
|
|
996
|
+
.filter((slot) => slot.refs === 0 && slot.waiters === 0 && slot.connection)
|
|
997
|
+
.sort((left, right) => left.lastUsedAt - right.lastUsedAt);
|
|
998
|
+
const excess = idle.length - this.maxIdleConnections;
|
|
999
|
+
for (let index = 0; index < excess; index += 1) {
|
|
1000
|
+
void this.evict(idle[index]).catch(() => undefined);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
cancelIdleTimer(slot) {
|
|
1004
|
+
if (slot.idleTimer)
|
|
1005
|
+
clearTimeout(slot.idleTimer);
|
|
1006
|
+
slot.idleTimer = undefined;
|
|
1007
|
+
}
|
|
1008
|
+
assertOpen() {
|
|
1009
|
+
if (this.closed)
|
|
1010
|
+
throw managerError("closed", "Runtime connection manager is closed");
|
|
1011
|
+
}
|
|
1012
|
+
assertSlotCurrent(slot) {
|
|
1013
|
+
if (!this.isSlotCurrent(slot)) {
|
|
1014
|
+
throw managerError("closed", "Runtime connection lease is no longer active");
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
isSlotCurrent(slot) {
|
|
1018
|
+
return (!this.closed &&
|
|
1019
|
+
this.slots.get(slot.daemonId) === slot &&
|
|
1020
|
+
this.generation(slot.daemonId) === slot.generation);
|
|
1021
|
+
}
|
|
1022
|
+
generation(daemonId) {
|
|
1023
|
+
return this.generations.get(daemonId) ?? 0;
|
|
1024
|
+
}
|
|
1025
|
+
bumpGeneration(daemonId) {
|
|
1026
|
+
this.generations.set(daemonId, this.generation(daemonId) + 1);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function dedicatedDirectTerminal(connection, terminal, signal) {
|
|
1030
|
+
let closePromise;
|
|
1031
|
+
const closeConnection = () => {
|
|
1032
|
+
closePromise ??= connection.close();
|
|
1033
|
+
return closePromise;
|
|
1034
|
+
};
|
|
1035
|
+
const onAbort = () => {
|
|
1036
|
+
void close().catch(() => undefined);
|
|
1037
|
+
};
|
|
1038
|
+
const close = async () => {
|
|
1039
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1040
|
+
try {
|
|
1041
|
+
await terminal.close();
|
|
1042
|
+
}
|
|
1043
|
+
finally {
|
|
1044
|
+
await closeConnection();
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
if (signal?.aborted)
|
|
1048
|
+
onAbort();
|
|
1049
|
+
else
|
|
1050
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1051
|
+
const closed = terminal.closed
|
|
1052
|
+
.then((frame) => ({
|
|
1053
|
+
reason: frame.reason,
|
|
1054
|
+
...(frame.exitCode === undefined ? {} : { exitCode: frame.exitCode }),
|
|
1055
|
+
}))
|
|
1056
|
+
.finally(() => {
|
|
1057
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1058
|
+
void closeConnection().catch(() => undefined);
|
|
1059
|
+
});
|
|
1060
|
+
void closed.catch(() => undefined);
|
|
1061
|
+
return {
|
|
1062
|
+
sessionId: terminal.sessionId,
|
|
1063
|
+
role: terminal.role,
|
|
1064
|
+
closed,
|
|
1065
|
+
write: (data) => terminal.write(data),
|
|
1066
|
+
resize: (cols, rows) => terminal.resize(cols, rows),
|
|
1067
|
+
close,
|
|
1068
|
+
[Symbol.asyncIterator]: () => {
|
|
1069
|
+
const iterator = terminal[Symbol.asyncIterator]();
|
|
1070
|
+
return {
|
|
1071
|
+
next: async () => {
|
|
1072
|
+
try {
|
|
1073
|
+
const result = await iterator.next();
|
|
1074
|
+
if (result.done)
|
|
1075
|
+
await closeConnection().catch(() => undefined);
|
|
1076
|
+
return result;
|
|
1077
|
+
}
|
|
1078
|
+
catch (error) {
|
|
1079
|
+
await closeConnection().catch(() => undefined);
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
return: async () => {
|
|
1084
|
+
await close();
|
|
1085
|
+
return { done: true, value: undefined };
|
|
1086
|
+
},
|
|
1087
|
+
};
|
|
1088
|
+
},
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function remoteTerminalOpenErrorCode(reason) {
|
|
1092
|
+
if (reason === "not_live")
|
|
1093
|
+
return "not_live";
|
|
1094
|
+
if (reason === "unauthorized")
|
|
1095
|
+
return "unauthorized";
|
|
1096
|
+
if (reason === "capacity" || reason === "backpressure")
|
|
1097
|
+
return "capacity";
|
|
1098
|
+
return "internal";
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Local dispatch has no WebSocket adapter to drain the process bus. Drain it
|
|
1102
|
+
* here into the same count/UTF-8-byte envelope used by the Direct client so a
|
|
1103
|
+
* stalled SSE consumer cannot grow the SessionBus subscriber queue forever.
|
|
1104
|
+
* Overflow is terminal only for this subscription; the Local lease remains
|
|
1105
|
+
* usable for unrelated calls.
|
|
1106
|
+
*/
|
|
1107
|
+
function boundedLocalSessionSubscription(opened) {
|
|
1108
|
+
const subscriptionId = `local_${randomUUID()}`;
|
|
1109
|
+
const source = opened.events[Symbol.asyncIterator]();
|
|
1110
|
+
const queue = [];
|
|
1111
|
+
let queuedBytes = 0;
|
|
1112
|
+
let waiter;
|
|
1113
|
+
let terminal;
|
|
1114
|
+
let cancelPromise;
|
|
1115
|
+
const cancelSource = () => {
|
|
1116
|
+
cancelPromise ??= Promise.resolve().then(() => opened.cancel());
|
|
1117
|
+
return cancelPromise;
|
|
1118
|
+
};
|
|
1119
|
+
const clearQueue = () => {
|
|
1120
|
+
queue.length = 0;
|
|
1121
|
+
queuedBytes = 0;
|
|
1122
|
+
};
|
|
1123
|
+
const finish = (nextTerminal, discardQueue) => {
|
|
1124
|
+
if (terminal)
|
|
1125
|
+
return;
|
|
1126
|
+
terminal = nextTerminal;
|
|
1127
|
+
if (discardQueue)
|
|
1128
|
+
clearQueue();
|
|
1129
|
+
const pending = waiter;
|
|
1130
|
+
waiter = undefined;
|
|
1131
|
+
if (!pending)
|
|
1132
|
+
return;
|
|
1133
|
+
if (nextTerminal.kind === "error")
|
|
1134
|
+
pending.reject(nextTerminal.error);
|
|
1135
|
+
else
|
|
1136
|
+
pending.resolve({ done: true, value: undefined });
|
|
1137
|
+
};
|
|
1138
|
+
const push = (event) => {
|
|
1139
|
+
if (terminal)
|
|
1140
|
+
return false;
|
|
1141
|
+
const encoded = JSON.stringify(event);
|
|
1142
|
+
if (encoded === undefined)
|
|
1143
|
+
throw new Error("Local Session event is not JSON-encodable");
|
|
1144
|
+
const bytes = Buffer.byteLength(encoded, "utf8");
|
|
1145
|
+
if (waiter) {
|
|
1146
|
+
const pending = waiter;
|
|
1147
|
+
waiter = undefined;
|
|
1148
|
+
pending.resolve({ done: false, value: event });
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
if (queue.length >= LOCAL_SESSION_MAX_QUEUED_EVENTS ||
|
|
1152
|
+
queuedBytes + bytes > LOCAL_SESSION_MAX_QUEUED_EVENT_BYTES) {
|
|
1153
|
+
finish({
|
|
1154
|
+
kind: "error",
|
|
1155
|
+
error: new DirectRuntimeSessionEventsError("backpressure", subscriptionId, opened.sessionId, "Local Runtime Session event queue exceeded its bounded capacity"),
|
|
1156
|
+
}, true);
|
|
1157
|
+
void cancelSource().catch(() => undefined);
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
queue.push({ event, bytes });
|
|
1161
|
+
queuedBytes += bytes;
|
|
1162
|
+
return true;
|
|
1163
|
+
};
|
|
1164
|
+
void (async () => {
|
|
1165
|
+
try {
|
|
1166
|
+
while (!terminal) {
|
|
1167
|
+
const next = await source.next();
|
|
1168
|
+
if (terminal)
|
|
1169
|
+
return;
|
|
1170
|
+
if (next.done) {
|
|
1171
|
+
finish({ kind: "done" }, false);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (!push(next.value))
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
catch (error) {
|
|
1179
|
+
if (!terminal)
|
|
1180
|
+
finish({ kind: "error", error }, true);
|
|
1181
|
+
}
|
|
1182
|
+
finally {
|
|
1183
|
+
if (terminal)
|
|
1184
|
+
await cancelSource().catch(() => undefined);
|
|
1185
|
+
}
|
|
1186
|
+
})();
|
|
1187
|
+
const close = async () => {
|
|
1188
|
+
finish({ kind: "done" }, true);
|
|
1189
|
+
await cancelSource();
|
|
1190
|
+
};
|
|
1191
|
+
const consumer = {
|
|
1192
|
+
next() {
|
|
1193
|
+
const queued = queue.shift();
|
|
1194
|
+
if (queued) {
|
|
1195
|
+
queuedBytes -= queued.bytes;
|
|
1196
|
+
return Promise.resolve({ done: false, value: queued.event });
|
|
1197
|
+
}
|
|
1198
|
+
if (terminal?.kind === "error")
|
|
1199
|
+
return Promise.reject(terminal.error);
|
|
1200
|
+
if (terminal)
|
|
1201
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
1202
|
+
if (waiter) {
|
|
1203
|
+
return Promise.reject(managerError("protocol_error", "concurrent reads from one Local Session subscription are not allowed"));
|
|
1204
|
+
}
|
|
1205
|
+
return new Promise((resolve, reject) => {
|
|
1206
|
+
waiter = { resolve, reject };
|
|
1207
|
+
});
|
|
1208
|
+
},
|
|
1209
|
+
async return() {
|
|
1210
|
+
await close();
|
|
1211
|
+
return { done: true, value: undefined };
|
|
1212
|
+
},
|
|
1213
|
+
};
|
|
1214
|
+
return {
|
|
1215
|
+
sessionId: opened.sessionId,
|
|
1216
|
+
daemonInstanceId: opened.daemonInstanceId,
|
|
1217
|
+
[Symbol.asyncIterator]: () => consumer,
|
|
1218
|
+
close,
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function localBrowserSurfaceConnection(attachment) {
|
|
1222
|
+
let closed = false;
|
|
1223
|
+
return {
|
|
1224
|
+
ready: attachment.ready,
|
|
1225
|
+
[Symbol.asyncIterator]: () => attachment.events[Symbol.asyncIterator](),
|
|
1226
|
+
send: async (frame) => {
|
|
1227
|
+
if (closed)
|
|
1228
|
+
throw managerError("closed", "Browser Surface is closed");
|
|
1229
|
+
await attachment.handle(frame);
|
|
1230
|
+
},
|
|
1231
|
+
close: async () => {
|
|
1232
|
+
if (closed)
|
|
1233
|
+
return;
|
|
1234
|
+
closed = true;
|
|
1235
|
+
await attachment.close("client_closed");
|
|
1236
|
+
},
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function localEmulatorSurfaceConnection(attachment, maximumFrameRate) {
|
|
1240
|
+
let closed = false;
|
|
1241
|
+
let sequence = 0;
|
|
1242
|
+
let inputSequence = 0;
|
|
1243
|
+
let lastCaptureAt = 0;
|
|
1244
|
+
const intervalMs = Math.ceil(1_000 / maximumFrameRate);
|
|
1245
|
+
const close = async () => {
|
|
1246
|
+
if (closed)
|
|
1247
|
+
return;
|
|
1248
|
+
closed = true;
|
|
1249
|
+
await attachment.close();
|
|
1250
|
+
};
|
|
1251
|
+
return {
|
|
1252
|
+
ready: attachment.ready,
|
|
1253
|
+
close,
|
|
1254
|
+
sendTouch: async (point) => {
|
|
1255
|
+
if (closed)
|
|
1256
|
+
throw managerError("closed", "Emulator Surface is closed");
|
|
1257
|
+
await attachment.handle({
|
|
1258
|
+
type: "emulator.surface.touch",
|
|
1259
|
+
surfaceId: attachment.ready.surfaceId,
|
|
1260
|
+
inputSequence: inputSequence++,
|
|
1261
|
+
...point,
|
|
1262
|
+
});
|
|
1263
|
+
},
|
|
1264
|
+
[Symbol.asyncIterator]: () => ({
|
|
1265
|
+
next: async () => {
|
|
1266
|
+
if (closed)
|
|
1267
|
+
return { done: true, value: undefined };
|
|
1268
|
+
const waitMs = intervalMs - (Date.now() - lastCaptureAt);
|
|
1269
|
+
if (waitMs > 0)
|
|
1270
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
1271
|
+
if (closed)
|
|
1272
|
+
return { done: true, value: undefined };
|
|
1273
|
+
const encodedBytes = await attachment.capture();
|
|
1274
|
+
lastCaptureAt = Date.now();
|
|
1275
|
+
return {
|
|
1276
|
+
done: false,
|
|
1277
|
+
value: {
|
|
1278
|
+
surfaceId: attachment.ready.surfaceId,
|
|
1279
|
+
frameSequence: sequence++,
|
|
1280
|
+
timestampMilliseconds: lastCaptureAt,
|
|
1281
|
+
format: attachment.ready.format,
|
|
1282
|
+
encodedBytes,
|
|
1283
|
+
},
|
|
1284
|
+
};
|
|
1285
|
+
},
|
|
1286
|
+
return: async () => {
|
|
1287
|
+
await close();
|
|
1288
|
+
return { done: true, value: undefined };
|
|
1289
|
+
},
|
|
1290
|
+
}),
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
function nonNegativeInteger(value, fallback, field) {
|
|
1294
|
+
if (value === undefined)
|
|
1295
|
+
return fallback;
|
|
1296
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
1297
|
+
throw new TypeError(`${field} must be a non-negative safe integer`);
|
|
1298
|
+
}
|
|
1299
|
+
return value;
|
|
1300
|
+
}
|
|
1301
|
+
function parseCallTimeout(value) {
|
|
1302
|
+
if (value === undefined)
|
|
1303
|
+
return undefined;
|
|
1304
|
+
if (!Number.isSafeInteger(value) ||
|
|
1305
|
+
value < MIN_CALL_TIMEOUT_MS ||
|
|
1306
|
+
value > MAX_CALL_TIMEOUT_MS) {
|
|
1307
|
+
throw managerError("protocol_error", `timeoutMs must be an integer between ${MIN_CALL_TIMEOUT_MS} and ${MAX_CALL_TIMEOUT_MS}`);
|
|
1308
|
+
}
|
|
1309
|
+
return value;
|
|
1310
|
+
}
|
|
1311
|
+
class LocalRuntimeDeadlineError extends Error {
|
|
1312
|
+
constructor() {
|
|
1313
|
+
super("Local Runtime call deadline exceeded");
|
|
1314
|
+
this.name = "LocalRuntimeDeadlineError";
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
async function withDeadline(promise, timeoutMs, onTimeout) {
|
|
1318
|
+
return await new Promise((resolve, reject) => {
|
|
1319
|
+
let settled = false;
|
|
1320
|
+
const timer = setTimeout(() => {
|
|
1321
|
+
if (settled)
|
|
1322
|
+
return;
|
|
1323
|
+
settled = true;
|
|
1324
|
+
onTimeout();
|
|
1325
|
+
reject(new LocalRuntimeDeadlineError());
|
|
1326
|
+
}, timeoutMs);
|
|
1327
|
+
timer.unref?.();
|
|
1328
|
+
void promise.then((value) => {
|
|
1329
|
+
if (settled)
|
|
1330
|
+
return;
|
|
1331
|
+
settled = true;
|
|
1332
|
+
clearTimeout(timer);
|
|
1333
|
+
resolve(value);
|
|
1334
|
+
}, (error) => {
|
|
1335
|
+
if (settled)
|
|
1336
|
+
return;
|
|
1337
|
+
settled = true;
|
|
1338
|
+
clearTimeout(timer);
|
|
1339
|
+
reject(error);
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
function combinedSignal(first, second) {
|
|
1344
|
+
if (!first)
|
|
1345
|
+
return second;
|
|
1346
|
+
if (!second)
|
|
1347
|
+
return first;
|
|
1348
|
+
return AbortSignal.any([first, second]);
|
|
1349
|
+
}
|
|
1350
|
+
async function waitFor(promise, signal) {
|
|
1351
|
+
if (!signal)
|
|
1352
|
+
return await promise;
|
|
1353
|
+
throwIfAborted(signal);
|
|
1354
|
+
return await new Promise((resolve, reject) => {
|
|
1355
|
+
const abort = () => reject(abortError());
|
|
1356
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1357
|
+
void promise.then((value) => {
|
|
1358
|
+
signal.removeEventListener("abort", abort);
|
|
1359
|
+
resolve(value);
|
|
1360
|
+
}, (error) => {
|
|
1361
|
+
signal.removeEventListener("abort", abort);
|
|
1362
|
+
reject(error);
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
function throwIfAborted(signal) {
|
|
1367
|
+
if (signal?.aborted)
|
|
1368
|
+
throw abortError();
|
|
1369
|
+
}
|
|
1370
|
+
function abortError() {
|
|
1371
|
+
return managerError("cancelled", "Runtime connection operation was cancelled");
|
|
1372
|
+
}
|
|
1373
|
+
function isAbort(error) {
|
|
1374
|
+
return error instanceof RuntimeConnectionManagerError && error.code === "cancelled";
|
|
1375
|
+
}
|
|
1376
|
+
function shouldEvictAfterSubscriptionError(error) {
|
|
1377
|
+
if (error instanceof DirectRuntimeSessionEventsError) {
|
|
1378
|
+
// A scoped capacity/not-found/backpressure close does not poison the
|
|
1379
|
+
// authenticated channel. Revocation does.
|
|
1380
|
+
return error.reason === "unauthorized";
|
|
1381
|
+
}
|
|
1382
|
+
return true;
|
|
1383
|
+
}
|
|
1384
|
+
function managerError(code, message, cause, outcome) {
|
|
1385
|
+
return new RuntimeConnectionManagerError(code, message, cause === undefined && outcome === undefined
|
|
1386
|
+
? undefined
|
|
1387
|
+
: {
|
|
1388
|
+
...(cause === undefined ? {} : { cause }),
|
|
1389
|
+
...(outcome === undefined ? {} : { outcome }),
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
function managerPreflightError(error, message) {
|
|
1393
|
+
if (error instanceof RuntimeConnectionManagerError) {
|
|
1394
|
+
return managerError(error.code, error.message, error, "not_started");
|
|
1395
|
+
}
|
|
1396
|
+
if (error instanceof DirectRuntimeClientError) {
|
|
1397
|
+
return managerError(error.code === "authentication_failed" ? "authentication_failed" : "protocol_error", message, error, "not_started");
|
|
1398
|
+
}
|
|
1399
|
+
return managerError("protocol_error", message, error, "not_started");
|
|
1400
|
+
}
|
|
1401
|
+
async function closeAll(closing) {
|
|
1402
|
+
const results = await Promise.allSettled(closing);
|
|
1403
|
+
const errors = results
|
|
1404
|
+
.filter((result) => result.status === "rejected")
|
|
1405
|
+
.map((result) => result.reason);
|
|
1406
|
+
if (errors.length > 0) {
|
|
1407
|
+
throw new AggregateError(errors, "failed to close Runtime connections");
|
|
1408
|
+
}
|
|
1409
|
+
}
|