bunite-core 0.17.1 → 0.17.3
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/host/core/App.ts +28 -18
- package/src/host/core/BrowserView.ts +333 -116
- package/src/host/core/BrowserWindow.ts +44 -22
- package/src/host/core/Socket.ts +26 -9
- package/src/host/core/SurfaceBrowserIPC.ts +15 -5
- package/src/host/core/SurfaceManager.ts +345 -158
- package/src/host/core/SurfaceRegistry.ts +1 -1
- package/src/host/core/inputDispatch.ts +71 -42
- package/src/host/core/singleInstanceLock.ts +10 -2
- package/src/host/core/windowCap.ts +42 -23
- package/src/host/encryptedPipe.ts +20 -3
- package/src/host/events/appEvents.ts +1 -1
- package/src/host/events/eventEmitter.ts +6 -6
- package/src/host/events/webviewEvents.ts +11 -4
- package/src/host/events/windowEvents.ts +1 -2
- package/src/host/index.ts +9 -17
- package/src/host/log.ts +6 -4
- package/src/host/native.ts +271 -163
- package/src/host/paths.ts +18 -15
- package/src/host/platform.ts +2 -4
- package/src/host/preloadBundle.ts +1 -1
- package/src/host/serveWeb.ts +45 -30
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +66 -25
- package/src/rpc/encrypt.ts +23 -4
- package/src/rpc/error.ts +2 -7
- package/src/rpc/framework.ts +105 -35
- package/src/rpc/index.ts +130 -140
- package/src/rpc/peer.ts +442 -164
- package/src/rpc/renderer.ts +29 -11
- package/src/rpc/schema.ts +86 -61
- package/src/rpc/stream.ts +12 -3
- package/src/rpc/transport.ts +76 -12
- package/src/rpc/wire.ts +23 -10
- package/src/webview/native.ts +150 -73
- package/src/webview/polyfill.ts +184 -48
package/src/rpc/renderer.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AnyCapDef,
|
|
3
|
+
type ClientOf,
|
|
4
|
+
type Connection,
|
|
2
5
|
createConnection,
|
|
3
6
|
createFrameTransport,
|
|
4
7
|
createWebSocketPipe,
|
|
5
|
-
type Connection,
|
|
6
|
-
type CapDef,
|
|
7
8
|
type Schema,
|
|
8
9
|
type SchemaRoots,
|
|
9
|
-
type ClientOf,
|
|
10
10
|
type WebSocketLike,
|
|
11
11
|
} from "./index";
|
|
12
12
|
|
|
@@ -22,8 +22,10 @@ export interface BuniteWebGlobal {
|
|
|
22
22
|
declare global {
|
|
23
23
|
interface Window {
|
|
24
24
|
host?: {
|
|
25
|
-
bootstrap<C extends
|
|
26
|
-
bootstrap<R extends SchemaRoots>(
|
|
25
|
+
bootstrap<C extends AnyCapDef>(cap: C): Promise<ClientOf<C>>;
|
|
26
|
+
bootstrap<R extends SchemaRoots>(
|
|
27
|
+
schema: Schema<R>,
|
|
28
|
+
): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
|
|
27
29
|
runtime(): Promise<ClientOf<typeof import("./framework").RuntimeCap>>;
|
|
28
30
|
releaseRef(proxy: unknown): Promise<void>;
|
|
29
31
|
/** Full Connection for renderer-as-server (serve / serveAll / unserve / replace / on). */
|
|
@@ -44,7 +46,13 @@ function isNative(): boolean {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
function ensureWebConnection(path = "/rpc"): Promise<Connection> {
|
|
47
|
-
|
|
49
|
+
// Drop a closed cached conn so a disconnect (WS close/error → onClose) leads to
|
|
50
|
+
// a fresh reconnect on the next call, instead of handing back a dead connection.
|
|
51
|
+
if (_webConn) {
|
|
52
|
+
if (!_webConn.closed) return Promise.resolve(_webConn);
|
|
53
|
+
_webConn = null;
|
|
54
|
+
_webConnPromise = null;
|
|
55
|
+
}
|
|
48
56
|
// Host-provided shared Connection — cross-bundle reachability for renderer
|
|
49
57
|
// ecosystems (e.g. extension hosts) that bundle bunite-core 0-externals per
|
|
50
58
|
// plugin. Page-author trust applies; bunite policy/attestation still gates.
|
|
@@ -61,8 +69,16 @@ function ensureWebConnection(path = "/rpc"): Promise<Connection> {
|
|
|
61
69
|
const ws = new WebSocket(`${proto}//${location.host}${path}`);
|
|
62
70
|
ws.binaryType = "arraybuffer";
|
|
63
71
|
await new Promise<void>((resolve, reject) => {
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
const onOpen = () => {
|
|
73
|
+
ws.removeEventListener("error", onError);
|
|
74
|
+
resolve();
|
|
75
|
+
};
|
|
76
|
+
const onError = () => {
|
|
77
|
+
ws.removeEventListener("open", onOpen);
|
|
78
|
+
reject(new Error("web RPC ws connect failed"));
|
|
79
|
+
};
|
|
80
|
+
ws.addEventListener("open", onOpen, { once: true });
|
|
81
|
+
ws.addEventListener("error", onError, { once: true });
|
|
66
82
|
});
|
|
67
83
|
const conn = createConnection({
|
|
68
84
|
transport: createFrameTransport(createWebSocketPipe(ws as unknown as WebSocketLike)),
|
|
@@ -79,9 +95,11 @@ function ensureWebConnection(path = "/rpc"): Promise<Connection> {
|
|
|
79
95
|
return attempt;
|
|
80
96
|
}
|
|
81
97
|
|
|
82
|
-
export function bootstrap<C extends
|
|
83
|
-
export function bootstrap<R extends SchemaRoots>(
|
|
84
|
-
|
|
98
|
+
export function bootstrap<C extends AnyCapDef>(cap: C): Promise<ClientOf<C>>;
|
|
99
|
+
export function bootstrap<R extends SchemaRoots>(
|
|
100
|
+
schema: Schema<R>,
|
|
101
|
+
): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
|
|
102
|
+
export async function bootstrap(target: AnyCapDef | Schema<any>): Promise<unknown> {
|
|
85
103
|
if (isNative()) {
|
|
86
104
|
if (!window.host?.bootstrap) throw new Error("host preload not ready");
|
|
87
105
|
return (window.host.bootstrap as (t: unknown) => Promise<unknown>)(target);
|
package/src/rpc/schema.ts
CHANGED
|
@@ -10,31 +10,37 @@ declare const EXPORTED_CAP_BRAND: unique symbol;
|
|
|
10
10
|
|
|
11
11
|
export type ReturnsKind = "type" | "cap" | "capArray" | "capRecord";
|
|
12
12
|
|
|
13
|
-
export type CapRefToken<C extends
|
|
13
|
+
export type CapRefToken<C extends AnyCapDef> = {
|
|
14
14
|
readonly [CAP_REF_TAG]: true;
|
|
15
15
|
readonly cap: C;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
export type CapArrayToken<C extends
|
|
18
|
+
export type CapArrayToken<C extends AnyCapDef> = {
|
|
19
19
|
readonly [CAP_ARRAY_TAG]: true;
|
|
20
20
|
readonly cap: C;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
export type CapRecordToken<C extends
|
|
23
|
+
export type CapRecordToken<C extends AnyCapDef> = {
|
|
24
24
|
readonly [CAP_RECORD_TAG]: true;
|
|
25
25
|
readonly cap: C;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
export type AnyCapToken<C extends
|
|
28
|
+
export type AnyCapToken<C extends AnyCapDef = AnyCapDef> =
|
|
29
29
|
| CapRefToken<C>
|
|
30
30
|
| CapArrayToken<C>
|
|
31
31
|
| CapRecordToken<C>;
|
|
32
32
|
|
|
33
|
-
function _cap<C extends
|
|
33
|
+
function _cap<C extends AnyCapDef>(c: C): CapRefToken<C> {
|
|
34
34
|
return { [CAP_REF_TAG]: true, cap: c };
|
|
35
35
|
}
|
|
36
|
-
_cap.array = <C extends
|
|
37
|
-
|
|
36
|
+
_cap.array = <C extends AnyCapDef>(c: C): CapArrayToken<C> => ({
|
|
37
|
+
[CAP_ARRAY_TAG]: true,
|
|
38
|
+
cap: c,
|
|
39
|
+
});
|
|
40
|
+
_cap.record = <C extends AnyCapDef>(c: C): CapRecordToken<C> => ({
|
|
41
|
+
[CAP_RECORD_TAG]: true,
|
|
42
|
+
cap: c,
|
|
43
|
+
});
|
|
38
44
|
export const cap = _cap;
|
|
39
45
|
|
|
40
46
|
export function isCapRef(v: unknown): v is CapRefToken<any> {
|
|
@@ -75,15 +81,18 @@ export function isStreamDef(v: unknown): v is StreamDef {
|
|
|
75
81
|
return typeof v === "object" && v !== null && (v as any)[STREAM_TAG] === true;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
export function call<P, C extends
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
export function call<P, C extends AnyCapDef>(opts: {
|
|
85
|
+
returns: CapRefToken<C>;
|
|
86
|
+
idempotent?: boolean;
|
|
87
|
+
}): CallDef<P, CapRefToken<C>>;
|
|
88
|
+
export function call<P, C extends AnyCapDef>(opts: {
|
|
89
|
+
returns: CapArrayToken<C>;
|
|
90
|
+
idempotent?: boolean;
|
|
91
|
+
}): CallDef<P, CapArrayToken<C>>;
|
|
92
|
+
export function call<P, C extends AnyCapDef>(opts: {
|
|
93
|
+
returns: CapRecordToken<C>;
|
|
94
|
+
idempotent?: boolean;
|
|
95
|
+
}): CallDef<P, CapRecordToken<C>>;
|
|
87
96
|
export function call<P = void, R = void>(opts?: { idempotent?: boolean }): CallDef<P, R>;
|
|
88
97
|
export function call(opts?: { idempotent?: boolean; returns?: AnyCapToken }): CallDef<any, any> {
|
|
89
98
|
return {
|
|
@@ -93,7 +102,9 @@ export function call(opts?: { idempotent?: boolean; returns?: AnyCapToken }): Ca
|
|
|
93
102
|
};
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
export function stream<P = void, Y = unknown>(opts?: {
|
|
105
|
+
export function stream<P = void, Y = unknown>(opts?: {
|
|
106
|
+
hint?: Record<string, unknown>;
|
|
107
|
+
}): StreamDef<P, Y> {
|
|
97
108
|
return { [STREAM_TAG]: true, hint: opts?.hint };
|
|
98
109
|
}
|
|
99
110
|
|
|
@@ -104,7 +115,10 @@ export interface DisposalSpec<M extends MethodsRecord = MethodsRecord> {
|
|
|
104
115
|
|
|
105
116
|
export type MethodsRecord = Record<string, MethodDef>;
|
|
106
117
|
|
|
107
|
-
export interface CapDef<
|
|
118
|
+
export interface CapDef<
|
|
119
|
+
M extends MethodsRecord = MethodsRecord,
|
|
120
|
+
D extends DisposalSpec<M> | undefined = undefined,
|
|
121
|
+
> {
|
|
108
122
|
readonly [CAP_TAG]: true;
|
|
109
123
|
readonly name: string;
|
|
110
124
|
readonly version?: string;
|
|
@@ -112,16 +126,17 @@ export interface CapDef<M extends MethodsRecord = MethodsRecord, D extends Dispo
|
|
|
112
126
|
readonly disposal: D;
|
|
113
127
|
}
|
|
114
128
|
|
|
129
|
+
export type AnyCapDef = CapDef<any, any>;
|
|
130
|
+
|
|
115
131
|
export interface DefineCapOpts<M extends MethodsRecord, D extends DisposalSpec<M> | undefined> {
|
|
116
132
|
version?: string | number;
|
|
117
133
|
disposal?: D;
|
|
118
134
|
}
|
|
119
135
|
|
|
120
|
-
export function defineCap<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
): CapDef<M, D> {
|
|
136
|
+
export function defineCap<
|
|
137
|
+
M extends MethodsRecord,
|
|
138
|
+
D extends DisposalSpec<M> | undefined = undefined,
|
|
139
|
+
>(name: string, methods: M, opts?: DefineCapOpts<M, D>): CapDef<M, D> {
|
|
125
140
|
return {
|
|
126
141
|
[CAP_TAG]: true,
|
|
127
142
|
name,
|
|
@@ -136,7 +151,7 @@ export function isCapDef(v: unknown): v is CapDef {
|
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
// Schema = grouping sugar (Record<rootName, CapDef>). TS atomicity for serveAll.
|
|
139
|
-
export type SchemaRoots = Record<string,
|
|
154
|
+
export type SchemaRoots = Record<string, AnyCapDef>;
|
|
140
155
|
|
|
141
156
|
export interface Schema<R extends SchemaRoots = SchemaRoots> {
|
|
142
157
|
readonly [SCHEMA_TAG]: true;
|
|
@@ -155,7 +170,7 @@ export function isSchema(v: unknown): v is Schema {
|
|
|
155
170
|
return typeof v === "object" && v !== null && (v as any)[SCHEMA_TAG] === true;
|
|
156
171
|
}
|
|
157
172
|
|
|
158
|
-
export interface ExportedCap<C extends
|
|
173
|
+
export interface ExportedCap<C extends AnyCapDef> {
|
|
159
174
|
readonly [EXPORTED_CAP_BRAND]: true;
|
|
160
175
|
readonly cap: C;
|
|
161
176
|
readonly capId: number;
|
|
@@ -168,7 +183,7 @@ export interface CallCtx {
|
|
|
168
183
|
attestation: Attestation;
|
|
169
184
|
signal: AbortSignal;
|
|
170
185
|
deadline?: number;
|
|
171
|
-
exportCap<C extends
|
|
186
|
+
exportCap<C extends AnyCapDef>(capDef: C, impl: ImplOf<C>): ExportedCap<C>;
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
export interface Attestation {
|
|
@@ -184,45 +199,55 @@ export interface Attestation {
|
|
|
184
199
|
type MaybePromise<T> = T | Promise<T>;
|
|
185
200
|
|
|
186
201
|
type ServerReturn<R> =
|
|
187
|
-
R extends CapRefToken<infer C>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
R extends CapRefToken<infer C>
|
|
203
|
+
? ExportedCap<C>
|
|
204
|
+
: R extends CapArrayToken<infer C>
|
|
205
|
+
? ExportedCap<C>[]
|
|
206
|
+
: R extends CapRecordToken<infer C>
|
|
207
|
+
? Record<string, ExportedCap<C>>
|
|
208
|
+
: R;
|
|
191
209
|
|
|
192
210
|
export type ClientReturn<R> =
|
|
193
|
-
R extends CapRefToken<infer C>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
?
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
R extends CapRefToken<infer C>
|
|
212
|
+
? ClientOf<C>
|
|
213
|
+
: R extends CapArrayToken<infer C>
|
|
214
|
+
? C extends CapDef<any, infer D>
|
|
215
|
+
? [D] extends [DisposalSpec]
|
|
216
|
+
? ClientOf<C>[] & Disposable
|
|
217
|
+
: ClientOf<C>[]
|
|
218
|
+
: ClientOf<C>[]
|
|
219
|
+
: R extends CapRecordToken<infer C>
|
|
220
|
+
? Record<string, ClientOf<C>>
|
|
221
|
+
: R;
|
|
201
222
|
|
|
202
223
|
export type Stream<T> = AsyncIterable<T> & Disposable & { cancel(): void };
|
|
203
224
|
|
|
204
|
-
export type ClientOf<T> =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
export type ClientOf<T> =
|
|
226
|
+
T extends CapDef<infer M, infer D>
|
|
227
|
+
? {
|
|
228
|
+
[K in keyof M]: M[K] extends CallDef<infer P, infer R>
|
|
229
|
+
? [P] extends [void]
|
|
230
|
+
? () => Promise<ClientReturn<R>>
|
|
231
|
+
: (params: P) => Promise<ClientReturn<R>>
|
|
232
|
+
: M[K] extends StreamDef<infer P, infer Y>
|
|
233
|
+
? [P] extends [void]
|
|
234
|
+
? () => Stream<Y>
|
|
235
|
+
: (params: P) => Stream<Y>
|
|
236
|
+
: never;
|
|
237
|
+
} & ([D] extends [DisposalSpec] ? Disposable : unknown)
|
|
238
|
+
: never;
|
|
239
|
+
|
|
240
|
+
export type ImplOf<T> =
|
|
241
|
+
T extends CapDef<infer M, any>
|
|
242
|
+
? {
|
|
243
|
+
[K in keyof M]: M[K] extends CallDef<infer P, infer R>
|
|
244
|
+
? (params: P, ctx: CallCtx) => MaybePromise<ServerReturn<R>>
|
|
245
|
+
: M[K] extends StreamDef<infer P, infer Y>
|
|
246
|
+
? (params: P, ctx: CallCtx) => Stream<Y>
|
|
247
|
+
: never;
|
|
248
|
+
}
|
|
249
|
+
: never;
|
|
250
|
+
|
|
251
|
+
export function methodKeys(cap: AnyCapDef): string[] {
|
|
227
252
|
return Object.keys(cap.methods);
|
|
228
253
|
}
|
package/src/rpc/stream.ts
CHANGED
|
@@ -4,7 +4,10 @@ type Setup<T> = (emit: (chunk: T) => void, signal: AbortSignal) => void | (() =>
|
|
|
4
4
|
|
|
5
5
|
class ServerStream<T> implements AsyncIterable<T>, Disposable {
|
|
6
6
|
private readonly buffer: T[] = [];
|
|
7
|
-
private readonly waiters: Array<{
|
|
7
|
+
private readonly waiters: Array<{
|
|
8
|
+
resolve: (r: IteratorResult<T>) => void;
|
|
9
|
+
reject: (e: unknown) => void;
|
|
10
|
+
}> = [];
|
|
8
11
|
private readonly ctrl = new AbortController();
|
|
9
12
|
private cleanup?: () => void;
|
|
10
13
|
private ended = false;
|
|
@@ -36,7 +39,9 @@ class ServerStream<T> implements AsyncIterable<T>, Disposable {
|
|
|
36
39
|
}
|
|
37
40
|
if (this.failure) throw this.failure;
|
|
38
41
|
if (this.ended) return { value: undefined as unknown as T, done: true };
|
|
39
|
-
return new Promise<IteratorResult<T>>((resolve, reject) =>
|
|
42
|
+
return new Promise<IteratorResult<T>>((resolve, reject) =>
|
|
43
|
+
this.waiters.push({ resolve, reject }),
|
|
44
|
+
);
|
|
40
45
|
},
|
|
41
46
|
return: async (): Promise<IteratorResult<T>> => {
|
|
42
47
|
this.dispose();
|
|
@@ -57,7 +62,11 @@ class ServerStream<T> implements AsyncIterable<T>, Disposable {
|
|
|
57
62
|
if (this.ended) return;
|
|
58
63
|
this.ended = true;
|
|
59
64
|
this.ctrl.abort();
|
|
60
|
-
try {
|
|
65
|
+
try {
|
|
66
|
+
this.cleanup?.();
|
|
67
|
+
} catch {
|
|
68
|
+
/* swallow */
|
|
69
|
+
}
|
|
61
70
|
while (this.waiters.length > 0) {
|
|
62
71
|
const w = this.waiters.shift()!;
|
|
63
72
|
w.resolve({ value: undefined as unknown as T, done: true });
|
package/src/rpc/transport.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type
|
|
1
|
+
import type { CloseInfo, Transport } from "./peer";
|
|
2
|
+
import { createCodec, type Frame, isFrame } from "./wire";
|
|
3
3
|
|
|
4
4
|
export interface BytesPipe {
|
|
5
5
|
send(bytes: Uint8Array): void;
|
|
6
6
|
setReceive(handler: (bytes: Uint8Array) => void): void;
|
|
7
7
|
close(): void;
|
|
8
|
+
onClose?(handler: (info?: CloseInfo) => void): void;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export interface FrameTransportOptions {
|
|
@@ -14,6 +15,16 @@ export interface FrameTransportOptions {
|
|
|
14
15
|
export function createFrameTransport(pipe: BytesPipe, opts: FrameTransportOptions = {}): Transport {
|
|
15
16
|
const codec = createCodec();
|
|
16
17
|
let handler: ((frame: Frame) => void) | undefined;
|
|
18
|
+
let closeHandler: ((info?: CloseInfo) => void) | undefined;
|
|
19
|
+
let closeFired = false;
|
|
20
|
+
// Fire once for any close cause — pipe disconnect (WS close/error) or a
|
|
21
|
+
// protocol error that forces pipe.close() — so the Connection always tears down.
|
|
22
|
+
const fireClose = (info?: CloseInfo) => {
|
|
23
|
+
if (closeFired) return;
|
|
24
|
+
closeFired = true;
|
|
25
|
+
closeHandler?.(info);
|
|
26
|
+
};
|
|
27
|
+
pipe.onClose?.(fireClose);
|
|
17
28
|
|
|
18
29
|
pipe.setReceive((bytes) => {
|
|
19
30
|
let frame: unknown;
|
|
@@ -21,11 +32,13 @@ export function createFrameTransport(pipe: BytesPipe, opts: FrameTransportOption
|
|
|
21
32
|
frame = codec.unpackr.unpack(bytes);
|
|
22
33
|
} catch (err) {
|
|
23
34
|
opts.onProtocolError?.(`unpack failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
35
|
+
fireClose();
|
|
24
36
|
pipe.close();
|
|
25
37
|
return;
|
|
26
38
|
}
|
|
27
39
|
if (!isFrame(frame)) {
|
|
28
40
|
opts.onProtocolError?.("malformed frame");
|
|
41
|
+
fireClose();
|
|
29
42
|
pipe.close();
|
|
30
43
|
return;
|
|
31
44
|
}
|
|
@@ -43,39 +56,90 @@ export function createFrameTransport(pipe: BytesPipe, opts: FrameTransportOption
|
|
|
43
56
|
close() {
|
|
44
57
|
pipe.close();
|
|
45
58
|
},
|
|
59
|
+
onClose(h) {
|
|
60
|
+
closeHandler = h;
|
|
61
|
+
},
|
|
46
62
|
};
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
type WsMessageEvent = { data: ArrayBuffer | Uint8Array | Blob | string };
|
|
66
|
+
type WsCloseEvent = { code?: number; reason?: string };
|
|
67
|
+
|
|
49
68
|
export interface WebSocketLike {
|
|
50
69
|
send(data: Uint8Array | ArrayBuffer): unknown;
|
|
51
|
-
addEventListener(type: "message", listener: (event:
|
|
52
|
-
removeEventListener?(type: "message", listener: (event:
|
|
70
|
+
addEventListener(type: "message", listener: (event: WsMessageEvent) => void): void;
|
|
71
|
+
removeEventListener?(type: "message", listener: (event: WsMessageEvent) => void): void;
|
|
53
72
|
close?(): void;
|
|
54
73
|
}
|
|
55
74
|
|
|
75
|
+
// close/error listeners — real WebSockets have them; kept off the public type so
|
|
76
|
+
// message-only implementations stay assignable.
|
|
77
|
+
type WsLifecycle = {
|
|
78
|
+
addEventListener(type: "close", listener: (event: WsCloseEvent) => void): void;
|
|
79
|
+
addEventListener(type: "error", listener: (event: unknown) => void): void;
|
|
80
|
+
removeEventListener?(type: "close", listener: (event: WsCloseEvent) => void): void;
|
|
81
|
+
removeEventListener?(type: "error", listener: (event: unknown) => void): void;
|
|
82
|
+
};
|
|
83
|
+
|
|
56
84
|
export function createWebSocketPipe(ws: WebSocketLike): BytesPipe {
|
|
57
85
|
if ("binaryType" in ws) {
|
|
58
|
-
try {
|
|
86
|
+
try {
|
|
87
|
+
(ws as { binaryType?: string }).binaryType = "arraybuffer";
|
|
88
|
+
} catch {
|
|
89
|
+
/* readonly in some envs */
|
|
90
|
+
}
|
|
59
91
|
}
|
|
92
|
+
const wsLife = ws as WebSocketLike & WsLifecycle;
|
|
60
93
|
let handler: ((bytes: Uint8Array) => void) | undefined;
|
|
61
|
-
|
|
94
|
+
let closeHandler: ((info?: CloseInfo) => void) | undefined;
|
|
95
|
+
let closeFired = false;
|
|
96
|
+
const onMessage = (event: WsMessageEvent) => {
|
|
62
97
|
if (!handler) return;
|
|
63
98
|
const d = event.data;
|
|
64
|
-
if (d instanceof Uint8Array) {
|
|
65
|
-
|
|
99
|
+
if (d instanceof Uint8Array) {
|
|
100
|
+
handler(d);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (d instanceof ArrayBuffer) {
|
|
104
|
+
handler(new Uint8Array(d));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
66
107
|
if (typeof Blob !== "undefined" && d instanceof Blob) {
|
|
67
108
|
void d.arrayBuffer().then((buf) => handler?.(new Uint8Array(buf)));
|
|
68
109
|
return;
|
|
69
110
|
}
|
|
70
111
|
};
|
|
112
|
+
const detach = () => {
|
|
113
|
+
ws.removeEventListener?.("message", onMessage);
|
|
114
|
+
wsLife.removeEventListener?.("close", onClose);
|
|
115
|
+
wsLife.removeEventListener?.("error", onError);
|
|
116
|
+
};
|
|
117
|
+
const fireClose = (info?: CloseInfo) => {
|
|
118
|
+
if (closeFired) return;
|
|
119
|
+
closeFired = true;
|
|
120
|
+
detach();
|
|
121
|
+
closeHandler?.(info);
|
|
122
|
+
};
|
|
123
|
+
const onClose = (event: WsCloseEvent) => fireClose({ code: event?.code, reason: event?.reason });
|
|
124
|
+
// `error` is usually followed by `close` (which carries code/reason); defer so
|
|
125
|
+
// close wins, and this still fires for an error with no following close.
|
|
126
|
+
const onError = () => setTimeout(() => fireClose(), 0);
|
|
71
127
|
ws.addEventListener("message", onMessage);
|
|
128
|
+
wsLife.addEventListener("close", onClose);
|
|
129
|
+
wsLife.addEventListener("error", onError);
|
|
72
130
|
return {
|
|
73
|
-
send(bytes) {
|
|
74
|
-
|
|
131
|
+
send(bytes) {
|
|
132
|
+
ws.send(bytes);
|
|
133
|
+
},
|
|
134
|
+
setReceive(h) {
|
|
135
|
+
handler = h;
|
|
136
|
+
},
|
|
137
|
+
onClose(h) {
|
|
138
|
+
closeHandler = h;
|
|
139
|
+
},
|
|
75
140
|
close() {
|
|
76
|
-
|
|
141
|
+
detach();
|
|
77
142
|
ws.close?.();
|
|
78
143
|
},
|
|
79
144
|
};
|
|
80
145
|
}
|
|
81
|
-
|
package/src/rpc/wire.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Packr, Unpackr
|
|
1
|
+
import { addExtension, Packr, Unpackr } from "msgpackr";
|
|
2
2
|
import type { IpcStatus } from "./error";
|
|
3
3
|
|
|
4
4
|
export type u32 = number;
|
|
@@ -75,28 +75,41 @@ export function isFrame(value: unknown): value is Frame {
|
|
|
75
75
|
if (typeof op !== "string" || !OPS.has(op as Frame["op"])) return false;
|
|
76
76
|
switch (op as Frame["op"]) {
|
|
77
77
|
case "hello":
|
|
78
|
-
return
|
|
79
|
-
|
|
78
|
+
return (
|
|
79
|
+
f.v === 1 &&
|
|
80
|
+
(f.mode === "native" || f.mode === "web") &&
|
|
81
|
+
Array.isArray(f.features) &&
|
|
82
|
+
typeof f.maxBytes === "number" &&
|
|
83
|
+
typeof f.origin === "string"
|
|
84
|
+
);
|
|
80
85
|
case "goaway":
|
|
81
86
|
return f.reason === undefined || typeof f.reason === "string";
|
|
82
87
|
case "call":
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
return (
|
|
89
|
+
typeof f.id === "number" &&
|
|
90
|
+
typeof f.method === "string" &&
|
|
91
|
+
typeof f.target === "object" &&
|
|
92
|
+
f.target !== null &&
|
|
93
|
+
(f.target as { kind?: unknown }).kind === "cap" &&
|
|
94
|
+
typeof (f.target as { id?: unknown }).id === "number"
|
|
95
|
+
);
|
|
87
96
|
case "result":
|
|
88
97
|
return typeof f.id === "number" && (f.ok === true || f.ok === false);
|
|
89
98
|
case "cancel":
|
|
90
99
|
return typeof f.id === "number";
|
|
91
100
|
case "stream":
|
|
92
|
-
return
|
|
93
|
-
|
|
101
|
+
return (
|
|
102
|
+
typeof f.id === "number" &&
|
|
103
|
+
typeof f.ev === "string" &&
|
|
104
|
+
(f.ev === "next" || f.ev === "credit" || f.ev === "end" || f.ev === "error")
|
|
105
|
+
);
|
|
94
106
|
case "drop":
|
|
95
107
|
return Array.isArray(f.caps);
|
|
96
108
|
case "cap_revoked":
|
|
97
109
|
if (!Array.isArray(f.capIds)) return false;
|
|
98
110
|
for (const id of f.capIds) {
|
|
99
|
-
if (typeof id !== "number" || !Number.isInteger(id) || id < 0 || id >
|
|
111
|
+
if (typeof id !== "number" || !Number.isInteger(id) || id < 0 || id > 0xffffffff)
|
|
112
|
+
return false;
|
|
100
113
|
}
|
|
101
114
|
return true;
|
|
102
115
|
}
|