bunite-core 0.0.13 → 0.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bunite-core",
3
3
  "description": "Uniting UI and Bun",
4
- "version": "0.0.13",
4
+ "version": "0.1.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "setup:cef": "bun ../tools/bunite-dev/scripts/setup-cef.ts",
@@ -31,6 +31,10 @@ type AppOptions = NativeBootstrapOptions & {
31
31
 
32
32
  export type GlobalIPCHandler = (params: unknown, ctx: { viewId: number }) => unknown | Promise<unknown>;
33
33
 
34
+ function normalizeAppResPath(path: string): string {
35
+ return path.replace(/^\/+/, "").replace(/\/+$/, "");
36
+ }
37
+
34
38
  export class AppRuntime {
35
39
  private stubKeepAliveTimer: ReturnType<typeof setInterval> | null = null;
36
40
  private readonly globalIPCHandlers = new Map<string, GlobalIPCHandler>();
@@ -209,13 +213,15 @@ export class AppRuntime {
209
213
  private readonly appresHandlers = new Map<string, () => string>();
210
214
 
211
215
  getAppRes(path: string, handler: () => string) {
212
- this.appresHandlers.set(path, handler);
213
- getNativeLibrary()?.symbols.bunite_register_appres_route(toCString(path));
216
+ const normalized = normalizeAppResPath(path);
217
+ this.appresHandlers.set(normalized, handler);
218
+ getNativeLibrary()?.symbols.bunite_register_appres_route(toCString(normalized));
214
219
  }
215
220
 
216
221
  removeAppRes(path: string) {
217
- this.appresHandlers.delete(path);
218
- getNativeLibrary()?.symbols.bunite_unregister_appres_route(toCString(path));
222
+ const normalized = normalizeAppResPath(path);
223
+ this.appresHandlers.delete(normalized);
224
+ getNativeLibrary()?.symbols.bunite_unregister_appres_route(toCString(normalized));
219
225
  }
220
226
 
221
227
  /** @internal */
@@ -2,7 +2,7 @@ import { ptr } from "bun:ffi";
2
2
  import { buildViewPreloadScript } from "../preload/inline";
3
3
  import { log } from "../../shared/log";
4
4
  import { buniteEventEmitter } from "../events/eventEmitter";
5
- import { defineBuniteRPC, type BuniteRPCConfig, type BuniteRPCSchema, type RPCWithTransport } from "../../shared/rpc";
5
+ import { defineBuniteRPC, type BuniteRPCConfig, type BuniteRPCSchema, type RPCPacket, type RPCTransport, type RPCWithTransport } from "../../shared/rpc";
6
6
  import { createWebRPCHandler } from "../../shared/webRpcHandler";
7
7
  import { ensureNativeRuntime, getNativeLibrary, toCString, waitForViewReady, cancelWaitForViewReady } from "../proc/native";
8
8
  import { attachBrowserViewRegistry, getRPCPort, sendMessageToView } from "./Socket";
@@ -14,6 +14,16 @@ import { cancelPendingMessageBoxesForView } from "./Utils";
14
14
  const BrowserViewMap: Record<number, BrowserView<any>> = {};
15
15
  let nextWebviewId = 1;
16
16
 
17
+ function createNativeViewPipe(viewId: number) {
18
+ let handler: ((packet: RPCPacket) => void) | undefined;
19
+ const transport: RPCTransport = {
20
+ send: (packet) => { sendMessageToView(viewId, packet); },
21
+ registerHandler: (h) => { handler = h; },
22
+ unregisterHandler: () => { handler = undefined; }
23
+ };
24
+ return { transport, receive: (packet: RPCPacket) => handler?.(packet) };
25
+ }
26
+
17
27
  export type BrowserViewOptions<T = undefined> = {
18
28
  url: string | null;
19
29
  html: string | null;
@@ -66,7 +76,8 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
66
76
  partition: string | null;
67
77
  frame: BrowserViewOptions["frame"];
68
78
  rpc?: T;
69
- rpcHandler?: (message: unknown) => void;
79
+ readonly transport: RPCTransport;
80
+ private pipe: ReturnType<typeof createNativeViewPipe>;
70
81
  autoResize: boolean;
71
82
  navigationRules: string[] | null;
72
83
  sandbox: boolean;
@@ -75,6 +86,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
75
86
  constructor(options: Partial<BrowserViewOptions<T>>) {
76
87
  ensureNativeRuntime();
77
88
 
89
+ this.pipe = createNativeViewPipe(this.id);
90
+ this.transport = this.pipe.transport;
91
+
78
92
  this.windowId = options.windowId ?? defaultOptions.windowId;
79
93
  this.url = options.url ?? defaultOptions.url;
80
94
  this.html = options.html ?? defaultOptions.html;
@@ -105,7 +119,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
105
119
  });
106
120
 
107
121
  BrowserViewMap[this.id] = this;
108
- this.rpc?.setTransport(this.createTransport());
122
+ this.rpc?.setTransport(this.transport);
109
123
  // Register ready waiter BEFORE native create — OnAfterCreated can fire
110
124
  // on the CEF UI thread before bunite_view_create returns to JS.
111
125
  this._readyPromise = waitForViewReady(this.id);
@@ -173,22 +187,8 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
173
187
  });
174
188
  }
175
189
 
176
- handleIncomingRPC(message: unknown) {
177
- this.rpcHandler?.(message);
178
- }
179
-
180
- createTransport() {
181
- return {
182
- send: (message: any) => {
183
- sendMessageToView(this.id, message);
184
- },
185
- registerHandler: (handler: (message: any) => void) => {
186
- this.rpcHandler = handler;
187
- },
188
- unregisterHandler: () => {
189
- this.rpcHandler = undefined;
190
- }
191
- };
190
+ handleIncomingRPC(packet: RPCPacket) {
191
+ this.pipe.receive(packet);
192
192
  }
193
193
 
194
194
  get rpcPort() {
@@ -202,6 +202,12 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
202
202
  this.webviewId = webview.id;
203
203
  }
204
204
 
205
+ get view(): BrowserView<T> {
206
+ const view = BrowserView.getById(this.webviewId);
207
+ if (!view) throw new Error(`BrowserWindow ${this.id} has no attached view`);
208
+ return view as BrowserView<T>;
209
+ }
210
+
205
211
  static getById(id: number) {
206
212
  return BrowserWindowMap[id];
207
213
  }
package/src/bun/index.ts CHANGED
@@ -13,6 +13,8 @@ import {
13
13
  type RPCSchema,
14
14
  type RPCWithTransport
15
15
  } from "../shared/rpc";
16
+ import { createTransportDemuxer, type TransportDemuxer } from "../shared/rpcDemux";
17
+ import { createWebSocketTransport, type WebSocketLike, type WebSocketTransportPipe } from "../shared/webSocketTransport";
16
18
  import type { MessageBoxOptions, MessageBoxResponse } from "./core/Utils";
17
19
  import { log, type LogLevel } from "../shared/log";
18
20
 
@@ -24,6 +26,8 @@ export {
24
26
  buniteEventEmitter,
25
27
  completePermissionRequest,
26
28
  createRPC,
29
+ createTransportDemuxer,
30
+ createWebSocketTransport,
27
31
  defineBuniteRPC,
28
32
  log
29
33
  };
@@ -38,5 +42,8 @@ export type {
38
42
  MessageBoxResponse,
39
43
  RPCSchema,
40
44
  RPCWithTransport,
45
+ TransportDemuxer,
46
+ WebSocketLike,
47
+ WebSocketTransportPipe,
41
48
  WindowOptionsType
42
49
  };
package/src/shared/rpc.ts CHANGED
@@ -343,6 +343,16 @@ export function createRPC<
343
343
  ) => void;
344
344
  };
345
345
 
346
+ function dispose() {
347
+ for (const [id, pending] of pendingRequests) {
348
+ clearTimeout(pending.timeout);
349
+ pending.reject(new Error("RPC disposed"));
350
+ }
351
+ pendingRequests.clear();
352
+ transport.unregisterHandler?.();
353
+ transport = {};
354
+ }
355
+
346
356
  return {
347
357
  setTransport,
348
358
  setRequestHandler,
@@ -352,6 +362,7 @@ export function createRPC<
352
362
  sendProxy,
353
363
  addMessageListener,
354
364
  removeMessageListener,
365
+ dispose,
355
366
  proxy: {
356
367
  request: requestProxy,
357
368
  send: sendProxy
@@ -0,0 +1,65 @@
1
+ import type { RPCPacket, RPCTransport } from "./rpc";
2
+
3
+ type DemuxEnvelope = { channel: string; packet: RPCPacket };
4
+
5
+ function isDemuxEnvelope(value: unknown): value is DemuxEnvelope {
6
+ if (typeof value !== "object" || value === null) return false;
7
+ const v = value as Partial<DemuxEnvelope>;
8
+ return typeof v.channel === "string" && typeof v.packet === "object" && v.packet !== null;
9
+ }
10
+
11
+ export type TransportDemuxer = {
12
+ channel(name: string): RPCTransport;
13
+ dispose(): void;
14
+ };
15
+
16
+ export function createTransportDemuxer(base: RPCTransport): TransportDemuxer {
17
+ if (!base.send || !base.registerHandler) {
18
+ throw new Error("createTransportDemuxer requires a base transport with both send and registerHandler");
19
+ }
20
+
21
+ const handlers = new Map<string, (packet: RPCPacket) => void>();
22
+ let disposed = false;
23
+
24
+ base.registerHandler((data) => {
25
+ // Envelopes missing or malformed are dropped. A future fallthrough hook
26
+ // would land here if we ever multiplex legacy RPC on the same transport.
27
+ if (!isDemuxEnvelope(data)) return;
28
+ handlers.get(data.channel)?.(data.packet);
29
+ });
30
+
31
+ return {
32
+ channel(name) {
33
+ let ownHandler: ((packet: RPCPacket) => void) | undefined;
34
+
35
+ return {
36
+ send(packet) {
37
+ if (disposed) throw new Error(`Demuxer disposed; cannot send on channel "${name}"`);
38
+ const envelope: DemuxEnvelope = { channel: name, packet };
39
+ // The wire layer (msgpackr) serializes the envelope opaquely, so
40
+ // routing the wider type through RPCTransport.send is safe in practice.
41
+ base.send!(envelope as unknown as RPCPacket);
42
+ },
43
+ registerHandler(handler) {
44
+ if (disposed) throw new Error(`Demuxer disposed; cannot register on channel "${name}"`);
45
+ if (handlers.has(name)) {
46
+ throw new Error(`Channel "${name}" already has a handler on this demuxer`);
47
+ }
48
+ ownHandler = handler;
49
+ handlers.set(name, handler);
50
+ },
51
+ unregisterHandler() {
52
+ if (ownHandler && handlers.get(name) === ownHandler) {
53
+ handlers.delete(name);
54
+ ownHandler = undefined;
55
+ }
56
+ }
57
+ };
58
+ },
59
+ dispose() {
60
+ disposed = true;
61
+ handlers.clear();
62
+ base.unregisterHandler?.();
63
+ }
64
+ };
65
+ }
@@ -1,18 +1,14 @@
1
1
  import {
2
2
  defineBuniteRPC,
3
3
  type BuniteRPCConfig,
4
- type BuniteRPCSchema,
5
- type RPCTransport,
6
- type RPCPacket
4
+ type BuniteRPCSchema
7
5
  } from "./rpc";
8
- import { decodeRPCPacket, encodeRPCPacket, asUint8Array } from "./rpcWire";
9
-
10
- type WebRPCSocket = { send(data: Uint8Array | ArrayBuffer): void | number };
6
+ import { createWebSocketTransport, type WebSocketLike } from "./webSocketTransport";
7
+ import { log } from "./log";
11
8
 
12
9
  export type WebRPCClient<Schema extends BuniteRPCSchema = BuniteRPCSchema> = {
13
- ws: WebRPCSocket;
10
+ ws: WebSocketLike;
14
11
  rpc: ReturnType<typeof defineBuniteRPC<Schema, "bun">>;
15
- handlePacket: (packet: RPCPacket) => void | Promise<void>;
16
12
  };
17
13
 
18
14
  export function createWebRPCHandler<Schema extends BuniteRPCSchema>(
@@ -20,67 +16,58 @@ export function createWebRPCHandler<Schema extends BuniteRPCSchema>(
20
16
  extraRequestHandlers?: Record<string, (...args: any[]) => unknown>;
21
17
  }
22
18
  ) {
23
- const connections = new WeakMap<WebRPCSocket, WebRPCClient<Schema>>();
19
+ type Entry = { client: WebRPCClient<Schema>; receive: (raw: ArrayBuffer | Uint8Array) => void };
20
+
21
+ const connections = new WeakMap<WebSocketLike, Entry>();
24
22
  const webClients = new Set<WebRPCClient<Schema>>();
25
23
 
26
24
  const handler = {
27
- open(ws: WebRPCSocket) {
28
- let handlePacket: ((packet: RPCPacket) => void | Promise<void>) | undefined;
29
-
30
- const transport: RPCTransport = {
31
- send(packet) {
32
- ws.send(encodeRPCPacket(packet));
33
- },
34
- registerHandler(h) {
35
- handlePacket = h;
36
- },
37
- unregisterHandler() {
38
- handlePacket = undefined;
39
- }
40
- };
41
-
25
+ open(ws: WebSocketLike) {
26
+ const pipe = createWebSocketTransport(ws);
42
27
  const rpc = defineBuniteRPC("bun", config);
43
- rpc.setTransport(transport);
28
+ rpc.setTransport(pipe.transport);
44
29
 
45
- const client: WebRPCClient<Schema> = {
46
- ws,
47
- rpc: rpc as WebRPCClient<Schema>["rpc"],
48
- handlePacket: (packet) => handlePacket?.(packet)
49
- };
30
+ const client: WebRPCClient<Schema> = { ws, rpc: rpc as WebRPCClient<Schema>["rpc"] };
50
31
 
51
- connections.set(ws, client);
32
+ connections.set(ws, { client, receive: pipe.receive });
52
33
  webClients.add(client);
53
34
  handler.onWebClientConnected?.(client);
54
35
  },
55
36
 
56
- message(ws: WebRPCSocket, raw: string | Buffer | ArrayBuffer | Uint8Array) {
37
+ message(ws: WebSocketLike, raw: string | Buffer | ArrayBuffer | Uint8Array) {
57
38
  if (typeof raw === "string") return;
58
-
59
- const client = connections.get(ws);
60
- if (!client) return;
39
+ const entry = connections.get(ws);
40
+ if (!entry) return;
61
41
 
62
42
  try {
63
- client.handlePacket(decodeRPCPacket(asUint8Array(raw)));
64
- } catch {
65
- // malformed packet
43
+ entry.receive(raw);
44
+ } catch (error) {
45
+ log.error("Web RPC packet handler error", error);
66
46
  }
67
47
  },
68
48
 
69
- close(ws: WebRPCSocket) {
70
- const client = connections.get(ws);
71
- if (!client) return;
49
+ close(ws: WebSocketLike) {
50
+ const entry = connections.get(ws);
51
+ if (!entry) return;
72
52
 
73
- webClients.delete(client);
74
- handler.onWebClientDisconnected?.(client);
75
- client.rpc.setTransport({});
53
+ entry.client.rpc.dispose();
54
+ webClients.delete(entry.client);
76
55
  connections.delete(ws);
56
+ handler.onWebClientDisconnected?.(entry.client);
77
57
  },
78
58
 
79
59
  webClients: webClients as ReadonlySet<WebRPCClient<Schema>>,
80
60
 
81
- broadcast(messageName: string, payload?: unknown) {
61
+ broadcast<M extends keyof Schema["bun"]["messages"]>(
62
+ messageName: M,
63
+ ...args: void extends Schema["bun"]["messages"][M]
64
+ ? []
65
+ : undefined extends Schema["bun"]["messages"][M]
66
+ ? [payload?: Schema["bun"]["messages"][M]]
67
+ : [payload: Schema["bun"]["messages"][M]]
68
+ ) {
82
69
  for (const client of webClients) {
83
- (client.rpc.send as any)(messageName, payload);
70
+ (client.rpc.send as any)(messageName, ...args);
84
71
  }
85
72
  },
86
73
 
@@ -0,0 +1,26 @@
1
+ import type { RPCPacket, RPCTransport } from "./rpc";
2
+ import { asUint8Array, decodeRPCPacket, encodeRPCPacket } from "./rpcWire";
3
+
4
+ export type WebSocketLike = {
5
+ send(data: Uint8Array | ArrayBuffer): void | number;
6
+ };
7
+
8
+ export type WebSocketTransportPipe = {
9
+ transport: RPCTransport;
10
+ receive(raw: ArrayBuffer | ArrayBufferView | Uint8Array): void;
11
+ };
12
+
13
+ export function createWebSocketTransport(ws: WebSocketLike): WebSocketTransportPipe {
14
+ let handler: ((packet: RPCPacket) => void) | undefined;
15
+
16
+ return {
17
+ transport: {
18
+ send(packet) { ws.send(encodeRPCPacket(packet)); },
19
+ registerHandler(h) { handler = h; },
20
+ unregisterHandler() { handler = undefined; }
21
+ },
22
+ receive(raw) {
23
+ handler?.(decodeRPCPacket(asUint8Array(raw)));
24
+ }
25
+ };
26
+ }
package/src/view/index.ts CHANGED
@@ -8,6 +8,8 @@ import {
8
8
  type RPCTransport,
9
9
  type RPCWithTransport
10
10
  } from "../shared/rpc";
11
+ import { createTransportDemuxer, type TransportDemuxer } from "../shared/rpcDemux";
12
+ import { createWebSocketTransport, type WebSocketLike, type WebSocketTransportPipe } from "../shared/webSocketTransport";
11
13
  import { decodeRPCPacket, encodeRPCPacket } from "../shared/rpcWire";
12
14
  import { log } from "../shared/log";
13
15
 
@@ -34,24 +36,37 @@ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
34
36
  return copy.buffer;
35
37
  }
36
38
 
37
- export class BuniteView<T extends RPCWithTransport> {
39
+ export class BuniteView<T extends RPCWithTransport = RPCWithTransport> {
38
40
  bunSocket?: WebSocket;
39
41
  rpc?: T;
40
- rpcHandler?: (message: unknown) => void;
42
+ readonly transport: RPCTransport;
43
+
44
+ private handler?: (packet: RPCPacket) => void;
41
45
  private pendingPackets: RPCPacket[] = [];
42
46
 
43
- constructor(config: { rpc: T }) {
44
- this.rpc = config.rpc;
45
- this.init();
46
- }
47
+ constructor(config?: { rpc?: T }) {
48
+ this.rpc = config?.rpc;
49
+
50
+ this.transport = {
51
+ send: (packet) => {
52
+ if (this.bunSocket?.readyState === WebSocket.OPEN) {
53
+ this.sendPacket(packet);
54
+ } else if (this.bunSocket?.readyState === WebSocket.CONNECTING) {
55
+ this.pendingPackets.push(packet);
56
+ }
57
+ },
58
+ registerHandler: (h) => { this.handler = h; },
59
+ unregisterHandler: () => { this.handler = undefined; }
60
+ };
47
61
 
48
- init() {
49
62
  this.initSocketToBun();
50
63
  if (isNative) {
51
64
  buniteWindow.__bunite ??= {};
52
- buniteWindow.__bunite.receiveMessageFromBun = this.receiveMessageFromBun.bind(this);
65
+ buniteWindow.__bunite.receiveMessageFromBun = (message) => {
66
+ this.handler?.(message as RPCPacket);
67
+ };
53
68
  }
54
- this.rpc?.setTransport(this.createTransport());
69
+ this.rpc?.setTransport(this.transport);
55
70
  }
56
71
 
57
72
  private sendPacket(packet: RPCPacket) {
@@ -72,10 +87,10 @@ export class BuniteView<T extends RPCWithTransport> {
72
87
  this.bunSocket = socket;
73
88
 
74
89
  socket.addEventListener("message", async (event) => {
75
- const bytes = await this.messageToUint8Array(event.data);
90
+ const bytes = await messageToUint8Array(event.data);
76
91
  if (!bytes) return;
77
92
  try {
78
- this.rpcHandler?.(decodeRPCPacket(bytes));
93
+ this.handler?.(decodeRPCPacket(bytes));
79
94
  } catch (error) {
80
95
  log.error("Failed to parse WebSocket message", error);
81
96
  }
@@ -97,7 +112,7 @@ export class BuniteView<T extends RPCWithTransport> {
97
112
  }
98
113
 
99
114
  this.bunSocket!.addEventListener("message", async (event) => {
100
- const binaryMessage = await this.messageToUint8Array(event.data);
115
+ const binaryMessage = await messageToUint8Array(event.data);
101
116
  if (!binaryMessage) return;
102
117
 
103
118
  try {
@@ -109,7 +124,7 @@ export class BuniteView<T extends RPCWithTransport> {
109
124
  const decrypted = await decrypt(binaryMessage);
110
125
  const packet = decodeRPCPacket(decrypted);
111
126
  if ((packet as any).scope === "global") return;
112
- this.rpcHandler?.(packet);
127
+ this.handler?.(packet);
113
128
  } catch (error) {
114
129
  log.error("Failed to parse message from Bun", error);
115
130
  }
@@ -133,35 +148,6 @@ export class BuniteView<T extends RPCWithTransport> {
133
148
  });
134
149
  }
135
150
 
136
- async messageToUint8Array(data: unknown) {
137
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
138
- if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
139
- if (data instanceof Uint8Array) return data;
140
- return null;
141
- }
142
-
143
- createTransport(): RPCTransport {
144
- return {
145
- send: (message) => {
146
- if (this.bunSocket?.readyState === WebSocket.OPEN) {
147
- this.sendPacket(message);
148
- } else if (this.bunSocket?.readyState === WebSocket.CONNECTING) {
149
- this.pendingPackets.push(message);
150
- }
151
- },
152
- registerHandler: (handler: (packet: any) => void) => {
153
- this.rpcHandler = handler;
154
- },
155
- unregisterHandler: () => {
156
- this.rpcHandler = undefined;
157
- }
158
- };
159
- }
160
-
161
- receiveMessageFromBun(message: unknown) {
162
- this.rpcHandler?.(message);
163
- }
164
-
165
151
  async bunBridge(message: RPCPacket) {
166
152
  if (this.bunSocket?.readyState !== WebSocket.OPEN) return;
167
153
 
@@ -184,10 +170,21 @@ export class BuniteView<T extends RPCWithTransport> {
184
170
  }
185
171
  }
186
172
 
173
+ async function messageToUint8Array(data: unknown) {
174
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
175
+ if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
176
+ if (data instanceof Uint8Array) return data;
177
+ return null;
178
+ }
179
+
187
180
  export { log, type LogLevel } from "../shared/log";
181
+ export { createTransportDemuxer, createWebSocketTransport, defineBuniteRPC };
188
182
 
189
183
  export type {
190
184
  BuniteRPCConfig,
191
185
  BuniteRPCSchema,
192
- RPCSchema
186
+ RPCSchema,
187
+ TransportDemuxer,
188
+ WebSocketLike,
189
+ WebSocketTransportPipe
193
190
  };