electrobun 1.18.4-beta.3 → 1.18.4-beta.5

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 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 RPC_SOCKET_PORT = window.__electrobunRpcSocketPort;
18
+ const HOST_SOCKET_PORT =
19
+ window.__electrobunHostSocketPort ?? window.__electrobunRpcSocketPort;
19
20
 
20
21
  class Electroview<T extends RPCWithTransport> {
21
- bunSocket?: WebSocket;
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.initSocketToBun();
42
+ this.initSocketToHost();
33
43
 
34
- // Set up handler for user RPC messages from bun
35
- // Note: receiveInternalMessageFromBun is set up by the preload script
36
- window.__electrobun!.receiveMessageFromBun =
37
- this.receiveMessageFromBun.bind(this);
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
- initSocketToBun() {
62
+ initSocketToHost() {
45
63
  // Skip native socket when running in a remote browser (no port/webview ID)
46
- if (!RPC_SOCKET_PORT || !WEBVIEW_ID) {
64
+ if (!HOST_SOCKET_PORT || !WEBVIEW_ID) {
47
65
  return;
48
66
  }
49
67
 
50
- // Note: Using ws:// for localhost is intentional - all RPC messages are
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://localhost:${RPC_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`,
71
+ `ws://127.0.0.1:${HOST_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`,
54
72
  );
55
73
 
56
- this.bunSocket = socket;
74
+ this.hostSocket = socket;
57
75
 
58
76
  socket.addEventListener("open", () => {
59
- // this.bunSocket?.send("Hello from webview " + WEBVIEW_ID);
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.bunBridge(messageString);
121
+ that.sendMessageToHost(messageString);
101
122
  } catch (error) {
102
- console.error("bun: failed to serialize message to webview", 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 bunBridge(msg: string) {
112
- if (this.bunSocket?.readyState === WebSocket.OPEN) {
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.bunSocket.send(encryptedPacketString);
147
+ this.hostSocket.send(encryptedPacketString);
124
148
  return;
125
149
  } catch (error) {
126
- console.error("Error sending message to bun via socket:", error);
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.__electrobunBunBridge?.postMessage(msg);
155
+ window.__electrobunHostBridge?.postMessage(msg);
132
156
  }
133
157
 
134
- receiveMessageFromBun(msg: unknown) {
135
- // NOTE: in the webview messages are passed by executing ElectrobunView.receiveMessageFromBun(object)
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, { entrypoint: string; [key: string]: unknown }>;
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
- sendMessageToWebviewViaExecute(jsonMessage: unknown) {
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.receiveMessageFromBun(${stringifiedMessage})`;
186
+ const wrappedMessage = `window.__electrobun.receiveMessageFromHost(${stringifiedMessage})`;
187
187
  this.executeJavascript(wrappedMessage);
188
188
  }
189
189
 
190
- sendInternalMessageViaExecute(jsonMessage: unknown) {
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.receiveInternalMessageFromBun(${stringifiedMessage})`;
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.sendMessageToWebviewViaExecute(messageString);
315
+ that.sendHostMessageToWebviewViaExecute(messageString);
316
316
  } catch (error) {
317
- console.error("bun: failed to serialize message to webview", 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 (!this.ptr || this.isRemoved) {
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
- ffi.request.webviewRemove({ id: this.id });
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 the window map when a window closes and optionally quit the app
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
- // If ptr is null, the view was already cleaned up by the renderer or native cleanup
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 the window map when a window closes and optionally quit the app
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 type { Server, ServerWebSocket } from "bun";
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
- const rpc = socketMap[webviewId];
52
- if (!rpc) return;
53
-
54
- rpc.socket = null;
55
- delete socketMap[webviewId];
4
+ ffi.request.clearWebviewHostTransport({ id: webviewId });
56
5
  };
57
6
 
58
- const startRPCServer = () => {
59
- const startPort = 50000;
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: any,
11
+ message: unknown,
176
12
  ): boolean => {
177
- const rpc = socketMap[webviewId];
178
- const browserView = BrowserView.getById(webviewId);
179
-
180
- if (!browserView) return false;
181
-
182
- if (rpc?.socket?.readyState === WebSocket.OPEN) {
183
- try {
184
- const unencryptedString = JSON.stringify(message);
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
- native.symbols.stopEventLoop();
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
- native.symbols.forceExit(code ?? 0);
151
+ ffi.request.quitGracefully({ code: code ?? 0, timeoutMs: 0 });
154
152
  return;
155
153
  }
156
154
  quit();