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,1055 @@
1
+ import {
2
+ type CapDef,
3
+ type Schema,
4
+ type SchemaShape,
5
+ type ClientOf,
6
+ type ImplOf,
7
+ type ServerDescriptor,
8
+ type CallCtx,
9
+ type Attestation,
10
+ type ExportedCap,
11
+ type MethodDef,
12
+ type AnyCapToken,
13
+ type Stream as StreamType,
14
+ type DisposalSpec,
15
+ call,
16
+ cap,
17
+ defineCap,
18
+ isCallDef,
19
+ isStreamDef,
20
+ isCapRef,
21
+ isCapArray,
22
+ isCapRecord,
23
+ } from "./schema";
24
+ import { RuntimeCap, frameworkTypeIdOf } from "./framework";
25
+ import {
26
+ type Frame,
27
+ type CallFrame,
28
+ type ResultFrame,
29
+ type StreamFrame,
30
+ type CancelFrame,
31
+ type DropFrame,
32
+ type HelloFrame,
33
+ type CallMeta,
34
+ CapRef,
35
+ DEFAULT_MAX_BYTES,
36
+ PROTOCOL_VERSION,
37
+ } from "./wire";
38
+ import { IpcError, type IpcStatus, type IpcCode } from "./error";
39
+
40
+ export const USER_ROOTS_CAP_ID = 0;
41
+ export const RUNTIME_CAP_ID = 1;
42
+ export const USER_ROOTS_TYPE_ID = 0;
43
+ export const RUNTIME_TYPE_ID = 1;
44
+
45
+ export const FIRST_USER_CAP_ID = 2;
46
+ export const FIRST_USER_TYPE_ID = 128;
47
+
48
+ export const MAX_CAPS_PER_CONNECTION = 1024;
49
+
50
+ const DEFAULT_DEADLINE_GRACE_MS = 500;
51
+ const DEFAULT_STREAM_INITIAL_CREDIT = 32;
52
+ const DEFAULT_STREAM_CREDIT_BATCH = 8;
53
+ const MAX_STREAM_INITIAL_CREDIT = 1024;
54
+
55
+ function resolveStreamBudget(hint: Record<string, unknown> | undefined): number {
56
+ const raw = hint?.initialBudget;
57
+ if (typeof raw !== "number" || !Number.isFinite(raw) || !Number.isInteger(raw) || raw < 1) {
58
+ return DEFAULT_STREAM_INITIAL_CREDIT;
59
+ }
60
+ return Math.min(raw, MAX_STREAM_INITIAL_CREDIT);
61
+ }
62
+
63
+ interface CallScope {
64
+ readonly callId: number;
65
+ }
66
+
67
+ export interface CallContextStorage {
68
+ getStore(): CallScope | undefined;
69
+ run<R>(store: CallScope, fn: () => R): R;
70
+ }
71
+
72
+ let callContextStorage: CallContextStorage | null = null;
73
+
74
+ export function _setCallContextStorage(als: CallContextStorage | null): void {
75
+ callContextStorage = als;
76
+ }
77
+
78
+ export interface CapTableEntry {
79
+ capId: number;
80
+ typeId: number;
81
+ cap: CapDef<any, any> | null;
82
+ impl: unknown;
83
+ refCount: number;
84
+ }
85
+
86
+ export class CapTable {
87
+ private readonly entries = new Map<number, CapTableEntry>();
88
+ private nextCapId = FIRST_USER_CAP_ID;
89
+ private readonly capLimit: number;
90
+
91
+ constructor(capLimit = MAX_CAPS_PER_CONNECTION) {
92
+ this.capLimit = capLimit;
93
+ }
94
+
95
+ install(capId: number, entry: Omit<CapTableEntry, "capId">): CapTableEntry {
96
+ if (this.entries.has(capId)) throw new Error(`cap-id ${capId} already installed`);
97
+ const full = { capId, ...entry };
98
+ this.entries.set(capId, full);
99
+ return full;
100
+ }
101
+
102
+ allocate(entry: Omit<CapTableEntry, "capId">): CapTableEntry {
103
+ if (this.entries.size >= this.capLimit) {
104
+ throw new IpcError({ code: "resource_exhausted", message: `cap-table limit ${this.capLimit}` });
105
+ }
106
+ let capId = this.nextCapId++;
107
+ while (this.entries.has(capId)) capId = this.nextCapId++;
108
+ return this.install(capId, entry);
109
+ }
110
+
111
+ get(capId: number): CapTableEntry | undefined {
112
+ return this.entries.get(capId);
113
+ }
114
+
115
+ release(capId: number, delta = 1): boolean {
116
+ const entry = this.entries.get(capId);
117
+ if (!entry) return false;
118
+ entry.refCount = Math.max(0, entry.refCount - delta);
119
+ if (entry.refCount === 0 && capId >= FIRST_USER_CAP_ID) {
120
+ this.entries.delete(capId);
121
+ return true;
122
+ }
123
+ return false;
124
+ }
125
+
126
+ clear(): void {
127
+ this.entries.clear();
128
+ this.nextCapId = FIRST_USER_CAP_ID;
129
+ }
130
+
131
+ size(): number {
132
+ return this.entries.size;
133
+ }
134
+
135
+ values(): IterableIterator<CapTableEntry> {
136
+ return this.entries.values();
137
+ }
138
+ }
139
+
140
+ export interface Transport {
141
+ send(frame: Frame): void;
142
+ setReceive(handler: (frame: Frame) => void): void;
143
+ close(): void;
144
+ }
145
+
146
+ export interface Connection {
147
+ bootstrap<S extends SchemaShape, K extends keyof S["roots"] & string>(
148
+ schema: Schema<S>,
149
+ name: K
150
+ ): Promise<ClientOf<S["roots"][K]>>;
151
+ serve<S extends SchemaShape>(descriptor: ServerDescriptor<S>): void;
152
+ runtime(): ClientOf<typeof RuntimeCap>;
153
+ releaseRef(proxy: unknown): void;
154
+ onClose(handler: () => void): () => void;
155
+ readonly closed: boolean;
156
+ }
157
+
158
+ export interface ConnectionOptions {
159
+ transport: Transport;
160
+ mode: "native" | "web";
161
+ origin: string;
162
+ features?: string[];
163
+ maxBytes?: number;
164
+ capLimit?: number;
165
+ peerId?: string;
166
+ attestation?: Attestation;
167
+ runtime?: ImplOf<typeof RuntimeCap>;
168
+ }
169
+
170
+ export interface PendingCall {
171
+ resolve(value: unknown): void;
172
+ reject(error: Error): void;
173
+ abort: AbortController;
174
+ decodeReturn?: ReturnDecoder;
175
+ timer?: ReturnType<typeof setTimeout>;
176
+ }
177
+
178
+ type ReturnDecoder = (raw: unknown) => unknown;
179
+
180
+ const DEFAULT_ATTESTATION: Attestation = {
181
+ origin: "bunite://internal",
182
+ topOrigin: "bunite://internal",
183
+ partition: "default",
184
+ isAppRes: true,
185
+ isMainFrame: true,
186
+ userGesture: false,
187
+ level: "app-internal",
188
+ };
189
+
190
+ const EXPORTED_CAP_BRAND = Symbol("bunite.rpc.ExportedCap");
191
+ const CAP_PROXY_META = Symbol("bunite.rpc.CapProxyMeta");
192
+
193
+ function isExportedCap(v: unknown): v is ExportedCap<any> {
194
+ return typeof v === "object" && v !== null && (v as any)[EXPORTED_CAP_BRAND] === true;
195
+ }
196
+
197
+ const proxyFinalizers = typeof FinalizationRegistry !== "undefined"
198
+ ? new FinalizationRegistry<{ connRef: WeakRef<ConnectionImpl>; capId: number; dropped: () => boolean }>((held) => {
199
+ if (held.dropped()) return;
200
+ const conn = held.connRef.deref();
201
+ if (!conn || conn.closed) return;
202
+ conn._dropFromFinalizer(held.capId);
203
+ })
204
+ : ({ register: () => {} } as { register: (target: object, held: unknown) => void });
205
+
206
+ interface ServerStreamCtx {
207
+ iter: AsyncIterator<unknown> | null;
208
+ abort: AbortController;
209
+ cancelled: boolean;
210
+ credit: number;
211
+ creditWaker: (() => void) | null;
212
+ }
213
+
214
+ interface ClientStreamCtx {
215
+ push(chunk: unknown): void;
216
+ end(): void;
217
+ fail(error: IpcError): void;
218
+ }
219
+
220
+ class ConnectionImpl implements Connection {
221
+ private readonly transport: Transport;
222
+ private readonly capTable: CapTable;
223
+ private readonly pending = new Map<number, PendingCall>();
224
+ private readonly clientStreams = new Map<number, ClientStreamCtx>();
225
+ private readonly serverStreams = new Map<number, ServerStreamCtx>();
226
+ private readonly serverCallChildren = new Map<number, { parentId: number }>();
227
+ private readonly serverActiveCalls = new Map<number, AbortController>();
228
+ private readonly rootInstances = new Map<string, number>();
229
+ private userRootsSchema: Schema<any> | null = null;
230
+ private readonly closeHandlers = new Set<() => void>();
231
+ private nextCallId = 1;
232
+ private remoteHello: HelloFrame | null = null;
233
+ private readonly remoteReady: Promise<HelloFrame>;
234
+ private resolveRemoteReady!: (h: HelloFrame) => void;
235
+ private rejectRemoteReady!: (e: Error) => void;
236
+ private closed_ = false;
237
+ private readonly maxBytes: number;
238
+ private readonly mode: "native" | "web";
239
+ private readonly origin: string;
240
+ private readonly features: string[];
241
+ private readonly attestation: Attestation;
242
+ private readonly peerId: string;
243
+
244
+ constructor(opts: ConnectionOptions) {
245
+ this.transport = opts.transport;
246
+ this.mode = opts.mode;
247
+ this.origin = opts.origin;
248
+ this.features = opts.features ?? [];
249
+ this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
250
+ this.attestation = opts.attestation ?? DEFAULT_ATTESTATION;
251
+ this.peerId = opts.peerId ?? "peer";
252
+ this.capTable = new CapTable(opts.capLimit ?? MAX_CAPS_PER_CONNECTION);
253
+
254
+ this.capTable.install(USER_ROOTS_CAP_ID, {
255
+ typeId: USER_ROOTS_TYPE_ID,
256
+ cap: null,
257
+ impl: null,
258
+ refCount: 1,
259
+ });
260
+ this.capTable.install(RUNTIME_CAP_ID, {
261
+ typeId: RUNTIME_TYPE_ID,
262
+ cap: RuntimeCap,
263
+ impl: opts.runtime ?? null,
264
+ refCount: 1,
265
+ });
266
+
267
+ this.remoteReady = new Promise<HelloFrame>((res, rej) => {
268
+ this.resolveRemoteReady = res;
269
+ this.rejectRemoteReady = rej;
270
+ });
271
+ this.remoteReady.catch(() => {});
272
+
273
+ this.transport.setReceive((frame) => this.handleFrame(frame));
274
+ this.transport.send({
275
+ op: "hello",
276
+ v: PROTOCOL_VERSION,
277
+ mode: this.mode,
278
+ features: this.features,
279
+ maxBytes: this.maxBytes,
280
+ origin: this.origin,
281
+ });
282
+ }
283
+
284
+ get closed(): boolean {
285
+ return this.closed_;
286
+ }
287
+
288
+ onClose(handler: () => void): () => void {
289
+ this.closeHandlers.add(handler);
290
+ return () => this.closeHandlers.delete(handler);
291
+ }
292
+
293
+ serve<S extends SchemaShape>(descriptor: ServerDescriptor<S>): void {
294
+ const entry = this.capTable.get(USER_ROOTS_CAP_ID);
295
+ if (!entry) throw new Error("UserRoots slot missing");
296
+ this.rootInstances.clear();
297
+ this.userRootsSchema = descriptor.schema;
298
+ const built = this.buildUserRootsCap(descriptor.schema, descriptor.impls as Record<string, unknown>);
299
+ entry.cap = built.def;
300
+ entry.impl = built.impl;
301
+ }
302
+
303
+ private buildUserRootsCap(
304
+ schema: Schema<any>,
305
+ impls: Record<string, unknown>
306
+ ): { def: CapDef<any, any>; impl: Record<string, (params: unknown, ctx: CallCtx) => unknown> } {
307
+ const methods: Record<string, MethodDef> = {};
308
+ const impl: Record<string, (params: unknown, ctx: CallCtx) => unknown> = {};
309
+ for (const name of Object.keys(schema.roots)) {
310
+ const rootCap = schema.roots[name] as CapDef<any, any>;
311
+ methods[name] = call({ returns: cap(rootCap as never), idempotent: true });
312
+ impl[name] = (_params, ctx) => {
313
+ const cachedId = this.rootInstances.get(name);
314
+ if (cachedId !== undefined) {
315
+ const cached = this.capTable.get(cachedId);
316
+ if (cached) {
317
+ cached.refCount += 1;
318
+ return {
319
+ [EXPORTED_CAP_BRAND]: true,
320
+ cap: rootCap,
321
+ capId: cachedId,
322
+ typeId: cached.typeId,
323
+ } as unknown as ExportedCap<typeof rootCap>;
324
+ }
325
+ this.rootInstances.delete(name);
326
+ }
327
+ const exported = ctx.exportCap(rootCap, impls[name] as never);
328
+ this.rootInstances.set(name, exported.capId);
329
+ return exported;
330
+ };
331
+ }
332
+ return { def: defineCap(methods), impl };
333
+ }
334
+
335
+ private runtimeProxy: ClientOf<typeof RuntimeCap> | null = null;
336
+
337
+ runtime(): ClientOf<typeof RuntimeCap> {
338
+ if (!this.runtimeProxy) {
339
+ this.runtimeProxy = this.makeCapProxy(RuntimeCap, RUNTIME_CAP_ID);
340
+ }
341
+ return this.runtimeProxy;
342
+ }
343
+
344
+ async bootstrap<S extends SchemaShape, K extends keyof S["roots"] & string>(
345
+ schema: Schema<S>,
346
+ name: K
347
+ ): Promise<ClientOf<S["roots"][K]>> {
348
+ await this.remoteReady;
349
+ const rootNames = Object.keys(schema.roots);
350
+ const idx = rootNames.indexOf(name);
351
+ if (idx < 0) {
352
+ throw new IpcError({ code: "invalid_argument", message: `root "${name}" not in schema` });
353
+ }
354
+ const rootCap = schema.roots[name] as CapDef<any, any>;
355
+ const hash = await schema.topologyHash();
356
+ const raw = await this.sendCallTyped(USER_ROOTS_CAP_ID, idx, undefined, undefined, { topologyHash: hash });
357
+ if (!(raw instanceof CapRef)) {
358
+ throw new IpcError({ code: "protocol_error", message: "bootstrap did not return a CapRef" });
359
+ }
360
+ return this.makeCapProxy(rootCap, raw.capId) as ClientOf<S["roots"][K]>;
361
+ }
362
+
363
+ private handleFrame(frame: Frame): void {
364
+ if (this.closed_) return;
365
+ switch (frame.op) {
366
+ case "hello":
367
+ this.handleHello(frame);
368
+ return;
369
+ case "call":
370
+ void this.handleCall(frame);
371
+ return;
372
+ case "result":
373
+ this.handleResult(frame);
374
+ return;
375
+ case "cancel":
376
+ this.handleCancel(frame);
377
+ return;
378
+ case "stream":
379
+ this.handleStreamFrame(frame);
380
+ return;
381
+ case "drop":
382
+ this.handleDrop(frame);
383
+ return;
384
+ case "goaway":
385
+ this.handleGoaway(frame);
386
+ return;
387
+ default:
388
+ this.handleUnknownFrame(frame);
389
+ return;
390
+ }
391
+ }
392
+
393
+ private handleUnknownFrame(frame: unknown): void {
394
+ const id = (frame as { id?: unknown })?.id;
395
+ if (typeof id === "number") {
396
+ this.transport.send({ op: "result", id, ok: false, error: { code: "protocol_error", message: "unknown opcode" } });
397
+ return;
398
+ }
399
+ this.transport.send({ op: "goaway", reason: "protocol_error", error: { code: "protocol_error", message: "unknown opcode" } });
400
+ this.shutdown("protocol_error");
401
+ }
402
+
403
+ private handleHello(frame: HelloFrame): void {
404
+ this.remoteHello = frame;
405
+ this.resolveRemoteReady(frame);
406
+ }
407
+
408
+ private handleGoaway(frame: Extract<Frame, { op: "goaway" }>): void {
409
+ this.rejectRemoteReady(
410
+ new IpcError(frame.error ?? { code: "unavailable", message: frame.reason ?? "peer goaway" })
411
+ );
412
+ this.shutdown(frame.reason ?? "remote goaway");
413
+ }
414
+
415
+ private async handleCall(frame: CallFrame): Promise<void> {
416
+ const entry = this.capTable.get(frame.target.id);
417
+ if (!entry) return this.sendError(frame.id, "not_found", `cap-id ${frame.target.id} not found`);
418
+
419
+ if (entry.capId === USER_ROOTS_CAP_ID) {
420
+ if (!this.userRootsSchema) return this.sendError(frame.id, "not_supported", "no server attached");
421
+ const clientHash = frame.meta?.topologyHash;
422
+ if (clientHash) {
423
+ const ourHash = await this.userRootsSchema.topologyHash();
424
+ if (clientHash !== ourHash) {
425
+ return this.sendError(
426
+ frame.id,
427
+ "failed_precondition",
428
+ `topologyHash mismatch (client ${clientHash.slice(0, 8)} vs server ${ourHash.slice(0, 8)})`
429
+ );
430
+ }
431
+ }
432
+ }
433
+
434
+ const cap = entry.cap;
435
+ if (!cap || !entry.impl) return this.sendError(frame.id, "not_supported", "cap has no impl");
436
+
437
+ const methodNames = Object.keys(cap.methods);
438
+ if (frame.method >= methodNames.length) {
439
+ return this.sendError(frame.id, "not_supported", `method index ${frame.method} out of range`);
440
+ }
441
+ const methodName = methodNames[frame.method];
442
+ const methodDef = cap.methods[methodName] as MethodDef;
443
+ const impl = (entry.impl as Record<string, unknown>)[methodName];
444
+ if (typeof impl !== "function") {
445
+ return this.sendError(frame.id, "not_supported", `method "${methodName}" has no handler`);
446
+ }
447
+
448
+ await this.invokeServerMethod(frame, methodDef, impl as (params: unknown, ctx: CallCtx) => unknown);
449
+ }
450
+
451
+ private async invokeServerMethod(
452
+ frame: CallFrame,
453
+ methodDef: MethodDef,
454
+ impl: (params: unknown, ctx: CallCtx) => unknown
455
+ ): Promise<void> {
456
+ const ctx = this.makeCallCtx(frame);
457
+
458
+ if (isStreamDef(methodDef)) {
459
+ const hintBudget = resolveStreamBudget(methodDef.hint);
460
+ try {
461
+ const runStream = () => impl(frame.args, ctx) as StreamType<unknown>;
462
+ const als = callContextStorage;
463
+ const scoped = als
464
+ ? () => als.run({ callId: frame.id }, runStream)
465
+ : runStream;
466
+ const stream = scoped();
467
+ this.runServerStream(frame.id, stream, ctx, hintBudget);
468
+ } catch (err) {
469
+ this.transport.send({ op: "stream", id: frame.id, ev: "error", error: toStatus(err) });
470
+ }
471
+ return;
472
+ }
473
+
474
+ if (isCallDef(methodDef)) {
475
+ const deadlineTimer = methodDef.idempotent ? undefined : this.armServerDeadline(frame, ctx);
476
+ this.serverActiveCalls.set(frame.id, (ctx as unknown as { _ctrl: AbortController })._ctrl);
477
+ const encoder = makeReturnEncoder(methodDef.returns);
478
+ const runImpl = () => Promise.resolve(impl(frame.args, ctx));
479
+ const als = callContextStorage;
480
+ const scoped = als
481
+ ? () => als.run({ callId: frame.id }, runImpl)
482
+ : runImpl;
483
+ try {
484
+ const result = await scoped();
485
+ if (deadlineTimer) clearTimeout(deadlineTimer);
486
+ if (!this.serverActiveCalls.delete(frame.id)) return;
487
+ const encoded = encoder(result);
488
+ this.transport.send({ op: "result", id: frame.id, ok: true, value: encoded });
489
+ } catch (err) {
490
+ if (deadlineTimer) clearTimeout(deadlineTimer);
491
+ if (!this.serverActiveCalls.delete(frame.id)) return;
492
+ this.sendErrorFromException(frame.id, err);
493
+ }
494
+ return;
495
+ }
496
+
497
+ this.sendError(frame.id, "not_supported", "unknown method kind");
498
+ }
499
+
500
+ private armServerDeadline(frame: CallFrame, ctx: CallCtx): ReturnType<typeof setTimeout> | undefined {
501
+ const ms = frame.meta?.deadlineMs;
502
+ if (!ms) return;
503
+ return setTimeout(() => {
504
+ (ctx as unknown as { _ctrl: AbortController })._ctrl.abort();
505
+ }, ms);
506
+ }
507
+
508
+ private makeCallCtx(frame: CallFrame): CallCtx {
509
+ const ctrl = new AbortController();
510
+ const ctx: CallCtx & { _ctrl: AbortController } = {
511
+ callId: frame.id,
512
+ peerId: this.peerId,
513
+ attestation: this.attestation,
514
+ signal: ctrl.signal,
515
+ deadline: frame.meta?.deadlineMs,
516
+ context: frame.meta?.context,
517
+ _ctrl: ctrl,
518
+ exportCap: (capDef, impl) => this.exportCap(capDef, impl),
519
+ };
520
+ return ctx;
521
+ }
522
+
523
+ private exportCap<C extends CapDef<any, any>>(capDef: C, impl: unknown): ExportedCap<C> {
524
+ const typeId = frameworkTypeIdOf(capDef) ?? FIRST_USER_TYPE_ID;
525
+ const entry = this.capTable.allocate({
526
+ typeId,
527
+ cap: capDef,
528
+ impl,
529
+ refCount: 1,
530
+ });
531
+ return {
532
+ [EXPORTED_CAP_BRAND]: true,
533
+ cap: capDef,
534
+ capId: entry.capId,
535
+ typeId: entry.typeId,
536
+ } as unknown as ExportedCap<C>;
537
+ }
538
+
539
+ private runServerStream(callId: number, stream: StreamType<unknown>, ctx: CallCtx, initialCredit: number): void {
540
+ const iter = (stream as AsyncIterable<unknown>)[Symbol.asyncIterator]();
541
+ const abort = (ctx as unknown as { _ctrl: AbortController })._ctrl;
542
+ const slot: ServerStreamCtx = {
543
+ iter,
544
+ abort,
545
+ cancelled: false,
546
+ credit: initialCredit,
547
+ creditWaker: null,
548
+ };
549
+ this.serverStreams.set(callId, slot);
550
+
551
+ const waitForCredit = (): Promise<void> => {
552
+ if (slot.credit > 0 || slot.cancelled) return Promise.resolve();
553
+ return new Promise<void>((resolve) => { slot.creditWaker = resolve; });
554
+ };
555
+
556
+ const drain = async () => {
557
+ let errored = false;
558
+ try {
559
+ while (!slot.cancelled) {
560
+ if (slot.credit === 0) await waitForCredit();
561
+ if (slot.cancelled) break;
562
+ const { done, value } = await iter.next();
563
+ if (done) break;
564
+ if (slot.cancelled) break;
565
+ slot.credit -= 1;
566
+ this.transport.send({ op: "stream", id: callId, ev: "next", value });
567
+ }
568
+ } catch (err) {
569
+ errored = true;
570
+ this.transport.send({ op: "stream", id: callId, ev: "error", error: toStatus(err) });
571
+ } finally {
572
+ if (!errored) this.transport.send({ op: "stream", id: callId, ev: "end" });
573
+ this.serverStreams.delete(callId);
574
+ }
575
+ };
576
+ void drain();
577
+ }
578
+
579
+ private handleResult(frame: ResultFrame): void {
580
+ const pending = this.pending.get(frame.id);
581
+ if (pending) {
582
+ this.pending.delete(frame.id);
583
+ this.serverCallChildren.delete(frame.id);
584
+ if (pending.timer) clearTimeout(pending.timer);
585
+ if (frame.ok) {
586
+ const value = pending.decodeReturn ? pending.decodeReturn(frame.value) : frame.value;
587
+ pending.resolve(value);
588
+ } else {
589
+ pending.reject(new IpcError(frame.error));
590
+ }
591
+ return;
592
+ }
593
+ const stream = this.clientStreams.get(frame.id);
594
+ if (stream) {
595
+ this.clientStreams.delete(frame.id);
596
+ const err = frame.ok
597
+ ? new IpcError({ code: "protocol_error", message: "stream method returned result frame" })
598
+ : new IpcError(frame.error);
599
+ stream.fail(err);
600
+ }
601
+ }
602
+
603
+ private handleCancel(frame: CancelFrame): void {
604
+ const stream = this.serverStreams.get(frame.id);
605
+ if (stream) {
606
+ stream.cancelled = true;
607
+ stream.abort.abort();
608
+ void stream.iter?.return?.();
609
+ this.serverStreams.delete(frame.id);
610
+ }
611
+ const active = this.serverActiveCalls.get(frame.id);
612
+ if (active) {
613
+ this.serverActiveCalls.delete(frame.id);
614
+ active.abort();
615
+ this.transport.send({
616
+ op: "result",
617
+ id: frame.id,
618
+ ok: false,
619
+ error: { code: "cancelled", message: frame.reason },
620
+ });
621
+ }
622
+ for (const [childId, child] of this.serverCallChildren) {
623
+ if (child.parentId !== frame.id) continue;
624
+ this.serverCallChildren.delete(childId);
625
+ if (this.pending.has(childId)) {
626
+ this.transport.send({ op: "cancel", id: childId, reason: frame.reason });
627
+ }
628
+ }
629
+ }
630
+
631
+ releaseRef(proxy: unknown): void {
632
+ if (typeof proxy !== "object" || proxy === null) return;
633
+ const meta = (proxy as Record<symbol, unknown>)[CAP_PROXY_META] as { capId: number; dropped: boolean } | undefined;
634
+ if (!meta || meta.dropped) return;
635
+ meta.dropped = true;
636
+ this.sendDrop(meta.capId);
637
+ }
638
+
639
+ private sendDrop(capId: number): void {
640
+ if (this.closed_) return;
641
+ if (capId < FIRST_USER_CAP_ID) return;
642
+ this.transport.send({ op: "drop", caps: [{ id: capId, delta: 1 }] });
643
+ }
644
+
645
+ private handleStreamFrame(frame: StreamFrame): void {
646
+ if (frame.ev === "credit") {
647
+ const slot = this.serverStreams.get(frame.id);
648
+ if (!slot) return;
649
+ slot.credit += frame.credit?.messages ?? 0;
650
+ const waker = slot.creditWaker;
651
+ slot.creditWaker = null;
652
+ waker?.();
653
+ return;
654
+ }
655
+ const stream = this.clientStreams.get(frame.id);
656
+ if (!stream) return;
657
+ switch (frame.ev) {
658
+ case "next":
659
+ stream.push(frame.value);
660
+ return;
661
+ case "end":
662
+ stream.end();
663
+ this.clientStreams.delete(frame.id);
664
+ return;
665
+ case "error":
666
+ stream.fail(new IpcError(frame.error));
667
+ this.clientStreams.delete(frame.id);
668
+ return;
669
+ }
670
+ }
671
+
672
+ private handleDrop(frame: DropFrame): void {
673
+ for (const { id, delta } of frame.caps) {
674
+ const released = this.capTable.release(id, delta);
675
+ if (released) {
676
+ for (const [rootName, capId] of this.rootInstances) {
677
+ if (capId === id) { this.rootInstances.delete(rootName); break; }
678
+ }
679
+ }
680
+ }
681
+ }
682
+
683
+ private sendError(callId: number, code: IpcCode, message?: string): void {
684
+ this.transport.send({ op: "result", id: callId, ok: false, error: { code, message } });
685
+ }
686
+
687
+ private sendErrorFromException(callId: number, err: unknown): void {
688
+ this.transport.send({ op: "result", id: callId, ok: false, error: toStatus(err) });
689
+ }
690
+
691
+ private nextId(): number {
692
+ return this.nextCallId++;
693
+ }
694
+
695
+ sendCallRaw(capId: number, methodIdx: number, args: unknown, meta?: CallMeta): Promise<unknown> {
696
+ return this.sendCallTyped(capId, methodIdx, args, undefined, meta);
697
+ }
698
+
699
+ sendCallTyped(
700
+ capId: number,
701
+ methodIdx: number,
702
+ args: unknown,
703
+ decodeReturn: ReturnDecoder | undefined,
704
+ meta?: CallMeta
705
+ ): Promise<unknown> {
706
+ if (this.closed_) return Promise.reject(new IpcError({ code: "unavailable", message: "connection closed" }));
707
+ const id = this.nextId();
708
+ return new Promise((resolve, reject) => {
709
+ const abort = new AbortController();
710
+ const pending: PendingCall = { resolve, reject, abort, decodeReturn };
711
+ if (meta?.deadlineMs) {
712
+ pending.timer = setTimeout(() => {
713
+ if (this.pending.delete(id)) {
714
+ this.transport.send({ op: "cancel", id, reason: "deadline_exceeded" });
715
+ reject(new IpcError({ code: "deadline_exceeded" }));
716
+ }
717
+ }, meta.deadlineMs + DEFAULT_DEADLINE_GRACE_MS);
718
+ }
719
+ this.pending.set(id, pending);
720
+ let finalMeta = meta;
721
+ if ((finalMeta?.parentCallId === undefined) && callContextStorage) {
722
+ const scope = callContextStorage.getStore();
723
+ if (scope) finalMeta = { ...(finalMeta ?? {}), parentCallId: scope.callId };
724
+ }
725
+ if (finalMeta?.parentCallId !== undefined) {
726
+ this.serverCallChildren.set(id, { parentId: finalMeta.parentCallId });
727
+ }
728
+ this.transport.send({ op: "call", id, target: { kind: "cap", id: capId }, method: methodIdx, args, meta: finalMeta });
729
+ });
730
+ }
731
+
732
+ openClientStream<T>(capId: number, methodIdx: number, args: unknown, meta?: CallMeta): StreamType<T> {
733
+ if (this.closed_) {
734
+ const empty = makeClientStream<T>(0, () => {}, () => {});
735
+ empty.fail(new IpcError({ code: "unavailable", message: "connection closed" }));
736
+ return empty.stream;
737
+ }
738
+ const id = this.nextId();
739
+ const cancel = () => {
740
+ if (this.clientStreams.delete(id)) {
741
+ this.transport.send({ op: "cancel", id, reason: "client_cancel" });
742
+ }
743
+ };
744
+ const sendCredit = (messages: number) => {
745
+ if (this.closed_ || !this.clientStreams.has(id)) return;
746
+ this.transport.send({ op: "stream", id, ev: "credit", credit: { messages } });
747
+ };
748
+ const ctx = makeClientStream<T>(id, cancel, sendCredit);
749
+ this.clientStreams.set(id, ctx);
750
+ this.transport.send({ op: "call", id, target: { kind: "cap", id: capId }, method: methodIdx, args, meta });
751
+ return ctx.stream;
752
+ }
753
+
754
+ makeCapProxy<C extends CapDef<any, any>>(capDef: C, capId: number): ClientOf<C> {
755
+ const methodNames = Object.keys(capDef.methods);
756
+ const proxy: Record<string | symbol, unknown> = {};
757
+ const meta = { capId, dropped: false };
758
+ proxy[CAP_PROXY_META] = meta;
759
+ const drop = () => {
760
+ if (meta.dropped) return;
761
+ meta.dropped = true;
762
+ this.sendDrop(capId);
763
+ };
764
+
765
+ for (let i = 0; i < methodNames.length; i++) {
766
+ const name = methodNames[i];
767
+ const def = capDef.methods[name] as MethodDef;
768
+ if (isStreamDef(def)) {
769
+ proxy[name] = (params?: unknown) => this.openClientStream(capId, i, params);
770
+ } else if (isCallDef(def)) {
771
+ const decoder = makeReturnDecoder(def.returns, (cd, cid) => this.makeCapProxy(cd, cid));
772
+ proxy[name] = (params?: unknown) => this.sendCallTyped(capId, i, params, decoder);
773
+ }
774
+ }
775
+
776
+ const disposal = capDef.disposal as DisposalSpec | undefined;
777
+ if (disposal) {
778
+ const invokeMethod = (): unknown => {
779
+ const fn = proxy[disposal.method] as (() => unknown) | undefined;
780
+ return fn?.();
781
+ };
782
+ proxy[Symbol.asyncDispose] = async () => {
783
+ const r = invokeMethod();
784
+ await Promise.resolve(r);
785
+ drop();
786
+ };
787
+ proxy[Symbol.dispose] = () => {
788
+ const r = invokeMethod();
789
+ drop();
790
+ void r;
791
+ };
792
+ }
793
+
794
+ if (typeof FinalizationRegistry !== "undefined") {
795
+ proxyFinalizers.register(proxy as object, {
796
+ connRef: new WeakRef(this),
797
+ capId,
798
+ dropped: () => meta.dropped,
799
+ });
800
+ }
801
+
802
+ return proxy as ClientOf<C>;
803
+ }
804
+
805
+ _dropFromFinalizer(capId: number): void {
806
+ try { this.sendDrop(capId); } catch { /* swallow */ }
807
+ }
808
+
809
+ shutdown(reason: string): void {
810
+ if (this.closed_) return;
811
+ this.closed_ = true;
812
+ for (const pending of this.pending.values()) {
813
+ if (pending.timer) clearTimeout(pending.timer);
814
+ pending.reject(new IpcError({ code: "unavailable", message: reason }));
815
+ }
816
+ this.pending.clear();
817
+ for (const stream of this.clientStreams.values()) {
818
+ stream.fail(new IpcError({ code: "unavailable", message: reason }));
819
+ }
820
+ this.clientStreams.clear();
821
+ for (const slot of this.serverStreams.values()) {
822
+ slot.cancelled = true;
823
+ slot.abort.abort();
824
+ void slot.iter?.return?.();
825
+ }
826
+ this.serverStreams.clear();
827
+ for (const ctrl of this.serverActiveCalls.values()) {
828
+ ctrl.abort();
829
+ }
830
+ this.serverActiveCalls.clear();
831
+ for (const entry of this.capTable.values()) {
832
+ this.invokeServerDisposal(entry);
833
+ }
834
+ this.capTable.clear();
835
+ if (!this.remoteHello) {
836
+ this.rejectRemoteReady(new IpcError({ code: "unavailable", message: reason }));
837
+ }
838
+ for (const fn of this.closeHandlers) {
839
+ try { fn(); } catch { /* swallow */ }
840
+ }
841
+ this.closeHandlers.clear();
842
+ try { this.transport.close(); } catch { /* swallow */ }
843
+ }
844
+
845
+ private invokeServerDisposal(entry: CapTableEntry): void {
846
+ const cap = entry.cap;
847
+ if (!cap) return;
848
+ const disposal = cap.disposal as DisposalSpec | undefined;
849
+ if (!disposal) return;
850
+ const impl = entry.impl as Record<string, unknown> | null;
851
+ const fn = impl?.[disposal.method] as ((p: unknown, c?: unknown) => unknown) | undefined;
852
+ if (!fn) return;
853
+ try { void fn.call(impl, undefined, undefined); } catch { /* swallow */ }
854
+ }
855
+ }
856
+
857
+ function makeReturnEncoder(returns: AnyCapToken | undefined): (v: unknown) => unknown {
858
+ if (!returns) return (v) => v;
859
+ const expectExportedCap = (v: unknown, expectedCap: CapDef<any, any>): CapRef => {
860
+ if (!isExportedCap(v)) {
861
+ throw new IpcError({ code: "protocol_error", message: "expected ctx.exportCap return for cap method" });
862
+ }
863
+ if (v.cap !== expectedCap) {
864
+ throw new IpcError({ code: "protocol_error", message: "exported cap type mismatch with method returns" });
865
+ }
866
+ return new CapRef(v.capId);
867
+ };
868
+ if (isCapRef(returns)) return (v) => expectExportedCap(v, returns.cap);
869
+ if (isCapArray(returns)) {
870
+ return (v) => {
871
+ if (!Array.isArray(v)) {
872
+ throw new IpcError({ code: "protocol_error", message: "expected ExportedCap[] for cap.array method" });
873
+ }
874
+ return v.map((item) => expectExportedCap(item, returns.cap));
875
+ };
876
+ }
877
+ if (isCapRecord(returns)) {
878
+ return (v) => {
879
+ if (!v || typeof v !== "object") {
880
+ throw new IpcError({ code: "protocol_error", message: "expected Record<string, ExportedCap> for cap.record method" });
881
+ }
882
+ const out: Record<string, CapRef> = {};
883
+ for (const k of Object.keys(v as object)) {
884
+ out[k] = expectExportedCap((v as Record<string, unknown>)[k], returns.cap);
885
+ }
886
+ return out;
887
+ };
888
+ }
889
+ return (v) => v;
890
+ }
891
+
892
+ function makeReturnDecoder(
893
+ returns: { cap?: CapDef<any, any> } | undefined,
894
+ spawn: (cap: CapDef<any, any>, capId: number) => unknown
895
+ ): ReturnDecoder | undefined {
896
+ if (!returns) return undefined;
897
+ if (isCapRef(returns)) {
898
+ const inner = returns.cap;
899
+ return (raw) => {
900
+ if (!(raw instanceof CapRef)) throw new IpcError({ code: "protocol_error", message: "expected CapRef" });
901
+ return spawn(inner, raw.capId);
902
+ };
903
+ }
904
+ if (isCapArray(returns)) {
905
+ const inner = returns.cap;
906
+ const disposal = inner.disposal as DisposalSpec | undefined;
907
+ return (raw) => {
908
+ if (!Array.isArray(raw)) throw new IpcError({ code: "protocol_error", message: "expected array" });
909
+ const proxies = raw.map((item) => {
910
+ if (!(item instanceof CapRef)) throw new IpcError({ code: "protocol_error", message: "expected CapRef in array" });
911
+ return spawn(inner, item.capId);
912
+ });
913
+ attachArrayDisposal(proxies, disposal);
914
+ return proxies;
915
+ };
916
+ }
917
+ if (isCapRecord(returns)) {
918
+ const inner = returns.cap;
919
+ return (raw) => {
920
+ if (!raw || typeof raw !== "object") throw new IpcError({ code: "protocol_error", message: "expected record" });
921
+ const out: Record<string, unknown> = {};
922
+ for (const k of Object.keys(raw as object)) {
923
+ const item = (raw as Record<string, unknown>)[k];
924
+ if (!(item instanceof CapRef)) throw new IpcError({ code: "protocol_error", message: "expected CapRef in record" });
925
+ out[k] = spawn(inner, item.capId);
926
+ }
927
+ return out;
928
+ };
929
+ }
930
+ return undefined;
931
+ }
932
+
933
+ function attachArrayDisposal(arr: unknown[], disposal: DisposalSpec | undefined): void {
934
+ if (!disposal) return;
935
+ const sym = disposal.async ? Symbol.asyncDispose : Symbol.dispose;
936
+ (arr as any)[sym] = disposal.async
937
+ ? () => Promise.all(arr.map((proxy) => {
938
+ const fn = (proxy as Record<symbol, unknown>)[Symbol.asyncDispose] as (() => Promise<void>) | undefined;
939
+ return fn ? fn.call(proxy) : undefined;
940
+ })).then(() => undefined)
941
+ : () => {
942
+ for (const proxy of arr) {
943
+ const fn = (proxy as Record<symbol, unknown>)[Symbol.dispose] as (() => void) | undefined;
944
+ fn?.call(proxy);
945
+ }
946
+ };
947
+ }
948
+
949
+ function toStatus(err: unknown): IpcStatus {
950
+ if (err instanceof IpcError) return err.toStatus();
951
+ if (err instanceof Error) return { code: "unknown", message: err.message };
952
+ return { code: "unknown", message: String(err) };
953
+ }
954
+
955
+ interface ClientStreamHandle<T> {
956
+ stream: StreamType<T>;
957
+ push(chunk: unknown): void;
958
+ end(): void;
959
+ fail(error: IpcError): void;
960
+ }
961
+
962
+ function makeClientStream<T>(
963
+ streamId: number,
964
+ cancel: () => void,
965
+ sendCredit: (messages: number) => void
966
+ ): ClientStreamHandle<T> {
967
+ const buffer: T[] = [];
968
+ const waiters: Array<{ resolve(r: IteratorResult<T>): void; reject(e: Error): void }> = [];
969
+ let ended = false;
970
+ let failure: IpcError | null = null;
971
+ let cancelled = false;
972
+ let consumedSinceCredit = 0;
973
+
974
+ function tally() {
975
+ consumedSinceCredit += 1;
976
+ if (consumedSinceCredit >= DEFAULT_STREAM_CREDIT_BATCH) {
977
+ sendCredit(consumedSinceCredit);
978
+ consumedSinceCredit = 0;
979
+ }
980
+ }
981
+
982
+ function pump(): IteratorResult<T> | null {
983
+ if (buffer.length > 0) {
984
+ const value = buffer.shift()!;
985
+ tally();
986
+ return { value, done: false };
987
+ }
988
+ if (failure) return null;
989
+ if (ended || cancelled) return { value: undefined as unknown as T, done: true };
990
+ return null;
991
+ }
992
+
993
+ function next(): Promise<IteratorResult<T>> {
994
+ const immediate = pump();
995
+ if (immediate) return Promise.resolve(immediate);
996
+ if (failure) return Promise.reject(failure);
997
+ return new Promise((resolve, reject) => waiters.push({ resolve, reject }));
998
+ }
999
+
1000
+ function drainWaiters(): void {
1001
+ while (waiters.length > 0) {
1002
+ const w = waiters.shift()!;
1003
+ const r = pump();
1004
+ if (r) { w.resolve(r); continue; }
1005
+ if (failure) { w.reject(failure); continue; }
1006
+ waiters.unshift(w);
1007
+ break;
1008
+ }
1009
+ }
1010
+
1011
+ function doCancel(): void {
1012
+ if (cancelled || ended || failure) return;
1013
+ cancelled = true;
1014
+ cancel();
1015
+ drainWaiters();
1016
+ }
1017
+
1018
+ const stream: StreamType<T> = {
1019
+ [Symbol.asyncIterator]: () => ({
1020
+ next,
1021
+ return: () => {
1022
+ doCancel();
1023
+ return Promise.resolve({ value: undefined as unknown as T, done: true });
1024
+ },
1025
+ throw: (err: unknown) => {
1026
+ doCancel();
1027
+ return Promise.reject(err);
1028
+ },
1029
+ }),
1030
+ [Symbol.dispose]: doCancel,
1031
+ cancel: doCancel,
1032
+ } as StreamType<T>;
1033
+ void streamId;
1034
+
1035
+ return {
1036
+ stream,
1037
+ push(chunk) {
1038
+ if (cancelled || ended || failure) return;
1039
+ buffer.push(chunk as T);
1040
+ drainWaiters();
1041
+ },
1042
+ end() {
1043
+ ended = true;
1044
+ drainWaiters();
1045
+ },
1046
+ fail(error) {
1047
+ failure = error;
1048
+ drainWaiters();
1049
+ },
1050
+ };
1051
+ }
1052
+
1053
+ export function createConnection(opts: ConnectionOptions): Connection & ConnectionImpl {
1054
+ return new ConnectionImpl(opts) as Connection & ConnectionImpl;
1055
+ }