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.
- 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 +18 -15
- 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,32 +1,47 @@
|
|
|
1
1
|
import { ptr } from "bun:ffi";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import type {
|
|
4
|
+
AccessibilitySnapshotResult,
|
|
5
|
+
AxNode,
|
|
6
|
+
EvaluateResult,
|
|
7
|
+
ListFramesResult,
|
|
8
|
+
Modifier,
|
|
9
|
+
ResolveAndClickArgs,
|
|
10
|
+
ResolveAndClickResult,
|
|
11
|
+
ScreenshotResult,
|
|
12
|
+
SurfaceCapabilities,
|
|
13
|
+
} from "../../rpc/framework";
|
|
5
14
|
import {
|
|
15
|
+
type BytesPipe,
|
|
16
|
+
type Connection,
|
|
6
17
|
createConnection,
|
|
7
18
|
createFrameTransport,
|
|
8
|
-
type Connection,
|
|
9
|
-
type BytesPipe,
|
|
10
19
|
} from "../../rpc/index";
|
|
11
|
-
import type {
|
|
12
|
-
EvaluateResult, SurfaceCapabilities, ScreenshotResult, Modifier,
|
|
13
|
-
AccessibilitySnapshotResult, AxNode, ListFramesResult,
|
|
14
|
-
ResolveAndClickArgs, ResolveAndClickResult,
|
|
15
|
-
} from "../../rpc/framework";
|
|
16
|
-
import { encodeModifiers, resolveKey } from "./inputDispatch";
|
|
17
20
|
import { createEncryptedPipe } from "../encryptedPipe";
|
|
21
|
+
import { buniteEventEmitter } from "../events/eventEmitter";
|
|
22
|
+
import { log } from "../log";
|
|
18
23
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
cancelWaitForViewReady,
|
|
25
|
+
ensureNativeRuntime,
|
|
26
|
+
getNativeLibrary,
|
|
27
|
+
type NativeAccessibilityResult,
|
|
28
|
+
type NativeEvaluateResult,
|
|
29
|
+
type NativeListFramesResult,
|
|
30
|
+
type NativeResolveAndClickResult,
|
|
31
|
+
type NativeScreenshotResult,
|
|
32
|
+
setAccessibilityResultHandler,
|
|
33
|
+
setEvaluateResultHandler,
|
|
34
|
+
setListFramesResultHandler,
|
|
35
|
+
setResolveAndClickResultHandler,
|
|
36
|
+
setScreenshotResultHandler,
|
|
37
|
+
toCString,
|
|
38
|
+
waitForViewReady,
|
|
25
39
|
} from "../native";
|
|
26
|
-
import { attachBrowserViewRegistry, getRpcPort } from "./Socket";
|
|
27
|
-
import { getAppRuntimeOrThrow } from "./App";
|
|
28
|
-
import { randomBytes } from "node:crypto";
|
|
29
40
|
import { resolveDefaultAppResRoot } from "../paths";
|
|
41
|
+
import { buildViewPreloadScript } from "../preloadBundle";
|
|
42
|
+
import { getAppRuntimeOrThrow } from "./App";
|
|
43
|
+
import { encodeModifiers, resolveKey } from "./inputDispatch";
|
|
44
|
+
import { attachBrowserViewRegistry, getRpcPort } from "./Socket";
|
|
30
45
|
import { removeSurfacesForHostView } from "./SurfaceRegistry";
|
|
31
46
|
|
|
32
47
|
const BrowserViewMap: Record<number, BrowserView> = {};
|
|
@@ -40,7 +55,10 @@ type EvaluatePending = { viewId: number; resolve: (result: EvaluateResult) => vo
|
|
|
40
55
|
let nextEvaluateRequestId = 1;
|
|
41
56
|
const evaluateResolvers = new Map<number, EvaluatePending>();
|
|
42
57
|
|
|
43
|
-
function registerEvaluateRequest(
|
|
58
|
+
function registerEvaluateRequest(
|
|
59
|
+
viewId: number,
|
|
60
|
+
resolve: (result: EvaluateResult) => void,
|
|
61
|
+
): number {
|
|
44
62
|
const id = nextEvaluateRequestId++;
|
|
45
63
|
evaluateResolvers.set(id, { viewId, resolve });
|
|
46
64
|
return id;
|
|
@@ -61,7 +79,10 @@ type ScreenshotPending = { viewId: number; resolve: (result: ScreenshotResult) =
|
|
|
61
79
|
let nextScreenshotRequestId = 1;
|
|
62
80
|
const screenshotResolvers = new Map<number, ScreenshotPending>();
|
|
63
81
|
|
|
64
|
-
function registerScreenshotRequest(
|
|
82
|
+
function registerScreenshotRequest(
|
|
83
|
+
viewId: number,
|
|
84
|
+
resolve: (result: ScreenshotResult) => void,
|
|
85
|
+
): number {
|
|
65
86
|
const id = nextScreenshotRequestId++;
|
|
66
87
|
screenshotResolvers.set(id, { viewId, resolve });
|
|
67
88
|
return id;
|
|
@@ -76,11 +97,19 @@ function rejectScreenshotsForView(viewId: number) {
|
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
|
|
79
|
-
type AxPending = {
|
|
100
|
+
type AxPending = {
|
|
101
|
+
viewId: number;
|
|
102
|
+
resolve: (result: AccessibilitySnapshotResult) => void;
|
|
103
|
+
interestingOnly: boolean;
|
|
104
|
+
};
|
|
80
105
|
let nextAxRequestId = 1;
|
|
81
106
|
const axResolvers = new Map<number, AxPending>();
|
|
82
107
|
|
|
83
|
-
function registerAxRequest(
|
|
108
|
+
function registerAxRequest(
|
|
109
|
+
viewId: number,
|
|
110
|
+
resolve: (result: AccessibilitySnapshotResult) => void,
|
|
111
|
+
interestingOnly: boolean,
|
|
112
|
+
): number {
|
|
84
113
|
const id = nextAxRequestId++;
|
|
85
114
|
axResolvers.set(id, { viewId, resolve, interestingOnly });
|
|
86
115
|
return id;
|
|
@@ -124,7 +153,7 @@ function convertAxTree(cdpResult: { nodes?: any[] } | undefined, interestingOnly
|
|
|
124
153
|
};
|
|
125
154
|
const build = (n: any): AxNode => {
|
|
126
155
|
const id = String(n?.nodeId ?? "");
|
|
127
|
-
if (id && seen.has(id)) return { nodeId: id, role: "", name: "" };
|
|
156
|
+
if (id && seen.has(id)) return { nodeId: id, role: "", name: "" }; // cycle guard
|
|
128
157
|
if (id) seen.add(id);
|
|
129
158
|
const props = new Map<string, unknown>();
|
|
130
159
|
if (Array.isArray(n?.properties)) {
|
|
@@ -139,15 +168,24 @@ function convertAxTree(cdpResult: { nodes?: any[] } | undefined, interestingOnly
|
|
|
139
168
|
};
|
|
140
169
|
if (n?.value?.value !== undefined) out.value = String(n.value.value);
|
|
141
170
|
if (n?.description?.value !== undefined) out.description = String(n.description.value);
|
|
142
|
-
const level = props.get("level");
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
const
|
|
171
|
+
const level = props.get("level");
|
|
172
|
+
if (typeof level === "number") out.level = level;
|
|
173
|
+
const checked = props.get("checked");
|
|
174
|
+
if (checked === true || checked === false || checked === "mixed") out.checked = checked;
|
|
175
|
+
const pressed = props.get("pressed");
|
|
176
|
+
if (pressed === true || pressed === false || pressed === "mixed") out.pressed = pressed;
|
|
177
|
+
const expanded = props.get("expanded");
|
|
178
|
+
if (typeof expanded === "boolean") out.expanded = expanded;
|
|
179
|
+
const disabled = props.get("disabled");
|
|
180
|
+
if (typeof disabled === "boolean") out.disabled = disabled;
|
|
181
|
+
const focused = props.get("focused");
|
|
182
|
+
if (typeof focused === "boolean") out.focused = focused;
|
|
183
|
+
const invalid = props.get("invalid");
|
|
184
|
+
if (typeof invalid === "boolean") out.invalid = invalid;
|
|
185
|
+
const required = props.get("required");
|
|
186
|
+
if (typeof required === "boolean") out.required = required;
|
|
187
|
+
const selected = props.get("selected");
|
|
188
|
+
if (typeof selected === "boolean") out.selected = selected;
|
|
151
189
|
const kids = interestingDescendants(n).map(build);
|
|
152
190
|
if (kids.length > 0) out.children = kids;
|
|
153
191
|
return out;
|
|
@@ -158,7 +196,10 @@ function convertAxTree(cdpResult: { nodes?: any[] } | undefined, interestingOnly
|
|
|
158
196
|
type FramesPending = { viewId: number; resolve: (result: ListFramesResult) => void };
|
|
159
197
|
let nextFramesRequestId = 1;
|
|
160
198
|
const framesResolvers = new Map<number, FramesPending>();
|
|
161
|
-
function registerFramesRequest(
|
|
199
|
+
function registerFramesRequest(
|
|
200
|
+
viewId: number,
|
|
201
|
+
resolve: (result: ListFramesResult) => void,
|
|
202
|
+
): number {
|
|
162
203
|
const id = nextFramesRequestId++;
|
|
163
204
|
framesResolvers.set(id, { viewId, resolve });
|
|
164
205
|
return id;
|
|
@@ -175,7 +216,10 @@ function rejectFramesForView(viewId: number) {
|
|
|
175
216
|
type ResolveAndClickPending = { viewId: number; resolve: (result: ResolveAndClickResult) => void };
|
|
176
217
|
let nextResolveAndClickRequestId = 1;
|
|
177
218
|
const resolveAndClickResolvers = new Map<number, ResolveAndClickPending>();
|
|
178
|
-
function registerResolveAndClickRequest(
|
|
219
|
+
function registerResolveAndClickRequest(
|
|
220
|
+
viewId: number,
|
|
221
|
+
resolve: (result: ResolveAndClickResult) => void,
|
|
222
|
+
): number {
|
|
179
223
|
const id = nextResolveAndClickRequestId++;
|
|
180
224
|
resolveAndClickResolvers.set(id, { viewId, resolve });
|
|
181
225
|
return id;
|
|
@@ -189,12 +233,26 @@ function rejectResolveAndClickForView(viewId: number) {
|
|
|
189
233
|
}
|
|
190
234
|
}
|
|
191
235
|
|
|
192
|
-
function flattenFrameTree(
|
|
193
|
-
|
|
236
|
+
function flattenFrameTree(
|
|
237
|
+
raw: any,
|
|
238
|
+
): { frameId: string; parentFrameId: string | null; origin: string; url: string; name?: string }[] {
|
|
239
|
+
const out: {
|
|
240
|
+
frameId: string;
|
|
241
|
+
parentFrameId: string | null;
|
|
242
|
+
origin: string;
|
|
243
|
+
url: string;
|
|
244
|
+
name?: string;
|
|
245
|
+
}[] = [];
|
|
194
246
|
const walk = (node: any, parent: string | null) => {
|
|
195
247
|
const f = node?.frame;
|
|
196
248
|
if (!f) return;
|
|
197
|
-
const entry: {
|
|
249
|
+
const entry: {
|
|
250
|
+
frameId: string;
|
|
251
|
+
parentFrameId: string | null;
|
|
252
|
+
origin: string;
|
|
253
|
+
url: string;
|
|
254
|
+
name?: string;
|
|
255
|
+
} = {
|
|
198
256
|
frameId: String(f.id ?? ""),
|
|
199
257
|
parentFrameId: parent,
|
|
200
258
|
origin: String(f.securityOrigin ?? ""),
|
|
@@ -218,7 +276,11 @@ setListFramesResultHandler((viewId, raw: NativeListFramesResult) => {
|
|
|
218
276
|
const frames = flattenFrameTree(raw.raw);
|
|
219
277
|
entry.resolve({ ok: true, frames });
|
|
220
278
|
} catch (e) {
|
|
221
|
-
entry.resolve({
|
|
279
|
+
entry.resolve({
|
|
280
|
+
ok: false,
|
|
281
|
+
code: "runtime_error",
|
|
282
|
+
message: `frame tree flatten failed: ${(e as Error).message}`,
|
|
283
|
+
});
|
|
222
284
|
}
|
|
223
285
|
} else {
|
|
224
286
|
entry.resolve({
|
|
@@ -234,8 +296,15 @@ setAccessibilityResultHandler((viewId, raw: NativeAccessibilityResult) => {
|
|
|
234
296
|
if (!entry || entry.viewId !== viewId) return;
|
|
235
297
|
axResolvers.delete(raw.requestId);
|
|
236
298
|
if (raw.ok && raw.tree) {
|
|
237
|
-
try {
|
|
238
|
-
|
|
299
|
+
try {
|
|
300
|
+
entry.resolve({ ok: true, tree: convertAxTree(raw.tree as any, entry.interestingOnly) });
|
|
301
|
+
} catch (e) {
|
|
302
|
+
entry.resolve({
|
|
303
|
+
ok: false,
|
|
304
|
+
code: "runtime_error",
|
|
305
|
+
message: `ax tree convert failed: ${(e as Error).message}`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
239
308
|
} else {
|
|
240
309
|
entry.resolve({
|
|
241
310
|
ok: false,
|
|
@@ -259,9 +328,18 @@ setScreenshotResultHandler((viewId, raw: NativeScreenshotResult) => {
|
|
|
259
328
|
screenshotResolvers.delete(raw.requestId);
|
|
260
329
|
if (raw.ok && raw.dataBase64 && raw.format && raw.mime) {
|
|
261
330
|
try {
|
|
262
|
-
entry.resolve({
|
|
331
|
+
entry.resolve({
|
|
332
|
+
ok: true,
|
|
333
|
+
data: decodeBase64(raw.dataBase64),
|
|
334
|
+
mime: raw.mime,
|
|
335
|
+
format: raw.format,
|
|
336
|
+
});
|
|
263
337
|
} catch (e) {
|
|
264
|
-
entry.resolve({
|
|
338
|
+
entry.resolve({
|
|
339
|
+
ok: false,
|
|
340
|
+
code: "runtime_error",
|
|
341
|
+
message: `base64 decode failed: ${(e as Error).message}`,
|
|
342
|
+
});
|
|
265
343
|
}
|
|
266
344
|
} else {
|
|
267
345
|
entry.resolve({
|
|
@@ -279,7 +357,12 @@ setResolveAndClickResultHandler((viewId, raw: NativeResolveAndClickResult) => {
|
|
|
279
357
|
if (raw.ok && raw.rect) {
|
|
280
358
|
entry.resolve({ ok: true, rect: raw.rect, isTrustedEvent: !!raw.isTrustedEvent });
|
|
281
359
|
} else {
|
|
282
|
-
type FailCode =
|
|
360
|
+
type FailCode =
|
|
361
|
+
| "not_found"
|
|
362
|
+
| "not_visible"
|
|
363
|
+
| "runtime_error"
|
|
364
|
+
| "cross_origin"
|
|
365
|
+
| "not_supported";
|
|
283
366
|
const code = (raw.code as FailCode | undefined) ?? "runtime_error";
|
|
284
367
|
entry.resolve({ ok: false, code, message: raw.message ?? "resolveAndClick failed" });
|
|
285
368
|
}
|
|
@@ -288,13 +371,17 @@ setResolveAndClickResultHandler((viewId, raw: NativeResolveAndClickResult) => {
|
|
|
288
371
|
setEvaluateResultHandler((viewId, raw: NativeEvaluateResult) => {
|
|
289
372
|
const entry = evaluateResolvers.get(raw.requestId);
|
|
290
373
|
if (!entry) return;
|
|
291
|
-
if (entry.viewId !== viewId) return;
|
|
374
|
+
if (entry.viewId !== viewId) return; // foreign event — ignore
|
|
292
375
|
evaluateResolvers.delete(raw.requestId);
|
|
293
376
|
if (raw.ok && raw.value !== undefined) {
|
|
294
377
|
try {
|
|
295
378
|
entry.resolve({ ok: true, value: JSON.parse(raw.value) });
|
|
296
379
|
} catch (e) {
|
|
297
|
-
entry.resolve({
|
|
380
|
+
entry.resolve({
|
|
381
|
+
ok: false,
|
|
382
|
+
code: "runtime_error",
|
|
383
|
+
message: `result JSON parse failed: ${(e as Error).message}`,
|
|
384
|
+
});
|
|
298
385
|
}
|
|
299
386
|
} else {
|
|
300
387
|
entry.resolve({
|
|
@@ -306,26 +393,26 @@ setEvaluateResultHandler((viewId, raw: NativeEvaluateResult) => {
|
|
|
306
393
|
});
|
|
307
394
|
|
|
308
395
|
// Bit positions match the native enum in `ffi_exports.h` (BuniteCapBit).
|
|
309
|
-
const CAP_EVALUATE
|
|
310
|
-
const CAP_CROSS_ORIGIN_EVAL
|
|
311
|
-
const CAP_SURFACE_EVENTS
|
|
396
|
+
const CAP_EVALUATE = 1 << 0;
|
|
397
|
+
const CAP_CROSS_ORIGIN_EVAL = 1 << 1;
|
|
398
|
+
const CAP_SURFACE_EVENTS = 1 << 2;
|
|
312
399
|
const CAP_NATIVE_INPUT_TRUSTED = 1 << 3;
|
|
313
|
-
const CAP_CLICK
|
|
314
|
-
const CAP_TYPE
|
|
315
|
-
const CAP_PRESS
|
|
316
|
-
const CAP_SCROLL
|
|
317
|
-
const CAP_SCREENSHOT
|
|
318
|
-
const CAP_FORMAT_PNG
|
|
319
|
-
const CAP_FORMAT_JPEG
|
|
320
|
-
const CAP_MOUSE
|
|
321
|
-
const CAP_DIALOGS
|
|
322
|
-
const CAP_CONSOLE
|
|
323
|
-
const CAP_AX
|
|
324
|
-
const CAP_BOUNDING_RECT
|
|
325
|
-
const CAP_FRAMES
|
|
326
|
-
const CAP_DOWNLOADS
|
|
327
|
-
const CAP_POPUPS
|
|
328
|
-
const CAP_RESOLVE_AND_CLICK
|
|
400
|
+
const CAP_CLICK = 1 << 4;
|
|
401
|
+
const CAP_TYPE = 1 << 5;
|
|
402
|
+
const CAP_PRESS = 1 << 6;
|
|
403
|
+
const CAP_SCROLL = 1 << 7;
|
|
404
|
+
const CAP_SCREENSHOT = 1 << 8;
|
|
405
|
+
const CAP_FORMAT_PNG = 1 << 9;
|
|
406
|
+
const CAP_FORMAT_JPEG = 1 << 10;
|
|
407
|
+
const CAP_MOUSE = 1 << 11;
|
|
408
|
+
const CAP_DIALOGS = 1 << 12;
|
|
409
|
+
const CAP_CONSOLE = 1 << 13;
|
|
410
|
+
const CAP_AX = 1 << 15;
|
|
411
|
+
const CAP_BOUNDING_RECT = 1 << 16;
|
|
412
|
+
const CAP_FRAMES = 1 << 17;
|
|
413
|
+
const CAP_DOWNLOADS = 1 << 18;
|
|
414
|
+
const CAP_POPUPS = 1 << 19;
|
|
415
|
+
const CAP_RESOLVE_AND_CLICK = 1 << 20;
|
|
329
416
|
|
|
330
417
|
function decodeCapabilityBits(bits: number): SurfaceCapabilities {
|
|
331
418
|
const formats: ("png" | "jpeg")[] = [];
|
|
@@ -386,7 +473,7 @@ const defaultOptions: BrowserViewOptions = {
|
|
|
386
473
|
windowId: 0,
|
|
387
474
|
autoResize: true,
|
|
388
475
|
navigationRules: null,
|
|
389
|
-
sandbox: false
|
|
476
|
+
sandbox: false,
|
|
390
477
|
};
|
|
391
478
|
|
|
392
479
|
export class BrowserView {
|
|
@@ -465,7 +552,7 @@ export class BrowserView {
|
|
|
465
552
|
appresRoot: this.appresRoot,
|
|
466
553
|
webviewId: this.id,
|
|
467
554
|
rpcSocketPort: getRpcPort(),
|
|
468
|
-
secretKey: this.secretKey
|
|
555
|
+
secretKey: this.secretKey,
|
|
469
556
|
});
|
|
470
557
|
|
|
471
558
|
BrowserViewMap[this.id] = this;
|
|
@@ -474,8 +561,12 @@ export class BrowserView {
|
|
|
474
561
|
// Native popup mint already created the view; bind to host window + bounds.
|
|
475
562
|
const lib = getNativeLibrary();
|
|
476
563
|
lib?.symbols.bunite_view_popup_accept(
|
|
477
|
-
this.id,
|
|
478
|
-
this.
|
|
564
|
+
this.id,
|
|
565
|
+
this.windowId,
|
|
566
|
+
this.frame.x,
|
|
567
|
+
this.frame.y,
|
|
568
|
+
this.frame.width,
|
|
569
|
+
this.frame.height,
|
|
479
570
|
);
|
|
480
571
|
this.nativeAttached = true;
|
|
481
572
|
} else {
|
|
@@ -494,7 +585,7 @@ export class BrowserView {
|
|
|
494
585
|
this.frame.height,
|
|
495
586
|
this.autoResize,
|
|
496
587
|
this.sandbox,
|
|
497
|
-
toCString(this.preloadOrigins ? JSON.stringify(this.preloadOrigins) : "")
|
|
588
|
+
toCString(this.preloadOrigins ? JSON.stringify(this.preloadOrigins) : ""),
|
|
498
589
|
) ?? false;
|
|
499
590
|
}
|
|
500
591
|
|
|
@@ -514,8 +605,11 @@ export class BrowserView {
|
|
|
514
605
|
return Promise.race([
|
|
515
606
|
this._readyPromise,
|
|
516
607
|
new Promise<never>((_, reject) =>
|
|
517
|
-
setTimeout(
|
|
518
|
-
|
|
608
|
+
setTimeout(
|
|
609
|
+
() => reject(new Error(`Browser creation timed out for view ${this.id}`)),
|
|
610
|
+
timeoutMs,
|
|
611
|
+
),
|
|
612
|
+
),
|
|
519
613
|
]);
|
|
520
614
|
}
|
|
521
615
|
|
|
@@ -531,12 +625,20 @@ export class BrowserView {
|
|
|
531
625
|
this.connectionGeneration += 1;
|
|
532
626
|
const myGen = this.connectionGeneration;
|
|
533
627
|
if (this.connection) {
|
|
534
|
-
try {
|
|
628
|
+
try {
|
|
629
|
+
(this.connection as { transport?: { close?(): void } }).transport?.close?.();
|
|
630
|
+
} catch {
|
|
631
|
+
/* swallow */
|
|
632
|
+
}
|
|
535
633
|
this.connection = null;
|
|
536
634
|
}
|
|
537
635
|
const encPipe = await createEncryptedPipe(pipe, this.secretKey);
|
|
538
636
|
if (myGen !== this.connectionGeneration) {
|
|
539
|
-
try {
|
|
637
|
+
try {
|
|
638
|
+
encPipe.close();
|
|
639
|
+
} catch {
|
|
640
|
+
/* swallow */
|
|
641
|
+
}
|
|
540
642
|
return;
|
|
541
643
|
}
|
|
542
644
|
const runtime = getAppRuntimeOrThrow().createViewRuntime(this.id);
|
|
@@ -561,7 +663,11 @@ export class BrowserView {
|
|
|
561
663
|
detachNewConnection(): void {
|
|
562
664
|
this.connectionGeneration += 1;
|
|
563
665
|
if (this.connection) {
|
|
564
|
-
try {
|
|
666
|
+
try {
|
|
667
|
+
(this.connection as { transport?: { close?(): void } }).transport?.close?.();
|
|
668
|
+
} catch {
|
|
669
|
+
/* swallow */
|
|
670
|
+
}
|
|
565
671
|
this.connection = null;
|
|
566
672
|
}
|
|
567
673
|
}
|
|
@@ -582,11 +688,18 @@ export class BrowserView {
|
|
|
582
688
|
|
|
583
689
|
evaluate(script: string, frameId?: string): Promise<EvaluateResult> {
|
|
584
690
|
if (!this.nativeAttached) {
|
|
585
|
-
return Promise.resolve({
|
|
691
|
+
return Promise.resolve({
|
|
692
|
+
ok: false,
|
|
693
|
+
code: "not_supported",
|
|
694
|
+
message: "native runtime unavailable",
|
|
695
|
+
});
|
|
586
696
|
}
|
|
587
697
|
return new Promise<EvaluateResult>((resolve) => {
|
|
588
698
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
589
|
-
const wrappedResolve = (r: EvaluateResult) => {
|
|
699
|
+
const wrappedResolve = (r: EvaluateResult) => {
|
|
700
|
+
if (timer) clearTimeout(timer);
|
|
701
|
+
resolve(r);
|
|
702
|
+
};
|
|
590
703
|
const requestId = registerEvaluateRequest(this.id, wrappedResolve);
|
|
591
704
|
// 30s timeout — two-hop CDP path (createIsolatedWorld → Runtime.evaluate)
|
|
592
705
|
// can silently hang if the frame is destroyed mid-flight.
|
|
@@ -597,7 +710,12 @@ export class BrowserView {
|
|
|
597
710
|
}, 30_000);
|
|
598
711
|
const lib = getNativeLibrary();
|
|
599
712
|
if (frameId) {
|
|
600
|
-
lib?.symbols.bunite_view_evaluate_in_frame(
|
|
713
|
+
lib?.symbols.bunite_view_evaluate_in_frame(
|
|
714
|
+
this.id,
|
|
715
|
+
requestId,
|
|
716
|
+
toCString(script),
|
|
717
|
+
toCString(frameId),
|
|
718
|
+
);
|
|
601
719
|
} else {
|
|
602
720
|
lib?.symbols.bunite_view_evaluate(this.id, requestId, toCString(script));
|
|
603
721
|
}
|
|
@@ -608,13 +726,19 @@ export class BrowserView {
|
|
|
608
726
|
if (!this.nativeAttached) return;
|
|
609
727
|
const policyCode = policy === "auto" ? 0 : policy === "ask" ? 1 : 2;
|
|
610
728
|
getNativeLibrary()?.symbols.bunite_view_set_download_policy(
|
|
611
|
-
this.id,
|
|
729
|
+
this.id,
|
|
730
|
+
policyCode,
|
|
731
|
+
toCString(downloadDir ?? ""),
|
|
612
732
|
);
|
|
613
733
|
}
|
|
614
734
|
|
|
615
735
|
listFrames(): Promise<ListFramesResult> {
|
|
616
736
|
if (!this.nativeAttached) {
|
|
617
|
-
return Promise.resolve({
|
|
737
|
+
return Promise.resolve({
|
|
738
|
+
ok: false,
|
|
739
|
+
code: "not_supported",
|
|
740
|
+
message: "native runtime unavailable",
|
|
741
|
+
});
|
|
618
742
|
}
|
|
619
743
|
return new Promise<ListFramesResult>((resolve) => {
|
|
620
744
|
const requestId = registerFramesRequest(this.id, resolve);
|
|
@@ -623,7 +747,10 @@ export class BrowserView {
|
|
|
623
747
|
resolve({ ok: false, code: "runtime_error", message: "list frames timed out after 10s" });
|
|
624
748
|
}
|
|
625
749
|
}, 10_000);
|
|
626
|
-
const wrappedResolve = (r: ListFramesResult) => {
|
|
750
|
+
const wrappedResolve = (r: ListFramesResult) => {
|
|
751
|
+
clearTimeout(timer);
|
|
752
|
+
resolve(r);
|
|
753
|
+
};
|
|
627
754
|
framesResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve });
|
|
628
755
|
getNativeLibrary()?.symbols.bunite_view_list_frames(this.id, requestId);
|
|
629
756
|
});
|
|
@@ -631,22 +758,37 @@ export class BrowserView {
|
|
|
631
758
|
|
|
632
759
|
resolveAndClick(args: ResolveAndClickArgs): Promise<ResolveAndClickResult> {
|
|
633
760
|
if (!this.nativeAttached) {
|
|
634
|
-
return Promise.resolve({
|
|
761
|
+
return Promise.resolve({
|
|
762
|
+
ok: false,
|
|
763
|
+
code: "not_supported",
|
|
764
|
+
message: "native runtime unavailable",
|
|
765
|
+
});
|
|
635
766
|
}
|
|
636
767
|
return new Promise<ResolveAndClickResult>((resolve) => {
|
|
637
768
|
const requestId = registerResolveAndClickRequest(this.id, resolve);
|
|
638
769
|
const timer = setTimeout(() => {
|
|
639
770
|
if (resolveAndClickResolvers.delete(requestId)) {
|
|
640
|
-
resolve({
|
|
771
|
+
resolve({
|
|
772
|
+
ok: false,
|
|
773
|
+
code: "runtime_error",
|
|
774
|
+
message: "resolveAndClick timed out after 5s",
|
|
775
|
+
});
|
|
641
776
|
}
|
|
642
777
|
}, 5_000);
|
|
643
|
-
const wrappedResolve = (r: ResolveAndClickResult) => {
|
|
778
|
+
const wrappedResolve = (r: ResolveAndClickResult) => {
|
|
779
|
+
clearTimeout(timer);
|
|
780
|
+
resolve(r);
|
|
781
|
+
};
|
|
644
782
|
resolveAndClickResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve });
|
|
645
783
|
const button = args.button === "right" ? 2 : args.button === "middle" ? 1 : 0;
|
|
646
784
|
getNativeLibrary()?.symbols.bunite_view_resolve_and_click(
|
|
647
|
-
this.id,
|
|
648
|
-
|
|
649
|
-
|
|
785
|
+
this.id,
|
|
786
|
+
requestId,
|
|
787
|
+
toCString(args.selector),
|
|
788
|
+
toCString(args.frameId ?? ""),
|
|
789
|
+
button,
|
|
790
|
+
args.clickCount ?? 1,
|
|
791
|
+
encodeModifiers(args.modifiers),
|
|
650
792
|
);
|
|
651
793
|
});
|
|
652
794
|
}
|
|
@@ -661,7 +803,8 @@ export class BrowserView {
|
|
|
661
803
|
// `send*` methods. Modifier translation + key resolution happen inside;
|
|
662
804
|
// callers never touch the FFI int contract.
|
|
663
805
|
click(args: {
|
|
664
|
-
x: number;
|
|
806
|
+
x: number;
|
|
807
|
+
y: number;
|
|
665
808
|
button?: "left" | "middle" | "right";
|
|
666
809
|
clickCount?: number;
|
|
667
810
|
modifiers?: Modifier[];
|
|
@@ -669,7 +812,12 @@ export class BrowserView {
|
|
|
669
812
|
if (!this.nativeAttached) return;
|
|
670
813
|
const button = args.button === "right" ? 2 : args.button === "middle" ? 1 : 0;
|
|
671
814
|
getNativeLibrary()?.symbols.bunite_view_click(
|
|
672
|
-
this.id,
|
|
815
|
+
this.id,
|
|
816
|
+
args.x,
|
|
817
|
+
args.y,
|
|
818
|
+
button,
|
|
819
|
+
args.clickCount ?? 1,
|
|
820
|
+
encodeModifiers(args.modifiers),
|
|
673
821
|
);
|
|
674
822
|
}
|
|
675
823
|
|
|
@@ -683,25 +831,35 @@ export class BrowserView {
|
|
|
683
831
|
const r = resolveKey(key);
|
|
684
832
|
const a = action === "down" ? 0 : action === "up" ? 1 : 2;
|
|
685
833
|
getNativeLibrary()?.symbols.bunite_view_press(
|
|
686
|
-
this.id,
|
|
687
|
-
|
|
688
|
-
|
|
834
|
+
this.id,
|
|
835
|
+
r.windowsVkCode,
|
|
836
|
+
r.macKeyCode,
|
|
837
|
+
toCString(r.key),
|
|
838
|
+
toCString(r.code),
|
|
839
|
+
toCString(r.character),
|
|
840
|
+
encodeModifiers(modifiers),
|
|
841
|
+
a,
|
|
842
|
+
r.extended,
|
|
843
|
+
r.location,
|
|
689
844
|
);
|
|
690
845
|
}
|
|
691
846
|
|
|
692
|
-
scroll(args: {
|
|
693
|
-
dx: number; dy: number; x?: number; y?: number;
|
|
694
|
-
modifiers?: Modifier[];
|
|
695
|
-
}) {
|
|
847
|
+
scroll(args: { dx: number; dy: number; x?: number; y?: number; modifiers?: Modifier[] }) {
|
|
696
848
|
if (!this.nativeAttached) return;
|
|
697
849
|
getNativeLibrary()?.symbols.bunite_view_scroll(
|
|
698
|
-
this.id,
|
|
850
|
+
this.id,
|
|
851
|
+
args.dx,
|
|
852
|
+
args.dy,
|
|
853
|
+
args.x ?? 0,
|
|
854
|
+
args.y ?? 0,
|
|
855
|
+
encodeModifiers(args.modifiers),
|
|
699
856
|
);
|
|
700
857
|
}
|
|
701
858
|
|
|
702
859
|
mouse(args: {
|
|
703
860
|
action: "move" | "down" | "up";
|
|
704
|
-
x: number;
|
|
861
|
+
x: number;
|
|
862
|
+
y: number;
|
|
705
863
|
button?: "left" | "middle" | "right";
|
|
706
864
|
modifiers?: Modifier[];
|
|
707
865
|
}) {
|
|
@@ -709,20 +867,32 @@ export class BrowserView {
|
|
|
709
867
|
const action = args.action === "move" ? 0 : args.action === "down" ? 1 : 2;
|
|
710
868
|
const button = args.button === "right" ? 2 : args.button === "middle" ? 1 : 0;
|
|
711
869
|
getNativeLibrary()?.symbols.bunite_view_mouse(
|
|
712
|
-
this.id,
|
|
870
|
+
this.id,
|
|
871
|
+
action,
|
|
872
|
+
args.x,
|
|
873
|
+
args.y,
|
|
874
|
+
button,
|
|
875
|
+
encodeModifiers(args.modifiers),
|
|
713
876
|
);
|
|
714
877
|
}
|
|
715
878
|
|
|
716
879
|
respondToDialog(requestId: number, accept: boolean, text?: string) {
|
|
717
880
|
if (!this.nativeAttached) return;
|
|
718
881
|
getNativeLibrary()?.symbols.bunite_view_respond_dialog(
|
|
719
|
-
this.id,
|
|
882
|
+
this.id,
|
|
883
|
+
requestId,
|
|
884
|
+
accept,
|
|
885
|
+
toCString(text ?? ""),
|
|
720
886
|
);
|
|
721
887
|
}
|
|
722
888
|
|
|
723
889
|
screenshot(format: "png" | "jpeg", quality: number): Promise<ScreenshotResult> {
|
|
724
890
|
if (!this.nativeAttached) {
|
|
725
|
-
return Promise.resolve({
|
|
891
|
+
return Promise.resolve({
|
|
892
|
+
ok: false,
|
|
893
|
+
code: "not_supported",
|
|
894
|
+
message: "native runtime unavailable",
|
|
895
|
+
});
|
|
726
896
|
}
|
|
727
897
|
return new Promise<ScreenshotResult>((resolve) => {
|
|
728
898
|
const requestId = registerScreenshotRequest(this.id, resolve);
|
|
@@ -732,28 +902,51 @@ export class BrowserView {
|
|
|
732
902
|
resolve({ ok: false, code: "timeout", message: "screenshot timed out after 30s" });
|
|
733
903
|
}
|
|
734
904
|
}, 30_000);
|
|
735
|
-
const wrappedResolve = (r: ScreenshotResult) => {
|
|
905
|
+
const wrappedResolve = (r: ScreenshotResult) => {
|
|
906
|
+
clearTimeout(timer);
|
|
907
|
+
resolve(r);
|
|
908
|
+
};
|
|
736
909
|
// Replace the registered resolver so the timeout-clearing wrapper runs on success.
|
|
737
910
|
screenshotResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve });
|
|
738
|
-
getNativeLibrary()?.symbols.bunite_view_screenshot(
|
|
911
|
+
getNativeLibrary()?.symbols.bunite_view_screenshot(
|
|
912
|
+
this.id,
|
|
913
|
+
requestId,
|
|
914
|
+
toCString(format),
|
|
915
|
+
quality,
|
|
916
|
+
);
|
|
739
917
|
});
|
|
740
918
|
}
|
|
741
919
|
|
|
742
920
|
accessibilitySnapshot(interestingOnly: boolean): Promise<AccessibilitySnapshotResult> {
|
|
743
921
|
if (!this.nativeAttached) {
|
|
744
|
-
return Promise.resolve({
|
|
922
|
+
return Promise.resolve({
|
|
923
|
+
ok: false,
|
|
924
|
+
code: "not_supported",
|
|
925
|
+
message: "native runtime unavailable",
|
|
926
|
+
});
|
|
745
927
|
}
|
|
746
928
|
return new Promise<AccessibilitySnapshotResult>((resolve) => {
|
|
747
929
|
const requestId = registerAxRequest(this.id, resolve, interestingOnly);
|
|
748
930
|
const timer = setTimeout(() => {
|
|
749
931
|
if (axResolvers.delete(requestId)) {
|
|
750
|
-
resolve({
|
|
932
|
+
resolve({
|
|
933
|
+
ok: false,
|
|
934
|
+
code: "timeout",
|
|
935
|
+
message: "accessibility snapshot timed out after 30s",
|
|
936
|
+
});
|
|
751
937
|
}
|
|
752
938
|
}, 30_000);
|
|
753
|
-
const wrappedResolve = (r: AccessibilitySnapshotResult) => {
|
|
939
|
+
const wrappedResolve = (r: AccessibilitySnapshotResult) => {
|
|
940
|
+
clearTimeout(timer);
|
|
941
|
+
resolve(r);
|
|
942
|
+
};
|
|
754
943
|
axResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve, interestingOnly });
|
|
755
944
|
// Native flag is currently unused (filter is TS-side); kept for ABI shape stability.
|
|
756
|
-
getNativeLibrary()?.symbols.bunite_view_accessibility_snapshot(
|
|
945
|
+
getNativeLibrary()?.symbols.bunite_view_accessibility_snapshot(
|
|
946
|
+
this.id,
|
|
947
|
+
requestId,
|
|
948
|
+
interestingOnly ? 1 : 0,
|
|
949
|
+
);
|
|
757
950
|
});
|
|
758
951
|
}
|
|
759
952
|
|
|
@@ -794,9 +987,7 @@ export class BrowserView {
|
|
|
794
987
|
buf[i * 4 + 2] = rects[i].w;
|
|
795
988
|
buf[i * 4 + 3] = rects[i].h;
|
|
796
989
|
}
|
|
797
|
-
getNativeLibrary()?.symbols.bunite_view_set_mask_region(
|
|
798
|
-
this.id, ptr(buf.buffer), rects.length
|
|
799
|
-
);
|
|
990
|
+
getNativeLibrary()?.symbols.bunite_view_set_mask_region(this.id, ptr(buf.buffer), rects.length);
|
|
800
991
|
}
|
|
801
992
|
|
|
802
993
|
bringToFront() {
|
|
@@ -868,8 +1059,19 @@ export class BrowserView {
|
|
|
868
1059
|
rejectResolveAndClickForView(this.id);
|
|
869
1060
|
this.nativeAttached = false;
|
|
870
1061
|
for (const eventName of [
|
|
871
|
-
"will-navigate",
|
|
872
|
-
"
|
|
1062
|
+
"will-navigate",
|
|
1063
|
+
"did-navigate",
|
|
1064
|
+
"dom-ready",
|
|
1065
|
+
"new-window-open",
|
|
1066
|
+
"permission-requested",
|
|
1067
|
+
"title-changed",
|
|
1068
|
+
"load-start",
|
|
1069
|
+
"load-finish",
|
|
1070
|
+
"load-fail",
|
|
1071
|
+
"dialog",
|
|
1072
|
+
"console-message",
|
|
1073
|
+
"download-event",
|
|
1074
|
+
"popup-requested",
|
|
873
1075
|
]) {
|
|
874
1076
|
buniteEventEmitter.removeAllListeners(`${eventName}-${this.id}`);
|
|
875
1077
|
}
|
|
@@ -877,8 +1079,21 @@ export class BrowserView {
|
|
|
877
1079
|
}
|
|
878
1080
|
|
|
879
1081
|
on(
|
|
880
|
-
name:
|
|
881
|
-
|
|
1082
|
+
name:
|
|
1083
|
+
| "will-navigate"
|
|
1084
|
+
| "did-navigate"
|
|
1085
|
+
| "dom-ready"
|
|
1086
|
+
| "new-window-open"
|
|
1087
|
+
| "permission-requested"
|
|
1088
|
+
| "title-changed"
|
|
1089
|
+
| "load-start"
|
|
1090
|
+
| "load-finish"
|
|
1091
|
+
| "load-fail"
|
|
1092
|
+
| "dialog"
|
|
1093
|
+
| "console-message"
|
|
1094
|
+
| "download-event"
|
|
1095
|
+
| "popup-requested",
|
|
1096
|
+
handler: (event: unknown) => void,
|
|
882
1097
|
) {
|
|
883
1098
|
const specificName = `${name}-${this.id}`;
|
|
884
1099
|
buniteEventEmitter.on(specificName, handler);
|
|
@@ -887,5 +1102,7 @@ export class BrowserView {
|
|
|
887
1102
|
}
|
|
888
1103
|
|
|
889
1104
|
attachBrowserViewRegistry({
|
|
890
|
-
getById(id) {
|
|
1105
|
+
getById(id) {
|
|
1106
|
+
return BrowserView.getById(id);
|
|
1107
|
+
},
|
|
891
1108
|
});
|