bunite-core 0.2.1 → 0.3.1
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 +1 -1
- package/src/bun/index.ts +3 -1
- package/src/shared/rpcDemux.ts +134 -25
- package/src/view/index.ts +3 -1
package/package.json
CHANGED
package/src/bun/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type RPCSchema,
|
|
14
14
|
type RPCWithTransport
|
|
15
15
|
} from "../shared/rpc";
|
|
16
|
-
import { createTransportDemuxer, type TransportDemuxer } from "../shared/rpcDemux";
|
|
16
|
+
import { createTransportDemuxer, type ChannelHandle, type TransportDemuxer, type TransportDemuxerOptions } from "../shared/rpcDemux";
|
|
17
17
|
import { createWebSocketTransport, type WebSocketLike, type WebSocketTransportPipe } from "../shared/webSocketTransport";
|
|
18
18
|
import { createWebRPCHandler, type WebRPCClient } from "../shared/webRpcHandler";
|
|
19
19
|
import type { MessageBoxOptions, MessageBoxResponse } from "./core/Utils";
|
|
@@ -40,11 +40,13 @@ export type {
|
|
|
40
40
|
BuniteRPCConfig,
|
|
41
41
|
BuniteRPCSchema,
|
|
42
42
|
BrowserViewOptions,
|
|
43
|
+
ChannelHandle,
|
|
43
44
|
MessageBoxOptions,
|
|
44
45
|
MessageBoxResponse,
|
|
45
46
|
RPCSchema,
|
|
46
47
|
RPCWithTransport,
|
|
47
48
|
TransportDemuxer,
|
|
49
|
+
TransportDemuxerOptions,
|
|
48
50
|
WebRPCClient,
|
|
49
51
|
WebSocketLike,
|
|
50
52
|
WebSocketTransportPipe,
|
package/src/shared/rpcDemux.ts
CHANGED
|
@@ -1,64 +1,173 @@
|
|
|
1
|
-
import type { RPCPacket, RPCTransport } from "./rpc";
|
|
1
|
+
import type { RPCPacket, RPCTransport, RPCWithTransport } from "./rpc";
|
|
2
2
|
|
|
3
|
-
type
|
|
3
|
+
type DemuxPacketEnvelope = { channel: string; packet: RPCPacket };
|
|
4
|
+
type DemuxHelloFrame = { channel: string; hello: true };
|
|
5
|
+
type DemuxFrame = DemuxPacketEnvelope | DemuxHelloFrame;
|
|
4
6
|
|
|
5
|
-
function
|
|
7
|
+
function isDemuxFrame(value: unknown): value is DemuxFrame {
|
|
6
8
|
if (typeof value !== "object" || value === null) return false;
|
|
7
|
-
|
|
8
|
-
return typeof v.channel === "string" && typeof v.packet === "object" && v.packet !== null;
|
|
9
|
+
return typeof (value as DemuxFrame).channel === "string";
|
|
9
10
|
}
|
|
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 ChannelHandle = {
|
|
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
|
+
|
|
11
30
|
export type TransportDemuxer = {
|
|
12
|
-
channel(name: string):
|
|
31
|
+
channel(name: string): ChannelHandle;
|
|
13
32
|
dispose(): void;
|
|
14
33
|
};
|
|
15
34
|
|
|
16
|
-
export
|
|
35
|
+
export type TransportDemuxerOptions = {
|
|
36
|
+
/** ms to wait for peer before `bindTo` rejects. Default 10_000. */
|
|
37
|
+
readyTimeout?: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ChannelState = {
|
|
41
|
+
handler?: (packet: RPCPacket) => void;
|
|
42
|
+
peerSawUs: boolean;
|
|
43
|
+
ready: Promise<void>;
|
|
44
|
+
resolveReady: () => void;
|
|
45
|
+
rejectReady: (error: Error) => void;
|
|
46
|
+
readySettled: boolean;
|
|
47
|
+
readyTimer?: ReturnType<typeof setTimeout>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const DEFAULT_READY_TIMEOUT = 10_000;
|
|
51
|
+
|
|
52
|
+
export function createTransportDemuxer(
|
|
53
|
+
base: RPCTransport,
|
|
54
|
+
options: TransportDemuxerOptions = {}
|
|
55
|
+
): TransportDemuxer {
|
|
17
56
|
if (!base.send || !base.registerHandler) {
|
|
18
57
|
throw new Error("createTransportDemuxer requires a base transport with both send and registerHandler");
|
|
19
58
|
}
|
|
20
59
|
|
|
21
|
-
const
|
|
60
|
+
const readyTimeout = options.readyTimeout ?? DEFAULT_READY_TIMEOUT;
|
|
61
|
+
const channels = new Map<string, ChannelState>();
|
|
22
62
|
let disposed = false;
|
|
23
63
|
|
|
64
|
+
function getOrCreateState(name: string): ChannelState {
|
|
65
|
+
let state = channels.get(name);
|
|
66
|
+
if (state) return state;
|
|
67
|
+
|
|
68
|
+
let resolveReady!: () => void;
|
|
69
|
+
let rejectReady!: (error: Error) => void;
|
|
70
|
+
const ready = new Promise<void>((resolve, reject) => {
|
|
71
|
+
resolveReady = resolve;
|
|
72
|
+
rejectReady = reject;
|
|
73
|
+
});
|
|
74
|
+
ready.catch(() => {}); // prevent unhandled rejection if consumer doesn't await
|
|
75
|
+
|
|
76
|
+
state = {
|
|
77
|
+
peerSawUs: false,
|
|
78
|
+
ready,
|
|
79
|
+
resolveReady,
|
|
80
|
+
rejectReady,
|
|
81
|
+
readySettled: false
|
|
82
|
+
};
|
|
83
|
+
channels.set(name, state);
|
|
84
|
+
return state;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function settleReady(state: ChannelState, action: () => void) {
|
|
88
|
+
if (state.readySettled) return;
|
|
89
|
+
state.readySettled = true;
|
|
90
|
+
if (state.readyTimer) clearTimeout(state.readyTimer);
|
|
91
|
+
action();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sendHello(name: string) {
|
|
95
|
+
const frame: DemuxHelloFrame = { channel: name, hello: true };
|
|
96
|
+
base.send!(frame as unknown as RPCPacket);
|
|
97
|
+
}
|
|
98
|
+
|
|
24
99
|
base.registerHandler((data) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
100
|
+
if (!isDemuxFrame(data)) return;
|
|
101
|
+
const state = getOrCreateState(data.channel);
|
|
102
|
+
|
|
103
|
+
if (isHelloFrame(data)) {
|
|
104
|
+
if (state.handler) {
|
|
105
|
+
const wasReady = state.readySettled;
|
|
106
|
+
settleReady(state, state.resolveReady);
|
|
107
|
+
if (!wasReady && !disposed) sendHello(data.channel); // echo so peer wakes up
|
|
108
|
+
} else {
|
|
109
|
+
state.peerSawUs = true;
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (isPacketEnvelope(data)) {
|
|
115
|
+
state.handler?.(data.packet);
|
|
116
|
+
}
|
|
29
117
|
});
|
|
30
118
|
|
|
31
119
|
return {
|
|
32
120
|
channel(name) {
|
|
33
|
-
|
|
121
|
+
const state = getOrCreateState(name);
|
|
34
122
|
|
|
35
|
-
|
|
123
|
+
const transport: RPCTransport = {
|
|
36
124
|
send(packet) {
|
|
37
125
|
if (disposed) throw new Error(`Demuxer disposed; cannot send on channel "${name}"`);
|
|
38
|
-
const envelope:
|
|
39
|
-
// The wire layer (msgpackr) serializes the envelope opaquely, so
|
|
40
|
-
// routing the wider type through RPCTransport.send is safe in practice.
|
|
126
|
+
const envelope: DemuxPacketEnvelope = { channel: name, packet };
|
|
41
127
|
base.send!(envelope as unknown as RPCPacket);
|
|
42
128
|
},
|
|
43
129
|
registerHandler(handler) {
|
|
44
130
|
if (disposed) throw new Error(`Demuxer disposed; cannot register on channel "${name}"`);
|
|
45
|
-
if (
|
|
131
|
+
if (state.handler) {
|
|
46
132
|
throw new Error(`Channel "${name}" already has a handler on this demuxer`);
|
|
47
133
|
}
|
|
48
|
-
|
|
49
|
-
|
|
134
|
+
state.handler = handler;
|
|
135
|
+
|
|
136
|
+
sendHello(name);
|
|
137
|
+
|
|
138
|
+
if (state.peerSawUs) {
|
|
139
|
+
settleReady(state, state.resolveReady);
|
|
140
|
+
} else if (!state.readySettled && !state.readyTimer) {
|
|
141
|
+
state.readyTimer = setTimeout(() => {
|
|
142
|
+
settleReady(state, () =>
|
|
143
|
+
state.rejectReady(new Error(`Channel "${name}" ready timed out after ${readyTimeout}ms`))
|
|
144
|
+
);
|
|
145
|
+
}, readyTimeout);
|
|
146
|
+
}
|
|
50
147
|
},
|
|
51
148
|
unregisterHandler() {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
149
|
+
state.handler = undefined;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
bindTo(rpc) {
|
|
155
|
+
rpc.setTransport(transport);
|
|
156
|
+
return state.ready;
|
|
56
157
|
}
|
|
57
158
|
};
|
|
58
159
|
},
|
|
59
160
|
dispose() {
|
|
161
|
+
if (disposed) return;
|
|
60
162
|
disposed = true;
|
|
61
|
-
|
|
163
|
+
for (const state of channels.values()) {
|
|
164
|
+
if (state.readyTimer) clearTimeout(state.readyTimer);
|
|
165
|
+
if (!state.readySettled) {
|
|
166
|
+
state.readySettled = true;
|
|
167
|
+
state.rejectReady(new Error("Demuxer disposed"));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
channels.clear();
|
|
62
171
|
base.unregisterHandler?.();
|
|
63
172
|
}
|
|
64
173
|
};
|
package/src/view/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type RPCTransport,
|
|
9
9
|
type RPCWithTransport
|
|
10
10
|
} from "../shared/rpc";
|
|
11
|
-
import { createTransportDemuxer, type TransportDemuxer } from "../shared/rpcDemux";
|
|
11
|
+
import { createTransportDemuxer, type ChannelHandle, type TransportDemuxer, type TransportDemuxerOptions } from "../shared/rpcDemux";
|
|
12
12
|
import { createWebSocketTransport, type WebSocketLike, type WebSocketTransportPipe } from "../shared/webSocketTransport";
|
|
13
13
|
import { decodeRPCPacket, encodeRPCPacket } from "../shared/rpcWire";
|
|
14
14
|
import { log } from "../shared/log";
|
|
@@ -176,8 +176,10 @@ export { createTransportDemuxer, createWebSocketTransport, defineWebviewRPC };
|
|
|
176
176
|
export type {
|
|
177
177
|
BuniteRPCConfig,
|
|
178
178
|
BuniteRPCSchema,
|
|
179
|
+
ChannelHandle,
|
|
179
180
|
RPCSchema,
|
|
180
181
|
TransportDemuxer,
|
|
182
|
+
TransportDemuxerOptions,
|
|
181
183
|
WebSocketLike,
|
|
182
184
|
WebSocketTransportPipe
|
|
183
185
|
};
|