bunite-core 0.8.0 → 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.
- package/package.json +9 -7
- package/src/{bun → host}/core/App.ts +45 -81
- package/src/{bun → host}/core/BrowserView.ts +64 -64
- package/src/{bun → host}/core/BrowserWindow.ts +14 -14
- package/src/host/core/Socket.ts +98 -0
- package/src/host/core/SurfaceBrowserIPC.ts +7 -0
- package/src/host/core/SurfaceManager.ts +154 -0
- package/src/host/encryptedPipe.ts +62 -0
- package/src/{bun → host}/events/appEvents.ts +0 -1
- package/src/host/index.ts +29 -0
- package/src/{bun/proc → host}/native.ts +38 -52
- package/src/{shared → host}/paths.ts +20 -26
- package/src/{bun/preload/inline.ts → host/preloadBundle.ts} +2 -2
- package/src/host/serveWeb.ts +81 -0
- package/src/native/linux/bunite_linux_runtime.cpp +2 -2
- package/src/native/mac/bunite_mac_ffi.mm +2 -2
- package/src/native/shared/ffi_exports.h +1 -1
- package/src/native/win/native_host_ffi.cpp +2 -2
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +54 -219
- package/src/preload/tsconfig.json +3 -10
- package/src/rpc/encrypt.ts +74 -0
- package/src/rpc/error.ts +58 -0
- package/src/rpc/framework.ts +132 -0
- package/src/rpc/hash.ts +142 -0
- package/src/rpc/index.ts +129 -0
- package/src/rpc/peer.ts +1055 -0
- package/src/rpc/renderer.ts +82 -0
- package/src/rpc/schema.ts +246 -0
- package/src/rpc/stream.ts +72 -0
- package/src/rpc/transport.ts +81 -0
- package/src/rpc/wire.ts +150 -0
- package/src/{preload/webviewElement.ts → webview/native.ts} +68 -48
- package/src/{shared/webviewPolyfill.ts → webview/polyfill.ts} +4 -7
- package/src/bun/core/Socket.ts +0 -187
- package/src/bun/core/SurfaceBrowserIPC.ts +0 -65
- package/src/bun/core/SurfaceManager.ts +0 -201
- package/src/bun/index.ts +0 -53
- package/src/bun/preload/index.ts +0 -73
- package/src/preload/tsconfig.tsbuildinfo +0 -1
- package/src/shared/rpc.ts +0 -424
- package/src/shared/rpcDemux.ts +0 -219
- package/src/shared/rpcWire.ts +0 -54
- package/src/shared/rpcWireConstants.ts +0 -3
- package/src/shared/webRpcHandler.ts +0 -77
- package/src/shared/webSocketTransport.ts +0 -26
- package/src/view/index.ts +0 -196
- /package/src/{shared → host}/cefVersion.ts +0 -0
- /package/src/{bun → host}/core/SurfaceRegistry.ts +0 -0
- /package/src/{bun → host}/core/singleInstanceLock.ts +0 -0
- /package/src/{bun → host}/core/windowIds.ts +0 -0
- /package/src/{bun → host}/events/event.ts +0 -0
- /package/src/{bun → host}/events/eventEmitter.ts +0 -0
- /package/src/{bun → host}/events/webviewEvents.ts +0 -0
- /package/src/{bun → host}/events/windowEvents.ts +0 -0
- /package/src/{shared → host}/log.ts +0 -0
- /package/src/{shared → host}/platform.ts +0 -0
package/src/shared/rpcDemux.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import type { RpcPacket, RpcTransport, RpcWithTransport } from "./rpc";
|
|
2
|
-
|
|
3
|
-
type DemuxPacketEnvelope = { channel: string; packet: RpcPacket };
|
|
4
|
-
type DemuxHelloFrame = { channel: string; hello: true };
|
|
5
|
-
type DemuxFrame = DemuxPacketEnvelope | DemuxHelloFrame;
|
|
6
|
-
|
|
7
|
-
function isDemuxFrame(value: unknown): value is DemuxFrame {
|
|
8
|
-
if (typeof value !== "object" || value === null) return false;
|
|
9
|
-
return typeof (value as DemuxFrame).channel === "string";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isPacketEnvelope(frame: DemuxFrame): frame is DemuxPacketEnvelope {
|
|
13
|
-
const v = frame as DemuxPacketEnvelope;
|
|
14
|
-
return typeof v.packet === "object" && v.packet !== null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isHelloFrame(frame: DemuxFrame): frame is DemuxHelloFrame {
|
|
18
|
-
return (frame as DemuxHelloFrame).hello === true;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type RpcChannelHandle = {
|
|
22
|
-
/**
|
|
23
|
-
* Connect an RPC instance to this channel. Returns a promise that resolves
|
|
24
|
-
* once both sides have registered a handler (HELLO handshake). Awaiting
|
|
25
|
-
* guarantees the first subsequent request reaches the peer.
|
|
26
|
-
*/
|
|
27
|
-
bindTo(rpc: RpcWithTransport): Promise<void>;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type RpcTransportDemuxer = {
|
|
31
|
-
channel(name: string): RpcChannelHandle;
|
|
32
|
-
dispose(): void;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type RpcDemuxBufferPolicy = "drop-oldest" | "drop-newest";
|
|
36
|
-
|
|
37
|
-
export type RpcTransportDemuxerOptions = {
|
|
38
|
-
/** ms to wait for peer before `bindTo` rejects. Default 10_000. */
|
|
39
|
-
readyTimeout?: number;
|
|
40
|
-
/**
|
|
41
|
-
* Per-channel cap for packets received before a handler is registered.
|
|
42
|
-
* Drained FIFO on registerHandler. Default 64. Set 0 to disable buffering.
|
|
43
|
-
*/
|
|
44
|
-
bufferSize?: number;
|
|
45
|
-
/** Overflow behaviour when bufferSize is exceeded. Default "drop-oldest". */
|
|
46
|
-
bufferPolicy?: RpcDemuxBufferPolicy;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
type ChannelState = {
|
|
50
|
-
handler?: (packet: RpcPacket) => void;
|
|
51
|
-
peerSawUs: boolean;
|
|
52
|
-
ready: Promise<void>;
|
|
53
|
-
resolveReady: () => void;
|
|
54
|
-
rejectReady: (error: Error) => void;
|
|
55
|
-
readySettled: boolean;
|
|
56
|
-
readyTimer?: ReturnType<typeof setTimeout>;
|
|
57
|
-
pending: RpcPacket[];
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const DEFAULT_READY_TIMEOUT = 10_000;
|
|
61
|
-
const DEFAULT_BUFFER_SIZE = 64;
|
|
62
|
-
|
|
63
|
-
export function createRpcTransportDemuxer(
|
|
64
|
-
base: RpcTransport,
|
|
65
|
-
options: RpcTransportDemuxerOptions = {}
|
|
66
|
-
): RpcTransportDemuxer {
|
|
67
|
-
if (!base.send || !base.registerHandler) {
|
|
68
|
-
throw new Error("createRpcTransportDemuxer requires a base transport with both send and registerHandler");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const readyTimeout = options.readyTimeout ?? DEFAULT_READY_TIMEOUT;
|
|
72
|
-
const bufferSize = Math.max(0, options.bufferSize ?? DEFAULT_BUFFER_SIZE);
|
|
73
|
-
const bufferPolicy: RpcDemuxBufferPolicy = options.bufferPolicy ?? "drop-oldest";
|
|
74
|
-
const channels = new Map<string, ChannelState>();
|
|
75
|
-
let disposed = false;
|
|
76
|
-
|
|
77
|
-
function getOrCreateState(name: string): ChannelState {
|
|
78
|
-
let state = channels.get(name);
|
|
79
|
-
if (state) return state;
|
|
80
|
-
|
|
81
|
-
let resolveReady!: () => void;
|
|
82
|
-
let rejectReady!: (error: Error) => void;
|
|
83
|
-
const ready = new Promise<void>((resolve, reject) => {
|
|
84
|
-
resolveReady = resolve;
|
|
85
|
-
rejectReady = reject;
|
|
86
|
-
});
|
|
87
|
-
ready.catch(() => {}); // prevent unhandled rejection if consumer doesn't await
|
|
88
|
-
|
|
89
|
-
state = {
|
|
90
|
-
peerSawUs: false,
|
|
91
|
-
ready,
|
|
92
|
-
resolveReady,
|
|
93
|
-
rejectReady,
|
|
94
|
-
readySettled: false,
|
|
95
|
-
pending: []
|
|
96
|
-
};
|
|
97
|
-
channels.set(name, state);
|
|
98
|
-
return state;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function bufferIncoming(state: ChannelState, packet: RpcPacket) {
|
|
102
|
-
if (bufferSize === 0) return;
|
|
103
|
-
if (state.pending.length < bufferSize) {
|
|
104
|
-
state.pending.push(packet);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (bufferPolicy === "drop-oldest") {
|
|
108
|
-
state.pending.shift();
|
|
109
|
-
state.pending.push(packet);
|
|
110
|
-
}
|
|
111
|
-
// drop-newest: leave the queue alone, discard the new packet
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function settleReady(state: ChannelState, action: () => void) {
|
|
115
|
-
if (state.readySettled) return;
|
|
116
|
-
state.readySettled = true;
|
|
117
|
-
if (state.readyTimer) clearTimeout(state.readyTimer);
|
|
118
|
-
action();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function sendHello(name: string) {
|
|
122
|
-
const frame: DemuxHelloFrame = { channel: name, hello: true };
|
|
123
|
-
base.send!(frame as unknown as RpcPacket);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
base.registerHandler((data) => {
|
|
127
|
-
if (!isDemuxFrame(data)) return;
|
|
128
|
-
const state = getOrCreateState(data.channel);
|
|
129
|
-
|
|
130
|
-
if (isHelloFrame(data)) {
|
|
131
|
-
if (state.handler) {
|
|
132
|
-
const wasReady = state.readySettled;
|
|
133
|
-
settleReady(state, state.resolveReady);
|
|
134
|
-
if (!wasReady && !disposed) sendHello(data.channel); // echo so peer wakes up
|
|
135
|
-
} else {
|
|
136
|
-
state.peerSawUs = true;
|
|
137
|
-
}
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (isPacketEnvelope(data)) {
|
|
142
|
-
if (state.handler) {
|
|
143
|
-
state.handler(data.packet);
|
|
144
|
-
} else {
|
|
145
|
-
bufferIncoming(state, data.packet);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
channel(name) {
|
|
152
|
-
if (disposed) throw new Error(`Demuxer disposed; cannot open channel "${name}"`);
|
|
153
|
-
const state = getOrCreateState(name);
|
|
154
|
-
|
|
155
|
-
const transport: RpcTransport = {
|
|
156
|
-
send(packet) {
|
|
157
|
-
if (disposed) throw new Error(`Demuxer disposed; cannot send on channel "${name}"`);
|
|
158
|
-
const envelope: DemuxPacketEnvelope = { channel: name, packet };
|
|
159
|
-
base.send!(envelope as unknown as RpcPacket);
|
|
160
|
-
},
|
|
161
|
-
registerHandler(handler) {
|
|
162
|
-
if (disposed) throw new Error(`Demuxer disposed; cannot register on channel "${name}"`);
|
|
163
|
-
if (state.handler) {
|
|
164
|
-
throw new Error(`Channel "${name}" already has a handler on this demuxer`);
|
|
165
|
-
}
|
|
166
|
-
state.handler = handler;
|
|
167
|
-
|
|
168
|
-
sendHello(name);
|
|
169
|
-
|
|
170
|
-
if (state.peerSawUs) {
|
|
171
|
-
settleReady(state, state.resolveReady);
|
|
172
|
-
} else if (!state.readySettled && !state.readyTimer) {
|
|
173
|
-
state.readyTimer = setTimeout(() => {
|
|
174
|
-
settleReady(state, () =>
|
|
175
|
-
state.rejectReady(new Error(`Channel "${name}" ready timed out after ${readyTimeout}ms`))
|
|
176
|
-
);
|
|
177
|
-
}, readyTimeout);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Drain after handshake bookkeeping so a thrown handler doesn't
|
|
181
|
-
// leave HELLO unsent or the ready promise un-armed. Handler errors
|
|
182
|
-
// during drain are swallowed so one bad packet doesn't drop the rest;
|
|
183
|
-
// synchronous throws from RPC handlers are a consumer bug.
|
|
184
|
-
if (state.pending.length > 0) {
|
|
185
|
-
const drained = state.pending;
|
|
186
|
-
state.pending = [];
|
|
187
|
-
for (const packet of drained) {
|
|
188
|
-
try { handler(packet); } catch { /* drain continues */ }
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
unregisterHandler() {
|
|
193
|
-
state.handler = undefined;
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
bindTo(rpc) {
|
|
199
|
-
rpc.setTransport(transport);
|
|
200
|
-
return state.ready;
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
},
|
|
204
|
-
dispose() {
|
|
205
|
-
if (disposed) return;
|
|
206
|
-
disposed = true;
|
|
207
|
-
for (const state of channels.values()) {
|
|
208
|
-
if (state.readyTimer) clearTimeout(state.readyTimer);
|
|
209
|
-
if (!state.readySettled) {
|
|
210
|
-
state.readySettled = true;
|
|
211
|
-
state.rejectReady(new Error("Demuxer disposed"));
|
|
212
|
-
}
|
|
213
|
-
state.pending.length = 0;
|
|
214
|
-
}
|
|
215
|
-
channels.clear();
|
|
216
|
-
base.unregisterHandler?.();
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
}
|
package/src/shared/rpcWire.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { pack, unpack } from "msgpackr";
|
|
2
|
-
import type { RpcPacket } from "./rpc";
|
|
3
|
-
import {
|
|
4
|
-
RPC_AUTH_TAG_LENGTH,
|
|
5
|
-
RPC_FRAME_VERSION,
|
|
6
|
-
RPC_IV_LENGTH
|
|
7
|
-
} from "./rpcWireConstants";
|
|
8
|
-
|
|
9
|
-
export function encodeRpcPacket(packet: RpcPacket): Uint8Array {
|
|
10
|
-
return pack(packet) as Uint8Array;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function decodeRpcPacket(data: Uint8Array): RpcPacket {
|
|
14
|
-
return unpack(data) as RpcPacket;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function createEncryptedRpcFrame(
|
|
18
|
-
iv: Uint8Array,
|
|
19
|
-
encryptedPayload: Uint8Array
|
|
20
|
-
): Uint8Array {
|
|
21
|
-
if (iv.byteLength !== RPC_IV_LENGTH) {
|
|
22
|
-
throw new Error(`Invalid RPC IV length: expected ${RPC_IV_LENGTH}, got ${iv.byteLength}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const frame = new Uint8Array(1 + RPC_IV_LENGTH + encryptedPayload.byteLength);
|
|
26
|
-
frame[0] = RPC_FRAME_VERSION;
|
|
27
|
-
frame.set(iv, 1);
|
|
28
|
-
frame.set(encryptedPayload, 1 + RPC_IV_LENGTH);
|
|
29
|
-
return frame;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function parseEncryptedRpcFrame(frame: Uint8Array) {
|
|
33
|
-
if (frame.byteLength < 1 + RPC_IV_LENGTH + RPC_AUTH_TAG_LENGTH) {
|
|
34
|
-
throw new Error("Invalid RPC frame: payload is too short.");
|
|
35
|
-
}
|
|
36
|
-
if (frame[0] !== RPC_FRAME_VERSION) {
|
|
37
|
-
throw new Error(`Unsupported RPC frame version: ${frame[0]}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
iv: frame.subarray(1, 1 + RPC_IV_LENGTH),
|
|
42
|
-
encryptedPayload: frame.subarray(1 + RPC_IV_LENGTH)
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function asUint8Array(data: ArrayBuffer | ArrayBufferView | Uint8Array): Uint8Array {
|
|
47
|
-
if (data instanceof Uint8Array) {
|
|
48
|
-
return data;
|
|
49
|
-
}
|
|
50
|
-
if (ArrayBuffer.isView(data)) {
|
|
51
|
-
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
52
|
-
}
|
|
53
|
-
return new Uint8Array(data);
|
|
54
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineBunRpc,
|
|
3
|
-
type BuniteRpcConfig,
|
|
4
|
-
type BuniteRpcSchema
|
|
5
|
-
} from "./rpc";
|
|
6
|
-
import { createWebSocketTransport, type WebSocketLike } from "./webSocketTransport";
|
|
7
|
-
import { log } from "./log";
|
|
8
|
-
|
|
9
|
-
export type WebRpcClient<Schema extends BuniteRpcSchema = BuniteRpcSchema> = {
|
|
10
|
-
ws: WebSocketLike;
|
|
11
|
-
rpc: ReturnType<typeof defineBunRpc<Schema>>;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function createWebRpcHandler<Schema extends BuniteRpcSchema>(
|
|
15
|
-
config: BuniteRpcConfig<Schema, "bun">
|
|
16
|
-
) {
|
|
17
|
-
type Entry = { client: WebRpcClient<Schema>; receive: (raw: ArrayBuffer | Uint8Array) => void };
|
|
18
|
-
|
|
19
|
-
const connections = new WeakMap<WebSocketLike, Entry>();
|
|
20
|
-
const webClients = new Set<WebRpcClient<Schema>>();
|
|
21
|
-
|
|
22
|
-
const handler = {
|
|
23
|
-
open(ws: WebSocketLike) {
|
|
24
|
-
const pipe = createWebSocketTransport(ws);
|
|
25
|
-
const rpc = defineBunRpc(config);
|
|
26
|
-
rpc.setTransport(pipe.transport);
|
|
27
|
-
|
|
28
|
-
const client: WebRpcClient<Schema> = { ws, rpc: rpc as WebRpcClient<Schema>["rpc"] };
|
|
29
|
-
|
|
30
|
-
connections.set(ws, { client, receive: pipe.receive });
|
|
31
|
-
webClients.add(client);
|
|
32
|
-
handler.onWebClientConnected?.(client);
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
message(ws: WebSocketLike, raw: string | Buffer | ArrayBuffer | Uint8Array) {
|
|
36
|
-
if (typeof raw === "string") return;
|
|
37
|
-
const entry = connections.get(ws);
|
|
38
|
-
if (!entry) return;
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
entry.receive(raw);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
log.error("Web RPC packet handler error", error);
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
close(ws: WebSocketLike) {
|
|
48
|
-
const entry = connections.get(ws);
|
|
49
|
-
if (!entry) return;
|
|
50
|
-
|
|
51
|
-
entry.client.rpc.dispose();
|
|
52
|
-
webClients.delete(entry.client);
|
|
53
|
-
connections.delete(ws);
|
|
54
|
-
handler.onWebClientDisconnected?.(entry.client);
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
webClients: webClients as ReadonlySet<WebRpcClient<Schema>>,
|
|
58
|
-
|
|
59
|
-
broadcast<M extends keyof Schema["bun"]["messages"]>(
|
|
60
|
-
messageName: M,
|
|
61
|
-
...args: void extends Schema["bun"]["messages"][M]
|
|
62
|
-
? []
|
|
63
|
-
: undefined extends Schema["bun"]["messages"][M]
|
|
64
|
-
? [payload?: Schema["bun"]["messages"][M]]
|
|
65
|
-
: [payload: Schema["bun"]["messages"][M]]
|
|
66
|
-
) {
|
|
67
|
-
for (const client of webClients) {
|
|
68
|
-
(client.rpc.send as any)(messageName, ...args);
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
onWebClientConnected: undefined as ((client: WebRpcClient<Schema>) => void) | undefined,
|
|
73
|
-
onWebClientDisconnected: undefined as ((client: WebRpcClient<Schema>) => void) | undefined,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return handler;
|
|
77
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { registerBuniteWebviewPolyfill } from "../shared/webviewPolyfill";
|
|
2
|
-
import {
|
|
3
|
-
defineWebviewRpc,
|
|
4
|
-
type BuniteRpcConfig,
|
|
5
|
-
type RpcPacket,
|
|
6
|
-
type BuniteRpcSchema,
|
|
7
|
-
type RpcSchema,
|
|
8
|
-
type RpcTransport,
|
|
9
|
-
type RpcWithTransport
|
|
10
|
-
} from "../shared/rpc";
|
|
11
|
-
import { createRpcTransportDemuxer, type RpcChannelHandle, type RpcTransportDemuxer, type RpcTransportDemuxerOptions } from "../shared/rpcDemux";
|
|
12
|
-
import { createWebSocketTransport, type WebSocketLike, type WebSocketTransportPipe } from "../shared/webSocketTransport";
|
|
13
|
-
import { decodeRpcPacket, encodeRpcPacket } from "../shared/rpcWire";
|
|
14
|
-
import { log } from "../shared/log";
|
|
15
|
-
|
|
16
|
-
type BuniteWindowGlobals = Window &
|
|
17
|
-
typeof globalThis & {
|
|
18
|
-
__buniteWebviewId?: number;
|
|
19
|
-
__buniteRpcSocketPort?: number;
|
|
20
|
-
__bunite?: {
|
|
21
|
-
receiveMessageFromBun?: (message: unknown) => void;
|
|
22
|
-
};
|
|
23
|
-
__bunite_encrypt?: (data: Uint8Array) => Promise<Uint8Array>;
|
|
24
|
-
__bunite_decrypt?: (data: Uint8Array) => Promise<Uint8Array>;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type BuniteEnv = {
|
|
28
|
-
window: BuniteWindowGlobals | null;
|
|
29
|
-
webviewId: number | undefined;
|
|
30
|
-
rpcPort: number | undefined;
|
|
31
|
-
isNative: boolean;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function readBuniteEnv(): BuniteEnv {
|
|
35
|
-
const w = typeof window !== "undefined" ? (window as BuniteWindowGlobals) : null;
|
|
36
|
-
const webviewId = w?.__buniteWebviewId;
|
|
37
|
-
const rpcPort = w?.__buniteRpcSocketPort;
|
|
38
|
-
return { window: w, webviewId, rpcPort, isNative: webviewId != null && rpcPort != null };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
42
|
-
const copy = new Uint8Array(bytes.byteLength);
|
|
43
|
-
copy.set(bytes);
|
|
44
|
-
return copy.buffer;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class BuniteView<T extends RpcWithTransport = RpcWithTransport> {
|
|
48
|
-
bunSocket?: WebSocket;
|
|
49
|
-
rpc?: T;
|
|
50
|
-
readonly transport: RpcTransport;
|
|
51
|
-
|
|
52
|
-
private env: BuniteEnv;
|
|
53
|
-
private handler?: (packet: RpcPacket) => void;
|
|
54
|
-
private pendingPackets: RpcPacket[] = [];
|
|
55
|
-
|
|
56
|
-
constructor(config?: { rpc?: T }) {
|
|
57
|
-
registerBuniteWebviewPolyfill();
|
|
58
|
-
this.env = readBuniteEnv();
|
|
59
|
-
this.rpc = config?.rpc;
|
|
60
|
-
|
|
61
|
-
this.transport = {
|
|
62
|
-
send: (packet) => {
|
|
63
|
-
if (this.bunSocket?.readyState === WebSocket.OPEN) {
|
|
64
|
-
this.sendPacket(packet);
|
|
65
|
-
} else if (this.bunSocket?.readyState === WebSocket.CONNECTING) {
|
|
66
|
-
this.pendingPackets.push(packet);
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
registerHandler: (h) => { this.handler = h; },
|
|
70
|
-
unregisterHandler: () => { this.handler = undefined; }
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
this.initSocketToBun();
|
|
74
|
-
if (this.env.isNative && this.env.window) {
|
|
75
|
-
this.env.window.__bunite ??= {};
|
|
76
|
-
this.env.window.__bunite.receiveMessageFromBun = (message) => {
|
|
77
|
-
this.handler?.(message as RpcPacket);
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
this.rpc?.setTransport(this.transport);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private sendPacket(packet: RpcPacket) {
|
|
84
|
-
if (this.env.isNative) {
|
|
85
|
-
void this.bunBridge(packet).catch((error) => {
|
|
86
|
-
log.error("Failed to send RPC packet", error);
|
|
87
|
-
});
|
|
88
|
-
} else {
|
|
89
|
-
this.bunSocket!.send(toArrayBuffer(encodeRpcPacket(packet)));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
initSocketToBun() {
|
|
94
|
-
if (!this.env.isNative) {
|
|
95
|
-
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
|
96
|
-
const socket = new WebSocket(`${proto}//${location.host}/rpc`);
|
|
97
|
-
socket.binaryType = "arraybuffer";
|
|
98
|
-
this.bunSocket = socket;
|
|
99
|
-
|
|
100
|
-
socket.addEventListener("message", async (event) => {
|
|
101
|
-
const bytes = await messageToUint8Array(event.data);
|
|
102
|
-
if (!bytes) return;
|
|
103
|
-
try {
|
|
104
|
-
this.handler?.(decodeRpcPacket(bytes));
|
|
105
|
-
} catch (error) {
|
|
106
|
-
log.error("Failed to parse WebSocket message", error);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
} else {
|
|
110
|
-
// Share a single WebSocket with the preload's bunite.invoke.
|
|
111
|
-
const globals = this.env.window as any;
|
|
112
|
-
globals.__bunite ??= {};
|
|
113
|
-
const existing = globals.__bunite._socket;
|
|
114
|
-
if (existing && existing.readyState <= WebSocket.OPEN) {
|
|
115
|
-
this.bunSocket = existing;
|
|
116
|
-
} else {
|
|
117
|
-
const socket = new WebSocket(
|
|
118
|
-
`ws://localhost:${this.env.rpcPort}/socket?webviewId=${this.env.webviewId}`
|
|
119
|
-
);
|
|
120
|
-
socket.binaryType = "arraybuffer";
|
|
121
|
-
this.bunSocket = socket;
|
|
122
|
-
globals.__bunite._socket = socket;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
this.bunSocket!.addEventListener("message", async (event) => {
|
|
126
|
-
const binaryMessage = await messageToUint8Array(event.data);
|
|
127
|
-
if (!binaryMessage) return;
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const decrypt = this.env.window?.__bunite_decrypt;
|
|
131
|
-
if (!decrypt) {
|
|
132
|
-
log.error("No decrypt function available in preload globals");
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
const decrypted = await decrypt(binaryMessage);
|
|
136
|
-
const packet = decodeRpcPacket(decrypted);
|
|
137
|
-
if ((packet as any).scope === "global") return;
|
|
138
|
-
this.handler?.(packet);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
log.error("Failed to parse message from Bun", error);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
this.bunSocket!.addEventListener("open", () => {
|
|
146
|
-
for (const packet of this.pendingPackets) this.sendPacket(packet);
|
|
147
|
-
this.pendingPackets = [];
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
this.bunSocket!.addEventListener("error", () => {
|
|
151
|
-
log.error("RPC WebSocket error");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
this.bunSocket!.addEventListener("close", () => {
|
|
155
|
-
if (this.pendingPackets.length > 0) {
|
|
156
|
-
log.error(`RPC WebSocket closed with ${this.pendingPackets.length} pending packets`);
|
|
157
|
-
this.pendingPackets = [];
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async bunBridge(message: RpcPacket) {
|
|
163
|
-
if (this.bunSocket?.readyState !== WebSocket.OPEN) return;
|
|
164
|
-
|
|
165
|
-
const encrypt = this.env.window?.__bunite_encrypt;
|
|
166
|
-
if (!encrypt) {
|
|
167
|
-
log.error("No encrypt function available in preload globals");
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const encrypted = await encrypt(encodeRpcPacket(message));
|
|
172
|
-
this.bunSocket.send(toArrayBuffer(encrypted));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function messageToUint8Array(data: unknown) {
|
|
178
|
-
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
179
|
-
if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
|
|
180
|
-
if (data instanceof Uint8Array) return data;
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export { log, type LogLevel } from "../shared/log";
|
|
185
|
-
export { createRpcTransportDemuxer, createWebSocketTransport, defineWebviewRpc, registerBuniteWebviewPolyfill };
|
|
186
|
-
|
|
187
|
-
export type {
|
|
188
|
-
BuniteRpcConfig,
|
|
189
|
-
BuniteRpcSchema,
|
|
190
|
-
RpcChannelHandle,
|
|
191
|
-
RpcSchema,
|
|
192
|
-
RpcTransportDemuxer,
|
|
193
|
-
RpcTransportDemuxerOptions,
|
|
194
|
-
WebSocketLike,
|
|
195
|
-
WebSocketTransportPipe
|
|
196
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|