@rynx-ai/server 0.1.0 → 0.1.10-beta.2
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-surface-control-queue.d.ts +42 -0
- package/dist/browser-surface-control-queue.js +187 -0
- package/dist/browser-surface-ws.d.ts +16 -0
- package/dist/browser-surface-ws.js +356 -0
- package/dist/control-api.d.ts +76 -4
- package/dist/control-api.js +3014 -133
- package/dist/control-web/assets/highlighted-body-OFNGDK62-DHu2g-rk.js +1 -0
- package/dist/control-web/assets/index-B6Pb5j9M.js +666 -0
- package/dist/control-web/assets/index-BU-_Qud_.css +32 -0
- package/dist/control-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/control-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/control-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/control-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/control-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/control-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/control-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/control-web/assets/mermaid-GHXKKRXX-CSYzOmBR.js +131 -0
- package/dist/control-web/assets/rynx-wordmark-LL1QRYHD.png +0 -0
- package/dist/control-web/favicon.png +0 -0
- package/dist/control-web/favicon.svg +38 -0
- package/dist/control-web/index.html +16 -0
- package/dist/control-web-dist.d.ts +1 -1
- package/dist/control-web-dist.js +19 -14
- package/dist/desktop-browser-host.d.ts +170 -0
- package/dist/desktop-browser-host.js +1018 -0
- package/dist/direct-runtime-browser-inspect-server.d.ts +72 -0
- package/dist/direct-runtime-browser-inspect-server.js +749 -0
- package/dist/direct-runtime-browser-surface-server.d.ts +74 -0
- package/dist/direct-runtime-browser-surface-server.js +821 -0
- package/dist/direct-runtime-server.d.ts +143 -0
- package/dist/direct-runtime-server.js +1959 -0
- package/dist/emulator-surface-ws.d.ts +8 -0
- package/dist/emulator-surface-ws.js +198 -0
- package/dist/machine-session-service.d.ts +265 -0
- package/dist/machine-session-service.js +822 -0
- package/dist/provider-cli-host.d.ts +10 -0
- package/dist/provider-cli-host.js +1 -0
- package/dist/remote-runtime-dispatcher.d.ts +94 -0
- package/dist/remote-runtime-dispatcher.js +403 -0
- package/dist/remote-runtime-session-projection.d.ts +19 -0
- package/dist/remote-runtime-session-projection.js +566 -0
- package/dist/remote-runtime.d.ts +22 -0
- package/dist/remote-runtime.js +32 -0
- package/dist/runtime-web-auth.d.ts +26 -0
- package/dist/runtime-web-auth.js +132 -0
- package/dist/server.d.ts +364 -12
- package/dist/server.js +341 -100
- package/dist/session-browser-service.d.ts +248 -0
- package/dist/session-browser-service.js +772 -0
- package/dist/session-browser-surface-coordinator.d.ts +24 -0
- package/dist/session-browser-surface-coordinator.js +669 -0
- package/dist/session-emulator-service.d.ts +167 -0
- package/dist/session-emulator-service.js +464 -0
- package/dist/session-runtime-index.d.ts +23 -0
- package/dist/session-runtime-index.js +96 -0
- package/dist/session-terminal-host.d.ts +51 -0
- package/dist/session-terminal-host.js +270 -0
- package/dist/terminal-ws.d.ts +12 -20
- package/dist/terminal-ws.js +421 -97
- package/package.json +14 -7
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type RuntimeBrowserSurfaceOpenFrame } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
2
|
+
import { type DirectRuntimeBrowserSurfaceAttachment, type DirectRuntimeBrowserSurfaceHost } from "./direct-runtime-browser-surface-server.js";
|
|
3
|
+
import { SessionBrowserService } from "./session-browser-service.js";
|
|
4
|
+
export interface SessionBrowserSurfaceCoordinatorOptions {
|
|
5
|
+
frameAcknowledgeTimeoutMs?: number;
|
|
6
|
+
maxPendingEvents?: number;
|
|
7
|
+
maxPendingEventBytes?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Owns one physical capture stream per Page, fans it out to bounded logical
|
|
11
|
+
* subscribers, and arbitrates the single human-driver lease. It never exposes a
|
|
12
|
+
* Host Page ID or CDP.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SessionBrowserSurfaceCoordinator implements DirectRuntimeBrowserSurfaceHost {
|
|
15
|
+
private readonly browsers;
|
|
16
|
+
private readonly pages;
|
|
17
|
+
private readonly pageQueues;
|
|
18
|
+
private readonly frameAcknowledgeTimeoutMs;
|
|
19
|
+
private readonly maxPendingEvents;
|
|
20
|
+
private readonly maxPendingEventBytes;
|
|
21
|
+
constructor(browsers: SessionBrowserService, options?: SessionBrowserSurfaceCoordinatorOptions);
|
|
22
|
+
open(frame: RuntimeBrowserSurfaceOpenFrame, context: Parameters<DirectRuntimeBrowserSurfaceHost["open"]>[1]): Promise<DirectRuntimeBrowserSurfaceAttachment>;
|
|
23
|
+
private exclusivePage;
|
|
24
|
+
}
|
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { RUNTIME_BROWSER_SURFACE_MAX_CONTROL_FRAME_BYTES, RUNTIME_BROWSER_SURFACE_MAX_ENCODED_FRAME_BYTES, RUNTIME_BROWSER_SURFACE_FRAME_ACK_TIMEOUT_MS, RUNTIME_BROWSER_SURFACE_MAX_RELIABLE_INPUT_QUEUE, RUNTIME_BROWSER_SURFACE_MAX_SUBSCRIBERS_PER_PAGE, } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
2
|
+
import { DirectRuntimeBrowserSurfaceHostError, } from "./direct-runtime-browser-surface-server.js";
|
|
3
|
+
import { SessionBrowserHostError, SessionBrowserServiceError, SessionBrowserSurfaceUnsupportedError, } from "./session-browser-service.js";
|
|
4
|
+
const DEFAULT_MAX_PENDING_EVENTS = 8;
|
|
5
|
+
const DEFAULT_MAX_PENDING_EVENT_BYTES = RUNTIME_BROWSER_SURFACE_MAX_ENCODED_FRAME_BYTES +
|
|
6
|
+
RUNTIME_BROWSER_SURFACE_MAX_CONTROL_FRAME_BYTES * DEFAULT_MAX_PENDING_EVENTS;
|
|
7
|
+
/**
|
|
8
|
+
* Owns one physical capture stream per Page, fans it out to bounded logical
|
|
9
|
+
* subscribers, and arbitrates the single human-driver lease. It never exposes a
|
|
10
|
+
* Host Page ID or CDP.
|
|
11
|
+
*/
|
|
12
|
+
export class SessionBrowserSurfaceCoordinator {
|
|
13
|
+
browsers;
|
|
14
|
+
pages = new Map();
|
|
15
|
+
pageQueues = new Map();
|
|
16
|
+
frameAcknowledgeTimeoutMs;
|
|
17
|
+
maxPendingEvents;
|
|
18
|
+
maxPendingEventBytes;
|
|
19
|
+
constructor(browsers, options = {}) {
|
|
20
|
+
this.browsers = browsers;
|
|
21
|
+
this.frameAcknowledgeTimeoutMs = options.frameAcknowledgeTimeoutMs
|
|
22
|
+
?? RUNTIME_BROWSER_SURFACE_FRAME_ACK_TIMEOUT_MS;
|
|
23
|
+
if (!Number.isSafeInteger(this.frameAcknowledgeTimeoutMs) || this.frameAcknowledgeTimeoutMs < 1) {
|
|
24
|
+
throw new Error("Browser Surface frame acknowledgement timeout must be positive");
|
|
25
|
+
}
|
|
26
|
+
this.maxPendingEvents = positiveLimit(options.maxPendingEvents, DEFAULT_MAX_PENDING_EVENTS, "Browser Surface pending event limit");
|
|
27
|
+
this.maxPendingEventBytes = positiveLimit(options.maxPendingEventBytes, DEFAULT_MAX_PENDING_EVENT_BYTES, "Browser Surface pending event byte limit");
|
|
28
|
+
}
|
|
29
|
+
async open(frame, context) {
|
|
30
|
+
if (frame.requestedRole === "control" &&
|
|
31
|
+
!context.peer.subject.effectiveAuthorities.includes("daemon.full")) {
|
|
32
|
+
throw new DirectRuntimeBrowserSurfaceHostError("unauthorized", "Runtime grant does not allow Browser control");
|
|
33
|
+
}
|
|
34
|
+
const pageKey = `${frame.sessionId}\u0000${frame.pageId}`;
|
|
35
|
+
return this.exclusivePage(pageKey, async () => {
|
|
36
|
+
context.signal.throwIfAborted();
|
|
37
|
+
try {
|
|
38
|
+
await this.browsers.validateSurfaceTarget({
|
|
39
|
+
sessionId: frame.sessionId,
|
|
40
|
+
browserGeneration: frame.browserGeneration,
|
|
41
|
+
pageId: frame.pageId,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw mapSurfaceOpenError(error);
|
|
46
|
+
}
|
|
47
|
+
context.signal.throwIfAborted();
|
|
48
|
+
let page = this.pages.get(pageKey);
|
|
49
|
+
if (page?.isClosing()) {
|
|
50
|
+
await page.closed();
|
|
51
|
+
page = undefined;
|
|
52
|
+
}
|
|
53
|
+
if (page && page.browserGeneration !== frame.browserGeneration) {
|
|
54
|
+
await page.finish("generation_changed", "Browser generation changed", true);
|
|
55
|
+
page = undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!page) {
|
|
58
|
+
const sourceController = new AbortController();
|
|
59
|
+
let source;
|
|
60
|
+
try {
|
|
61
|
+
source = await this.browsers.openSurface({
|
|
62
|
+
sessionId: frame.sessionId,
|
|
63
|
+
browserGeneration: frame.browserGeneration,
|
|
64
|
+
pageId: frame.pageId,
|
|
65
|
+
format: frame.format,
|
|
66
|
+
quality: frame.quality,
|
|
67
|
+
maximumFrameRate: frame.maximumFrameRate,
|
|
68
|
+
signal: sourceController.signal,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
sourceController.abort();
|
|
73
|
+
throw mapSurfaceOpenError(error);
|
|
74
|
+
}
|
|
75
|
+
if (context.signal.aborted) {
|
|
76
|
+
sourceController.abort();
|
|
77
|
+
await source.close().catch(() => undefined);
|
|
78
|
+
throw new DirectRuntimeBrowserSurfaceHostError("cancelled", "Browser Surface open was cancelled");
|
|
79
|
+
}
|
|
80
|
+
page = new SharedPageSurface({
|
|
81
|
+
source,
|
|
82
|
+
sourceController,
|
|
83
|
+
frameAcknowledgeTimeoutMs: this.frameAcknowledgeTimeoutMs,
|
|
84
|
+
maxPendingEvents: this.maxPendingEvents,
|
|
85
|
+
maxPendingEventBytes: this.maxPendingEventBytes,
|
|
86
|
+
onClose: () => {
|
|
87
|
+
if (this.pages.get(pageKey) === page)
|
|
88
|
+
this.pages.delete(pageKey);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
this.pages.set(pageKey, page);
|
|
92
|
+
}
|
|
93
|
+
const attachment = page.attach(frame, context.signal);
|
|
94
|
+
page.start();
|
|
95
|
+
return attachment;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
exclusivePage(pageKey, operation) {
|
|
99
|
+
const previous = this.pageQueues.get(pageKey) ?? Promise.resolve();
|
|
100
|
+
const result = previous.then(operation, operation);
|
|
101
|
+
const completion = result.then(() => undefined, () => undefined);
|
|
102
|
+
this.pageQueues.set(pageKey, completion);
|
|
103
|
+
void completion.finally(() => {
|
|
104
|
+
if (this.pageQueues.get(pageKey) === completion)
|
|
105
|
+
this.pageQueues.delete(pageKey);
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
class SharedPageSurface {
|
|
111
|
+
page;
|
|
112
|
+
browserGeneration;
|
|
113
|
+
subscribers = new Map();
|
|
114
|
+
restingDriver;
|
|
115
|
+
physicalDriver;
|
|
116
|
+
owner;
|
|
117
|
+
desiredOwner;
|
|
118
|
+
pendingInputCount = 0;
|
|
119
|
+
inputTail = Promise.resolve();
|
|
120
|
+
inputEpoch = 0;
|
|
121
|
+
acceptingInput = false;
|
|
122
|
+
inputResetRequired = false;
|
|
123
|
+
driverTransitionTail = Promise.resolve();
|
|
124
|
+
started = false;
|
|
125
|
+
closing = false;
|
|
126
|
+
closePromise;
|
|
127
|
+
constructor(page) {
|
|
128
|
+
this.page = page;
|
|
129
|
+
this.browserGeneration = page.source.browserGeneration;
|
|
130
|
+
this.restingDriver = page.source.setHumanDriver ? "desktop" : "none";
|
|
131
|
+
this.physicalDriver = this.restingDriver;
|
|
132
|
+
}
|
|
133
|
+
start() {
|
|
134
|
+
if (this.started)
|
|
135
|
+
return;
|
|
136
|
+
this.started = true;
|
|
137
|
+
void this.pumpFrames();
|
|
138
|
+
}
|
|
139
|
+
isClosing() {
|
|
140
|
+
return this.closing;
|
|
141
|
+
}
|
|
142
|
+
closed() {
|
|
143
|
+
return this.closePromise ?? Promise.resolve();
|
|
144
|
+
}
|
|
145
|
+
attach(open, signal) {
|
|
146
|
+
if (this.closing) {
|
|
147
|
+
throw new DirectRuntimeBrowserSurfaceHostError("browser_closed", "Browser Surface is closing");
|
|
148
|
+
}
|
|
149
|
+
if (open.browserGeneration !== this.browserGeneration) {
|
|
150
|
+
throw new DirectRuntimeBrowserSurfaceHostError("generation_changed", "Browser generation changed");
|
|
151
|
+
}
|
|
152
|
+
if (open.format !== this.page.source.format) {
|
|
153
|
+
throw new DirectRuntimeBrowserSurfaceHostError("capacity", `Browser Surface Page is already streaming ${this.page.source.format}`);
|
|
154
|
+
}
|
|
155
|
+
if (this.subscribers.size >= RUNTIME_BROWSER_SURFACE_MAX_SUBSCRIBERS_PER_PAGE) {
|
|
156
|
+
throw new DirectRuntimeBrowserSurfaceHostError("capacity", "Browser Surface Page reached its Preview limit");
|
|
157
|
+
}
|
|
158
|
+
if (this.subscribers.has(open.subscriptionId)) {
|
|
159
|
+
throw new DirectRuntimeBrowserSurfaceHostError("protocol_error", "Browser Surface subscription already exists");
|
|
160
|
+
}
|
|
161
|
+
const attachment = new CoordinatedSurfaceAttachment({
|
|
162
|
+
open,
|
|
163
|
+
page: this,
|
|
164
|
+
signal,
|
|
165
|
+
frameAcknowledgeTimeoutMs: this.page.frameAcknowledgeTimeoutMs,
|
|
166
|
+
maxPendingEvents: this.page.maxPendingEvents,
|
|
167
|
+
maxPendingEventBytes: this.page.maxPendingEventBytes,
|
|
168
|
+
});
|
|
169
|
+
this.subscribers.set(open.subscriptionId, attachment);
|
|
170
|
+
attachment.start();
|
|
171
|
+
return attachment;
|
|
172
|
+
}
|
|
173
|
+
getViewport() {
|
|
174
|
+
return this.page.source.getViewport();
|
|
175
|
+
}
|
|
176
|
+
driverState() {
|
|
177
|
+
return this.owner
|
|
178
|
+
? { driver: "stream", driverSubscriptionId: this.owner.subscriptionId }
|
|
179
|
+
: { driver: this.restingDriver };
|
|
180
|
+
}
|
|
181
|
+
async pumpFrames() {
|
|
182
|
+
try {
|
|
183
|
+
for await (const frame of this.page.source.frames) {
|
|
184
|
+
if (this.closing) {
|
|
185
|
+
await frame.acknowledge();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (frame.browserGeneration !== this.browserGeneration) {
|
|
189
|
+
await frame.acknowledge();
|
|
190
|
+
await this.finish("generation_changed", "Browser generation changed", true);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (frame.format !== this.page.source.format) {
|
|
194
|
+
await frame.acknowledge();
|
|
195
|
+
throw new Error("Browser Host changed the Surface image format");
|
|
196
|
+
}
|
|
197
|
+
for (const subscriber of [...this.subscribers.values()]) {
|
|
198
|
+
subscriber.offerFrame(frame);
|
|
199
|
+
}
|
|
200
|
+
await frame.acknowledge();
|
|
201
|
+
if (this.closing)
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!this.closing) {
|
|
205
|
+
await this.finish("browser_closed", "Browser Surface source ended", true);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (!this.closing) {
|
|
210
|
+
const mapped = mapSurfaceHostError(error, "Browser Surface source failed");
|
|
211
|
+
await this.finish(mapped.reason, mapped.message, true);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
takeDriver(attachment) {
|
|
216
|
+
if (this.closing || !this.subscribers.has(attachment.subscriptionId))
|
|
217
|
+
return Promise.resolve();
|
|
218
|
+
if (this.desiredOwner === attachment && this.owner === attachment && this.acceptingInput) {
|
|
219
|
+
return Promise.resolve();
|
|
220
|
+
}
|
|
221
|
+
if (this.owner !== attachment) {
|
|
222
|
+
this.fenceInput();
|
|
223
|
+
if (this.owner)
|
|
224
|
+
this.inputResetRequired = true;
|
|
225
|
+
}
|
|
226
|
+
this.desiredOwner = attachment;
|
|
227
|
+
return this.enqueueDriverTransition(() => this.reconcileDriver()).catch((error) => {
|
|
228
|
+
throw mapSurfaceHostError(error, "Browser Surface driver handoff failed");
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
releaseDriver(attachment) {
|
|
232
|
+
if (this.desiredOwner !== attachment && this.owner !== attachment)
|
|
233
|
+
return Promise.resolve();
|
|
234
|
+
this.fenceInput();
|
|
235
|
+
this.inputResetRequired = true;
|
|
236
|
+
if (this.desiredOwner === attachment)
|
|
237
|
+
this.desiredOwner = undefined;
|
|
238
|
+
return this.enqueueDriverTransition(() => this.reconcileDriver()).catch((error) => {
|
|
239
|
+
throw mapSurfaceHostError(error, "Browser Surface driver release failed");
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async dispatchInput(attachment, frame) {
|
|
243
|
+
if (this.owner !== attachment ||
|
|
244
|
+
!this.acceptingInput ||
|
|
245
|
+
!this.subscribers.has(attachment.subscriptionId)) {
|
|
246
|
+
throw new DirectRuntimeBrowserSurfaceHostError("unauthorized", "Browser Surface does not own the human driver");
|
|
247
|
+
}
|
|
248
|
+
if (this.pendingInputCount >= RUNTIME_BROWSER_SURFACE_MAX_RELIABLE_INPUT_QUEUE) {
|
|
249
|
+
throw new DirectRuntimeBrowserSurfaceHostError("capacity", "Browser Surface input queue is full");
|
|
250
|
+
}
|
|
251
|
+
this.pendingInputCount += 1;
|
|
252
|
+
const inputEpoch = this.inputEpoch;
|
|
253
|
+
try {
|
|
254
|
+
const operation = this.inputTail.then(async () => {
|
|
255
|
+
if (this.closing ||
|
|
256
|
+
!this.acceptingInput ||
|
|
257
|
+
this.owner !== attachment ||
|
|
258
|
+
inputEpoch !== this.inputEpoch)
|
|
259
|
+
return;
|
|
260
|
+
await this.page.source.dispatchTrustedInput({
|
|
261
|
+
browserGeneration: frame.browserGeneration,
|
|
262
|
+
viewportRevision: frame.viewportRevision,
|
|
263
|
+
event: frame.event,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
this.inputTail = operation.then(() => undefined, () => undefined);
|
|
267
|
+
await operation;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
throw mapSurfaceHostError(error, "Browser Surface input failed");
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
this.pendingInputCount -= 1;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
detach(attachment) {
|
|
277
|
+
if (this.subscribers.get(attachment.subscriptionId) !== attachment) {
|
|
278
|
+
return Promise.resolve();
|
|
279
|
+
}
|
|
280
|
+
this.subscribers.delete(attachment.subscriptionId);
|
|
281
|
+
if (this.owner === attachment || this.desiredOwner === attachment) {
|
|
282
|
+
this.fenceInput();
|
|
283
|
+
this.inputResetRequired = true;
|
|
284
|
+
if (this.desiredOwner === attachment)
|
|
285
|
+
this.desiredOwner = undefined;
|
|
286
|
+
}
|
|
287
|
+
if (this.subscribers.size === 0) {
|
|
288
|
+
return this.finish("cancelled", undefined, false);
|
|
289
|
+
}
|
|
290
|
+
return this.enqueueDriverTransition(() => this.reconcileDriver()).catch(() => undefined);
|
|
291
|
+
}
|
|
292
|
+
async reconcileDriver() {
|
|
293
|
+
await this.inputTail;
|
|
294
|
+
if (this.inputResetRequired) {
|
|
295
|
+
await this.page.source.resetTrustedInput?.();
|
|
296
|
+
this.inputResetRequired = false;
|
|
297
|
+
}
|
|
298
|
+
const desired = this.desiredOwner &&
|
|
299
|
+
this.subscribers.get(this.desiredOwner.subscriptionId) === this.desiredOwner
|
|
300
|
+
? this.desiredOwner
|
|
301
|
+
: undefined;
|
|
302
|
+
const targetDriver = desired ? "stream" : this.restingDriver;
|
|
303
|
+
if (this.physicalDriver !== targetDriver) {
|
|
304
|
+
await this.page.source.setHumanDriver?.(targetDriver === "stream" ? "stream" : "desktop");
|
|
305
|
+
this.physicalDriver = targetDriver;
|
|
306
|
+
}
|
|
307
|
+
const currentDesired = this.desiredOwner &&
|
|
308
|
+
this.subscribers.get(this.desiredOwner.subscriptionId) === this.desiredOwner
|
|
309
|
+
? this.desiredOwner
|
|
310
|
+
: undefined;
|
|
311
|
+
if (currentDesired !== desired || this.closing)
|
|
312
|
+
return;
|
|
313
|
+
const changed = this.owner !== desired;
|
|
314
|
+
this.owner = desired;
|
|
315
|
+
this.inputEpoch += 1;
|
|
316
|
+
this.acceptingInput = desired !== undefined;
|
|
317
|
+
if (changed)
|
|
318
|
+
this.broadcastDriverChanged();
|
|
319
|
+
}
|
|
320
|
+
enqueueDriverTransition(operation) {
|
|
321
|
+
const result = this.driverTransitionTail.then(operation, operation);
|
|
322
|
+
this.driverTransitionTail = result.then(() => undefined, () => undefined);
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
fenceInput() {
|
|
326
|
+
this.acceptingInput = false;
|
|
327
|
+
this.inputEpoch += 1;
|
|
328
|
+
}
|
|
329
|
+
broadcastDriverChanged() {
|
|
330
|
+
const state = this.driverState();
|
|
331
|
+
for (const subscriber of [...this.subscribers.values()]) {
|
|
332
|
+
subscriber.emitDriverChanged(state);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
finish(reason, message, notify) {
|
|
336
|
+
if (this.closePromise)
|
|
337
|
+
return this.closePromise;
|
|
338
|
+
this.closing = true;
|
|
339
|
+
this.fenceInput();
|
|
340
|
+
if (this.owner || this.desiredOwner || this.physicalDriver === "stream") {
|
|
341
|
+
this.inputResetRequired = true;
|
|
342
|
+
}
|
|
343
|
+
this.desiredOwner = undefined;
|
|
344
|
+
for (const subscriber of [...this.subscribers.values()]) {
|
|
345
|
+
subscriber.finishFromPage(reason, message, notify);
|
|
346
|
+
}
|
|
347
|
+
this.subscribers.clear();
|
|
348
|
+
this.page.sourceController.abort();
|
|
349
|
+
const releaseDriver = this.enqueueDriverTransition(async () => {
|
|
350
|
+
await this.inputTail;
|
|
351
|
+
try {
|
|
352
|
+
if (this.inputResetRequired)
|
|
353
|
+
await this.page.source.resetTrustedInput?.();
|
|
354
|
+
this.inputResetRequired = false;
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Closing remains fail-safe: capture must still be released if the
|
|
358
|
+
// renderer no longer accepts synthesized key/button releases.
|
|
359
|
+
}
|
|
360
|
+
if (this.physicalDriver !== this.restingDriver) {
|
|
361
|
+
try {
|
|
362
|
+
await this.page.source.setHumanDriver?.("desktop");
|
|
363
|
+
this.physicalDriver = this.restingDriver;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Closing remains fail-safe: capture is released even if Host handoff fails.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
this.owner = undefined;
|
|
370
|
+
});
|
|
371
|
+
this.closePromise = releaseDriver.then(() => this.page.source.close())
|
|
372
|
+
.catch(() => undefined)
|
|
373
|
+
.finally(this.page.onClose);
|
|
374
|
+
return this.closePromise;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
class CoordinatedSurfaceAttachment {
|
|
378
|
+
attachment;
|
|
379
|
+
ready;
|
|
380
|
+
events;
|
|
381
|
+
subscriptionId;
|
|
382
|
+
eventQueue;
|
|
383
|
+
abortHandler;
|
|
384
|
+
frameSequence = 0;
|
|
385
|
+
lastInputSequence = -1;
|
|
386
|
+
pendingFrame;
|
|
387
|
+
closing = false;
|
|
388
|
+
closePromise;
|
|
389
|
+
constructor(attachment) {
|
|
390
|
+
this.attachment = attachment;
|
|
391
|
+
this.subscriptionId = attachment.open.subscriptionId;
|
|
392
|
+
this.eventQueue = new SurfaceEventQueue({
|
|
393
|
+
maxPendingEvents: attachment.maxPendingEvents,
|
|
394
|
+
maxPendingBytes: attachment.maxPendingEventBytes,
|
|
395
|
+
});
|
|
396
|
+
const viewport = attachment.page.getViewport();
|
|
397
|
+
this.ready = {
|
|
398
|
+
type: "browser.surface.ready",
|
|
399
|
+
subscriptionId: this.subscriptionId,
|
|
400
|
+
browserGeneration: attachment.open.browserGeneration,
|
|
401
|
+
viewportRevision: viewport.revision,
|
|
402
|
+
sourceWidth: viewport.width,
|
|
403
|
+
sourceHeight: viewport.height,
|
|
404
|
+
deviceScaleFactor: viewport.deviceScaleFactor,
|
|
405
|
+
format: attachment.open.format,
|
|
406
|
+
grantedRole: attachment.open.requestedRole,
|
|
407
|
+
};
|
|
408
|
+
this.events = this.eventQueue;
|
|
409
|
+
this.abortHandler = () => {
|
|
410
|
+
void this.finish("cancelled", "Browser Surface connection was cancelled", true);
|
|
411
|
+
};
|
|
412
|
+
attachment.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
413
|
+
}
|
|
414
|
+
start() {
|
|
415
|
+
this.emitDriverChanged(this.attachment.page.driverState());
|
|
416
|
+
}
|
|
417
|
+
async handle(frame) {
|
|
418
|
+
if (this.closing)
|
|
419
|
+
return;
|
|
420
|
+
if (frame.subscriptionId !== this.subscriptionId) {
|
|
421
|
+
throw new DirectRuntimeBrowserSurfaceHostError("protocol_error", "Browser Surface subscription does not match");
|
|
422
|
+
}
|
|
423
|
+
switch (frame.type) {
|
|
424
|
+
case "browser.surface.frame.ack":
|
|
425
|
+
this.acknowledgeFrame(frame.frameSequence);
|
|
426
|
+
return;
|
|
427
|
+
case "browser.surface.driver.take":
|
|
428
|
+
this.requireGeneration(frame.browserGeneration);
|
|
429
|
+
this.requireControlRole();
|
|
430
|
+
if (!this.matchesViewport(frame.viewportRevision))
|
|
431
|
+
return;
|
|
432
|
+
await this.attachment.page.takeDriver(this);
|
|
433
|
+
return;
|
|
434
|
+
case "browser.surface.driver.release":
|
|
435
|
+
this.requireGeneration(frame.browserGeneration);
|
|
436
|
+
await this.attachment.page.releaseDriver(this);
|
|
437
|
+
return;
|
|
438
|
+
case "browser.surface.input":
|
|
439
|
+
this.requireGeneration(frame.browserGeneration);
|
|
440
|
+
this.requireControlRole();
|
|
441
|
+
if (!this.matchesViewport(frame.viewportRevision))
|
|
442
|
+
return;
|
|
443
|
+
if (frame.inputSequence <= this.lastInputSequence) {
|
|
444
|
+
throw new DirectRuntimeBrowserSurfaceHostError("protocol_error", "Browser Surface input sequence must increase");
|
|
445
|
+
}
|
|
446
|
+
this.lastInputSequence = frame.inputSequence;
|
|
447
|
+
await this.attachment.page.dispatchInput(this, frame);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
close(_reason) {
|
|
451
|
+
return this.finish("cancelled", undefined, false);
|
|
452
|
+
}
|
|
453
|
+
offerFrame(frame) {
|
|
454
|
+
if (this.closing || this.pendingFrame)
|
|
455
|
+
return;
|
|
456
|
+
const sequence = ++this.frameSequence;
|
|
457
|
+
const timer = setTimeout(() => {
|
|
458
|
+
void this.finish("idle_timeout", "Browser Surface frame was not acknowledged", true);
|
|
459
|
+
}, this.attachment.frameAcknowledgeTimeoutMs);
|
|
460
|
+
timer.unref?.();
|
|
461
|
+
this.pendingFrame = { sequence, timer };
|
|
462
|
+
const image = {
|
|
463
|
+
subscriptionId: this.subscriptionId,
|
|
464
|
+
frameSequence: sequence,
|
|
465
|
+
viewportRevision: frame.viewportRevision,
|
|
466
|
+
timestampMilliseconds: frame.timestampMilliseconds,
|
|
467
|
+
width: frame.width,
|
|
468
|
+
height: frame.height,
|
|
469
|
+
format: frame.format,
|
|
470
|
+
encodedBytes: frame.encodedBytes,
|
|
471
|
+
};
|
|
472
|
+
if (!this.eventQueue.push(image)) {
|
|
473
|
+
clearTimeout(timer);
|
|
474
|
+
this.pendingFrame = undefined;
|
|
475
|
+
void this.finish("capacity", "Browser Surface event queue is full", true);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
emitDriverChanged(state) {
|
|
479
|
+
if (this.closing)
|
|
480
|
+
return;
|
|
481
|
+
const event = {
|
|
482
|
+
type: "browser.surface.driver.changed",
|
|
483
|
+
subscriptionId: this.subscriptionId,
|
|
484
|
+
browserGeneration: this.ready.browserGeneration,
|
|
485
|
+
driver: state.driver,
|
|
486
|
+
...(state.driverSubscriptionId === undefined
|
|
487
|
+
? {}
|
|
488
|
+
: { driverSubscriptionId: state.driverSubscriptionId }),
|
|
489
|
+
};
|
|
490
|
+
if (!this.eventQueue.push(event)) {
|
|
491
|
+
void this.finish("capacity", "Browser Surface event queue is full", true);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
finishFromPage(reason, message, notify) {
|
|
495
|
+
void this.finish(reason, message, notify, false);
|
|
496
|
+
}
|
|
497
|
+
acknowledgeFrame(sequence) {
|
|
498
|
+
const pending = this.pendingFrame;
|
|
499
|
+
if (!pending || pending.sequence !== sequence) {
|
|
500
|
+
throw new DirectRuntimeBrowserSurfaceHostError("protocol_error", "Browser Surface frame acknowledgement is stale or unknown");
|
|
501
|
+
}
|
|
502
|
+
clearTimeout(pending.timer);
|
|
503
|
+
this.pendingFrame = undefined;
|
|
504
|
+
}
|
|
505
|
+
requireGeneration(generation) {
|
|
506
|
+
if (generation !== this.ready.browserGeneration) {
|
|
507
|
+
throw new DirectRuntimeBrowserSurfaceHostError("generation_changed", "Browser generation changed");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
matchesViewport(revision) {
|
|
511
|
+
return revision === this.attachment.page.getViewport().revision;
|
|
512
|
+
}
|
|
513
|
+
requireControlRole() {
|
|
514
|
+
if (this.ready.grantedRole !== "control") {
|
|
515
|
+
throw new DirectRuntimeBrowserSurfaceHostError("unauthorized", "Browser Surface is view-only");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
finish(reason, message, notify, detach = true) {
|
|
519
|
+
if (this.closePromise)
|
|
520
|
+
return this.closePromise;
|
|
521
|
+
this.closing = true;
|
|
522
|
+
this.attachment.signal.removeEventListener("abort", this.abortHandler);
|
|
523
|
+
const pending = this.pendingFrame;
|
|
524
|
+
this.pendingFrame = undefined;
|
|
525
|
+
if (pending)
|
|
526
|
+
clearTimeout(pending.timer);
|
|
527
|
+
this.eventQueue.close(notify
|
|
528
|
+
? {
|
|
529
|
+
type: "browser.surface.closed",
|
|
530
|
+
subscriptionId: this.subscriptionId,
|
|
531
|
+
reason,
|
|
532
|
+
...(message === undefined ? {} : { message: message.slice(0, 1_024) }),
|
|
533
|
+
}
|
|
534
|
+
: undefined);
|
|
535
|
+
this.closePromise = detach
|
|
536
|
+
? this.attachment.page.detach(this)
|
|
537
|
+
: Promise.resolve();
|
|
538
|
+
return this.closePromise;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
class SurfaceEventQueue {
|
|
542
|
+
options;
|
|
543
|
+
items = [];
|
|
544
|
+
queuedBytes = 0;
|
|
545
|
+
waiter;
|
|
546
|
+
ended = false;
|
|
547
|
+
constructor(options) {
|
|
548
|
+
this.options = options;
|
|
549
|
+
}
|
|
550
|
+
push(event) {
|
|
551
|
+
if (this.ended)
|
|
552
|
+
return false;
|
|
553
|
+
if (this.waiter) {
|
|
554
|
+
const waiter = this.waiter;
|
|
555
|
+
this.waiter = undefined;
|
|
556
|
+
waiter({ done: false, value: event });
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
const bytes = surfaceEventBytes(event);
|
|
560
|
+
const tail = this.items.at(-1);
|
|
561
|
+
if (tail &&
|
|
562
|
+
isDriverChanged(tail.event) &&
|
|
563
|
+
isDriverChanged(event)) {
|
|
564
|
+
const nextBytes = this.queuedBytes - tail.bytes + bytes;
|
|
565
|
+
if (nextBytes > this.options.maxPendingBytes)
|
|
566
|
+
return false;
|
|
567
|
+
tail.event = event;
|
|
568
|
+
tail.bytes = bytes;
|
|
569
|
+
this.queuedBytes = nextBytes;
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
if (this.items.length >= this.options.maxPendingEvents ||
|
|
573
|
+
this.queuedBytes + bytes > this.options.maxPendingBytes)
|
|
574
|
+
return false;
|
|
575
|
+
this.items.push({ event, bytes });
|
|
576
|
+
this.queuedBytes += bytes;
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
close(finalEvent) {
|
|
580
|
+
if (this.ended)
|
|
581
|
+
return;
|
|
582
|
+
this.ended = true;
|
|
583
|
+
this.items.length = 0;
|
|
584
|
+
this.queuedBytes = 0;
|
|
585
|
+
if (finalEvent) {
|
|
586
|
+
if (this.waiter)
|
|
587
|
+
this.waiter({ done: false, value: finalEvent });
|
|
588
|
+
else {
|
|
589
|
+
const bytes = surfaceEventBytes(finalEvent);
|
|
590
|
+
this.items.push({ event: finalEvent, bytes });
|
|
591
|
+
this.queuedBytes = bytes;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
this.waiter?.({ done: true, value: undefined });
|
|
596
|
+
}
|
|
597
|
+
this.waiter = undefined;
|
|
598
|
+
}
|
|
599
|
+
[Symbol.asyncIterator]() {
|
|
600
|
+
return { next: () => this.next() };
|
|
601
|
+
}
|
|
602
|
+
next() {
|
|
603
|
+
const pending = this.items.shift();
|
|
604
|
+
if (pending) {
|
|
605
|
+
this.queuedBytes -= pending.bytes;
|
|
606
|
+
return Promise.resolve({ done: false, value: pending.event });
|
|
607
|
+
}
|
|
608
|
+
if (this.ended)
|
|
609
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
610
|
+
if (this.waiter)
|
|
611
|
+
return Promise.reject(new Error("Browser Surface event read is already pending"));
|
|
612
|
+
return new Promise((resolve) => {
|
|
613
|
+
this.waiter = resolve;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function isDriverChanged(event) {
|
|
618
|
+
return "type" in event && event.type === "browser.surface.driver.changed";
|
|
619
|
+
}
|
|
620
|
+
function surfaceEventBytes(event) {
|
|
621
|
+
return "encodedBytes" in event
|
|
622
|
+
? event.encodedBytes.byteLength + 128
|
|
623
|
+
: Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
624
|
+
}
|
|
625
|
+
function positiveLimit(value, fallback, field) {
|
|
626
|
+
const resolved = value ?? fallback;
|
|
627
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1) {
|
|
628
|
+
throw new Error(`${field} must be a positive safe integer`);
|
|
629
|
+
}
|
|
630
|
+
return resolved;
|
|
631
|
+
}
|
|
632
|
+
function mapSurfaceOpenError(error) {
|
|
633
|
+
if (error instanceof SessionBrowserSurfaceUnsupportedError) {
|
|
634
|
+
return new DirectRuntimeBrowserSurfaceHostError("unsupported", error.message, { cause: error });
|
|
635
|
+
}
|
|
636
|
+
if (error instanceof SessionBrowserServiceError) {
|
|
637
|
+
switch (error.code) {
|
|
638
|
+
case "invalid_request":
|
|
639
|
+
return new DirectRuntimeBrowserSurfaceHostError("protocol_error", error.message, { cause: error });
|
|
640
|
+
case "not_found":
|
|
641
|
+
return new DirectRuntimeBrowserSurfaceHostError("page_closed", error.message, { cause: error });
|
|
642
|
+
case "generation_mismatch":
|
|
643
|
+
return new DirectRuntimeBrowserSurfaceHostError("generation_changed", error.message, { cause: error });
|
|
644
|
+
case "capacity":
|
|
645
|
+
return new DirectRuntimeBrowserSurfaceHostError("capacity", error.message, { cause: error });
|
|
646
|
+
case "unavailable":
|
|
647
|
+
case "shutting_down":
|
|
648
|
+
return new DirectRuntimeBrowserSurfaceHostError("browser_closed", error.message, { cause: error });
|
|
649
|
+
case "host_failure":
|
|
650
|
+
case "outcome_unknown":
|
|
651
|
+
return new DirectRuntimeBrowserSurfaceHostError("internal_error", error.message, { cause: error });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return mapSurfaceHostError(error, "Browser Surface could not open");
|
|
655
|
+
}
|
|
656
|
+
function mapSurfaceHostError(error, fallbackMessage) {
|
|
657
|
+
if (error instanceof DirectRuntimeBrowserSurfaceHostError)
|
|
658
|
+
return error;
|
|
659
|
+
if (error instanceof SessionBrowserHostError) {
|
|
660
|
+
const reason = error.code === "page_not_found"
|
|
661
|
+
? "page_closed"
|
|
662
|
+
: error.code === "capacity"
|
|
663
|
+
? "capacity"
|
|
664
|
+
: "browser_closed";
|
|
665
|
+
return new DirectRuntimeBrowserSurfaceHostError(reason, error.message, { cause: error });
|
|
666
|
+
}
|
|
667
|
+
const message = error instanceof Error ? error.message : fallbackMessage;
|
|
668
|
+
return new DirectRuntimeBrowserSurfaceHostError("internal_error", message, { cause: error });
|
|
669
|
+
}
|