electrobun 1.18.4-beta.3 → 1.18.4-beta.6
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/README.md +1 -0
- package/dist/api/browser/global.d.ts +5 -0
- package/dist/api/browser/index.ts +64 -22
- package/dist/api/bun/ElectrobunConfig.ts +25 -1
- package/dist/api/bun/core/BrowserView.ts +67 -8
- package/dist/api/bun/core/BrowserWindow.ts +5 -22
- package/dist/api/bun/core/GpuWindow.ts +7 -1
- package/dist/api/bun/core/Socket.ts +13 -196
- package/dist/api/bun/core/Utils.ts +2 -4
- package/dist/api/bun/core/WGPUView.ts +43 -13
- package/dist/api/bun/index.ts +35 -0
- package/dist/api/bun/preload/.generated/compiled.ts +1 -1
- package/dist/api/bun/preload/globals.d.ts +7 -0
- package/dist/api/bun/preload/index.ts +18 -8
- package/dist/api/bun/preload/webviewTag.ts +32 -3
- package/dist/api/bun/proc/native.ts +240 -98
- package/dist/api/bun/webGPU.ts +1 -1
- package/dist/preload-full.js +38 -10
- package/dist/zig-sdk/electrobun.zig +18 -4
- package/package.json +1 -1
- package/src/cli/index.ts +97 -4
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ Don't miss our:
|
|
|
63
63
|
- [GOG Achievements GUI](https://github.com/timendum/gog-achievements-gui) - desktop app for managing GOG achievements
|
|
64
64
|
- [groov](https://github.com/laurenzcodes/groov) - desktop audio deck monitor
|
|
65
65
|
- [Guerilla Glass](https://github.com/okikeSolutions/guerillaglass) - open-source cross-platform creator studio for fast Record -> Edit -> Deliver workflows
|
|
66
|
+
- [Invoke](https://getinvoke.com) - macOS UI automation & shortcut platform
|
|
66
67
|
- [Marginalia](https://github.com/lars-hoeijmans/Marginalia) - a simple note taking app
|
|
67
68
|
- [MarkBun](https://github.com/xiaochong/markbun) - fast, beautiful, Typora-like markdown desktop editor
|
|
68
69
|
- [md-browse](https://github.com/needle-tools/md-browse) - a markdown-first browser that converts web pages to clean markdown
|
|
@@ -7,6 +7,8 @@ interface ElectrobunEncryptResult {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
interface ElectrobunBridge {
|
|
10
|
+
receiveMessageFromHost: (msg: unknown) => void;
|
|
11
|
+
receiveInternalMessageFromHost: (msg: unknown) => void;
|
|
10
12
|
receiveMessageFromBun: (msg: unknown) => void;
|
|
11
13
|
receiveInternalMessageFromBun: (msg: unknown) => void;
|
|
12
14
|
}
|
|
@@ -20,10 +22,13 @@ declare global {
|
|
|
20
22
|
__electrobunWebviewId: number;
|
|
21
23
|
__electrobunWindowId: number;
|
|
22
24
|
__electrobunRpcSocketPort: number;
|
|
25
|
+
__electrobunHostSocketPort?: number;
|
|
23
26
|
__electrobun?: ElectrobunBridge;
|
|
27
|
+
__electrobunPendingHostMessages?: unknown[];
|
|
24
28
|
__electrobun_encrypt: (msg: string) => Promise<ElectrobunEncryptResult>;
|
|
25
29
|
__electrobun_decrypt: (encryptedData: string, iv: string, tag: string) => Promise<string>;
|
|
26
30
|
__electrobunInternalBridge?: MessageHandler;
|
|
31
|
+
__electrobunHostBridge?: MessageHandler;
|
|
27
32
|
__electrobunBunBridge?: MessageHandler;
|
|
28
33
|
}
|
|
29
34
|
}
|
|
@@ -15,13 +15,23 @@ import { type WgpuTagElement, type WgpuEventTypes } from "./wgputag";
|
|
|
15
15
|
import "./global.d.ts";
|
|
16
16
|
|
|
17
17
|
const WEBVIEW_ID = window.__electrobunWebviewId;
|
|
18
|
-
const
|
|
18
|
+
const HOST_SOCKET_PORT =
|
|
19
|
+
window.__electrobunHostSocketPort ?? window.__electrobunRpcSocketPort;
|
|
19
20
|
|
|
20
21
|
class Electroview<T extends RPCWithTransport> {
|
|
21
|
-
|
|
22
|
+
hostSocket?: WebSocket;
|
|
23
|
+
hostSocketCanSend = false;
|
|
22
24
|
// user's custom rpc browser <-> bun
|
|
23
25
|
rpc?: T;
|
|
24
26
|
rpcHandler?: (msg: unknown) => void;
|
|
27
|
+
carrots = {
|
|
28
|
+
invoke: <R = unknown>(
|
|
29
|
+
carrotId: string,
|
|
30
|
+
method: string,
|
|
31
|
+
params?: unknown,
|
|
32
|
+
options?: { windowId?: string },
|
|
33
|
+
) => this.invokeCarrot<R>(carrotId, method, params, options),
|
|
34
|
+
};
|
|
25
35
|
|
|
26
36
|
constructor(config: { rpc: T }) {
|
|
27
37
|
this.rpc = config.rpc;
|
|
@@ -29,34 +39,42 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
init() {
|
|
32
|
-
this.
|
|
42
|
+
this.initSocketToHost();
|
|
33
43
|
|
|
34
|
-
// Set up handler for user RPC messages from
|
|
35
|
-
|
|
36
|
-
window.__electrobun!.
|
|
37
|
-
|
|
44
|
+
// Set up handler for user RPC messages from the host runtime.
|
|
45
|
+
const hostMessageHandler = this.receiveMessageFromHost.bind(this);
|
|
46
|
+
window.__electrobun!.receiveMessageFromHost = hostMessageHandler;
|
|
47
|
+
window.__electrobun!.receiveMessageFromBun = hostMessageHandler;
|
|
38
48
|
|
|
39
49
|
if (this.rpc) {
|
|
40
50
|
this.rpc.setTransport(this.createTransport());
|
|
41
51
|
}
|
|
52
|
+
|
|
53
|
+
const pendingMessages = window.__electrobunPendingHostMessages;
|
|
54
|
+
if (pendingMessages?.length) {
|
|
55
|
+
window.__electrobunPendingHostMessages = [];
|
|
56
|
+
for (const message of pendingMessages) {
|
|
57
|
+
hostMessageHandler(message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
initSocketToHost() {
|
|
45
63
|
// Skip native socket when running in a remote browser (no port/webview ID)
|
|
46
|
-
if (!
|
|
64
|
+
if (!HOST_SOCKET_PORT || !WEBVIEW_ID) {
|
|
47
65
|
return;
|
|
48
66
|
}
|
|
49
67
|
|
|
50
|
-
// Note: Using ws:// for
|
|
68
|
+
// Note: Using ws:// for loopback is intentional - all RPC messages are
|
|
51
69
|
// encrypted with per-webview AES-GCM keys, making TLS redundant
|
|
52
70
|
const socket = new WebSocket(
|
|
53
|
-
`ws://
|
|
71
|
+
`ws://127.0.0.1:${HOST_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`,
|
|
54
72
|
);
|
|
55
73
|
|
|
56
|
-
this.
|
|
74
|
+
this.hostSocket = socket;
|
|
57
75
|
|
|
58
76
|
socket.addEventListener("open", () => {
|
|
59
|
-
|
|
77
|
+
this.hostSocketCanSend = true;
|
|
60
78
|
});
|
|
61
79
|
|
|
62
80
|
socket.addEventListener("message", async (event) => {
|
|
@@ -71,6 +89,7 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
71
89
|
encryptedPacket.tag,
|
|
72
90
|
);
|
|
73
91
|
|
|
92
|
+
this.hostSocketCanSend = true;
|
|
74
93
|
this.rpcHandler?.(JSON.parse(decrypted));
|
|
75
94
|
} catch (err) {
|
|
76
95
|
console.error("Error parsing bun message:", err);
|
|
@@ -83,10 +102,12 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
83
102
|
});
|
|
84
103
|
|
|
85
104
|
socket.addEventListener("error", (event) => {
|
|
105
|
+
this.hostSocketCanSend = false;
|
|
86
106
|
console.error("Socket error:", event);
|
|
87
107
|
});
|
|
88
108
|
|
|
89
109
|
socket.addEventListener("close", (_event) => {
|
|
110
|
+
this.hostSocketCanSend = false;
|
|
90
111
|
// console.log("Socket closed:", event);
|
|
91
112
|
});
|
|
92
113
|
}
|
|
@@ -97,9 +118,9 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
97
118
|
send(message: unknown) {
|
|
98
119
|
try {
|
|
99
120
|
const messageString = JSON.stringify(message);
|
|
100
|
-
that.
|
|
121
|
+
that.sendMessageToHost(messageString);
|
|
101
122
|
} catch (error) {
|
|
102
|
-
console.error("
|
|
123
|
+
console.error("host: failed to serialize message to webview", error);
|
|
103
124
|
}
|
|
104
125
|
},
|
|
105
126
|
registerHandler(handler: (msg: unknown) => void) {
|
|
@@ -108,8 +129,11 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
108
129
|
};
|
|
109
130
|
}
|
|
110
131
|
|
|
111
|
-
async
|
|
112
|
-
if (
|
|
132
|
+
async sendMessageToHost(msg: string) {
|
|
133
|
+
if (
|
|
134
|
+
this.hostSocketCanSend &&
|
|
135
|
+
this.hostSocket?.readyState === WebSocket.OPEN
|
|
136
|
+
) {
|
|
113
137
|
try {
|
|
114
138
|
const { encryptedData, iv, tag } =
|
|
115
139
|
await window.__electrobun_encrypt(msg);
|
|
@@ -120,24 +144,42 @@ class Electroview<T extends RPCWithTransport> {
|
|
|
120
144
|
tag: tag,
|
|
121
145
|
};
|
|
122
146
|
const encryptedPacketString = JSON.stringify(encryptedPacket);
|
|
123
|
-
this.
|
|
147
|
+
this.hostSocket.send(encryptedPacketString);
|
|
124
148
|
return;
|
|
125
149
|
} catch (error) {
|
|
126
|
-
console.error("Error sending message to
|
|
150
|
+
console.error("Error sending message to host via socket:", error);
|
|
127
151
|
}
|
|
128
152
|
}
|
|
129
153
|
|
|
130
154
|
// if socket's are unavailable, fallback to postMessage
|
|
131
|
-
window.
|
|
155
|
+
window.__electrobunHostBridge?.postMessage(msg);
|
|
132
156
|
}
|
|
133
157
|
|
|
134
|
-
|
|
135
|
-
// NOTE: in the webview messages are passed by executing
|
|
158
|
+
receiveMessageFromHost(msg: unknown) {
|
|
159
|
+
// NOTE: in the webview messages are passed by executing window.__electrobun.receiveMessageFromHost(object)
|
|
136
160
|
// so they're already parsed into an object here
|
|
137
161
|
if (this.rpcHandler) {
|
|
138
162
|
this.rpcHandler(msg);
|
|
139
163
|
}
|
|
140
164
|
}
|
|
165
|
+
|
|
166
|
+
async invokeCarrot<R = unknown>(
|
|
167
|
+
carrotId: string,
|
|
168
|
+
method: string,
|
|
169
|
+
params?: unknown,
|
|
170
|
+
options?: { windowId?: string },
|
|
171
|
+
): Promise<R> {
|
|
172
|
+
const requestProxy = (this.rpc as any)?.request;
|
|
173
|
+
if (!requestProxy || typeof requestProxy.invokeCarrot !== "function") {
|
|
174
|
+
throw new Error("Renderer carrot invocation is not available in this Electrobun host.");
|
|
175
|
+
}
|
|
176
|
+
return requestProxy.invokeCarrot({
|
|
177
|
+
carrotId,
|
|
178
|
+
method,
|
|
179
|
+
params,
|
|
180
|
+
windowId: options?.windowId,
|
|
181
|
+
}) as Promise<R>;
|
|
182
|
+
}
|
|
141
183
|
static defineRPC<Schema extends ElectrobunRPCSchema>(
|
|
142
184
|
config: ElectrobunRPCConfig<Schema, "webview">,
|
|
143
185
|
) {
|
|
@@ -13,6 +13,28 @@ type BunBuildOptions = Omit<
|
|
|
13
13
|
"entrypoints" | "outdir" | "target"
|
|
14
14
|
>;
|
|
15
15
|
|
|
16
|
+
type CarrotFileActivatorConfig = {
|
|
17
|
+
baseName?: string;
|
|
18
|
+
nodeType?: "file" | "dir" | "any";
|
|
19
|
+
slate: {
|
|
20
|
+
type: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
icon?: string;
|
|
23
|
+
config?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type CarrotContributionsConfig = {
|
|
28
|
+
fileActivators?: CarrotFileActivatorConfig[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type CarrotUIDefinition = {
|
|
32
|
+
name?: string;
|
|
33
|
+
entrypoint?: string;
|
|
34
|
+
path?: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
};
|
|
37
|
+
|
|
16
38
|
export interface ElectrobunConfig {
|
|
17
39
|
/**
|
|
18
40
|
* Application metadata configuration
|
|
@@ -264,7 +286,9 @@ export interface ElectrobunConfig {
|
|
|
264
286
|
carrotOnly?: boolean;
|
|
265
287
|
permissions?: Record<string, unknown>;
|
|
266
288
|
dependencies?: Record<string, string>;
|
|
267
|
-
remoteUIs?: Record<string,
|
|
289
|
+
remoteUIs?: Record<string, CarrotUIDefinition>;
|
|
290
|
+
slateUIs?: Record<string, CarrotUIDefinition>;
|
|
291
|
+
contributions?: CarrotContributionsConfig;
|
|
268
292
|
};
|
|
269
293
|
|
|
270
294
|
/**
|
|
@@ -177,23 +177,23 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
177
177
|
this.rpc!.setTransport(this.createTransport());
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
sendHostMessageToWebviewViaExecute(jsonMessage: unknown) {
|
|
181
181
|
const stringifiedMessage =
|
|
182
182
|
typeof jsonMessage === "string"
|
|
183
183
|
? jsonMessage
|
|
184
184
|
: JSON.stringify(jsonMessage);
|
|
185
185
|
// todo (yoav): make this a shared const with the browser api
|
|
186
|
-
const wrappedMessage = `window.__electrobun.
|
|
186
|
+
const wrappedMessage = `window.__electrobun.receiveMessageFromHost(${stringifiedMessage})`;
|
|
187
187
|
this.executeJavascript(wrappedMessage);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
sendInternalHostMessageViaExecute(jsonMessage: unknown) {
|
|
191
191
|
const stringifiedMessage =
|
|
192
192
|
typeof jsonMessage === "string"
|
|
193
193
|
? jsonMessage
|
|
194
194
|
: JSON.stringify(jsonMessage);
|
|
195
195
|
// todo (yoav): make this a shared const with the browser api
|
|
196
|
-
const wrappedMessage = `window.__electrobun.
|
|
196
|
+
const wrappedMessage = `window.__electrobun.receiveInternalMessageFromHost(${stringifiedMessage})`;
|
|
197
197
|
this.executeJavascript(wrappedMessage);
|
|
198
198
|
}
|
|
199
199
|
|
|
@@ -312,9 +312,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
312
312
|
if (!sentOverSocket) {
|
|
313
313
|
try {
|
|
314
314
|
const messageString = JSON.stringify(message);
|
|
315
|
-
that.
|
|
315
|
+
that.sendHostMessageToWebviewViaExecute(messageString);
|
|
316
316
|
} catch (error) {
|
|
317
|
-
console.error("
|
|
317
|
+
console.error("host: failed to serialize message to webview", error);
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
},
|
|
@@ -328,7 +328,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
328
328
|
};
|
|
329
329
|
|
|
330
330
|
remove() {
|
|
331
|
-
if (
|
|
331
|
+
if (this.isRemoved) {
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
334
334
|
this.isRemoved = true;
|
|
@@ -341,13 +341,72 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
341
341
|
unregisterHandler() {},
|
|
342
342
|
});
|
|
343
343
|
this.rpcHandler = undefined;
|
|
344
|
-
|
|
344
|
+
try {
|
|
345
|
+
ffi.request.webviewRemove({ id: this.id });
|
|
346
|
+
} catch (error) {
|
|
347
|
+
console.error(`Error removing webview ${this.id}:`, error);
|
|
348
|
+
}
|
|
345
349
|
}
|
|
346
350
|
|
|
347
351
|
static getById(id: number) {
|
|
348
352
|
return BrowserViewMap[id];
|
|
349
353
|
}
|
|
350
354
|
|
|
355
|
+
// Core can create webviews before Bun has constructed a JS wrapper for them.
|
|
356
|
+
// Use this in native/runtime paths that need to ensure a wrapper exists.
|
|
357
|
+
static ensureWrapped<T extends RPCWithTransport = RPCWithTransport>(
|
|
358
|
+
id: number,
|
|
359
|
+
options: Partial<BrowserViewOptions<T>> = {},
|
|
360
|
+
) {
|
|
361
|
+
return (
|
|
362
|
+
(BrowserViewMap[id] as BrowserView<T> | undefined) ??
|
|
363
|
+
BrowserView.adoptExisting(id, options)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
static adoptExisting<T extends RPCWithTransport = RPCWithTransport>(
|
|
368
|
+
id: number,
|
|
369
|
+
options: Partial<BrowserViewOptions<T>> = {},
|
|
370
|
+
) {
|
|
371
|
+
const existing = BrowserViewMap[id] as BrowserView<T> | undefined;
|
|
372
|
+
if (existing) {
|
|
373
|
+
return existing;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const ptr = ffi.request.getWebviewPointer({ id }) as Pointer | null;
|
|
377
|
+
if (!ptr) {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const view = Object.create(BrowserView.prototype) as BrowserView<T>;
|
|
382
|
+
view.id = id;
|
|
383
|
+
view.hostWebviewId = options.hostWebviewId;
|
|
384
|
+
view.windowId = options.windowId ?? 0;
|
|
385
|
+
view.renderer = options.renderer ?? defaultOptions.renderer ?? "native";
|
|
386
|
+
view.url = options.url ?? defaultOptions.url ?? null;
|
|
387
|
+
view.html = options.html ?? defaultOptions.html ?? null;
|
|
388
|
+
view.preload = options.preload ?? defaultOptions.preload ?? null;
|
|
389
|
+
view.viewsRoot = options.viewsRoot ?? defaultOptions.viewsRoot ?? null;
|
|
390
|
+
view.partition = options.partition ?? null;
|
|
391
|
+
view.frame = {
|
|
392
|
+
x: options.frame?.x ?? defaultOptions.frame!.x,
|
|
393
|
+
y: options.frame?.y ?? defaultOptions.frame!.y,
|
|
394
|
+
width: options.frame?.width ?? defaultOptions.frame!.width,
|
|
395
|
+
height: options.frame?.height ?? defaultOptions.frame!.height,
|
|
396
|
+
};
|
|
397
|
+
view.secretKey = new Uint8Array(0);
|
|
398
|
+
view.rpc = options.rpc;
|
|
399
|
+
view.rpcHandler = undefined;
|
|
400
|
+
view.autoResize = options.autoResize === false ? false : true;
|
|
401
|
+
view.navigationRules = options.navigationRules ?? null;
|
|
402
|
+
view.sandbox = options.sandbox ?? false;
|
|
403
|
+
view.startTransparent = options.startTransparent ?? false;
|
|
404
|
+
view.startPassthrough = options.startPassthrough ?? false;
|
|
405
|
+
view.isRemoved = false;
|
|
406
|
+
BrowserViewMap[id] = view as BrowserView<any>;
|
|
407
|
+
return view;
|
|
408
|
+
}
|
|
409
|
+
|
|
351
410
|
static getAll() {
|
|
352
411
|
return Object.values(BrowserViewMap);
|
|
353
412
|
}
|
|
@@ -3,12 +3,13 @@ import electrobunEventEmitter from "../events/eventEmitter";
|
|
|
3
3
|
import { BrowserView } from "./BrowserView";
|
|
4
4
|
import { type Pointer } from "bun:ffi";
|
|
5
5
|
import { BuildConfig } from "./BuildConfig";
|
|
6
|
-
import { quit } from "./Utils";
|
|
7
6
|
import { type RPCWithTransport } from "../../shared/rpc.js";
|
|
8
|
-
import { GpuWindowMap } from "./GpuWindow";
|
|
9
7
|
import { WGPUView } from "./WGPUView";
|
|
10
8
|
|
|
11
9
|
const buildConfig = BuildConfig.getSync();
|
|
10
|
+
ffi.request.setExitOnLastWindowClosed({
|
|
11
|
+
enabled: buildConfig.runtime?.exitOnLastWindowClosed ?? true,
|
|
12
|
+
});
|
|
12
13
|
|
|
13
14
|
export type WindowOptionsType<T = undefined> = {
|
|
14
15
|
trafficLightOffset?: {
|
|
@@ -72,7 +73,7 @@ export const BrowserWindowMap: {
|
|
|
72
73
|
[id: number]: BrowserWindow<RPCWithTransport>;
|
|
73
74
|
} = {};
|
|
74
75
|
|
|
75
|
-
// Clean up
|
|
76
|
+
// Clean up JS wrapper state when a window closes. Native child cleanup is core-owned.
|
|
76
77
|
electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
|
|
77
78
|
const windowId = event.data.id;
|
|
78
79
|
delete BrowserWindowMap[windowId];
|
|
@@ -88,30 +89,12 @@ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
|
|
|
88
89
|
const wgpuViews = WGPUView.getAll().filter(v => v.windowId === windowId);
|
|
89
90
|
for (const view of wgpuViews) {
|
|
90
91
|
try {
|
|
91
|
-
|
|
92
|
-
if (view.ptr === null) {
|
|
93
|
-
// Already cleaned up, skip
|
|
94
|
-
} else {
|
|
95
|
-
// Programmatic close path - remove the view
|
|
96
|
-
view.remove();
|
|
97
|
-
}
|
|
92
|
+
view.remove();
|
|
98
93
|
} catch (e) {
|
|
99
94
|
console.error(`Error cleaning up WGPU view ${view.id}:`, e);
|
|
100
|
-
// If remove() failed, at least mark it as cleaned up
|
|
101
|
-
view.ptr = null as any;
|
|
102
95
|
}
|
|
103
96
|
}
|
|
104
97
|
|
|
105
|
-
const exitOnLastWindowClosed =
|
|
106
|
-
buildConfig.runtime?.exitOnLastWindowClosed ?? true;
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
exitOnLastWindowClosed &&
|
|
110
|
-
Object.keys(BrowserWindowMap).length === 0 &&
|
|
111
|
-
Object.keys(GpuWindowMap).length === 0
|
|
112
|
-
) {
|
|
113
|
-
quit();
|
|
114
|
-
}
|
|
115
98
|
});
|
|
116
99
|
|
|
117
100
|
export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
@@ -2,6 +2,7 @@ import { ffi } from "../proc/native";
|
|
|
2
2
|
import electrobunEventEmitter from "../events/eventEmitter";
|
|
3
3
|
import { type Pointer } from "bun:ffi";
|
|
4
4
|
import { WGPUView } from "./WGPUView";
|
|
5
|
+
import { BuildConfig } from "./BuildConfig";
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
export type GpuWindowOptionsType = {
|
|
@@ -34,11 +35,16 @@ const defaultOptions: GpuWindowOptionsType = {
|
|
|
34
35
|
transparent: false,
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
const buildConfig = BuildConfig.getSync();
|
|
39
|
+
ffi.request.setExitOnLastWindowClosed({
|
|
40
|
+
enabled: buildConfig.runtime?.exitOnLastWindowClosed ?? true,
|
|
41
|
+
});
|
|
42
|
+
|
|
37
43
|
export const GpuWindowMap: {
|
|
38
44
|
[id: number]: GpuWindow;
|
|
39
45
|
} = {};
|
|
40
46
|
|
|
41
|
-
// Clean up
|
|
47
|
+
// Clean up JS wrapper state when a window closes. Native child cleanup is core-owned.
|
|
42
48
|
electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
|
|
43
49
|
const windowId = event.data.id;
|
|
44
50
|
delete GpuWindowMap[windowId];
|
|
@@ -1,205 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { BrowserView } from "./BrowserView";
|
|
3
|
-
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
4
|
-
|
|
5
|
-
function base64ToUint8Array(base64: string) {
|
|
6
|
-
{
|
|
7
|
-
return new Uint8Array(
|
|
8
|
-
atob(base64)
|
|
9
|
-
.split("")
|
|
10
|
-
.map((char) => char.charCodeAt(0)),
|
|
11
|
-
);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Encrypt function
|
|
16
|
-
function encrypt(secretKey: Uint8Array, text: string) {
|
|
17
|
-
const iv = new Uint8Array(randomBytes(12)); // IV for AES-GCM
|
|
18
|
-
const cipher = createCipheriv("aes-256-gcm", secretKey, iv);
|
|
19
|
-
const encrypted = Buffer.concat([
|
|
20
|
-
new Uint8Array(cipher.update(text, "utf8")),
|
|
21
|
-
new Uint8Array(cipher.final()),
|
|
22
|
-
]).toString("base64");
|
|
23
|
-
const tag = cipher.getAuthTag().toString("base64");
|
|
24
|
-
return { encrypted, iv: Buffer.from(iv).toString("base64"), tag };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Decrypt function
|
|
28
|
-
function decrypt(
|
|
29
|
-
secretKey: Uint8Array,
|
|
30
|
-
encryptedData: Uint8Array,
|
|
31
|
-
iv: Uint8Array,
|
|
32
|
-
tag: Uint8Array,
|
|
33
|
-
) {
|
|
34
|
-
const decipher = createDecipheriv("aes-256-gcm", secretKey, iv);
|
|
35
|
-
decipher.setAuthTag(tag);
|
|
36
|
-
const decrypted = Buffer.concat([
|
|
37
|
-
new Uint8Array(decipher.update(encryptedData)),
|
|
38
|
-
new Uint8Array(decipher.final()),
|
|
39
|
-
]);
|
|
40
|
-
return decrypted.toString("utf8");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const socketMap: {
|
|
44
|
-
[webviewId: string]: {
|
|
45
|
-
socket: null | ServerWebSocket<unknown>;
|
|
46
|
-
queue: string[];
|
|
47
|
-
};
|
|
48
|
-
} = {};
|
|
1
|
+
import { ffi } from "../proc/native";
|
|
49
2
|
|
|
50
3
|
export const removeSocketForWebview = (webviewId: number) => {
|
|
51
|
-
|
|
52
|
-
if (!rpc) return;
|
|
53
|
-
|
|
54
|
-
rpc.socket = null;
|
|
55
|
-
delete socketMap[webviewId];
|
|
4
|
+
ffi.request.clearWebviewHostTransport({ id: webviewId });
|
|
56
5
|
};
|
|
57
6
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const endPort = 65535;
|
|
61
|
-
const payloadLimit = 1024 * 1024 * 500; // 500MB
|
|
62
|
-
let port = startPort;
|
|
63
|
-
let server = null;
|
|
64
|
-
|
|
65
|
-
while (port <= endPort) {
|
|
66
|
-
try {
|
|
67
|
-
server = Bun.serve<{ webviewId: number }>({
|
|
68
|
-
port,
|
|
69
|
-
fetch(req: Request, server: Server<{ webviewId: number }>) {
|
|
70
|
-
const url = new URL(req.url);
|
|
71
|
-
// const token = new URL(req.url).searchParams.get("token");
|
|
72
|
-
// if (token !== AUTH_TOKEN)
|
|
73
|
-
// return new Response("Unauthorized", { status: 401 });
|
|
74
|
-
// console.log("fetch!!", url.pathname);
|
|
75
|
-
if (url.pathname === "/socket") {
|
|
76
|
-
const webviewIdString = url.searchParams.get("webviewId");
|
|
77
|
-
if (!webviewIdString) {
|
|
78
|
-
return new Response("Missing webviewId", { status: 400 });
|
|
79
|
-
}
|
|
80
|
-
const webviewId = parseInt(webviewIdString, 10);
|
|
81
|
-
const success = server.upgrade(req, { data: { webviewId } });
|
|
82
|
-
return success
|
|
83
|
-
? undefined
|
|
84
|
-
: new Response("Upgrade failed", { status: 500 });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
console.log("unhandled RPC Server request", req.url);
|
|
88
|
-
},
|
|
89
|
-
websocket: {
|
|
90
|
-
idleTimeout: 960,
|
|
91
|
-
// 500MB max payload should be plenty
|
|
92
|
-
maxPayloadLength: payloadLimit,
|
|
93
|
-
// Anything beyond the backpressure limit will be dropped
|
|
94
|
-
backpressureLimit: payloadLimit * 2,
|
|
95
|
-
open(ws: ServerWebSocket<{ webviewId: number }>) {
|
|
96
|
-
if (!ws?.data) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
const { webviewId } = ws.data;
|
|
100
|
-
|
|
101
|
-
if (!socketMap[webviewId]) {
|
|
102
|
-
socketMap[webviewId] = { socket: ws, queue: [] };
|
|
103
|
-
} else {
|
|
104
|
-
socketMap[webviewId].socket = ws;
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
close(ws: ServerWebSocket<{ webviewId: number }>, _code: number, _reason: string) {
|
|
108
|
-
if (!ws?.data) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
const { webviewId } = ws.data;
|
|
112
|
-
// console.log("Closed:", webviewId, code, reason);
|
|
113
|
-
if (socketMap[webviewId]) {
|
|
114
|
-
socketMap[webviewId].socket = null;
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
message(ws: ServerWebSocket<{ webviewId: number }>, message: string | Buffer) {
|
|
119
|
-
if (!ws?.data) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const { webviewId } = ws.data;
|
|
123
|
-
const browserView = BrowserView.getById(webviewId);
|
|
124
|
-
if (!browserView) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (browserView.rpcHandler) {
|
|
129
|
-
if (typeof message === "string") {
|
|
130
|
-
try {
|
|
131
|
-
const encryptedPacket = JSON.parse(message);
|
|
132
|
-
const decrypted = decrypt(
|
|
133
|
-
browserView.secretKey,
|
|
134
|
-
base64ToUint8Array(encryptedPacket.encryptedData),
|
|
135
|
-
base64ToUint8Array(encryptedPacket.iv),
|
|
136
|
-
base64ToUint8Array(encryptedPacket.tag),
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
// Note: At this point the secretKey for the webview id would
|
|
140
|
-
// have had to match the encrypted packet data, so we can trust
|
|
141
|
-
// that this message can be passed to this browserview's rpc
|
|
142
|
-
// methods.
|
|
143
|
-
browserView.rpcHandler(JSON.parse(decrypted));
|
|
144
|
-
} catch (error) {
|
|
145
|
-
console.log("Error handling message:", error);
|
|
146
|
-
}
|
|
147
|
-
} else if (message instanceof ArrayBuffer) {
|
|
148
|
-
console.log("TODO: Received ArrayBuffer message:", message);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
break;
|
|
156
|
-
} catch (error: any) {
|
|
157
|
-
if (error.code === "EADDRINUSE") {
|
|
158
|
-
console.log(`Port ${port} in use, trying next port...`);
|
|
159
|
-
port++;
|
|
160
|
-
} else {
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return { rpcServer: server, rpcPort: port };
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
export const { rpcServer, rpcPort } = startRPCServer();
|
|
170
|
-
|
|
171
|
-
// Will return true if message was sent over websocket
|
|
172
|
-
// false if it was not (caller should fallback to postMessage/evaluateJS rpc)
|
|
7
|
+
// Will return true if message was sent over the core-owned websocket transport.
|
|
8
|
+
// False means the caller should fall back to the native bridge / evaluateJS path.
|
|
173
9
|
export const sendMessageToWebviewViaSocket = (
|
|
174
10
|
webviewId: number,
|
|
175
|
-
message:
|
|
11
|
+
message: unknown,
|
|
176
12
|
): boolean => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const encrypted = encrypt(browserView.secretKey, unencryptedString);
|
|
186
|
-
|
|
187
|
-
const encryptedPacket = {
|
|
188
|
-
encryptedData: encrypted.encrypted,
|
|
189
|
-
iv: encrypted.iv,
|
|
190
|
-
tag: encrypted.tag,
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const encryptedPacketString = JSON.stringify(encryptedPacket);
|
|
194
|
-
|
|
195
|
-
rpc.socket.send(encryptedPacketString);
|
|
196
|
-
return true;
|
|
197
|
-
} catch (error) {
|
|
198
|
-
console.error("Error sending message to webview via socket:", error);
|
|
199
|
-
}
|
|
13
|
+
try {
|
|
14
|
+
return ffi.request.sendHostMessageToWebviewViaTransport({
|
|
15
|
+
id: webviewId,
|
|
16
|
+
messageJson: JSON.stringify(message),
|
|
17
|
+
}) as boolean;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error("Error sending message to webview via host transport:", error);
|
|
20
|
+
return false;
|
|
200
21
|
}
|
|
201
|
-
|
|
202
|
-
return false;
|
|
203
22
|
};
|
|
204
|
-
|
|
205
|
-
console.log("Server started at", rpcServer?.url.origin);
|
|
@@ -137,9 +137,7 @@ export const quit = () => {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
if (native) {
|
|
140
|
-
|
|
141
|
-
native.symbols.waitForShutdownComplete(5000);
|
|
142
|
-
native.symbols.forceExit(0);
|
|
140
|
+
ffi.request.quitGracefully({ code: 0, timeoutMs: 5000 });
|
|
143
141
|
} else {
|
|
144
142
|
process.exit(0);
|
|
145
143
|
}
|
|
@@ -150,7 +148,7 @@ const _originalProcessExit = process.exit;
|
|
|
150
148
|
process.exit = ((code?: number) => {
|
|
151
149
|
if (native) {
|
|
152
150
|
if (isQuitting) {
|
|
153
|
-
|
|
151
|
+
ffi.request.quitGracefully({ code: code ?? 0, timeoutMs: 0 });
|
|
154
152
|
return;
|
|
155
153
|
}
|
|
156
154
|
quit();
|