@rynx-ai/server 0.1.9 → 0.1.10-beta.3

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.
Files changed (39) hide show
  1. package/dist/browser-surface-ws.d.ts +2 -0
  2. package/dist/browser-surface-ws.js +1 -0
  3. package/dist/control-api.d.ts +17 -0
  4. package/dist/control-api.js +809 -9
  5. package/dist/control-web/assets/{highlighted-body-OFNGDK62-e-w61YLy.js → highlighted-body-OFNGDK62-D-FhYROw.js} +1 -1
  6. package/dist/control-web/assets/index-BKzSsGxH.js +671 -0
  7. package/dist/control-web/assets/index-DykWY_Vk.css +32 -0
  8. package/dist/control-web/assets/{mermaid-GHXKKRXX-DZn7Hbr2.js → mermaid-GHXKKRXX-DE-Ovg9T.js} +3 -3
  9. package/dist/control-web/index.html +2 -2
  10. package/dist/desktop-browser-host.d.ts +20 -3
  11. package/dist/desktop-browser-host.js +126 -44
  12. package/dist/direct-runtime-browser-inspect-server.d.ts +72 -0
  13. package/dist/direct-runtime-browser-inspect-server.js +749 -0
  14. package/dist/direct-runtime-server.d.ts +6 -0
  15. package/dist/direct-runtime-server.js +29 -1
  16. package/dist/emulator-surface-ws.d.ts +2 -0
  17. package/dist/emulator-surface-ws.js +1 -0
  18. package/dist/machine-session-service.d.ts +88 -5
  19. package/dist/machine-session-service.js +283 -20
  20. package/dist/remote-runtime-dispatcher.d.ts +1 -1
  21. package/dist/remote-runtime-dispatcher.js +25 -2
  22. package/dist/remote-runtime-session-projection.js +10 -0
  23. package/dist/runtime-web-auth.d.ts +5 -0
  24. package/dist/runtime-web-auth.js +43 -3
  25. package/dist/server.d.ts +78 -8
  26. package/dist/server.js +42 -17
  27. package/dist/session-browser-service.d.ts +30 -0
  28. package/dist/session-browser-service.js +89 -3
  29. package/dist/session-browser-surface-coordinator.d.ts +4 -3
  30. package/dist/session-browser-surface-coordinator.js +374 -268
  31. package/dist/session-runtime-index.d.ts +6 -0
  32. package/dist/session-runtime-index.js +10 -0
  33. package/dist/session-terminal-host.d.ts +6 -1
  34. package/dist/session-terminal-host.js +18 -0
  35. package/dist/terminal-ws.d.ts +3 -0
  36. package/dist/terminal-ws.js +1 -0
  37. package/package.json +12 -7
  38. package/dist/control-web/assets/index-BQ7XVBKO.css +0 -32
  39. 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 the one-stream-per-Page lease, frame acknowledgement, and human-driver
9
- * rules above physical Browser capture. It never exposes a Host Page ID or CDP.
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
- attachments = new Map();
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
- const previous = this.attachments.get(pageKey);
48
- if (previous)
49
- await previous.replace();
50
- let source;
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
- catch (error) {
63
- throw mapSurfaceOpenError(error);
53
+ if (page && page.browserGeneration !== frame.browserGeneration) {
54
+ await page.finish("generation_changed", "Browser generation changed", true);
55
+ page = undefined;
64
56
  }
65
- if (context.signal.aborted) {
66
- await source.close().catch(() => undefined);
67
- throw new DirectRuntimeBrowserSurfaceHostError("cancelled", "Browser Surface open was cancelled");
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
- let attachment;
70
- attachment = new CoordinatedSurfaceAttachment({
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.source.getViewport();
396
+ const viewport = attachment.page.getViewport();
126
397
  this.ready = {
127
398
  type: "browser.surface.ready",
128
- subscriptionId: attachment.open.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
- try {
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.attachment.open.subscriptionId) {
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.pendingInputCount += 1;
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
- replace() {
222
- return this.finish("replaced", "Browser Surface was opened elsewhere", true);
223
- }
224
- async pumpFrames() {
225
- try {
226
- for await (const frame of this.attachment.source.frames) {
227
- if (this.closing) {
228
- await frame.acknowledge();
229
- return;
230
- }
231
- if (frame.browserGeneration !== this.attachment.open.browserGeneration) {
232
- await frame.acknowledge();
233
- await this.finish("generation_changed", "Browser generation changed", true);
234
- return;
235
- }
236
- if (frame.format !== this.attachment.open.format) {
237
- await frame.acknowledge();
238
- throw new Error("Browser Host changed the Surface image format");
239
- }
240
- const sequence = ++this.frameSequence;
241
- const released = new Promise((resolve) => {
242
- const timer = setTimeout(() => {
243
- void this.finish("idle_timeout", "Browser Surface frame was not acknowledged", true);
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
- catch (error) {
279
- if (!this.closing) {
280
- const mapped = mapSurfaceHostError(error, "Browser Surface source failed");
281
- await this.finish(mapped.reason, mapped.message, true);
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.attachment.open.browserGeneration) {
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.source.getViewport().revision;
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
- emitDriverChanged() {
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.attachment.open.subscriptionId,
530
+ subscriptionId: this.subscriptionId,
403
531
  reason,
404
532
  ...(message === undefined ? {} : { message: message.slice(0, 1_024) }),
405
533
  }
406
534
  : undefined);
407
- const restingDriver = this.attachment.source.setHumanDriver ? "desktop" : "none";
408
- const releaseDriver = this.enqueueDriverTransition(async () => {
409
- await this.inputTail;
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
  }