bunite-core 0.8.1 → 0.9.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 (57) 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 +64 -64
  4. package/src/{bun → host}/core/BrowserWindow.ts +14 -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 +81 -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 +54 -219
  21. package/src/preload/tsconfig.json +3 -10
  22. package/src/rpc/encrypt.ts +74 -0
  23. package/src/rpc/error.ts +58 -0
  24. package/src/rpc/framework.ts +132 -0
  25. package/src/rpc/hash.ts +142 -0
  26. package/src/rpc/index.ts +129 -0
  27. package/src/rpc/peer.ts +1055 -0
  28. package/src/rpc/renderer.ts +82 -0
  29. package/src/rpc/schema.ts +246 -0
  30. package/src/rpc/stream.ts +72 -0
  31. package/src/rpc/transport.ts +81 -0
  32. package/src/rpc/wire.ts +150 -0
  33. package/src/{preload/webviewElement.ts → webview/native.ts} +68 -48
  34. package/src/{shared/webviewPolyfill.ts → webview/polyfill.ts} +4 -7
  35. package/src/bun/core/Socket.ts +0 -187
  36. package/src/bun/core/SurfaceBrowserIPC.ts +0 -65
  37. package/src/bun/core/SurfaceManager.ts +0 -201
  38. package/src/bun/index.ts +0 -53
  39. package/src/bun/preload/index.ts +0 -73
  40. package/src/preload/tsconfig.tsbuildinfo +0 -1
  41. package/src/shared/rpc.ts +0 -424
  42. package/src/shared/rpcDemux.ts +0 -219
  43. package/src/shared/rpcWire.ts +0 -54
  44. package/src/shared/rpcWireConstants.ts +0 -3
  45. package/src/shared/webRpcHandler.ts +0 -77
  46. package/src/shared/webSocketTransport.ts +0 -26
  47. package/src/view/index.ts +0 -196
  48. /package/src/{shared → host}/cefVersion.ts +0 -0
  49. /package/src/{bun → host}/core/SurfaceRegistry.ts +0 -0
  50. /package/src/{bun → host}/core/singleInstanceLock.ts +0 -0
  51. /package/src/{bun → host}/core/windowIds.ts +0 -0
  52. /package/src/{bun → host}/events/event.ts +0 -0
  53. /package/src/{bun → host}/events/eventEmitter.ts +0 -0
  54. /package/src/{bun → host}/events/webviewEvents.ts +0 -0
  55. /package/src/{bun → host}/events/windowEvents.ts +0 -0
  56. /package/src/{shared → host}/log.ts +0 -0
  57. /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.9.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_supported", 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,27 @@
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
+ type SchemaShape,
11
+ type ServerDescriptor,
12
+ } from "../../rpc/index";
13
+ import { createEncryptedPipe } from "../encryptedPipe";
14
+ import { ensureNativeRuntime, getNativeLibrary, toCString, waitForViewReady, cancelWaitForViewReady } from "../native";
15
+ import { attachBrowserViewRegistry, getRpcPort } from "./Socket";
16
+ import { getAppRuntimeOrThrow } from "./App";
8
17
  import { randomBytes } from "node:crypto";
9
- import { resolveDefaultAppResRoot } from "../../shared/paths";
18
+ import { resolveDefaultAppResRoot } from "../paths";
10
19
  import { removeSurfacesForHostView } from "./SurfaceRegistry";
11
20
 
12
21
  const BrowserViewMap: Record<number, BrowserView<any>> = {};
13
22
  let nextWebviewId = 1;
14
23
 
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> = {
24
+ export type BrowserViewOptions<S extends SchemaShape = SchemaShape> = {
26
25
  url: string | null;
27
26
  html: string | null;
28
27
  preload: string | null;
@@ -35,7 +34,7 @@ export type BrowserViewOptions<T = undefined> = {
35
34
  width: number;
36
35
  height: number;
37
36
  };
38
- rpc?: T;
37
+ serve?: ServerDescriptor<S>;
39
38
  windowId: number;
40
39
  autoResize: boolean;
41
40
  navigationRules: string[] | null;
@@ -49,19 +48,14 @@ const defaultOptions: BrowserViewOptions = {
49
48
  appresRoot: null,
50
49
  preloadOrigins: undefined,
51
50
  partition: null,
52
- frame: {
53
- x: 0,
54
- y: 0,
55
- width: 800,
56
- height: 600
57
- },
51
+ frame: { x: 0, y: 0, width: 800, height: 600 },
58
52
  windowId: 0,
59
53
  autoResize: true,
60
54
  navigationRules: null,
61
55
  sandbox: false
62
56
  };
63
57
 
64
- export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
58
+ export class BrowserView<S extends SchemaShape = SchemaShape> {
65
59
  id = nextWebviewId++;
66
60
  private nativeAttached = false;
67
61
  private _readyPromise: Promise<void>;
@@ -73,20 +67,17 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
73
67
  preloadOrigins?: string[];
74
68
  partition: string | null;
75
69
  frame: BrowserViewOptions["frame"];
76
- rpc?: T;
77
- readonly transport: RpcTransport;
78
- private pipe: ReturnType<typeof createNativeViewPipe>;
70
+ readonly serveDescriptor?: ServerDescriptor<S>;
71
+ private connection: Connection | null = null;
72
+ private connectionGeneration = 0;
79
73
  autoResize: boolean;
80
74
  navigationRules: string[] | null;
81
75
  sandbox: boolean;
82
76
  secretKey: Uint8Array;
83
77
 
84
- constructor(options: Partial<BrowserViewOptions<T>>) {
78
+ constructor(options: Partial<BrowserViewOptions<S>>) {
85
79
  ensureNativeRuntime();
86
80
 
87
- this.pipe = createNativeViewPipe(this.id);
88
- this.transport = this.pipe.transport;
89
-
90
81
  this.windowId = options.windowId ?? defaultOptions.windowId;
91
82
  this.url = options.url ?? defaultOptions.url;
92
83
  this.html = options.html ?? defaultOptions.html;
@@ -95,17 +86,17 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
95
86
  this.preloadOrigins = options.preloadOrigins ?? defaultOptions.preloadOrigins;
96
87
  this.partition = options.partition ?? defaultOptions.partition;
97
88
  this.frame = options.frame ?? defaultOptions.frame;
98
- this.rpc = options.rpc;
89
+ this.serveDescriptor = options.serve;
99
90
  this.autoResize = options.autoResize ?? defaultOptions.autoResize;
100
91
  this.navigationRules = options.navigationRules ?? defaultOptions.navigationRules;
101
92
  this.sandbox = options.sandbox ?? defaultOptions.sandbox;
102
93
  this.secretKey = new Uint8Array(randomBytes(32));
103
94
 
104
95
  if (this.sandbox) {
105
- throw new Error("sandboxed BrowserView is not implemented in Bunite Windows Phase 1 yet.");
96
+ throw new Error("sandboxed BrowserView is not implemented yet.");
106
97
  }
107
98
  if (this.partition) {
108
- log.warn("BrowserView.partition is not implemented in Bunite Windows Phase 1 yet.");
99
+ log.warn("BrowserView.partition is not implemented yet.");
109
100
  }
110
101
 
111
102
  const preloadScript = buildViewPreloadScript({
@@ -117,8 +108,6 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
117
108
  });
118
109
 
119
110
  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
111
  this._readyPromise = waitForViewReady(this.id);
123
112
  this.nativeAttached =
124
113
  getNativeLibrary()?.symbols.bunite_view_create(
@@ -139,8 +128,6 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
139
128
  ) ?? false;
140
129
 
141
130
  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
131
  this.on("did-navigate", (event: any) => {
145
132
  this.url = event.data?.detail ?? this.url;
146
133
  removeSurfacesForHostView(this.id);
@@ -148,7 +135,7 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
148
135
  } else {
149
136
  cancelWaitForViewReady(this.id);
150
137
  this._readyPromise = Promise.reject(new Error("Native view creation failed"));
151
- this._readyPromise.catch(() => {}); // prevent unhandled rejection
138
+ this._readyPromise.catch(() => {});
152
139
  }
153
140
  }
154
141
 
@@ -169,19 +156,44 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
169
156
  return Object.values(BrowserViewMap);
170
157
  }
171
158
 
172
- handleIncomingRpc(packet: RpcPacket) {
173
- this.pipe.receive(packet);
159
+ async attachNewConnection(pipe: BytesPipe): Promise<void> {
160
+ this.connectionGeneration += 1;
161
+ const myGen = this.connectionGeneration;
162
+ if (this.connection) {
163
+ try { (this.connection as { transport?: { close?(): void } }).transport?.close?.(); } catch { /* swallow */ }
164
+ this.connection = null;
165
+ }
166
+ const encPipe = await createEncryptedPipe(pipe, this.secretKey);
167
+ if (myGen !== this.connectionGeneration) {
168
+ try { encPipe.close(); } catch { /* swallow */ }
169
+ return;
170
+ }
171
+ const runtime = getAppRuntimeOrThrow().createViewRuntime(this.id);
172
+ this.connection = createConnection({
173
+ transport: createFrameTransport(encPipe),
174
+ mode: "native",
175
+ origin: "appres://app.internal",
176
+ runtime,
177
+ });
178
+ if (this.serveDescriptor) {
179
+ this.connection.serve(this.serveDescriptor);
180
+ }
181
+ }
182
+
183
+ detachNewConnection(): void {
184
+ this.connectionGeneration += 1;
185
+ if (this.connection) {
186
+ try { (this.connection as { transport?: { close?(): void } }).transport?.close?.(); } catch { /* swallow */ }
187
+ this.connection = null;
188
+ }
174
189
  }
175
190
 
176
- get rpcPort() {
177
- return getRpcPort();
191
+ get rpcConnection(): Connection | null {
192
+ return this.connection;
178
193
  }
179
194
 
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
- }
195
+ get rpcPort() {
196
+ return getRpcPort();
185
197
  }
186
198
 
187
199
  executeJavaScript(script: string) {
@@ -245,7 +257,6 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
245
257
  }
246
258
  }
247
259
 
248
- /** Fire-and-forget setBounds — does not block on the UI thread. */
249
260
  setBoundsAsync(x: number, y: number, width: number, height: number) {
250
261
  this.frame = { x, y, width, height };
251
262
  if (this.nativeAttached) {
@@ -297,11 +308,7 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
297
308
  cancelWaitForViewReady(this.id);
298
309
  this.nativeAttached = false;
299
310
  for (const eventName of [
300
- "will-navigate",
301
- "did-navigate",
302
- "dom-ready",
303
- "new-window-open",
304
- "permission-requested"
311
+ "will-navigate", "did-navigate", "dom-ready", "new-window-open", "permission-requested"
305
312
  ]) {
306
313
  buniteEventEmitter.removeAllListeners(`${eventName}-${this.id}`);
307
314
  }
@@ -309,12 +316,7 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
309
316
  }
310
317
 
311
318
  on(
312
- name:
313
- | "will-navigate"
314
- | "did-navigate"
315
- | "dom-ready"
316
- | "new-window-open"
317
- | "permission-requested",
319
+ name: "will-navigate" | "did-navigate" | "dom-ready" | "new-window-open" | "permission-requested",
318
320
  handler: (event: unknown) => void
319
321
  ) {
320
322
  const specificName = `${name}-${this.id}`;
@@ -324,7 +326,5 @@ export class BrowserView<T extends RpcWithTransport = RpcWithTransport> {
324
326
  }
325
327
 
326
328
  attachBrowserViewRegistry({
327
- getById(id) {
328
- return BrowserView.getById(id);
329
- }
329
+ getById(id) { return BrowserView.getById(id); }
330
330
  });
@@ -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 { SchemaShape, ServerDescriptor } 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<S extends SchemaShape = SchemaShape> = {
11
11
  title: string;
12
12
  frame: {
13
13
  x: number;
@@ -22,7 +22,7 @@ export type WindowOptionsType<T = undefined> = {
22
22
  preload: string | null;
23
23
  appresRoot: string | null;
24
24
  preloadOrigins?: string[];
25
- rpc?: T;
25
+ serve?: ServerDescriptor<S>;
26
26
  titleBarStyle: "hidden" | "hiddenInset" | "default";
27
27
  transparent: boolean;
28
28
  hidden?: boolean;
@@ -58,7 +58,7 @@ export function getLastFocusedWindowId(): number | null {
58
58
  return lastFocusedWindowId;
59
59
  }
60
60
 
61
- export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
61
+ export class BrowserWindow<S extends SchemaShape = SchemaShape> {
62
62
  id = getNextWindowId();
63
63
  private nativeAttached = false;
64
64
  title: string;
@@ -127,7 +127,7 @@ export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
127
127
  buniteEventEmitter.removeAllListeners(`close-requested-${this.id}`);
128
128
  };
129
129
 
130
- constructor(options: Partial<WindowOptionsType<T>> = {}) {
130
+ constructor(options: Partial<WindowOptionsType<S>> = {}) {
131
131
  ensureNativeRuntime();
132
132
 
133
133
  this.title = options.title ?? defaultOptions.title;
@@ -181,7 +181,7 @@ export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
181
181
  buniteEventEmitter.on(`resize-${this.id}`, this.handleNativeResize);
182
182
  buniteEventEmitter.on(`close-${this.id}`, this.handleNativeClose);
183
183
 
184
- const webview = new BrowserView({
184
+ const webview = new BrowserView<S>({
185
185
  url: this.url,
186
186
  html: this.html,
187
187
  preload: this.preload,
@@ -193,7 +193,7 @@ export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
193
193
  width: this.frame.width,
194
194
  height: this.frame.height
195
195
  },
196
- rpc: options.rpc as BrowserViewOptions<T>["rpc"],
196
+ serve: options.serve,
197
197
  windowId: this.id,
198
198
  navigationRules: this.navigationRules,
199
199
  sandbox: this.sandbox
@@ -202,10 +202,10 @@ export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
202
202
  this.webviewId = webview.id;
203
203
  }
204
204
 
205
- get view(): BrowserView<T> {
205
+ get view(): BrowserView<S> {
206
206
  const view = BrowserView.getById(this.webviewId);
207
207
  if (!view) throw new Error(`BrowserWindow ${this.id} has no attached view`);
208
- return view as BrowserView<T>;
208
+ return view as BrowserView<S>;
209
209
  }
210
210
 
211
211
  static getById(id: number) {
@@ -216,8 +216,8 @@ export class BrowserWindow<T extends RpcWithTransport = RpcWithTransport> {
216
216
  return Object.values(BrowserWindowMap);
217
217
  }
218
218
 
219
- get webview() {
220
- return BrowserView.getById(this.webviewId) as BrowserView<T>;
219
+ get webview(): BrowserView<S> | undefined {
220
+ return BrowserView.getById(this.webviewId) as BrowserView<S> | undefined;
221
221
  }
222
222
 
223
223
  show() {