@rynx-ai/server 0.1.9 → 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-ws.d.ts +2 -0
- package/dist/browser-surface-ws.js +1 -0
- package/dist/control-api.d.ts +17 -0
- package/dist/control-api.js +809 -9
- package/dist/control-web/assets/{highlighted-body-OFNGDK62-e-w61YLy.js → highlighted-body-OFNGDK62-DHu2g-rk.js} +1 -1
- 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/{mermaid-GHXKKRXX-DZn7Hbr2.js → mermaid-GHXKKRXX-CSYzOmBR.js} +3 -3
- package/dist/control-web/index.html +2 -2
- package/dist/desktop-browser-host.d.ts +20 -3
- package/dist/desktop-browser-host.js +126 -44
- 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-server.d.ts +6 -0
- package/dist/direct-runtime-server.js +29 -1
- package/dist/emulator-surface-ws.d.ts +2 -0
- package/dist/emulator-surface-ws.js +1 -0
- package/dist/machine-session-service.d.ts +88 -5
- package/dist/machine-session-service.js +283 -20
- package/dist/remote-runtime-dispatcher.d.ts +1 -1
- package/dist/remote-runtime-dispatcher.js +25 -2
- package/dist/remote-runtime-session-projection.js +10 -0
- package/dist/runtime-web-auth.d.ts +5 -0
- package/dist/runtime-web-auth.js +43 -3
- package/dist/server.d.ts +78 -8
- package/dist/server.js +42 -17
- package/dist/session-browser-service.d.ts +30 -0
- package/dist/session-browser-service.js +89 -3
- package/dist/session-browser-surface-coordinator.d.ts +4 -3
- package/dist/session-browser-surface-coordinator.js +374 -268
- package/dist/session-runtime-index.d.ts +6 -0
- package/dist/session-runtime-index.js +10 -0
- package/dist/session-terminal-host.d.ts +6 -1
- package/dist/session-terminal-host.js +18 -0
- package/dist/terminal-ws.d.ts +3 -0
- package/dist/terminal-ws.js +1 -0
- package/package.json +12 -7
- package/dist/control-web/assets/index-BQ7XVBKO.css +0 -32
- package/dist/control-web/assets/index-ClpzuBJx.js +0 -606
|
@@ -1,16 +1,17 @@
|
|
|
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, } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
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
2
|
import { DirectRuntimeBrowserSurfaceHostError, } from "./direct-runtime-browser-surface-server.js";
|
|
3
3
|
import { SessionBrowserHostError, SessionBrowserServiceError, SessionBrowserSurfaceUnsupportedError, } from "./session-browser-service.js";
|
|
4
4
|
const DEFAULT_MAX_PENDING_EVENTS = 8;
|
|
5
5
|
const DEFAULT_MAX_PENDING_EVENT_BYTES = RUNTIME_BROWSER_SURFACE_MAX_ENCODED_FRAME_BYTES +
|
|
6
6
|
RUNTIME_BROWSER_SURFACE_MAX_CONTROL_FRAME_BYTES * DEFAULT_MAX_PENDING_EVENTS;
|
|
7
7
|
/**
|
|
8
|
-
* Owns
|
|
9
|
-
*
|
|
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.
|
|
10
11
|
*/
|
|
11
12
|
export class SessionBrowserSurfaceCoordinator {
|
|
12
13
|
browsers;
|
|
13
|
-
|
|
14
|
+
pages = new Map();
|
|
14
15
|
pageQueues = new Map();
|
|
15
16
|
frameAcknowledgeTimeoutMs;
|
|
16
17
|
maxPendingEvents;
|
|
@@ -44,43 +45,53 @@ export class SessionBrowserSurfaceCoordinator {
|
|
|
44
45
|
throw mapSurfaceOpenError(error);
|
|
45
46
|
}
|
|
46
47
|
context.signal.throwIfAborted();
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
await
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
source = await this.browsers.openSurface({
|
|
53
|
-
sessionId: frame.sessionId,
|
|
54
|
-
browserGeneration: frame.browserGeneration,
|
|
55
|
-
pageId: frame.pageId,
|
|
56
|
-
format: frame.format,
|
|
57
|
-
quality: frame.quality,
|
|
58
|
-
maximumFrameRate: frame.maximumFrameRate,
|
|
59
|
-
signal: context.signal,
|
|
60
|
-
});
|
|
48
|
+
let page = this.pages.get(pageKey);
|
|
49
|
+
if (page?.isClosing()) {
|
|
50
|
+
await page.closed();
|
|
51
|
+
page = undefined;
|
|
61
52
|
}
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
if (page && page.browserGeneration !== frame.browserGeneration) {
|
|
54
|
+
await page.finish("generation_changed", "Browser generation changed", true);
|
|
55
|
+
page = undefined;
|
|
64
56
|
}
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
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);
|
|
68
92
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
open: frame,
|
|
72
|
-
source,
|
|
73
|
-
signal: context.signal,
|
|
74
|
-
frameAcknowledgeTimeoutMs: this.frameAcknowledgeTimeoutMs,
|
|
75
|
-
maxPendingEvents: this.maxPendingEvents,
|
|
76
|
-
maxPendingEventBytes: this.maxPendingEventBytes,
|
|
77
|
-
onClose: () => {
|
|
78
|
-
if (this.attachments.get(pageKey) === attachment)
|
|
79
|
-
this.attachments.delete(pageKey);
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
this.attachments.set(pageKey, attachment);
|
|
83
|
-
attachment.start();
|
|
93
|
+
const attachment = page.attach(frame, context.signal);
|
|
94
|
+
page.start();
|
|
84
95
|
return attachment;
|
|
85
96
|
});
|
|
86
97
|
}
|
|
@@ -96,36 +107,296 @@ export class SessionBrowserSurfaceCoordinator {
|
|
|
96
107
|
return result;
|
|
97
108
|
}
|
|
98
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
|
+
}
|
|
99
377
|
class CoordinatedSurfaceAttachment {
|
|
100
378
|
attachment;
|
|
101
379
|
ready;
|
|
102
380
|
events;
|
|
381
|
+
subscriptionId;
|
|
103
382
|
eventQueue;
|
|
104
383
|
abortHandler;
|
|
105
384
|
frameSequence = 0;
|
|
106
385
|
lastInputSequence = -1;
|
|
107
|
-
pendingInputCount = 0;
|
|
108
|
-
inputTail = Promise.resolve();
|
|
109
|
-
inputEpoch = 0;
|
|
110
|
-
acceptingInput = false;
|
|
111
|
-
inputResetRequired = false;
|
|
112
386
|
pendingFrame;
|
|
113
|
-
driver;
|
|
114
|
-
physicalDriver;
|
|
115
|
-
driverIntent = 0;
|
|
116
|
-
driverTransitionTail = Promise.resolve();
|
|
117
387
|
closing = false;
|
|
118
388
|
closePromise;
|
|
119
389
|
constructor(attachment) {
|
|
120
390
|
this.attachment = attachment;
|
|
391
|
+
this.subscriptionId = attachment.open.subscriptionId;
|
|
121
392
|
this.eventQueue = new SurfaceEventQueue({
|
|
122
393
|
maxPendingEvents: attachment.maxPendingEvents,
|
|
123
394
|
maxPendingBytes: attachment.maxPendingEventBytes,
|
|
124
395
|
});
|
|
125
|
-
const viewport = attachment.
|
|
396
|
+
const viewport = attachment.page.getViewport();
|
|
126
397
|
this.ready = {
|
|
127
398
|
type: "browser.surface.ready",
|
|
128
|
-
subscriptionId:
|
|
399
|
+
subscriptionId: this.subscriptionId,
|
|
129
400
|
browserGeneration: attachment.open.browserGeneration,
|
|
130
401
|
viewportRevision: viewport.revision,
|
|
131
402
|
sourceWidth: viewport.width,
|
|
@@ -134,8 +405,6 @@ class CoordinatedSurfaceAttachment {
|
|
|
134
405
|
format: attachment.open.format,
|
|
135
406
|
grantedRole: attachment.open.requestedRole,
|
|
136
407
|
};
|
|
137
|
-
this.driver = attachment.source.setHumanDriver ? "desktop" : "none";
|
|
138
|
-
this.physicalDriver = this.driver;
|
|
139
408
|
this.events = this.eventQueue;
|
|
140
409
|
this.abortHandler = () => {
|
|
141
410
|
void this.finish("cancelled", "Browser Surface connection was cancelled", true);
|
|
@@ -143,20 +412,12 @@ class CoordinatedSurfaceAttachment {
|
|
|
143
412
|
attachment.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
144
413
|
}
|
|
145
414
|
start() {
|
|
146
|
-
|
|
147
|
-
this.emitDriverChanged();
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
const mapped = mapSurfaceHostError(error, "Browser Surface event queue is full");
|
|
151
|
-
void this.finish(mapped.reason, mapped.message, true);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
void this.pumpFrames();
|
|
415
|
+
this.emitDriverChanged(this.attachment.page.driverState());
|
|
155
416
|
}
|
|
156
417
|
async handle(frame) {
|
|
157
418
|
if (this.closing)
|
|
158
419
|
return;
|
|
159
|
-
if (frame.subscriptionId !== this.
|
|
420
|
+
if (frame.subscriptionId !== this.subscriptionId) {
|
|
160
421
|
throw new DirectRuntimeBrowserSurfaceHostError("protocol_error", "Browser Surface subscription does not match");
|
|
161
422
|
}
|
|
162
423
|
switch (frame.type) {
|
|
@@ -168,120 +429,71 @@ class CoordinatedSurfaceAttachment {
|
|
|
168
429
|
this.requireControlRole();
|
|
169
430
|
if (!this.matchesViewport(frame.viewportRevision))
|
|
170
431
|
return;
|
|
171
|
-
await this.takeDriver();
|
|
432
|
+
await this.attachment.page.takeDriver(this);
|
|
172
433
|
return;
|
|
173
434
|
case "browser.surface.driver.release":
|
|
174
435
|
this.requireGeneration(frame.browserGeneration);
|
|
175
|
-
await this.releaseDriver();
|
|
436
|
+
await this.attachment.page.releaseDriver(this);
|
|
176
437
|
return;
|
|
177
438
|
case "browser.surface.input":
|
|
178
439
|
this.requireGeneration(frame.browserGeneration);
|
|
179
440
|
this.requireControlRole();
|
|
180
|
-
if (this.driver !== "stream" || !this.acceptingInput) {
|
|
181
|
-
throw new DirectRuntimeBrowserSurfaceHostError("unauthorized", "Browser Surface does not own the human driver");
|
|
182
|
-
}
|
|
183
441
|
if (!this.matchesViewport(frame.viewportRevision))
|
|
184
442
|
return;
|
|
185
443
|
if (frame.inputSequence <= this.lastInputSequence) {
|
|
186
444
|
throw new DirectRuntimeBrowserSurfaceHostError("protocol_error", "Browser Surface input sequence must increase");
|
|
187
445
|
}
|
|
188
|
-
if (this.pendingInputCount >= RUNTIME_BROWSER_SURFACE_MAX_RELIABLE_INPUT_QUEUE) {
|
|
189
|
-
throw new DirectRuntimeBrowserSurfaceHostError("capacity", "Browser Surface input queue is full");
|
|
190
|
-
}
|
|
191
446
|
this.lastInputSequence = frame.inputSequence;
|
|
192
|
-
this.
|
|
193
|
-
const inputEpoch = this.inputEpoch;
|
|
194
|
-
try {
|
|
195
|
-
const operation = this.inputTail.then(async () => {
|
|
196
|
-
if (this.closing ||
|
|
197
|
-
!this.acceptingInput ||
|
|
198
|
-
this.driver !== "stream" ||
|
|
199
|
-
inputEpoch !== this.inputEpoch)
|
|
200
|
-
return;
|
|
201
|
-
await this.attachment.source.dispatchTrustedInput({
|
|
202
|
-
browserGeneration: frame.browserGeneration,
|
|
203
|
-
viewportRevision: frame.viewportRevision,
|
|
204
|
-
event: frame.event,
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
this.inputTail = operation.then(() => undefined, () => undefined);
|
|
208
|
-
await operation;
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
throw mapSurfaceHostError(error, "Browser Surface input failed");
|
|
212
|
-
}
|
|
213
|
-
finally {
|
|
214
|
-
this.pendingInputCount -= 1;
|
|
215
|
-
}
|
|
447
|
+
await this.attachment.page.dispatchInput(this, frame);
|
|
216
448
|
}
|
|
217
449
|
}
|
|
218
450
|
close(_reason) {
|
|
219
451
|
return this.finish("cancelled", undefined, false);
|
|
220
452
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}, this.attachment.frameAcknowledgeTimeoutMs);
|
|
245
|
-
timer.unref?.();
|
|
246
|
-
this.pendingFrame = { sequence, release: resolve, timer };
|
|
247
|
-
});
|
|
248
|
-
const image = {
|
|
249
|
-
subscriptionId: this.attachment.open.subscriptionId,
|
|
250
|
-
frameSequence: sequence,
|
|
251
|
-
viewportRevision: frame.viewportRevision,
|
|
252
|
-
timestampMilliseconds: frame.timestampMilliseconds,
|
|
253
|
-
width: frame.width,
|
|
254
|
-
height: frame.height,
|
|
255
|
-
format: frame.format,
|
|
256
|
-
encodedBytes: frame.encodedBytes,
|
|
257
|
-
};
|
|
258
|
-
if (!this.eventQueue.push(image)) {
|
|
259
|
-
const pending = this.pendingFrame;
|
|
260
|
-
this.pendingFrame = undefined;
|
|
261
|
-
if (pending) {
|
|
262
|
-
clearTimeout(pending.timer);
|
|
263
|
-
pending.release();
|
|
264
|
-
}
|
|
265
|
-
await frame.acknowledge();
|
|
266
|
-
await this.finish("capacity", "Browser Surface event queue is full", true);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
await released;
|
|
270
|
-
await frame.acknowledge();
|
|
271
|
-
if (this.closing)
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (!this.closing) {
|
|
275
|
-
await this.finish("browser_closed", "Browser Surface source ended", true);
|
|
276
|
-
}
|
|
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);
|
|
277
476
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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);
|
|
283
492
|
}
|
|
284
493
|
}
|
|
494
|
+
finishFromPage(reason, message, notify) {
|
|
495
|
+
void this.finish(reason, message, notify, false);
|
|
496
|
+
}
|
|
285
497
|
acknowledgeFrame(sequence) {
|
|
286
498
|
const pending = this.pendingFrame;
|
|
287
499
|
if (!pending || pending.sequence !== sequence) {
|
|
@@ -289,146 +501,40 @@ class CoordinatedSurfaceAttachment {
|
|
|
289
501
|
}
|
|
290
502
|
clearTimeout(pending.timer);
|
|
291
503
|
this.pendingFrame = undefined;
|
|
292
|
-
pending.release();
|
|
293
504
|
}
|
|
294
505
|
requireGeneration(generation) {
|
|
295
|
-
if (generation !== this.
|
|
506
|
+
if (generation !== this.ready.browserGeneration) {
|
|
296
507
|
throw new DirectRuntimeBrowserSurfaceHostError("generation_changed", "Browser generation changed");
|
|
297
508
|
}
|
|
298
509
|
}
|
|
299
510
|
matchesViewport(revision) {
|
|
300
|
-
return revision === this.attachment.
|
|
511
|
+
return revision === this.attachment.page.getViewport().revision;
|
|
301
512
|
}
|
|
302
513
|
requireControlRole() {
|
|
303
514
|
if (this.ready.grantedRole !== "control") {
|
|
304
515
|
throw new DirectRuntimeBrowserSurfaceHostError("unauthorized", "Browser Surface is view-only");
|
|
305
516
|
}
|
|
306
517
|
}
|
|
307
|
-
|
|
308
|
-
const event = {
|
|
309
|
-
type: "browser.surface.driver.changed",
|
|
310
|
-
subscriptionId: this.attachment.open.subscriptionId,
|
|
311
|
-
browserGeneration: this.attachment.open.browserGeneration,
|
|
312
|
-
driver: this.driver,
|
|
313
|
-
...(this.driver === "stream"
|
|
314
|
-
? { driverSubscriptionId: this.attachment.open.subscriptionId }
|
|
315
|
-
: {}),
|
|
316
|
-
};
|
|
317
|
-
if (!this.eventQueue.push(event)) {
|
|
318
|
-
throw new DirectRuntimeBrowserSurfaceHostError("capacity", "Browser Surface event queue is full");
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
takeDriver() {
|
|
322
|
-
const intent = ++this.driverIntent;
|
|
323
|
-
const operation = this.enqueueDriverTransition(async () => {
|
|
324
|
-
if (this.closing || intent !== this.driverIntent)
|
|
325
|
-
return;
|
|
326
|
-
if (this.inputResetRequired) {
|
|
327
|
-
await this.inputTail;
|
|
328
|
-
await this.attachment.source.resetTrustedInput?.();
|
|
329
|
-
this.inputResetRequired = false;
|
|
330
|
-
}
|
|
331
|
-
if (this.closing || intent !== this.driverIntent)
|
|
332
|
-
return;
|
|
333
|
-
if (this.physicalDriver !== "stream") {
|
|
334
|
-
await this.attachment.source.setHumanDriver?.("stream");
|
|
335
|
-
this.physicalDriver = "stream";
|
|
336
|
-
}
|
|
337
|
-
if (this.closing || intent !== this.driverIntent)
|
|
338
|
-
return;
|
|
339
|
-
const changed = this.driver !== "stream";
|
|
340
|
-
this.inputEpoch += 1;
|
|
341
|
-
this.acceptingInput = true;
|
|
342
|
-
this.driver = "stream";
|
|
343
|
-
if (changed)
|
|
344
|
-
this.emitDriverChanged();
|
|
345
|
-
});
|
|
346
|
-
return operation.catch((error) => {
|
|
347
|
-
throw mapSurfaceHostError(error, "Browser Surface driver handoff failed");
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
releaseDriver() {
|
|
351
|
-
this.fenceInput();
|
|
352
|
-
this.inputResetRequired = true;
|
|
353
|
-
const intent = ++this.driverIntent;
|
|
354
|
-
const operation = this.enqueueDriverTransition(async () => {
|
|
355
|
-
await this.inputTail;
|
|
356
|
-
await this.attachment.source.resetTrustedInput?.();
|
|
357
|
-
this.inputResetRequired = false;
|
|
358
|
-
if (this.closing || intent !== this.driverIntent)
|
|
359
|
-
return;
|
|
360
|
-
const restingDriver = this.attachment.source.setHumanDriver ? "desktop" : "none";
|
|
361
|
-
if (this.physicalDriver !== restingDriver) {
|
|
362
|
-
await this.attachment.source.setHumanDriver?.("desktop");
|
|
363
|
-
this.physicalDriver = restingDriver;
|
|
364
|
-
}
|
|
365
|
-
if (this.closing || intent !== this.driverIntent)
|
|
366
|
-
return;
|
|
367
|
-
const changed = this.driver !== restingDriver;
|
|
368
|
-
this.driver = restingDriver;
|
|
369
|
-
if (changed)
|
|
370
|
-
this.emitDriverChanged();
|
|
371
|
-
});
|
|
372
|
-
return operation.catch((error) => {
|
|
373
|
-
throw mapSurfaceHostError(error, "Browser Surface driver release failed");
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
enqueueDriverTransition(operation) {
|
|
377
|
-
const result = this.driverTransitionTail.then(operation, operation);
|
|
378
|
-
this.driverTransitionTail = result.then(() => undefined, () => undefined);
|
|
379
|
-
return result;
|
|
380
|
-
}
|
|
381
|
-
fenceInput() {
|
|
382
|
-
this.acceptingInput = false;
|
|
383
|
-
this.inputEpoch += 1;
|
|
384
|
-
}
|
|
385
|
-
finish(reason, message, notify) {
|
|
518
|
+
finish(reason, message, notify, detach = true) {
|
|
386
519
|
if (this.closePromise)
|
|
387
520
|
return this.closePromise;
|
|
388
521
|
this.closing = true;
|
|
389
|
-
this.fenceInput();
|
|
390
|
-
this.inputResetRequired = true;
|
|
391
|
-
this.driverIntent += 1;
|
|
392
522
|
this.attachment.signal.removeEventListener("abort", this.abortHandler);
|
|
393
523
|
const pending = this.pendingFrame;
|
|
394
524
|
this.pendingFrame = undefined;
|
|
395
|
-
if (pending)
|
|
525
|
+
if (pending)
|
|
396
526
|
clearTimeout(pending.timer);
|
|
397
|
-
pending.release();
|
|
398
|
-
}
|
|
399
527
|
this.eventQueue.close(notify
|
|
400
528
|
? {
|
|
401
529
|
type: "browser.surface.closed",
|
|
402
|
-
subscriptionId: this.
|
|
530
|
+
subscriptionId: this.subscriptionId,
|
|
403
531
|
reason,
|
|
404
532
|
...(message === undefined ? {} : { message: message.slice(0, 1_024) }),
|
|
405
533
|
}
|
|
406
534
|
: undefined);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
await this.attachment.source.resetTrustedInput?.();
|
|
412
|
-
this.inputResetRequired = false;
|
|
413
|
-
}
|
|
414
|
-
catch {
|
|
415
|
-
// Closing remains fail-safe: capture must still be released if the
|
|
416
|
-
// renderer no longer accepts synthesized key/button releases.
|
|
417
|
-
}
|
|
418
|
-
if (this.physicalDriver !== restingDriver) {
|
|
419
|
-
try {
|
|
420
|
-
await this.attachment.source.setHumanDriver?.("desktop");
|
|
421
|
-
this.physicalDriver = restingDriver;
|
|
422
|
-
}
|
|
423
|
-
catch {
|
|
424
|
-
// Closing remains fail-safe: capture is released even if Host handoff fails.
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
this.driver = restingDriver;
|
|
428
|
-
});
|
|
429
|
-
this.closePromise = releaseDriver.then(() => this.attachment.source.close())
|
|
430
|
-
.catch(() => undefined)
|
|
431
|
-
.finally(this.attachment.onClose);
|
|
535
|
+
this.closePromise = detach
|
|
536
|
+
? this.attachment.page.detach(this)
|
|
537
|
+
: Promise.resolve();
|
|
432
538
|
return this.closePromise;
|
|
433
539
|
}
|
|
434
540
|
}
|