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
@@ -0,0 +1,82 @@
1
+ import {
2
+ createConnection,
3
+ createFrameTransport,
4
+ createWebSocketPipe,
5
+ type Connection,
6
+ type Schema,
7
+ type SchemaShape,
8
+ type ClientOf,
9
+ type ServerDescriptor,
10
+ type WebSocketLike,
11
+ } from "./index";
12
+
13
+ declare global {
14
+ interface Window {
15
+ host?: {
16
+ bootstrap<S extends SchemaShape, K extends keyof S["roots"] & string>(
17
+ schema: Schema<S>,
18
+ name: K
19
+ ): Promise<ClientOf<S["roots"][K]>>;
20
+ serve<S extends SchemaShape>(descriptor: ServerDescriptor<S>): Promise<void>;
21
+ runtime(): Promise<ClientOf<typeof import("./framework").RuntimeCap>>;
22
+ releaseRef(proxy: unknown): Promise<void>;
23
+ };
24
+ }
25
+ }
26
+
27
+ export * from "./index";
28
+ export { Stream } from "./stream";
29
+
30
+ let _webConn: Connection | null = null;
31
+ let _webConnPromise: Promise<Connection> | null = null;
32
+
33
+ function isNative(): boolean {
34
+ return typeof (globalThis as { __buniteWebviewId?: number }).__buniteWebviewId === "number";
35
+ }
36
+
37
+ function ensureWebConnection(path = "/rpc"): Promise<Connection> {
38
+ if (_webConn) return Promise.resolve(_webConn);
39
+ if (_webConnPromise) return _webConnPromise;
40
+ const attempt = (async () => {
41
+ const proto = location.protocol === "https:" ? "wss:" : "ws:";
42
+ const ws = new WebSocket(`${proto}//${location.host}${path}`);
43
+ ws.binaryType = "arraybuffer";
44
+ await new Promise<void>((resolve, reject) => {
45
+ ws.addEventListener("open", () => resolve(), { once: true });
46
+ ws.addEventListener("error", () => reject(new Error("web RPC ws connect failed")), { once: true });
47
+ });
48
+ const conn = createConnection({
49
+ transport: createFrameTransport(createWebSocketPipe(ws as unknown as WebSocketLike)),
50
+ mode: "web",
51
+ origin: location.origin,
52
+ });
53
+ _webConn = conn;
54
+ return conn;
55
+ })();
56
+ _webConnPromise = attempt;
57
+ attempt.catch(() => {
58
+ if (_webConnPromise === attempt) _webConnPromise = null;
59
+ });
60
+ return attempt;
61
+ }
62
+
63
+ export async function bootstrap<S extends SchemaShape, K extends keyof S["roots"] & string>(
64
+ schema: Schema<S>,
65
+ name: K
66
+ ): Promise<ClientOf<S["roots"][K]>> {
67
+ if (isNative()) {
68
+ if (!window.host?.bootstrap) throw new Error("host preload not ready");
69
+ return window.host.bootstrap(schema, name);
70
+ }
71
+ const conn = await ensureWebConnection();
72
+ return conn.bootstrap(schema, name);
73
+ }
74
+
75
+ export async function serve<S extends SchemaShape>(descriptor: ServerDescriptor<S>): Promise<void> {
76
+ if (isNative()) {
77
+ if (!window.host?.serve) throw new Error("host preload not ready");
78
+ return window.host.serve(descriptor);
79
+ }
80
+ const conn = await ensureWebConnection();
81
+ conn.serve(descriptor);
82
+ }
@@ -0,0 +1,246 @@
1
+ const CALL_TAG = Symbol("CallDef");
2
+ const STREAM_TAG = Symbol("StreamDef");
3
+ const CAP_TAG = Symbol("CapDef");
4
+ const CAP_REF_TAG = Symbol("CapRefToken");
5
+ const CAP_ARRAY_TAG = Symbol("CapArrayToken");
6
+ const CAP_RECORD_TAG = Symbol("CapRecordToken");
7
+ const SCHEMA_TAG = Symbol("Schema");
8
+ declare const EXPORTED_CAP_BRAND: unique symbol;
9
+
10
+ export type ReturnsKind = "type" | "cap" | "capArray" | "capRecord";
11
+
12
+ export type CapRefToken<C extends CapDef<any, any>> = {
13
+ readonly [CAP_REF_TAG]: true;
14
+ readonly cap: C;
15
+ };
16
+
17
+ export type CapArrayToken<C extends CapDef<any, any>> = {
18
+ readonly [CAP_ARRAY_TAG]: true;
19
+ readonly cap: C;
20
+ };
21
+
22
+ export type CapRecordToken<C extends CapDef<any, any>> = {
23
+ readonly [CAP_RECORD_TAG]: true;
24
+ readonly cap: C;
25
+ };
26
+
27
+ export type AnyCapToken<C extends CapDef<any, any> = CapDef<any, any>> =
28
+ | CapRefToken<C>
29
+ | CapArrayToken<C>
30
+ | CapRecordToken<C>;
31
+
32
+ function _cap<C extends CapDef<any, any>>(c: C): CapRefToken<C> {
33
+ return { [CAP_REF_TAG]: true, cap: c };
34
+ }
35
+ _cap.array = <C extends CapDef<any, any>>(c: C): CapArrayToken<C> => ({ [CAP_ARRAY_TAG]: true, cap: c });
36
+ _cap.record = <C extends CapDef<any, any>>(c: C): CapRecordToken<C> => ({ [CAP_RECORD_TAG]: true, cap: c });
37
+ export const cap = _cap;
38
+
39
+ export function isCapRef(v: unknown): v is CapRefToken<any> {
40
+ return typeof v === "object" && v !== null && (v as any)[CAP_REF_TAG] === true;
41
+ }
42
+ export function isCapArray(v: unknown): v is CapArrayToken<any> {
43
+ return typeof v === "object" && v !== null && (v as any)[CAP_ARRAY_TAG] === true;
44
+ }
45
+ export function isCapRecord(v: unknown): v is CapRecordToken<any> {
46
+ return typeof v === "object" && v !== null && (v as any)[CAP_RECORD_TAG] === true;
47
+ }
48
+ export function returnsKindOf(v: unknown): ReturnsKind {
49
+ if (isCapRef(v)) return "cap";
50
+ if (isCapArray(v)) return "capArray";
51
+ if (isCapRecord(v)) return "capRecord";
52
+ return "type";
53
+ }
54
+
55
+ export interface CallDef<P = unknown, R = unknown> {
56
+ readonly [CALL_TAG]: true;
57
+ readonly idempotent: boolean;
58
+ readonly returns?: AnyCapToken;
59
+ readonly _phantom?: (p: P) => R;
60
+ }
61
+
62
+ export interface StreamDef<P = unknown, Y = unknown> {
63
+ readonly [STREAM_TAG]: true;
64
+ readonly hint?: Record<string, unknown>;
65
+ readonly _phantom?: (p: P) => Y;
66
+ }
67
+
68
+ export type MethodDef = CallDef<any, any> | StreamDef<any, any>;
69
+
70
+ export function isCallDef(v: unknown): v is CallDef {
71
+ return typeof v === "object" && v !== null && (v as any)[CALL_TAG] === true;
72
+ }
73
+ export function isStreamDef(v: unknown): v is StreamDef {
74
+ return typeof v === "object" && v !== null && (v as any)[STREAM_TAG] === true;
75
+ }
76
+
77
+ export function call<P, C extends CapDef<any, any>>(
78
+ opts: { returns: CapRefToken<C>; idempotent?: boolean }
79
+ ): CallDef<P, CapRefToken<C>>;
80
+ export function call<P, C extends CapDef<any, any>>(
81
+ opts: { returns: CapArrayToken<C>; idempotent?: boolean }
82
+ ): CallDef<P, CapArrayToken<C>>;
83
+ export function call<P, C extends CapDef<any, any>>(
84
+ opts: { returns: CapRecordToken<C>; idempotent?: boolean }
85
+ ): CallDef<P, CapRecordToken<C>>;
86
+ export function call<P = void, R = void>(opts?: { idempotent?: boolean }): CallDef<P, R>;
87
+ export function call(opts?: { idempotent?: boolean; returns?: AnyCapToken }): CallDef<any, any> {
88
+ return {
89
+ [CALL_TAG]: true,
90
+ idempotent: !!opts?.idempotent,
91
+ returns: opts?.returns,
92
+ };
93
+ }
94
+
95
+ export function stream<P = void, Y = unknown>(opts?: { hint?: Record<string, unknown> }): StreamDef<P, Y> {
96
+ return { [STREAM_TAG]: true, hint: opts?.hint };
97
+ }
98
+
99
+ export interface DisposalSpec<M extends MethodsRecord = MethodsRecord> {
100
+ method: keyof M & string;
101
+ async?: boolean;
102
+ }
103
+
104
+ export type MethodsRecord = Record<string, MethodDef>;
105
+
106
+ export interface CapDef<M extends MethodsRecord = MethodsRecord, D extends DisposalSpec<M> | undefined = undefined> {
107
+ readonly [CAP_TAG]: true;
108
+ readonly methods: M;
109
+ readonly disposal: D;
110
+ }
111
+
112
+ export function defineCap<M extends MethodsRecord, D extends DisposalSpec<M> | undefined = undefined>(
113
+ methods: M,
114
+ opts?: { disposal?: D }
115
+ ): CapDef<M, D> {
116
+ return {
117
+ [CAP_TAG]: true,
118
+ methods,
119
+ disposal: (opts?.disposal as D) ?? (undefined as D),
120
+ };
121
+ }
122
+
123
+ export function isCapDef(v: unknown): v is CapDef {
124
+ return typeof v === "object" && v !== null && (v as any)[CAP_TAG] === true;
125
+ }
126
+
127
+ export interface SchemaShape {
128
+ roots: Record<string, CapDef<any, any>>;
129
+ caps?: readonly CapDef<any, any>[];
130
+ }
131
+
132
+ export interface Schema<S extends SchemaShape = SchemaShape> {
133
+ readonly [SCHEMA_TAG]: true;
134
+ readonly roots: S["roots"];
135
+ readonly caps: readonly CapDef<any, any>[];
136
+ topologyHash(): Promise<string>;
137
+ serve(impls: ImplsOf<S>): ServerDescriptor<S>;
138
+ }
139
+
140
+ export type ImplsOf<S extends SchemaShape> = {
141
+ [K in keyof S["roots"]]: ImplOf<S["roots"][K]>;
142
+ };
143
+
144
+ export interface ServerDescriptor<S extends SchemaShape = SchemaShape> {
145
+ readonly schema: Schema<S>;
146
+ readonly impls: ImplsOf<S>;
147
+ }
148
+
149
+ export function defineSchema<S extends SchemaShape>(shape: S): Schema<S> {
150
+ const schema: Schema<S> = {
151
+ [SCHEMA_TAG]: true,
152
+ roots: shape.roots,
153
+ caps: shape.caps ?? [],
154
+ topologyHash: () => topologyHashImpl(schema),
155
+ serve(impls) {
156
+ return { schema, impls };
157
+ },
158
+ };
159
+ return schema;
160
+ }
161
+
162
+ let topologyHashImpl: (s: Schema<any>) => Promise<string> = () => {
163
+ throw new Error("schema.topologyHash bound after hash.ts import");
164
+ };
165
+
166
+ export function _bindTopologyHash(fn: (s: Schema<any>) => Promise<string>) {
167
+ topologyHashImpl = fn;
168
+ }
169
+
170
+ export function isSchema(v: unknown): v is Schema {
171
+ return typeof v === "object" && v !== null && (v as any)[SCHEMA_TAG] === true;
172
+ }
173
+
174
+ export interface ExportedCap<C extends CapDef<any, any>> {
175
+ readonly [EXPORTED_CAP_BRAND]: true;
176
+ readonly cap: C;
177
+ readonly capId: number;
178
+ readonly typeId: number;
179
+ }
180
+
181
+ export interface CallCtx {
182
+ callId: number;
183
+ peerId: string;
184
+ attestation: Attestation;
185
+ signal: AbortSignal;
186
+ deadline?: number;
187
+ context?: Record<string, string>;
188
+ exportCap<C extends CapDef<any, any>>(capDef: C, impl: ImplOf<C>): ExportedCap<C>;
189
+ }
190
+
191
+ export interface Attestation {
192
+ origin: string;
193
+ topOrigin: string;
194
+ partition: string;
195
+ isAppRes: boolean;
196
+ isMainFrame: boolean;
197
+ userGesture: boolean;
198
+ level: "app-internal" | "trusted-origin" | "untrusted";
199
+ }
200
+
201
+ type MaybePromise<T> = T | Promise<T>;
202
+
203
+ type ServerReturn<R> =
204
+ R extends CapRefToken<infer C> ? ExportedCap<C> :
205
+ R extends CapArrayToken<infer C> ? ExportedCap<C>[] :
206
+ R extends CapRecordToken<infer C> ? Record<string, ExportedCap<C>> :
207
+ R;
208
+
209
+ export type ClientReturn<R> =
210
+ R extends CapRefToken<infer C> ? ClientOf<C> :
211
+ R extends CapArrayToken<infer C> ?
212
+ C extends CapDef<any, infer D>
213
+ ? [D] extends [{ async: true }] ? ClientOf<C>[] & AsyncDisposable
214
+ : [D] extends [DisposalSpec] ? ClientOf<C>[] & Disposable
215
+ : ClientOf<C>[]
216
+ : ClientOf<C>[] :
217
+ R extends CapRecordToken<infer C> ? Record<string, ClientOf<C>> :
218
+ R;
219
+
220
+ export type Stream<T> = AsyncIterable<T> & Disposable & { cancel(): void };
221
+
222
+ export type ClientOf<T> = T extends CapDef<infer M, infer D>
223
+ ? {
224
+ [K in keyof M]: M[K] extends CallDef<infer P, infer R>
225
+ ? [P] extends [void] ? () => Promise<ClientReturn<R>>
226
+ : (params: P) => Promise<ClientReturn<R>>
227
+ : M[K] extends StreamDef<infer P, infer Y>
228
+ ? [P] extends [void] ? () => Stream<Y>
229
+ : (params: P) => Stream<Y>
230
+ : never;
231
+ } & ([D] extends [{ async: true }] ? AsyncDisposable : [D] extends [DisposalSpec] ? Disposable : {})
232
+ : never;
233
+
234
+ export type ImplOf<T> = T extends CapDef<infer M, any>
235
+ ? {
236
+ [K in keyof M]: M[K] extends CallDef<infer P, infer R>
237
+ ? (params: P, ctx: CallCtx) => MaybePromise<ServerReturn<R>>
238
+ : M[K] extends StreamDef<infer P, infer Y>
239
+ ? (params: P, ctx: CallCtx) => Stream<Y>
240
+ : never;
241
+ }
242
+ : never;
243
+
244
+ export function methodKeys(cap: CapDef<any, any>): string[] {
245
+ return Object.keys(cap.methods);
246
+ }
@@ -0,0 +1,72 @@
1
+ import type { Stream as StreamType } from "./schema";
2
+
3
+ type Setup<T> = (emit: (chunk: T) => void, signal: AbortSignal) => void | (() => void);
4
+
5
+ class ServerStream<T> implements AsyncIterable<T>, Disposable {
6
+ private readonly buffer: T[] = [];
7
+ private readonly waiters: Array<{ resolve: (r: IteratorResult<T>) => void; reject: (e: unknown) => void }> = [];
8
+ private readonly ctrl = new AbortController();
9
+ private cleanup?: () => void;
10
+ private ended = false;
11
+ private failure: unknown = null;
12
+
13
+ constructor(setup: Setup<T>) {
14
+ const emit = (chunk: T) => {
15
+ if (this.ended || this.failure) return;
16
+ const w = this.waiters.shift();
17
+ if (w) {
18
+ w.resolve({ value: chunk, done: false });
19
+ return;
20
+ }
21
+ this.buffer.push(chunk);
22
+ };
23
+ try {
24
+ const ret = setup(emit, this.ctrl.signal);
25
+ if (typeof ret === "function") this.cleanup = ret;
26
+ } catch (err) {
27
+ this.failure = err;
28
+ }
29
+ }
30
+
31
+ [Symbol.asyncIterator](): AsyncIterator<T> {
32
+ return {
33
+ next: async (): Promise<IteratorResult<T>> => {
34
+ if (this.buffer.length > 0) {
35
+ return { value: this.buffer.shift()!, done: false };
36
+ }
37
+ if (this.failure) throw this.failure;
38
+ if (this.ended) return { value: undefined as unknown as T, done: true };
39
+ return new Promise<IteratorResult<T>>((resolve, reject) => this.waiters.push({ resolve, reject }));
40
+ },
41
+ return: async (): Promise<IteratorResult<T>> => {
42
+ this.dispose();
43
+ return { value: undefined as unknown as T, done: true };
44
+ },
45
+ };
46
+ }
47
+
48
+ cancel(): void {
49
+ this.dispose();
50
+ }
51
+
52
+ [Symbol.dispose](): void {
53
+ this.dispose();
54
+ }
55
+
56
+ private dispose(): void {
57
+ if (this.ended) return;
58
+ this.ended = true;
59
+ this.ctrl.abort();
60
+ try { this.cleanup?.(); } catch { /* swallow */ }
61
+ while (this.waiters.length > 0) {
62
+ const w = this.waiters.shift()!;
63
+ w.resolve({ value: undefined as unknown as T, done: true });
64
+ }
65
+ }
66
+ }
67
+
68
+ export const Stream = {
69
+ from<T>(setup: Setup<T>): StreamType<T> {
70
+ return new ServerStream(setup) as unknown as StreamType<T>;
71
+ },
72
+ };
@@ -0,0 +1,81 @@
1
+ import { type Frame, createCodec, isFrame } from "./wire";
2
+ import type { Transport } from "./peer";
3
+
4
+ export interface BytesPipe {
5
+ send(bytes: Uint8Array): void;
6
+ setReceive(handler: (bytes: Uint8Array) => void): void;
7
+ close(): void;
8
+ }
9
+
10
+ export interface FrameTransportOptions {
11
+ onProtocolError?(reason: string): void;
12
+ }
13
+
14
+ export function createFrameTransport(pipe: BytesPipe, opts: FrameTransportOptions = {}): Transport {
15
+ const codec = createCodec();
16
+ let handler: ((frame: Frame) => void) | undefined;
17
+
18
+ pipe.setReceive((bytes) => {
19
+ let frame: unknown;
20
+ try {
21
+ frame = codec.unpackr.unpack(bytes);
22
+ } catch (err) {
23
+ opts.onProtocolError?.(`unpack failed: ${err instanceof Error ? err.message : String(err)}`);
24
+ pipe.close();
25
+ return;
26
+ }
27
+ if (!isFrame(frame)) {
28
+ opts.onProtocolError?.("malformed frame");
29
+ pipe.close();
30
+ return;
31
+ }
32
+ handler?.(frame);
33
+ });
34
+
35
+ return {
36
+ send(frame) {
37
+ const bytes = codec.packr.pack(frame);
38
+ pipe.send(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
39
+ },
40
+ setReceive(h) {
41
+ handler = h;
42
+ },
43
+ close() {
44
+ pipe.close();
45
+ },
46
+ };
47
+ }
48
+
49
+ export interface WebSocketLike {
50
+ send(data: Uint8Array | ArrayBuffer): unknown;
51
+ addEventListener(type: "message", listener: (event: { data: ArrayBuffer | Uint8Array | Blob | string }) => void): void;
52
+ removeEventListener?(type: "message", listener: (event: { data: ArrayBuffer | Uint8Array | Blob | string }) => void): void;
53
+ close?(): void;
54
+ }
55
+
56
+ export function createWebSocketPipe(ws: WebSocketLike): BytesPipe {
57
+ if ("binaryType" in ws) {
58
+ try { (ws as { binaryType?: string }).binaryType = "arraybuffer"; } catch { /* readonly in some envs */ }
59
+ }
60
+ let handler: ((bytes: Uint8Array) => void) | undefined;
61
+ const onMessage = (event: { data: ArrayBuffer | Uint8Array | Blob | string }) => {
62
+ if (!handler) return;
63
+ const d = event.data;
64
+ if (d instanceof Uint8Array) { handler(d); return; }
65
+ if (d instanceof ArrayBuffer) { handler(new Uint8Array(d)); return; }
66
+ if (typeof Blob !== "undefined" && d instanceof Blob) {
67
+ void d.arrayBuffer().then((buf) => handler?.(new Uint8Array(buf)));
68
+ return;
69
+ }
70
+ };
71
+ ws.addEventListener("message", onMessage);
72
+ return {
73
+ send(bytes) { ws.send(bytes); },
74
+ setReceive(h) { handler = h; },
75
+ close() {
76
+ ws.removeEventListener?.("message", onMessage);
77
+ ws.close?.();
78
+ },
79
+ };
80
+ }
81
+
@@ -0,0 +1,150 @@
1
+ import { Packr, Unpackr, addExtension } from "msgpackr";
2
+ import type { IpcStatus } from "./error";
3
+
4
+ export type u32 = number;
5
+ export type u53 = number;
6
+
7
+ export const CAP_REF_EXT = 0x50;
8
+
9
+ export class CapRef {
10
+ constructor(public readonly capId: u32) {}
11
+ }
12
+
13
+ export type Target = { kind: "cap"; id: u32 };
14
+
15
+ export interface CallMeta {
16
+ parentCallId?: u53;
17
+ deadlineMs?: u32;
18
+ context?: Record<string, string>;
19
+ topologyHash?: string;
20
+ }
21
+
22
+ export type StreamEvent =
23
+ | { ev: "next"; value: unknown }
24
+ | { ev: "credit"; credit: { messages?: u32 } }
25
+ | { ev: "end" }
26
+ | { ev: "error"; error: IpcStatus };
27
+
28
+ export type Frame =
29
+ | {
30
+ op: "hello";
31
+ v: 1;
32
+ mode: "native" | "web";
33
+ features: string[];
34
+ maxBytes: number;
35
+ origin: string;
36
+ }
37
+ | { op: "goaway"; reason?: string; error?: IpcStatus }
38
+ | {
39
+ op: "call";
40
+ id: u53;
41
+ target: Target;
42
+ method: u32;
43
+ args: unknown;
44
+ meta?: CallMeta;
45
+ }
46
+ | { op: "result"; id: u53; ok: true; value: unknown }
47
+ | { op: "result"; id: u53; ok: false; error: IpcStatus }
48
+ | { op: "cancel"; id: u53; reason?: string }
49
+ | ({ op: "stream"; id: u53 } & StreamEvent)
50
+ | { op: "drop"; caps: { id: u32; delta: u32 }[] };
51
+
52
+ export type CallFrame = Extract<Frame, { op: "call" }>;
53
+ export type ResultFrame = Extract<Frame, { op: "result" }>;
54
+ export type StreamFrame = Extract<Frame, { op: "stream" }>;
55
+ export type CancelFrame = Extract<Frame, { op: "cancel" }>;
56
+ export type DropFrame = Extract<Frame, { op: "drop" }>;
57
+ export type HelloFrame = Extract<Frame, { op: "hello" }>;
58
+ export type GoAwayFrame = Extract<Frame, { op: "goaway" }>;
59
+
60
+ const OPS = new Set<Frame["op"]>([
61
+ "hello",
62
+ "goaway",
63
+ "call",
64
+ "result",
65
+ "cancel",
66
+ "stream",
67
+ "drop",
68
+ ]);
69
+
70
+ export function isFrame(value: unknown): value is Frame {
71
+ if (typeof value !== "object" || value === null) return false;
72
+ const f = value as Record<string, unknown>;
73
+ const op = f.op;
74
+ if (typeof op !== "string" || !OPS.has(op as Frame["op"])) return false;
75
+ switch (op as Frame["op"]) {
76
+ case "hello":
77
+ return f.v === 1 && (f.mode === "native" || f.mode === "web")
78
+ && Array.isArray(f.features) && typeof f.maxBytes === "number" && typeof f.origin === "string";
79
+ case "goaway":
80
+ return f.reason === undefined || typeof f.reason === "string";
81
+ case "call":
82
+ return typeof f.id === "number" && typeof f.method === "number"
83
+ && typeof f.target === "object" && f.target !== null
84
+ && (f.target as { kind?: unknown }).kind === "cap"
85
+ && typeof (f.target as { id?: unknown }).id === "number";
86
+ case "result":
87
+ return typeof f.id === "number" && (f.ok === true || f.ok === false);
88
+ case "cancel":
89
+ return typeof f.id === "number";
90
+ case "stream":
91
+ return typeof f.id === "number" && typeof f.ev === "string"
92
+ && (f.ev === "next" || f.ev === "credit" || f.ev === "end" || f.ev === "error");
93
+ case "drop":
94
+ return Array.isArray(f.caps);
95
+ }
96
+ }
97
+
98
+ let extensionsRegistered = false;
99
+ function registerExtensions() {
100
+ if (extensionsRegistered) return;
101
+ extensionsRegistered = true;
102
+ addExtension({
103
+ Class: CapRef,
104
+ type: CAP_REF_EXT,
105
+ pack: (cap: CapRef) => packCapId(cap.capId),
106
+ unpack: (buf: Buffer | Uint8Array) => new CapRef(readVarUint(buf)),
107
+ });
108
+ }
109
+
110
+ export interface CodecPair {
111
+ packr: Packr;
112
+ unpackr: Unpackr;
113
+ }
114
+
115
+ export function createCodec(): CodecPair {
116
+ registerExtensions();
117
+ return {
118
+ packr: new Packr({ useRecords: true, sequential: true, moreTypes: true }),
119
+ unpackr: new Unpackr({ useRecords: true, sequential: true, moreTypes: true }),
120
+ };
121
+ }
122
+
123
+ function packCapId(capId: number): Uint8Array {
124
+ const buf = new Uint8Array(5);
125
+ let n = capId >>> 0;
126
+ let i = 0;
127
+ while (n >= 0x80) {
128
+ buf[i++] = (n & 0x7f) | 0x80;
129
+ n >>>= 7;
130
+ }
131
+ buf[i++] = n & 0x7f;
132
+ return buf.subarray(0, i);
133
+ }
134
+
135
+ const MAX_VARUINT_BYTES = 5;
136
+ function readVarUint(buf: Buffer | Uint8Array): number {
137
+ let result = 0;
138
+ let shift = 0;
139
+ const limit = Math.min(buf.length, MAX_VARUINT_BYTES);
140
+ for (let i = 0; i < limit; i++) {
141
+ const byte = buf[i];
142
+ result |= (byte & 0x7f) << shift;
143
+ if ((byte & 0x80) === 0) return result >>> 0;
144
+ shift += 7;
145
+ }
146
+ throw new Error("Truncated or oversize varuint");
147
+ }
148
+
149
+ export const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
150
+ export const PROTOCOL_VERSION = 1;