bunite-core 0.8.1 → 0.10.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.
Files changed (56) hide show
  1. package/package.json +7 -6
  2. package/src/{bun → host}/core/App.ts +45 -81
  3. package/src/{bun → host}/core/BrowserView.ts +71 -65
  4. package/src/{bun → host}/core/BrowserWindow.ts +15 -14
  5. package/src/host/core/Socket.ts +98 -0
  6. package/src/host/core/SurfaceBrowserIPC.ts +7 -0
  7. package/src/host/core/SurfaceManager.ts +154 -0
  8. package/src/host/encryptedPipe.ts +62 -0
  9. package/src/{bun → host}/events/appEvents.ts +0 -1
  10. package/src/host/index.ts +29 -0
  11. package/src/{bun/proc → host}/native.ts +38 -52
  12. package/src/{shared → host}/paths.ts +20 -26
  13. package/src/{bun/preload/inline.ts → host/preloadBundle.ts} +2 -2
  14. package/src/host/serveWeb.ts +105 -0
  15. package/src/native/linux/bunite_linux_runtime.cpp +2 -2
  16. package/src/native/mac/bunite_mac_ffi.mm +2 -2
  17. package/src/native/shared/ffi_exports.h +1 -1
  18. package/src/native/win/native_host_ffi.cpp +2 -2
  19. package/src/preload/runtime.built.js +1 -1
  20. package/src/preload/runtime.ts +52 -219
  21. package/src/preload/tsconfig.json +3 -10
  22. package/src/rpc/encrypt.ts +74 -0
  23. package/src/rpc/error.ts +68 -0
  24. package/src/rpc/framework.ts +132 -0
  25. package/src/rpc/index.ts +138 -0
  26. package/src/rpc/peer.ts +1438 -0
  27. package/src/rpc/renderer.ts +80 -0
  28. package/src/rpc/schema.ts +229 -0
  29. package/src/rpc/stream.ts +72 -0
  30. package/src/rpc/transport.ts +81 -0
  31. package/src/rpc/wire.ts +164 -0
  32. package/src/{preload/webviewElement.ts → webview/native.ts} +68 -48
  33. package/src/{shared/webviewPolyfill.ts → webview/polyfill.ts} +4 -7
  34. package/src/bun/core/Socket.ts +0 -187
  35. package/src/bun/core/SurfaceBrowserIPC.ts +0 -65
  36. package/src/bun/core/SurfaceManager.ts +0 -201
  37. package/src/bun/index.ts +0 -53
  38. package/src/bun/preload/index.ts +0 -73
  39. package/src/preload/tsconfig.tsbuildinfo +0 -1
  40. package/src/shared/rpc.ts +0 -424
  41. package/src/shared/rpcDemux.ts +0 -219
  42. package/src/shared/rpcWire.ts +0 -54
  43. package/src/shared/rpcWireConstants.ts +0 -3
  44. package/src/shared/webRpcHandler.ts +0 -77
  45. package/src/shared/webSocketTransport.ts +0 -26
  46. package/src/view/index.ts +0 -196
  47. /package/src/{shared → host}/cefVersion.ts +0 -0
  48. /package/src/{bun → host}/core/SurfaceRegistry.ts +0 -0
  49. /package/src/{bun → host}/core/singleInstanceLock.ts +0 -0
  50. /package/src/{bun → host}/core/windowIds.ts +0 -0
  51. /package/src/{bun → host}/events/event.ts +0 -0
  52. /package/src/{bun → host}/events/eventEmitter.ts +0 -0
  53. /package/src/{bun → host}/events/webviewEvents.ts +0 -0
  54. /package/src/{bun → host}/events/windowEvents.ts +0 -0
  55. /package/src/{shared → host}/log.ts +0 -0
  56. /package/src/{shared → host}/platform.ts +0 -0
package/package.json CHANGED
@@ -1,19 +1,20 @@
1
1
  {
2
2
  "name": "bunite-core",
3
3
  "description": "Uniting UI and Bun",
4
- "version": "0.8.1",
4
+ "version": "0.10.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "setup:cef": "bun ../tools/bunite-dev/scripts/setup-cef.ts",
8
8
  "build:native:win": "cmake -S . -B build/win -DBUNITE_TARGET_ARCH=x64 && cmake --build build/win --config Release",
9
9
  "build:native:linux": "cmake -S . -B build/linux -DCMAKE_BUILD_TYPE=Release && cmake --build build/linux",
10
- "build:preload": "bun build src/preload/runtime.ts --outfile src/preload/runtime.built.js --target browser --minify"
10
+ "build:preload": "bun build src/preload/runtime.ts --outfile src/preload/runtime.built.js --target browser --minify",
11
+ "prepublishOnly": "bun run build:preload && rm -f src/preload/tsconfig.tsbuildinfo"
11
12
  },
12
13
  "exports": {
13
- ".": "./src/bun/index.ts",
14
- "./bun": "./src/bun/index.ts",
15
- "./view": "./src/view/index.ts",
16
- "./shared/rpc": "./src/shared/rpc.ts",
14
+ ".": "./src/host/index.ts",
15
+ "./rpc": "./src/rpc/index.ts",
16
+ "./rpc/renderer": "./src/rpc/renderer.ts",
17
+ "./polyfill": "./src/webview/polyfill.ts",
17
18
  "./package.json": "./package.json"
18
19
  },
19
20
  "files": [
@@ -1,7 +1,6 @@
1
1
  import { isAbsolute, join, resolve } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
- import { getBaseDir } from "../../shared/paths";
4
- import { BuniteEvent } from "../events/event";
3
+ import { getBaseDir } from "../paths";
5
4
  import { buniteEventEmitter } from "../events/eventEmitter";
6
5
  import {
7
6
  getNativeEngineName,
@@ -13,14 +12,15 @@ import {
13
12
  setNativeLogLevel,
14
13
  toCString,
15
14
  type NativeBootstrapOptions
16
- } from "../proc/native";
17
- import { attachGlobalIPCResolver, ensureRpcServer } from "./Socket";
15
+ } from "../native";
16
+ import { ensureRpcServer } from "./Socket";
18
17
  import { BrowserWindow } from "./BrowserWindow";
19
- import { getSurfaceIPCHandlers } from "./SurfaceManager";
20
- import { getWebviewIPCHandlers } from "./SurfaceBrowserIPC";
21
- import { log, logLevelToInt } from "../../shared/log";
18
+ import { createSurfaceCapImpl } from "./SurfaceManager";
19
+ import "./SurfaceBrowserIPC";
20
+ import { log, logLevelToInt } from "../log";
21
+ import { RuntimeCap, SurfaceCap, IpcError, type ImplOf } from "../../rpc/index";
22
22
 
23
- import type { LogLevel } from "../../shared/log";
23
+ import type { LogLevel } from "../log";
24
24
 
25
25
  type AppOptions = NativeBootstrapOptions & {
26
26
  userDataDir?: string;
@@ -28,15 +28,17 @@ type AppOptions = NativeBootstrapOptions & {
28
28
  logLevel?: LogLevel;
29
29
  };
30
30
 
31
- export type GlobalIPCHandler = (params: unknown, ctx: { viewId: number }) => unknown | Promise<unknown>;
31
+ let _instance: AppRuntime | null = null;
32
+ export function getAppRuntimeOrThrow(): AppRuntime {
33
+ if (!_instance) throw new Error("AppRuntime not yet instantiated");
34
+ return _instance;
35
+ }
32
36
 
33
37
  function normalizeAppResPath(path: string): string {
34
38
  return path.replace(/^\/+/, "").replace(/\/+$/, "");
35
39
  }
36
40
 
37
41
  export class AppRuntime {
38
- private stubKeepAliveTimer: ReturnType<typeof setInterval> | null = null;
39
- private readonly globalIPCHandlers = new Map<string, GlobalIPCHandler>();
40
42
  private exitOnLastWindowClosed = true;
41
43
  private quitting = false;
42
44
  private pumpActive = false;
@@ -44,7 +46,11 @@ export class AppRuntime {
44
46
  readonly ready: Promise<void>;
45
47
 
46
48
  constructor(options: AppOptions = {}) {
49
+ if (_instance) throw new Error("AppRuntime already instantiated");
50
+ _instance = this;
51
+ ensureRpcServer();
47
52
  this.ready = this.bootstrap(options);
53
+ this.ready.catch(() => { if (_instance === this) _instance = null; });
48
54
  }
49
55
 
50
56
  private async bootstrap(options: AppOptions) {
@@ -82,55 +88,31 @@ export class AppRuntime {
82
88
  process.env.BUNITE_USER_DATA_DIR = join(appDataDir, name);
83
89
  }
84
90
 
85
- const runtime = await initNativeRuntime({
86
- allowStub: options.allowStub,
91
+ await initNativeRuntime({
87
92
  hideConsole: options.hideConsole,
88
93
  popupBlocking: options.popupBlocking,
89
94
  engineFlags: options.engineFlags
90
95
  });
91
96
 
92
- if (options.logLevel && runtime.nativeLoaded) {
97
+ if (options.logLevel) {
93
98
  setNativeLogLevel(logLevelToInt(options.logLevel));
94
99
  }
95
100
 
96
- attachGlobalIPCResolver((channel) => this.getGlobalIPCHandler(channel));
97
-
98
- for (const [channel, handler] of getSurfaceIPCHandlers()) {
99
- this.globalIPCHandlers.set(channel, handler);
100
- }
101
- for (const [channel, handler] of getWebviewIPCHandlers()) {
102
- this.globalIPCHandlers.set(channel, handler);
103
- }
104
-
105
101
  setRouteRequestHandler((requestId, path) => this.handleRouteRequest(requestId, path));
106
102
 
107
103
  for (const path of this.appresHandlers.keys()) {
108
104
  getNativeLibrary()?.symbols.bunite_register_appres_route(toCString(path));
109
105
  }
110
106
 
111
- if (this.exitOnLastWindowClosed && runtime.nativeLoaded) {
107
+ if (this.exitOnLastWindowClosed) {
112
108
  buniteEventEmitter.on("all-windows-closed", () => {
113
- if (this.quitting) {
114
- return;
115
- }
109
+ if (this.quitting) return;
116
110
  queueMicrotask(() => {
117
- if (this.quitting) {
118
- return;
119
- }
120
- if (BrowserWindow.getAll().length === 0) {
121
- this.quit();
122
- }
111
+ if (this.quitting) return;
112
+ if (BrowserWindow.getAll().length === 0) this.quit();
123
113
  });
124
114
  });
125
115
  }
126
-
127
- ensureRpcServer();
128
- buniteEventEmitter.emitEvent(
129
- new BuniteEvent("ready", {
130
- usingStub: runtime.usingStub,
131
- artifacts: runtime.artifacts
132
- })
133
- );
134
116
  }
135
117
 
136
118
  on(name: string, handler: (payload: unknown) => void) {
@@ -144,20 +126,11 @@ export class AppRuntime {
144
126
  }
145
127
 
146
128
  run() {
147
- const runtime = getNativeRuntimeState();
148
- if (!runtime?.nativeLoaded) {
149
- if (!this.stubKeepAliveTimer) {
150
- log.warn("Running without a native event loop. Keeping the process alive in stub mode.");
151
- this.stubKeepAliveTimer = setInterval(() => {}, 60_000);
152
- }
153
- return;
154
- }
155
-
156
129
  const lib = getNativeLibrary();
157
130
  lib?.symbols.bunite_run_loop();
158
131
 
159
132
  if (process.platform === "darwin" || process.platform === "linux") {
160
- // AppKit / WebKitGTK share Bun's main thread step-drive via setImmediate (no blocking loop).
133
+ // mac/linux: cooperative pump on Bun's main thread (NSApp/GTK first-thread constraint).
161
134
  this.pumpActive = true;
162
135
  const pump = () => {
163
136
  if (!this.pumpActive) return;
@@ -165,16 +138,13 @@ export class AppRuntime {
165
138
  setImmediate(pump);
166
139
  };
167
140
  pump();
168
- } else if (!this.stubKeepAliveTimer) {
169
- // Engines with a dedicated UI thread (Windows CEF) only need Bun's loop kept alive.
170
- this.stubKeepAliveTimer = setInterval(() => {}, 60_000);
171
141
  }
142
+ // Windows: native UI thread is separate. Bun's loop stays alive via the RPC
143
+ // listening socket started by ensureRpcServer() in the constructor.
172
144
  }
173
145
 
174
146
  quit(code = 0) {
175
- if (this.quitting) {
176
- return;
177
- }
147
+ if (this.quitting) return;
178
148
  this.quitting = true;
179
149
 
180
150
  const event = buniteEventEmitter.events.app.beforeQuit({});
@@ -184,33 +154,29 @@ export class AppRuntime {
184
154
  return;
185
155
  }
186
156
  this.pumpActive = false;
187
- if (this.stubKeepAliveTimer) {
188
- clearInterval(this.stubKeepAliveTimer);
189
- this.stubKeepAliveTimer = null;
190
- }
191
157
  getNativeLibrary()?.symbols.bunite_quit();
158
+ if (_instance === this) _instance = null;
192
159
  process.exitCode = code;
193
160
  process.exit(code);
194
161
  }
195
162
 
196
- handle(channel: string, handler: GlobalIPCHandler) {
197
- if (channel.startsWith("__bunite:")) {
198
- throw new Error(`Channel prefix "__bunite:" is reserved: ${channel}`);
199
- }
200
- if (this.globalIPCHandlers.has(channel)) {
201
- throw new Error(`Global IPC handler already registered for: ${channel}`);
202
- }
203
- this.globalIPCHandlers.set(channel, handler);
204
- return () => this.globalIPCHandlers.delete(channel);
205
- }
206
-
207
- removeHandler(channel: string) {
208
- this.globalIPCHandlers.delete(channel);
209
- }
210
-
211
- /** @internal */
212
- getGlobalIPCHandler(channel: string): GlobalIPCHandler | undefined {
213
- return this.globalIPCHandlers.get(channel);
163
+ createViewRuntime(viewId: number): ImplOf<typeof RuntimeCap> {
164
+ const notImpl = (name: string) => {
165
+ throw new IpcError({ code: "not_found", message: `Runtime.${name}` });
166
+ };
167
+ const impl = {
168
+ window: () => notImpl("window"),
169
+ dialogs: () => notImpl("dialogs"),
170
+ clipboard: () => notImpl("clipboard"),
171
+ shell: () => notImpl("shell"),
172
+ appName: () => "bunite-app",
173
+ appVersion: () => this.version,
174
+ theme: (): "light" | "dark" => "light",
175
+ themeWatch: () => notImpl("themeWatch"),
176
+ surface: (_: void, ctx: Parameters<ImplOf<typeof RuntimeCap>["surface"]>[1]) =>
177
+ ctx.exportCap(SurfaceCap, createSurfaceCapImpl(viewId)),
178
+ } satisfies ImplOf<typeof RuntimeCap>;
179
+ return impl;
214
180
  }
215
181
 
216
182
  private readonly appresHandlers = new Map<string, () => string>();
@@ -262,14 +228,12 @@ export class AppRuntime {
262
228
  private cachedEngineName: string | null | undefined;
263
229
  private cachedEngineVersion: string | null | undefined;
264
230
 
265
- /** Active engine identifier reported by the native adapter (e.g. `"cef"`, `"wkwebview"`, `"webkitgtk"`). */
266
231
  get engineName(): string | null {
267
232
  if (this.cachedEngineName !== undefined) return this.cachedEngineName;
268
233
  this.cachedEngineName = getNativeEngineName();
269
234
  return this.cachedEngineName;
270
235
  }
271
236
 
272
- /** Engine version string reported by the native adapter. Format depends on engine. */
273
237
  get engineVersion(): string | null {
274
238
  if (this.cachedEngineVersion !== undefined) return this.cachedEngineVersion;
275
239
  this.cachedEngineVersion = getNativeEngineVersion();
@@ -1,28 +1,25 @@
1
1
  import { ptr } from "bun:ffi";
2
- import { buildViewPreloadScript } from "../preload/inline";
3
- import { log } from "../../shared/log";
2
+ import { buildViewPreloadScript } from "../preloadBundle";
3
+ import { log } from "../log";
4
4
  import { buniteEventEmitter } from "../events/eventEmitter";
5
- import { type RpcPacket, type RpcTransport, type RpcWithTransport } from "../../shared/rpc";
6
- import { ensureNativeRuntime, getNativeLibrary, toCString, waitForViewReady, cancelWaitForViewReady } from "../proc/native";
7
- import { attachBrowserViewRegistry, getRpcPort, sendMessageToView } from "./Socket";
5
+ import {
6
+ createConnection,
7
+ createFrameTransport,
8
+ type Connection,
9
+ type BytesPipe,
10
+ } from "../../rpc/index";
11
+ import { createEncryptedPipe } from "../encryptedPipe";
12
+ import { ensureNativeRuntime, getNativeLibrary, toCString, waitForViewReady, cancelWaitForViewReady } from "../native";
13
+ import { attachBrowserViewRegistry, getRpcPort } from "./Socket";
14
+ import { getAppRuntimeOrThrow } from "./App";
8
15
  import { randomBytes } from "node:crypto";
9
- import { resolveDefaultAppResRoot } from "../../shared/paths";
16
+ import { resolveDefaultAppResRoot } from "../paths";
10
17
  import { removeSurfacesForHostView } from "./SurfaceRegistry";
11
18
 
12
- const BrowserViewMap: Record<number, BrowserView<any>> = {};
19
+ const BrowserViewMap: Record<number, BrowserView> = {};
13
20
  let nextWebviewId = 1;
14
21
 
15
- function createNativeViewPipe(viewId: number) {
16
- let handler: ((packet: RpcPacket) => void) | undefined;
17
- const transport: RpcTransport = {
18
- send: (packet) => { sendMessageToView(viewId, packet); },
19
- registerHandler: (h) => { handler = h; },
20
- unregisterHandler: () => { handler = undefined; }
21
- };
22
- return { transport, receive: (packet: RpcPacket) => handler?.(packet) };
23
- }
24
-
25
- export type BrowserViewOptions<T = undefined> = {
22
+ export type BrowserViewOptions = {
26
23
  url: string | null;
27
24
  html: string | null;
28
25
  preload: string | null;
@@ -35,7 +32,8 @@ export type BrowserViewOptions<T = undefined> = {
35
32
  width: number;
36
33
  height: number;
37
34
  };
38
- rpc?: T;
35
+ /** Setup callback fired when a renderer connection attaches. Use `conn.serve(cap, impl)` or `conn.serveAll(schema, impls)`. */
36
+ serve?: (conn: Connection) => void;
39
37
  windowId: number;
40
38
  autoResize: boolean;
41
39
  navigationRules: string[] | null;
@@ -49,19 +47,14 @@ const defaultOptions: BrowserViewOptions = {
49
47
  appresRoot: null,
50
48
  preloadOrigins: undefined,
51
49
  partition: null,
52
- frame: {
53
- x: 0,
54
- y: 0,
55
- width: 800,
56
- height: 600
57
- },
50
+ frame: { x: 0, y: 0, width: 800, height: 600 },
58
51
  windowId: 0,
59
52
  autoResize: true,
60
53
  navigationRules: null,
61
54
  sandbox: false
62
55
  };
63
56
 
64
- export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
57
+ export class BrowserView {
65
58
  id = nextWebviewId++;
66
59
  private nativeAttached = false;
67
60
  private _readyPromise: Promise<void>;
@@ -73,20 +66,17 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
73
66
  preloadOrigins?: string[];
74
67
  partition: string | null;
75
68
  frame: BrowserViewOptions["frame"];
76
- rpc?: T;
77
- readonly transport: RpcTransport;
78
- private pipe: ReturnType<typeof createNativeViewPipe>;
69
+ readonly serveSetup?: (conn: Connection) => void;
70
+ private connection: Connection | null = null;
71
+ private connectionGeneration = 0;
79
72
  autoResize: boolean;
80
73
  navigationRules: string[] | null;
81
74
  sandbox: boolean;
82
75
  secretKey: Uint8Array;
83
76
 
84
- constructor(options: Partial<BrowserViewOptions<T>>) {
77
+ constructor(options: Partial<BrowserViewOptions>) {
85
78
  ensureNativeRuntime();
86
79
 
87
- this.pipe = createNativeViewPipe(this.id);
88
- this.transport = this.pipe.transport;
89
-
90
80
  this.windowId = options.windowId ?? defaultOptions.windowId;
91
81
  this.url = options.url ?? defaultOptions.url;
92
82
  this.html = options.html ?? defaultOptions.html;
@@ -95,17 +85,17 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
95
85
  this.preloadOrigins = options.preloadOrigins ?? defaultOptions.preloadOrigins;
96
86
  this.partition = options.partition ?? defaultOptions.partition;
97
87
  this.frame = options.frame ?? defaultOptions.frame;
98
- this.rpc = options.rpc;
88
+ this.serveSetup = options.serve;
99
89
  this.autoResize = options.autoResize ?? defaultOptions.autoResize;
100
90
  this.navigationRules = options.navigationRules ?? defaultOptions.navigationRules;
101
91
  this.sandbox = options.sandbox ?? defaultOptions.sandbox;
102
92
  this.secretKey = new Uint8Array(randomBytes(32));
103
93
 
104
94
  if (this.sandbox) {
105
- throw new Error("sandboxed BrowserView is not implemented in Bunite Windows Phase 1 yet.");
95
+ throw new Error("sandboxed BrowserView is not implemented yet.");
106
96
  }
107
97
  if (this.partition) {
108
- log.warn("BrowserView.partition is not implemented in Bunite Windows Phase 1 yet.");
98
+ log.warn("BrowserView.partition is not implemented yet.");
109
99
  }
110
100
 
111
101
  const preloadScript = buildViewPreloadScript({
@@ -117,8 +107,6 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
117
107
  });
118
108
 
119
109
  BrowserViewMap[this.id] = this;
120
- this.rpc?.setTransport(this.transport);
121
- // Register before native create — view-ready can fire on the UI thread before bunite_view_create returns.
122
110
  this._readyPromise = waitForViewReady(this.id);
123
111
  this.nativeAttached =
124
112
  getNativeLibrary()?.symbols.bunite_view_create(
@@ -139,8 +127,6 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
139
127
  ) ?? false;
140
128
 
141
129
  if (this.nativeAttached) {
142
- // did-navigate (not will-): nav destroys JS context without disconnectedCallback;
143
- // will-navigate fires even when rules deny → would leak surfaces.
144
130
  this.on("did-navigate", (event: any) => {
145
131
  this.url = event.data?.detail ?? this.url;
146
132
  removeSurfacesForHostView(this.id);
@@ -148,7 +134,7 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
148
134
  } else {
149
135
  cancelWaitForViewReady(this.id);
150
136
  this._readyPromise = Promise.reject(new Error("Native view creation failed"));
151
- this._readyPromise.catch(() => {}); // prevent unhandled rejection
137
+ this._readyPromise.catch(() => {});
152
138
  }
153
139
  }
154
140
 
@@ -169,19 +155,51 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
169
155
  return Object.values(BrowserViewMap);
170
156
  }
171
157
 
172
- handleIncomingRpc(packet: RpcPacket) {
173
- this.pipe.receive(packet);
158
+ async attachNewConnection(pipe: BytesPipe): Promise<void> {
159
+ this.connectionGeneration += 1;
160
+ const myGen = this.connectionGeneration;
161
+ if (this.connection) {
162
+ try { (this.connection as { transport?: { close?(): void } }).transport?.close?.(); } catch { /* swallow */ }
163
+ this.connection = null;
164
+ }
165
+ const encPipe = await createEncryptedPipe(pipe, this.secretKey);
166
+ if (myGen !== this.connectionGeneration) {
167
+ try { encPipe.close(); } catch { /* swallow */ }
168
+ return;
169
+ }
170
+ const runtime = getAppRuntimeOrThrow().createViewRuntime(this.id);
171
+ this.connection = createConnection({
172
+ transport: createFrameTransport(encPipe),
173
+ mode: "native",
174
+ origin: "appres://app.internal",
175
+ runtime,
176
+ attestation: {
177
+ origin: "appres://app.internal",
178
+ topOrigin: "appres://app.internal",
179
+ partition: this.partition ?? "default",
180
+ isAppRes: true,
181
+ isMainFrame: true,
182
+ userGesture: false,
183
+ level: "app-internal",
184
+ },
185
+ });
186
+ this.serveSetup?.(this.connection);
174
187
  }
175
188
 
176
- get rpcPort() {
177
- return getRpcPort();
189
+ detachNewConnection(): void {
190
+ this.connectionGeneration += 1;
191
+ if (this.connection) {
192
+ try { (this.connection as { transport?: { close?(): void } }).transport?.close?.(); } catch { /* swallow */ }
193
+ this.connection = null;
194
+ }
178
195
  }
179
196
 
180
- setAnchor(mode: "none" | "fill" | "top" | "below-top", inset = 0) {
181
- const modeInt = { none: 0, fill: 1, top: 2, "below-top": 3 }[mode];
182
- if (this.nativeAttached) {
183
- getNativeLibrary()?.symbols.bunite_view_set_anchor(this.id, modeInt, inset);
184
- }
197
+ get rpcConnection(): Connection | null {
198
+ return this.connection;
199
+ }
200
+
201
+ get rpcPort() {
202
+ return getRpcPort();
185
203
  }
186
204
 
187
205
  executeJavaScript(script: string) {
@@ -245,7 +263,6 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
245
263
  }
246
264
  }
247
265
 
248
- /** Fire-and-forget setBounds — does not block on the UI thread. */
249
266
  setBoundsAsync(x: number, y: number, width: number, height: number) {
250
267
  this.frame = { x, y, width, height };
251
268
  if (this.nativeAttached) {
@@ -297,11 +314,7 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
297
314
  cancelWaitForViewReady(this.id);
298
315
  this.nativeAttached = false;
299
316
  for (const eventName of [
300
- "will-navigate",
301
- "did-navigate",
302
- "dom-ready",
303
- "new-window-open",
304
- "permission-requested"
317
+ "will-navigate", "did-navigate", "dom-ready", "new-window-open", "permission-requested"
305
318
  ]) {
306
319
  buniteEventEmitter.removeAllListeners(`${eventName}-${this.id}`);
307
320
  }
@@ -309,12 +322,7 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
309
322
  }
310
323
 
311
324
  on(
312
- name:
313
- | "will-navigate"
314
- | "did-navigate"
315
- | "dom-ready"
316
- | "new-window-open"
317
- | "permission-requested",
325
+ name: "will-navigate" | "did-navigate" | "dom-ready" | "new-window-open" | "permission-requested",
318
326
  handler: (event: unknown) => void
319
327
  ) {
320
328
  const specificName = `${name}-${this.id}`;
@@ -324,7 +332,5 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
324
332
  }
325
333
 
326
334
  attachBrowserViewRegistry({
327
- getById(id) {
328
- return BrowserView.getById(id);
329
- }
335
+ getById(id) { return BrowserView.getById(id); }
330
336
  });
@@ -1,13 +1,13 @@
1
1
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
2
2
  import { BuniteEvent } from "../events/event";
3
3
  import { buniteEventEmitter } from "../events/eventEmitter";
4
- import { ensureNativeRuntime, getNativeLibrary, toCString } from "../proc/native";
5
- import { BrowserView, type BrowserViewOptions } from "./BrowserView";
6
- import type { RpcWithTransport } from "../../shared/rpc";
4
+ import { ensureNativeRuntime, getNativeLibrary, toCString } from "../native";
5
+ import { BrowserView } from "./BrowserView";
6
+ import type { Connection } from "../../rpc/index";
7
7
  import { getNextWindowId } from "./windowIds";
8
- import { getBaseDir, resolveDefaultAppResRoot } from "../../shared/paths";
8
+ import { getBaseDir, resolveDefaultAppResRoot } from "../paths";
9
9
 
10
- export type WindowOptionsType<T = undefined> = {
10
+ export type WindowOptionsType = {
11
11
  title: string;
12
12
  frame: {
13
13
  x: number;
@@ -22,7 +22,8 @@ export type WindowOptionsType<T = undefined> = {
22
22
  preload: string | null;
23
23
  appresRoot: string | null;
24
24
  preloadOrigins?: string[];
25
- rpc?: T;
25
+ /** Setup callback fired when the window's renderer connection attaches. */
26
+ serve?: (conn: Connection) => void;
26
27
  titleBarStyle: "hidden" | "hiddenInset" | "default";
27
28
  transparent: boolean;
28
29
  hidden?: boolean;
@@ -50,7 +51,7 @@ const defaultOptions: WindowOptionsType = {
50
51
  sandbox: false
51
52
  };
52
53
 
53
- const BrowserWindowMap: Record<number, BrowserWindow<any>> = {};
54
+ const BrowserWindowMap: Record<number, BrowserWindow> = {};
54
55
 
55
56
  let lastFocusedWindowId: number | null = null;
56
57
 
@@ -58,7 +59,7 @@ export function getLastFocusedWindowId(): number | null {
58
59
  return lastFocusedWindowId;
59
60
  }
60
61
 
61
- export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
62
+ export class BrowserWindow {
62
63
  id = getNextWindowId();
63
64
  private nativeAttached = false;
64
65
  title: string;
@@ -127,7 +128,7 @@ export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
127
128
  buniteEventEmitter.removeAllListeners(`close-requested-${this.id}`);
128
129
  };
129
130
 
130
- constructor(options: Partial<WindowOptionsType<T>> = {}) {
131
+ constructor(options: Partial<WindowOptionsType> = {}) {
131
132
  ensureNativeRuntime();
132
133
 
133
134
  this.title = options.title ?? defaultOptions.title;
@@ -193,7 +194,7 @@ export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
193
194
  width: this.frame.width,
194
195
  height: this.frame.height
195
196
  },
196
- rpc: options.rpc as BrowserViewOptions<T>["rpc"],
197
+ serve: options.serve,
197
198
  windowId: this.id,
198
199
  navigationRules: this.navigationRules,
199
200
  sandbox: this.sandbox
@@ -202,10 +203,10 @@ export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
202
203
  this.webviewId = webview.id;
203
204
  }
204
205
 
205
- get view(): BrowserView<T> {
206
+ get view(): BrowserView {
206
207
  const view = BrowserView.getById(this.webviewId);
207
208
  if (!view) throw new Error(`BrowserWindow ${this.id} has no attached view`);
208
- return view as BrowserView<T>;
209
+ return view as BrowserView;
209
210
  }
210
211
 
211
212
  static getById(id: number) {
@@ -216,8 +217,8 @@ export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
216
217
  return Object.values(BrowserWindowMap);
217
218
  }
218
219
 
219
- get webview() {
220
- return BrowserView.getById(this.webviewId) as BrowserView<T>;
220
+ get webview(): BrowserView | undefined {
221
+ return BrowserView.getById(this.webviewId) as BrowserView | undefined;
221
222
  }
222
223
 
223
224
  show() {