bunite-core 0.17.0 → 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.
- package/package.json +1 -1
- package/src/host/core/App.ts +28 -18
- package/src/host/core/BrowserView.ts +333 -116
- package/src/host/core/BrowserWindow.ts +44 -22
- package/src/host/core/Socket.ts +26 -9
- package/src/host/core/SurfaceBrowserIPC.ts +15 -5
- package/src/host/core/SurfaceManager.ts +345 -158
- package/src/host/core/SurfaceRegistry.ts +1 -1
- package/src/host/core/inputDispatch.ts +71 -42
- package/src/host/core/singleInstanceLock.ts +10 -2
- package/src/host/core/windowCap.ts +42 -23
- package/src/host/encryptedPipe.ts +12 -3
- package/src/host/events/appEvents.ts +1 -1
- package/src/host/events/eventEmitter.ts +6 -6
- package/src/host/events/webviewEvents.ts +11 -4
- package/src/host/events/windowEvents.ts +1 -2
- package/src/host/index.ts +9 -17
- package/src/host/log.ts +6 -4
- package/src/host/native.ts +271 -163
- package/src/host/paths.ts +24 -17
- package/src/host/platform.ts +2 -4
- package/src/host/preloadBundle.ts +1 -1
- package/src/host/serveWeb.ts +18 -11
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +61 -24
- package/src/rpc/encrypt.ts +15 -4
- package/src/rpc/error.ts +2 -7
- package/src/rpc/framework.ts +105 -35
- package/src/rpc/index.ts +129 -140
- package/src/rpc/peer.ts +430 -159
- package/src/rpc/renderer.ts +15 -9
- package/src/rpc/schema.ts +86 -61
- package/src/rpc/stream.ts +12 -3
- package/src/rpc/transport.ts +28 -9
- package/src/rpc/wire.ts +23 -10
- package/src/webview/native.ts +150 -73
- package/src/webview/polyfill.ts +184 -48
|
@@ -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
|
-
|
|
9
|
-
type
|
|
10
|
-
type
|
|
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;
|
|
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() {
|
|
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<
|
|
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) {
|
|
144
|
-
|
|
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) {
|
|
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)
|
|
154
|
-
w
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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,
|
|
176
|
-
|
|
177
|
-
|
|
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;
|
|
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)
|
|
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(
|
|
253
|
-
|
|
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: {
|
|
312
|
+
request: {
|
|
313
|
+
requestId: number;
|
|
314
|
+
kind: "alert" | "confirm" | "prompt" | "beforeunload";
|
|
315
|
+
message: string;
|
|
316
|
+
defaultPrompt?: string;
|
|
317
|
+
},
|
|
275
318
|
) {
|
|
276
|
-
log.debug(
|
|
277
|
-
|
|
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)
|
|
328
|
-
|
|
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({
|
|
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({
|
|
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);
|
|
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(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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)
|
|
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)
|
|
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 =
|
|
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 {
|
|
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 ({
|
|
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)
|
|
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 =
|
|
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 {
|
|
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;
|
|
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,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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 }) =>
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
subs =
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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)
|
|
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)
|
|
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 =
|
|
603
|
-
|
|
604
|
-
|
|
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 | {
|
|
608
|
-
|
|
609
|
-
|
|
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)
|
|
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)
|
|
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 {
|
|
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 }) =>
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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)
|
|
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 {
|
|
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) => {
|
|
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) {
|
|
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({
|
|
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;
|
|
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)
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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;
|
|
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 {
|
|
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")
|
|
728
|
-
|
|
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 {
|
|
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 {
|
|
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 }) =>
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
}
|