bunite-core 0.17.1 → 0.17.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.
@@ -1,16 +1,27 @@
1
- import { BrowserView } from "./BrowserView";
2
- import {
3
- trackSurface, untrackSurface, getOwnedSurface,
4
- getHostSurfaceIds, getSurfaceRecord, onSurfaceDispose,
5
- MAX_SURFACES_PER_HOST
6
- } from "./SurfaceRegistry";
7
1
  import {
8
- SurfaceCap, type ImplOf, IpcError,
9
- type SurfaceEvent, type SurfaceEventBase, type DialogEvent, type ConsoleEntry,
10
- type NavigationState, type DownloadEvent, type WaitForDownloadResult,
2
+ type ConsoleEntry,
3
+ type DialogEvent,
4
+ type DownloadEvent,
5
+ type ImplOf,
6
+ IpcError,
7
+ type NavigationState,
8
+ type SurfaceCap,
9
+ type SurfaceEvent,
10
+ type SurfaceEventBase,
11
+ type WaitForDownloadResult,
11
12
  } from "../../rpc/index";
12
13
  import { Stream } from "../../rpc/stream";
13
14
  import { log } from "../log";
15
+ import { BrowserView } from "./BrowserView";
16
+ import {
17
+ getHostSurfaceIds,
18
+ getOwnedSurface,
19
+ getSurfaceRecord,
20
+ MAX_SURFACES_PER_HOST,
21
+ onSurfaceDispose,
22
+ trackSurface,
23
+ untrackSurface,
24
+ } from "./SurfaceRegistry";
14
25
 
15
26
  function applyHostOffset(hostView: BrowserView, x: number, y: number) {
16
27
  return { x: x + hostView.frame.x, y: y + hostView.frame.y };
@@ -26,11 +37,7 @@ export function onSurfaceInit(cb: SurfaceInitCallback) {
26
37
  type SurfaceEventEmit = (event: { surfaceId: number; event: SurfaceEvent }) => void;
27
38
  const surfaceEventSubs = new Map<number, Set<SurfaceEventEmit>>();
28
39
 
29
- export function emitSurfaceEvent(
30
- hostViewId: number,
31
- surfaceId: number,
32
- event: SurfaceEventBase,
33
- ) {
40
+ export function emitSurfaceEvent(hostViewId: number, surfaceId: number, event: SurfaceEventBase) {
34
41
  // Drop late events after dispose so dead surfaceIds don't resurrect state.
35
42
  if (!getSurfaceRecord(surfaceId)) return;
36
43
  // Mutate before the subscriber guard so `getNavigationState` stays correct.
@@ -70,7 +77,7 @@ type PendingPopup = {
70
77
  url: string;
71
78
  disposition: "tab" | "window" | "popup";
72
79
  timer: ReturnType<typeof setTimeout> | null;
73
- armTs: number; // arm emit timestamp for the 60s extend cap
80
+ armTs: number; // arm emit timestamp for the 60s extend cap
74
81
  };
75
82
  const pendingPopups = new Map<number, PendingPopup>();
76
83
  const POPUP_ADOPT_TIMEOUT_MS = 5000;
@@ -90,7 +97,9 @@ function recordResolution(id: number, kind: "adopted" | "dismissed") {
90
97
 
91
98
  // Process-lifetime popup lifecycle counters; surfaced via RuntimeCap.popupMetrics.
92
99
  const popupCounters = { armed: 0, adopted: 0, dismissed: 0, timeoutFired: 0, extended: 0 };
93
- export function getPopupMetricsSnapshot() { return { ...popupCounters }; }
100
+ export function getPopupMetricsSnapshot() {
101
+ return { ...popupCounters };
102
+ }
94
103
 
95
104
  /** Called when a backend mints a popup view. Stashes the pending adoption +
96
105
  * arms a timer that auto-dismisses if the host doesn't respond. */
@@ -132,7 +141,10 @@ type DownloadWaiter = {
132
141
  pendingId: string | null;
133
142
  };
134
143
  const downloadWaiters = new Map<number, DownloadWaiter[]>();
135
- const downloadStartedMeta = new Map<number, Map<string, { url: string; suggestedFilename: string; mimeType?: string; sizeBytes?: number }>>();
144
+ const downloadStartedMeta = new Map<
145
+ number,
146
+ Map<string, { url: string; suggestedFilename: string; mimeType?: string; sizeBytes?: number }>
147
+ >();
136
148
  // Recent started events that no waiter has claimed yet — lets a `waitForDownload`
137
149
  // registered *after* the started event still bind. Per-surface, trimmed to 30s.
138
150
  const recentUnownedStarts = new Map<number, { id: string; ts: number }[]>();
@@ -140,23 +152,36 @@ const recentUnownedStarts = new Map<number, { id: string; ts: number }[]>();
140
152
  export function emitDownload(hostViewId: number, surfaceId: number, event: DownloadEvent) {
141
153
  if (event.kind === "started") {
142
154
  let bySurface = downloadStartedMeta.get(surfaceId);
143
- if (!bySurface) { bySurface = new Map(); downloadStartedMeta.set(surfaceId, bySurface); }
144
- bySurface.set(event.id, { url: event.url, suggestedFilename: event.suggestedFilename, mimeType: event.mimeType, sizeBytes: event.sizeBytes });
155
+ if (!bySurface) {
156
+ bySurface = new Map();
157
+ downloadStartedMeta.set(surfaceId, bySurface);
158
+ }
159
+ bySurface.set(event.id, {
160
+ url: event.url,
161
+ suggestedFilename: event.suggestedFilename,
162
+ mimeType: event.mimeType,
163
+ sizeBytes: event.sizeBytes,
164
+ });
145
165
  // Track unowned started events so a waitForDownload registering AFTER the
146
166
  // started event can still bind. Trimmed to recent 30s on each insert.
147
167
  let recents = recentUnownedStarts.get(surfaceId);
148
- if (!recents) { recents = []; recentUnownedStarts.set(surfaceId, recents); }
168
+ if (!recents) {
169
+ recents = [];
170
+ recentUnownedStarts.set(surfaceId, recents);
171
+ }
149
172
  const now = Date.now();
150
173
  recents.push({ id: event.id, ts: now });
151
174
  while (recents.length && now - recents[0].ts > 30_000) recents.shift();
152
175
  const queue = downloadWaiters.get(surfaceId);
153
- if (queue) for (const w of queue) if (w.pendingId === null) {
154
- w.pendingId = event.id;
155
- // Take the unowned entry out — it's now bound.
156
- const idx = recents.findIndex((r) => r.id === event.id);
157
- if (idx >= 0) recents.splice(idx, 1);
158
- break;
159
- }
176
+ if (queue)
177
+ for (const w of queue)
178
+ if (w.pendingId === null) {
179
+ w.pendingId = event.id;
180
+ // Take the unowned entry out — it's now bound.
181
+ const idx = recents.findIndex((r) => r.id === event.id);
182
+ if (idx >= 0) recents.splice(idx, 1);
183
+ break;
184
+ }
160
185
  }
161
186
  const subs = downloadSubs.get(hostViewId);
162
187
  if (subs) for (const emit of subs) emit({ surfaceId, event });
@@ -172,9 +197,13 @@ export function emitDownload(hostViewId: number, surfaceId: number, event: Downl
172
197
  if (event.kind === "completed") {
173
198
  const meta = downloadStartedMeta.get(surfaceId)?.get(event.id);
174
199
  waiter.resolve({
175
- ok: true, id: event.id, localPath: event.localPath,
176
- url: meta?.url ?? "", suggestedFilename: meta?.suggestedFilename ?? "",
177
- mimeType: meta?.mimeType, sizeBytes: meta?.sizeBytes,
200
+ ok: true,
201
+ id: event.id,
202
+ localPath: event.localPath,
203
+ url: meta?.url ?? "",
204
+ suggestedFilename: meta?.suggestedFilename ?? "",
205
+ mimeType: meta?.mimeType,
206
+ sizeBytes: meta?.sizeBytes,
178
207
  });
179
208
  } else if (event.kind === "failed") {
180
209
  waiter.resolve({ ok: false, code: "failed", message: event.reason });
@@ -199,7 +228,7 @@ type PendingDialog = {
199
228
 
200
229
  type SurfaceState = {
201
230
  consoleBuffer: ConsoleEntry[];
202
- dialogTimeoutMs: number | null; // null = no auto-dismiss
231
+ dialogTimeoutMs: number | null; // null = no auto-dismiss
203
232
  pendingDialogs: Map<number, PendingDialog>;
204
233
  lastLoadEpoch: number;
205
234
  isLoading: boolean;
@@ -232,7 +261,8 @@ export function disposeSurfaceState(surfaceId: number) {
232
261
  }
233
262
  const waiters = downloadWaiters.get(surfaceId);
234
263
  if (waiters) {
235
- for (const w of waiters) w.resolve({ ok: false, code: "not_supported", message: "surface destroyed" });
264
+ for (const w of waiters)
265
+ w.resolve({ ok: false, code: "not_supported", message: "surface destroyed" });
236
266
  downloadWaiters.delete(surfaceId);
237
267
  }
238
268
  downloadStartedMeta.delete(surfaceId);
@@ -249,8 +279,16 @@ export function clearConsoleBuffer(surfaceId: number) {
249
279
 
250
280
  export function emitDialog(hostViewId: number, surfaceId: number, event: DialogEvent) {
251
281
  const subs = dialogSubs.get(hostViewId);
252
- log.debug("dialog/emit hostViewId=" + hostViewId + " surfaceId=" + surfaceId +
253
- " kind=" + (event as { kind?: string }).kind + " subscribers=" + (subs?.size ?? 0));
282
+ log.debug(
283
+ "dialog/emit hostViewId=" +
284
+ hostViewId +
285
+ " surfaceId=" +
286
+ surfaceId +
287
+ " kind=" +
288
+ (event as { kind?: string }).kind +
289
+ " subscribers=" +
290
+ (subs?.size ?? 0),
291
+ );
254
292
  if (!subs) return;
255
293
  for (const emit of subs) emit({ surfaceId, event });
256
294
  }
@@ -271,10 +309,23 @@ export function emitConsole(hostViewId: number, surfaceId: number, entry: Consol
271
309
  export function registerDialogRequest(
272
310
  hostViewId: number,
273
311
  surfaceId: number,
274
- request: { requestId: number; kind: "alert" | "confirm" | "prompt" | "beforeunload"; message: string; defaultPrompt?: string }
312
+ request: {
313
+ requestId: number;
314
+ kind: "alert" | "confirm" | "prompt" | "beforeunload";
315
+ message: string;
316
+ defaultPrompt?: string;
317
+ },
275
318
  ) {
276
- log.debug("dialog/register hostViewId=" + hostViewId + " surfaceId=" + surfaceId +
277
- " kind=" + request.kind + " rid=" + request.requestId);
319
+ log.debug(
320
+ "dialog/register hostViewId=" +
321
+ hostViewId +
322
+ " surfaceId=" +
323
+ surfaceId +
324
+ " kind=" +
325
+ request.kind +
326
+ " rid=" +
327
+ request.requestId,
328
+ );
278
329
  const state = getOrCreateState(surfaceId);
279
330
  const view = getSurfaceRecord(surfaceId)?.view;
280
331
 
@@ -324,12 +375,17 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
324
375
  return {
325
376
  init: async ({ src, x, y, width, height, hidden = false }) => {
326
377
  const hostView = BrowserView.getById(hostViewId);
327
- if (!hostView) throw new IpcError({ code: "not_found", message: `Host view not found: ${hostViewId}` });
328
- if (!hostView.windowId) throw new IpcError({ code: "failed_precondition", message: `Host window not found` });
378
+ if (!hostView)
379
+ throw new IpcError({ code: "not_found", message: `Host view not found: ${hostViewId}` });
380
+ if (!hostView.windowId)
381
+ throw new IpcError({ code: "failed_precondition", message: `Host window not found` });
329
382
 
330
383
  const hostIds = getHostSurfaceIds(hostViewId);
331
384
  if (hostIds && hostIds.size >= MAX_SURFACES_PER_HOST) {
332
- throw new IpcError({ code: "resource_exhausted", message: `Surface limit reached (${MAX_SURFACES_PER_HOST})` });
385
+ throw new IpcError({
386
+ code: "resource_exhausted",
387
+ message: `Surface limit reached (${MAX_SURFACES_PER_HOST})`,
388
+ });
333
389
  }
334
390
 
335
391
  const offset = applyHostOffset(hostView, x, y);
@@ -347,10 +403,14 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
347
403
  } catch {
348
404
  untrackSurface(view.id);
349
405
  view.remove();
350
- throw new IpcError({ code: "unavailable", message: "Surface browser creation failed or timed out" });
406
+ throw new IpcError({
407
+ code: "unavailable",
408
+ message: "Surface browser creation failed or timed out",
409
+ });
351
410
  }
352
411
  for (const cb of initCallbacks) cb(view.id, hostViewId, view);
353
- if (hidden) view.setVisible(false); else view.bringToFront();
412
+ if (hidden) view.setVisible(false);
413
+ else view.bringToFront();
354
414
  return { surfaceId: view.id };
355
415
  },
356
416
 
@@ -385,12 +445,14 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
385
445
  const hostView = BrowserView.getById(hostViewId);
386
446
  if (!hostView) return;
387
447
  const offset = applyHostOffset(hostView, 0, 0);
388
- record.view.setMaskRegion(masks.map((m) => ({
389
- x: m.x + offset.x,
390
- y: m.y + offset.y,
391
- w: m.w,
392
- h: m.h,
393
- })));
448
+ record.view.setMaskRegion(
449
+ masks.map((m) => ({
450
+ x: m.x + offset.x,
451
+ y: m.y + offset.y,
452
+ w: m.w,
453
+ h: m.h,
454
+ })),
455
+ );
394
456
  },
395
457
 
396
458
  setAllPassthrough: ({ passthrough }) => {
@@ -464,47 +526,66 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
464
526
 
465
527
  screenshot: async ({ surfaceId, format = "png", quality = 90 }) => {
466
528
  const record = ownedSurface(surfaceId);
467
- if (!record) return { ok: false as const, code: "not_supported" as const, message: "surface not found" };
529
+ if (!record)
530
+ return { ok: false as const, code: "not_supported" as const, message: "surface not found" };
468
531
  return record.view.screenshot(format, quality);
469
532
  },
470
533
 
471
534
  waitForSelector: async ({ surfaceId, selector, timeoutMs = 5000, frameId }) => {
472
535
  const record = ownedSurface(surfaceId);
473
- if (!record) return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
536
+ if (!record)
537
+ return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
474
538
  const deadline = Date.now() + timeoutMs;
475
539
  const expr = `!!document.querySelector(${JSON.stringify(selector)})`;
476
540
  while (Date.now() < deadline) {
477
541
  const res = await record.view.evaluate(expr, frameId);
478
542
  if (res.ok && res.value === true) return { ok: true as const };
479
543
  if (!res.ok && res.code !== "timeout") {
480
- const code = res.code === "cross_origin" ? "cross_origin" as const : "runtime_error" as const;
544
+ const code =
545
+ res.code === "cross_origin" ? ("cross_origin" as const) : ("runtime_error" as const);
481
546
  return { ok: false as const, code, message: res.message };
482
547
  }
483
548
  await new Promise((r) => setTimeout(r, 50));
484
549
  }
485
- return { ok: false as const, code: "timeout" as const, message: `selector ${JSON.stringify(selector)} not found within ${timeoutMs}ms` };
550
+ return {
551
+ ok: false as const,
552
+ code: "timeout" as const,
553
+ message: `selector ${JSON.stringify(selector)} not found within ${timeoutMs}ms`,
554
+ };
486
555
  },
487
556
 
488
- waitForFunction: async ({ surfaceId, expression, timeoutMs = 5000, pollIntervalMs = 50, frameId }) => {
557
+ waitForFunction: async ({
558
+ surfaceId,
559
+ expression,
560
+ timeoutMs = 5000,
561
+ pollIntervalMs = 50,
562
+ frameId,
563
+ }) => {
489
564
  const record = ownedSurface(surfaceId);
490
- if (!record) return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
565
+ if (!record)
566
+ return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
491
567
  const deadline = Date.now() + timeoutMs;
492
568
  while (Date.now() < deadline) {
493
569
  const res = await record.view.evaluate(expression, frameId);
494
570
  if (res.ok && res.value) return { ok: true as const };
495
571
  if (!res.ok && res.code !== "timeout") {
496
- const code = res.code === "cross_origin" ? "cross_origin" as const : "runtime_error" as const;
572
+ const code =
573
+ res.code === "cross_origin" ? ("cross_origin" as const) : ("runtime_error" as const);
497
574
  return { ok: false as const, code, message: res.message };
498
575
  }
499
576
  await new Promise((r) => setTimeout(r, pollIntervalMs));
500
577
  }
501
- return { ok: false as const, code: "timeout" as const, message: `function did not satisfy within ${timeoutMs}ms` };
578
+ return {
579
+ ok: false as const,
580
+ code: "timeout" as const,
581
+ message: `function did not satisfy within ${timeoutMs}ms`,
582
+ };
502
583
  },
503
584
 
504
585
  respondToDialog: ({ surfaceId, requestId, accept, text }) => {
505
586
  const record = ownedSurface(surfaceId);
506
587
  if (!record) return;
507
- if (!consumePendingDialog(surfaceId, requestId)) return; // stale or already auto-dismissed
588
+ if (!consumePendingDialog(surfaceId, requestId)) return; // stale or already auto-dismissed
508
589
  record.view.respondToDialog(requestId, accept, text);
509
590
  },
510
591
 
@@ -528,53 +609,82 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
528
609
  const record = ownedSurface(surfaceId);
529
610
  if (!record) {
530
611
  return {
531
- evaluate: false, crossOriginEval: false, surfaceEvents: false,
532
- nativeInputTrusted: false, click: false, type: false, press: false,
533
- scroll: false, mouse: false, dialogs: false, console: false,
534
- screenshot: false, accessibilitySnapshot: false, getBoundingRect: false,
535
- frames: false, downloads: false, popups: false, resolveAndClick: false,
612
+ evaluate: false,
613
+ crossOriginEval: false,
614
+ surfaceEvents: false,
615
+ nativeInputTrusted: false,
616
+ click: false,
617
+ type: false,
618
+ press: false,
619
+ scroll: false,
620
+ mouse: false,
621
+ dialogs: false,
622
+ console: false,
623
+ screenshot: false,
624
+ accessibilitySnapshot: false,
625
+ getBoundingRect: false,
626
+ frames: false,
627
+ downloads: false,
628
+ popups: false,
629
+ resolveAndClick: false,
536
630
  };
537
631
  }
538
632
  return record.view.capabilities();
539
633
  },
540
634
 
541
- surfaceEvents: ({ surfaceId: filterId }) => Stream.from<SurfaceEvent>((emit, signal) => {
542
- let subs = surfaceEventSubs.get(hostViewId);
543
- if (!subs) {
544
- subs = new Set();
545
- surfaceEventSubs.set(hostViewId, subs);
546
- }
547
- const wrapped: SurfaceEventEmit = ({ surfaceId, event }) => {
548
- if (surfaceId === filterId) emit(event);
549
- };
550
- subs.add(wrapped);
551
- signal.addEventListener("abort", () => {
552
- const set = surfaceEventSubs.get(hostViewId);
553
- if (!set) return;
554
- set.delete(wrapped);
555
- if (set.size === 0) surfaceEventSubs.delete(hostViewId);
556
- });
557
- }),
558
-
559
- dialogs: ({ surfaceId: filterId }) => Stream.from<DialogEvent>((emit, signal) => {
560
- let subs = dialogSubs.get(hostViewId);
561
- if (!subs) {
562
- subs = new Set();
563
- dialogSubs.set(hostViewId, subs);
564
- }
565
- const wrapped: DialogEmit = ({ surfaceId, event }) => {
566
- if (surfaceId === filterId) emit(event);
567
- };
568
- subs.add(wrapped);
569
- log.debug("dialog/subscribe hostViewId=" + hostViewId + " filterId=" + filterId + " total=" + subs.size);
570
- signal.addEventListener("abort", () => {
571
- const set = dialogSubs.get(hostViewId);
572
- if (!set) return;
573
- set.delete(wrapped);
574
- log.debug("dialog/unsubscribe hostViewId=" + hostViewId + " filterId=" + filterId + " remaining=" + set.size);
575
- if (set.size === 0) dialogSubs.delete(hostViewId);
576
- });
577
- }),
635
+ surfaceEvents: ({ surfaceId: filterId }) =>
636
+ Stream.from<SurfaceEvent>((emit, signal) => {
637
+ let subs = surfaceEventSubs.get(hostViewId);
638
+ if (!subs) {
639
+ subs = new Set();
640
+ surfaceEventSubs.set(hostViewId, subs);
641
+ }
642
+ const wrapped: SurfaceEventEmit = ({ surfaceId, event }) => {
643
+ if (surfaceId === filterId) emit(event);
644
+ };
645
+ subs.add(wrapped);
646
+ signal.addEventListener("abort", () => {
647
+ const set = surfaceEventSubs.get(hostViewId);
648
+ if (!set) return;
649
+ set.delete(wrapped);
650
+ if (set.size === 0) surfaceEventSubs.delete(hostViewId);
651
+ });
652
+ }),
653
+
654
+ dialogs: ({ surfaceId: filterId }) =>
655
+ Stream.from<DialogEvent>((emit, signal) => {
656
+ let subs = dialogSubs.get(hostViewId);
657
+ if (!subs) {
658
+ subs = new Set();
659
+ dialogSubs.set(hostViewId, subs);
660
+ }
661
+ const wrapped: DialogEmit = ({ surfaceId, event }) => {
662
+ if (surfaceId === filterId) emit(event);
663
+ };
664
+ subs.add(wrapped);
665
+ log.debug(
666
+ "dialog/subscribe hostViewId=" +
667
+ hostViewId +
668
+ " filterId=" +
669
+ filterId +
670
+ " total=" +
671
+ subs.size,
672
+ );
673
+ signal.addEventListener("abort", () => {
674
+ const set = dialogSubs.get(hostViewId);
675
+ if (!set) return;
676
+ set.delete(wrapped);
677
+ log.debug(
678
+ "dialog/unsubscribe hostViewId=" +
679
+ hostViewId +
680
+ " filterId=" +
681
+ filterId +
682
+ " remaining=" +
683
+ set.size,
684
+ );
685
+ if (set.size === 0) dialogSubs.delete(hostViewId);
686
+ });
687
+ }),
578
688
 
579
689
  getNavigationState: ({ surfaceId }): NavigationState => {
580
690
  const record = ownedSurface(surfaceId);
@@ -589,79 +699,124 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
589
699
 
590
700
  accessibilitySnapshot: async ({ surfaceId, interestingOnly = true }) => {
591
701
  const record = ownedSurface(surfaceId);
592
- if (!record) return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
702
+ if (!record)
703
+ return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
593
704
  return record.view.accessibilitySnapshot(interestingOnly);
594
705
  },
595
706
 
596
707
  getBoundingRect: async ({ surfaceId, selector, frameId }) => {
597
708
  const record = ownedSurface(surfaceId);
598
- if (!record) return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
709
+ if (!record)
710
+ return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
599
711
  const expr = `(function(){var el=document.querySelector(${JSON.stringify(selector)});if(!el)return null;var r=el.getBoundingClientRect();return {x:r.x,y:r.y,width:r.width,height:r.height,visible:r.width>0&&r.height>0&&r.bottom>0&&r.right>0&&r.top<innerHeight&&r.left<innerWidth};})()`;
600
712
  const res = await record.view.evaluate(expr, frameId);
601
713
  if (!res.ok) {
602
- const code = res.code === "cross_origin" ? "cross_origin" as const
603
- : res.code === "not_supported" ? "not_supported" as const
604
- : "runtime_error" as const;
714
+ const code =
715
+ res.code === "cross_origin"
716
+ ? ("cross_origin" as const)
717
+ : res.code === "not_supported"
718
+ ? ("not_supported" as const)
719
+ : ("runtime_error" as const);
605
720
  return { ok: false as const, code, message: res.message };
606
721
  }
607
- const v = res.value as null | { x: number; y: number; width: number; height: number; visible: boolean };
608
- if (!v) return { ok: false as const, code: "not_found" as const, message: `selector ${JSON.stringify(selector)} not found` };
609
- return { ok: true as const, rect: { x: v.x, y: v.y, width: v.width, height: v.height }, visible: v.visible };
722
+ const v = res.value as null | {
723
+ x: number;
724
+ y: number;
725
+ width: number;
726
+ height: number;
727
+ visible: boolean;
728
+ };
729
+ if (!v)
730
+ return {
731
+ ok: false as const,
732
+ code: "not_found" as const,
733
+ message: `selector ${JSON.stringify(selector)} not found`,
734
+ };
735
+ return {
736
+ ok: true as const,
737
+ rect: { x: v.x, y: v.y, width: v.width, height: v.height },
738
+ visible: v.visible,
739
+ };
610
740
  },
611
741
 
612
742
  listFrames: async ({ surfaceId }) => {
613
743
  const record = ownedSurface(surfaceId);
614
- if (!record) return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
744
+ if (!record)
745
+ return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
615
746
  return record.view.listFrames();
616
747
  },
617
748
 
618
749
  resolveAndClick: async (args) => {
619
750
  const record = ownedSurface(args.surfaceId);
620
- if (!record) return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
751
+ if (!record)
752
+ return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
621
753
  if (!record.view.capabilities().resolveAndClick) {
622
- return { ok: false as const, code: "not_supported" as const, message: "resolveAndClick not supported on this backend" };
754
+ return {
755
+ ok: false as const,
756
+ code: "not_supported" as const,
757
+ message: "resolveAndClick not supported on this backend",
758
+ };
623
759
  }
624
760
  return record.view.resolveAndClick(args);
625
761
  },
626
762
 
627
- downloadEvents: ({ surfaceId: filterId }) => Stream.from<DownloadEvent>((emit, signal) => {
628
- let subs = downloadSubs.get(hostViewId);
629
- if (!subs) { subs = new Set(); downloadSubs.set(hostViewId, subs); }
630
- const wrapped: DownloadEmit = ({ surfaceId, event }) => {
631
- if (surfaceId === filterId) emit(event);
632
- };
633
- subs.add(wrapped);
634
- signal.addEventListener("abort", () => {
635
- const set = downloadSubs.get(hostViewId);
636
- if (!set) return;
637
- set.delete(wrapped);
638
- if (set.size === 0) downloadSubs.delete(hostViewId);
639
- });
640
- }),
763
+ downloadEvents: ({ surfaceId: filterId }) =>
764
+ Stream.from<DownloadEvent>((emit, signal) => {
765
+ let subs = downloadSubs.get(hostViewId);
766
+ if (!subs) {
767
+ subs = new Set();
768
+ downloadSubs.set(hostViewId, subs);
769
+ }
770
+ const wrapped: DownloadEmit = ({ surfaceId, event }) => {
771
+ if (surfaceId === filterId) emit(event);
772
+ };
773
+ subs.add(wrapped);
774
+ signal.addEventListener("abort", () => {
775
+ const set = downloadSubs.get(hostViewId);
776
+ if (!set) return;
777
+ set.delete(wrapped);
778
+ if (set.size === 0) downloadSubs.delete(hostViewId);
779
+ });
780
+ }),
641
781
 
642
782
  waitForDownload: async ({ surfaceId, timeoutMs = 30000 }) => {
643
783
  const record = ownedSurface(surfaceId);
644
- if (!record) return { ok: false as const, code: "not_supported" as const, message: "surface not found" };
784
+ if (!record)
785
+ return { ok: false as const, code: "not_supported" as const, message: "surface not found" };
645
786
  if (!record.view.capabilities().downloads) {
646
- return { ok: false as const, code: "not_supported" as const, message: "downloads not supported on this backend" };
787
+ return {
788
+ ok: false as const,
789
+ code: "not_supported" as const,
790
+ message: "downloads not supported on this backend",
791
+ };
647
792
  }
648
793
  return new Promise<WaitForDownloadResult>((resolve) => {
649
794
  // If a `started` already arrived without a waiter, consume it.
650
795
  const recents = recentUnownedStarts.get(surfaceId);
651
796
  const claimed = recents?.shift();
652
797
  const waiter: DownloadWaiter = {
653
- resolve: (r) => { clearTimeout(timer); resolve(r); },
798
+ resolve: (r) => {
799
+ clearTimeout(timer);
800
+ resolve(r);
801
+ },
654
802
  pendingId: claimed?.id ?? null,
655
803
  };
656
804
  let queue = downloadWaiters.get(surfaceId);
657
- if (!queue) { queue = []; downloadWaiters.set(surfaceId, queue); }
805
+ if (!queue) {
806
+ queue = [];
807
+ downloadWaiters.set(surfaceId, queue);
808
+ }
658
809
  queue.push(waiter);
659
810
  const timer = setTimeout(() => {
660
811
  const q = downloadWaiters.get(surfaceId);
661
812
  if (!q) return;
662
813
  const idx = q.indexOf(waiter);
663
814
  if (idx >= 0) q.splice(idx, 1);
664
- resolve({ ok: false, code: "timeout", message: `no download started within ${timeoutMs}ms` });
815
+ resolve({
816
+ ok: false,
817
+ code: "timeout",
818
+ message: `no download started within ${timeoutMs}ms`,
819
+ });
665
820
  }, timeoutMs);
666
821
  });
667
822
  },
@@ -669,27 +824,40 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
669
824
  setDownloadPolicy: ({ surfaceId, policy, downloadDir }) => {
670
825
  const record = ownedSurface(surfaceId);
671
826
  if (!record) return;
672
- if (!record.view.capabilities().downloads) return; // mac/linux silent no-op signal — caller should gate on cap.
827
+ if (!record.view.capabilities().downloads) return; // mac/linux silent no-op signal — caller should gate on cap.
673
828
  record.view.setDownloadPolicy(policy, downloadDir);
674
829
  },
675
830
 
676
831
  acceptPopup: async ({ newSurfaceId, hostViewId: targetHostId, bounds }) => {
677
832
  const pending = pendingPopups.get(newSurfaceId);
678
- if (!pending) return { ok: false as const, code: "not_found" as const, message: "popup not pending" };
833
+ if (!pending)
834
+ return { ok: false as const, code: "not_found" as const, message: "popup not pending" };
679
835
  // Only the opener's host page can adopt the popup. The target host
680
836
  // (where the new pane lands) is a separate decision.
681
837
  if (pending.openerHostViewId !== hostViewId) {
682
- return { ok: false as const, code: "not_found" as const, message: "popup not owned by this host" };
838
+ return {
839
+ ok: false as const,
840
+ code: "not_found" as const,
841
+ message: "popup not owned by this host",
842
+ };
683
843
  }
684
844
  const targetHost = BrowserView.getById(targetHostId);
685
- if (!targetHost || !targetHost.windowId) {
845
+ if (!targetHost?.windowId) {
686
846
  // Don't consume pending state on validation failure — host can retry
687
847
  // with a different target until the auto-dismiss timer fires.
688
- return { ok: false as const, code: "host_view_invalid" as const, message: "host view not found" };
848
+ return {
849
+ ok: false as const,
850
+ code: "host_view_invalid" as const,
851
+ message: "host view not found",
852
+ };
689
853
  }
690
854
  const existing = getHostSurfaceIds(targetHostId);
691
855
  if (existing && existing.size >= MAX_SURFACES_PER_HOST) {
692
- return { ok: false as const, code: "host_view_invalid" as const, message: `host surface limit reached (${MAX_SURFACES_PER_HOST})` };
856
+ return {
857
+ ok: false as const,
858
+ code: "host_view_invalid" as const,
859
+ message: `host surface limit reached (${MAX_SURFACES_PER_HOST})`,
860
+ };
693
861
  }
694
862
  if (pending.timer) clearTimeout(pending.timer);
695
863
  pendingPopups.delete(newSurfaceId);
@@ -710,7 +878,7 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
710
878
  dismissPopup: ({ newSurfaceId }) => {
711
879
  const pending = pendingPopups.get(newSurfaceId);
712
880
  if (!pending) return;
713
- if (pending.openerHostViewId !== hostViewId) return; // not this host's popup
881
+ if (pending.openerHostViewId !== hostViewId) return; // not this host's popup
714
882
  if (pending.timer) clearTimeout(pending.timer);
715
883
  pendingPopups.delete(newSurfaceId);
716
884
  recordResolution(newSurfaceId, "dismissed");
@@ -719,23 +887,41 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
719
887
 
720
888
  extendPopupTimeout: ({ newSurfaceId, gracePeriodMs }) => {
721
889
  if (!Number.isFinite(gracePeriodMs) || gracePeriodMs <= 0) {
722
- return { ok: false as const, code: "not_found" as const, message: "gracePeriodMs must be a positive finite number" };
890
+ return {
891
+ ok: false as const,
892
+ code: "not_found" as const,
893
+ message: "gracePeriodMs must be a positive finite number",
894
+ };
723
895
  }
724
896
  const pending = pendingPopups.get(newSurfaceId);
725
897
  if (!pending) {
726
898
  const prior = popupResolutionLog.get(newSurfaceId);
727
- if (prior === "adopted") return { ok: false as const, code: "already_adopted" as const, message: "popup adopted" };
728
- if (prior === "dismissed") return { ok: false as const, code: "already_dismissed" as const, message: "popup dismissed" };
899
+ if (prior === "adopted")
900
+ return { ok: false as const, code: "already_adopted" as const, message: "popup adopted" };
901
+ if (prior === "dismissed")
902
+ return {
903
+ ok: false as const,
904
+ code: "already_dismissed" as const,
905
+ message: "popup dismissed",
906
+ };
729
907
  return { ok: false as const, code: "not_found" as const, message: "popup not pending" };
730
908
  }
731
909
  if (pending.openerHostViewId !== hostViewId) {
732
- return { ok: false as const, code: "not_found" as const, message: "popup not owned by this host" };
910
+ return {
911
+ ok: false as const,
912
+ code: "not_found" as const,
913
+ message: "popup not owned by this host",
914
+ };
733
915
  }
734
916
  const now = Date.now();
735
917
  const requested = now + gracePeriodMs;
736
918
  const cap = pending.armTs + POPUP_EXTEND_CAP_MS;
737
919
  if (requested > cap) {
738
- return { ok: false as const, code: "cap_exceeded" as const, message: `extend exceeds ${POPUP_EXTEND_CAP_MS}ms cap since arm` };
920
+ return {
921
+ ok: false as const,
922
+ code: "cap_exceeded" as const,
923
+ message: `extend exceeds ${POPUP_EXTEND_CAP_MS}ms cap since arm`,
924
+ };
739
925
  }
740
926
  if (pending.timer) clearTimeout(pending.timer);
741
927
  pending.timer = setTimeout(() => {
@@ -748,22 +934,23 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
748
934
  return { ok: true as const, deadlineMs: requested };
749
935
  },
750
936
 
751
- consoleEvents: ({ surfaceId: filterId }) => Stream.from<ConsoleEntry>((emit, signal) => {
752
- let subs = consoleSubs.get(hostViewId);
753
- if (!subs) {
754
- subs = new Set();
755
- consoleSubs.set(hostViewId, subs);
756
- }
757
- const wrapped: ConsoleEmit = ({ surfaceId, entry }) => {
758
- if (surfaceId === filterId) emit(entry);
759
- };
760
- subs.add(wrapped);
761
- signal.addEventListener("abort", () => {
762
- const set = consoleSubs.get(hostViewId);
763
- if (!set) return;
764
- set.delete(wrapped);
765
- if (set.size === 0) consoleSubs.delete(hostViewId);
766
- });
767
- }),
937
+ consoleEvents: ({ surfaceId: filterId }) =>
938
+ Stream.from<ConsoleEntry>((emit, signal) => {
939
+ let subs = consoleSubs.get(hostViewId);
940
+ if (!subs) {
941
+ subs = new Set();
942
+ consoleSubs.set(hostViewId, subs);
943
+ }
944
+ const wrapped: ConsoleEmit = ({ surfaceId, entry }) => {
945
+ if (surfaceId === filterId) emit(entry);
946
+ };
947
+ subs.add(wrapped);
948
+ signal.addEventListener("abort", () => {
949
+ const set = consoleSubs.get(hostViewId);
950
+ if (!set) return;
951
+ set.delete(wrapped);
952
+ if (set.size === 0) consoleSubs.delete(hostViewId);
953
+ });
954
+ }),
768
955
  };
769
956
  }