bunite-core 0.8.1 → 0.10.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 (56) 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 +71 -65
  4. package/src/{bun → host}/core/BrowserWindow.ts +15 -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 +105 -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 +52 -219
  21. package/src/preload/tsconfig.json +3 -10
  22. package/src/rpc/encrypt.ts +74 -0
  23. package/src/rpc/error.ts +68 -0
  24. package/src/rpc/framework.ts +132 -0
  25. package/src/rpc/index.ts +138 -0
  26. package/src/rpc/peer.ts +1438 -0
  27. package/src/rpc/renderer.ts +80 -0
  28. package/src/rpc/schema.ts +229 -0
  29. package/src/rpc/stream.ts +72 -0
  30. package/src/rpc/transport.ts +81 -0
  31. package/src/rpc/wire.ts +164 -0
  32. package/src/{preload/webviewElement.ts → webview/native.ts} +68 -48
  33. package/src/{shared/webviewPolyfill.ts → webview/polyfill.ts} +4 -7
  34. package/src/bun/core/Socket.ts +0 -187
  35. package/src/bun/core/SurfaceBrowserIPC.ts +0 -65
  36. package/src/bun/core/SurfaceManager.ts +0 -201
  37. package/src/bun/index.ts +0 -53
  38. package/src/bun/preload/index.ts +0 -73
  39. package/src/preload/tsconfig.tsbuildinfo +0 -1
  40. package/src/shared/rpc.ts +0 -424
  41. package/src/shared/rpcDemux.ts +0 -219
  42. package/src/shared/rpcWire.ts +0 -54
  43. package/src/shared/rpcWireConstants.ts +0 -3
  44. package/src/shared/webRpcHandler.ts +0 -77
  45. package/src/shared/webSocketTransport.ts +0 -26
  46. package/src/view/index.ts +0 -196
  47. /package/src/{shared → host}/cefVersion.ts +0 -0
  48. /package/src/{bun → host}/core/SurfaceRegistry.ts +0 -0
  49. /package/src/{bun → host}/core/singleInstanceLock.ts +0 -0
  50. /package/src/{bun → host}/core/windowIds.ts +0 -0
  51. /package/src/{bun → host}/events/event.ts +0 -0
  52. /package/src/{bun → host}/events/eventEmitter.ts +0 -0
  53. /package/src/{bun → host}/events/webviewEvents.ts +0 -0
  54. /package/src/{bun → host}/events/windowEvents.ts +0 -0
  55. /package/src/{shared → host}/log.ts +0 -0
  56. /package/src/{shared → host}/platform.ts +0 -0
@@ -0,0 +1,1438 @@
1
+ import {
2
+ type CapDef,
3
+ type Schema,
4
+ type SchemaRoots,
5
+ type ClientOf,
6
+ type ImplOf,
7
+ type ImplsOf,
8
+ type CallCtx,
9
+ type Attestation,
10
+ type ExportedCap,
11
+ type MethodDef,
12
+ type AnyCapToken,
13
+ type Stream as StreamType,
14
+ type DisposalSpec,
15
+ isCallDef,
16
+ isStreamDef,
17
+ isCapRef,
18
+ isCapArray,
19
+ isCapRecord,
20
+ isCapDef,
21
+ isSchema,
22
+ } from "./schema";
23
+ import { RuntimeCap, frameworkTypeIdOf } from "./framework";
24
+ import {
25
+ type Frame,
26
+ type CallFrame,
27
+ type ResultFrame,
28
+ type StreamFrame,
29
+ type CancelFrame,
30
+ type DropFrame,
31
+ type HelloFrame,
32
+ type CapRevokedFrame,
33
+ type CallMeta,
34
+ CapRef,
35
+ DEFAULT_MAX_BYTES,
36
+ PROTOCOL_VERSION,
37
+ FRAMEWORK_NAME_PREFIX,
38
+ BOOTSTRAP_METHOD,
39
+ } from "./wire";
40
+ import {
41
+ IpcError,
42
+ type IpcStatus,
43
+ type IpcCode,
44
+ type FailedPreconditionReason,
45
+ type ResourceExhaustedReason,
46
+ type AlreadyExistsReason,
47
+ } from "./error";
48
+
49
+ export const USER_ROOTS_CAP_ID = 0;
50
+ export const RUNTIME_CAP_ID = 1;
51
+ export const USER_ROOTS_TYPE_ID = 0;
52
+ export const RUNTIME_TYPE_ID = 1;
53
+
54
+ export const FIRST_USER_CAP_ID = 2;
55
+ export const FIRST_USER_TYPE_ID = 128;
56
+
57
+ export const MAX_CAPS_PER_CONNECTION = 1024;
58
+ export const MAX_IN_FLIGHT_CALLS_PER_CONNECTION = 1024;
59
+
60
+ const DEFAULT_DEADLINE_GRACE_MS = 500;
61
+ const DEFAULT_STREAM_INITIAL_CREDIT = 32;
62
+ const DEFAULT_STREAM_CREDIT_BATCH = 8;
63
+ const MAX_STREAM_INITIAL_CREDIT = 1024;
64
+
65
+ function resolveStreamBudget(hint: Record<string, unknown> | undefined): number {
66
+ const raw = hint?.initialBudget;
67
+ if (typeof raw !== "number" || !Number.isFinite(raw) || !Number.isInteger(raw) || raw < 1) {
68
+ return DEFAULT_STREAM_INITIAL_CREDIT;
69
+ }
70
+ return Math.min(raw, MAX_STREAM_INITIAL_CREDIT);
71
+ }
72
+
73
+ interface CallScope {
74
+ readonly callId: number;
75
+ }
76
+
77
+ export interface CallContextStorage {
78
+ getStore(): CallScope | undefined;
79
+ run<R>(store: CallScope, fn: () => R): R;
80
+ }
81
+
82
+ let callContextStorage: CallContextStorage | null = null;
83
+
84
+ export function _setCallContextStorage(als: CallContextStorage | null): void {
85
+ callContextStorage = als;
86
+ }
87
+
88
+ export interface CapTableEntry {
89
+ capId: number;
90
+ typeId: number;
91
+ cap: CapDef<any, any> | null;
92
+ impl: unknown;
93
+ refCount: number;
94
+ }
95
+
96
+ export class CapTable {
97
+ private readonly entries = new Map<number, CapTableEntry>();
98
+ private nextCapId = FIRST_USER_CAP_ID;
99
+ private readonly capLimit: number;
100
+
101
+ constructor(capLimit = MAX_CAPS_PER_CONNECTION) {
102
+ this.capLimit = capLimit;
103
+ }
104
+
105
+ install(capId: number, entry: Omit<CapTableEntry, "capId">): CapTableEntry {
106
+ if (this.entries.has(capId)) throw new Error(`cap-id ${capId} already installed`);
107
+ const full = { capId, ...entry };
108
+ this.entries.set(capId, full);
109
+ return full;
110
+ }
111
+
112
+ allocate(entry: Omit<CapTableEntry, "capId">): CapTableEntry {
113
+ if (this.entries.size >= this.capLimit) {
114
+ throw new IpcError({
115
+ code: "resource_exhausted",
116
+ message: `cap-table limit ${this.capLimit}`,
117
+ details: { reason: "max_caps_per_connection" as ResourceExhaustedReason },
118
+ });
119
+ }
120
+ let capId = this.nextCapId++;
121
+ while (this.entries.has(capId)) capId = this.nextCapId++;
122
+ return this.install(capId, entry);
123
+ }
124
+
125
+ get(capId: number): CapTableEntry | undefined {
126
+ return this.entries.get(capId);
127
+ }
128
+
129
+ release(capId: number, delta = 1): boolean {
130
+ const entry = this.entries.get(capId);
131
+ if (!entry) return false;
132
+ entry.refCount = Math.max(0, entry.refCount - delta);
133
+ if (entry.refCount === 0 && capId >= FIRST_USER_CAP_ID) {
134
+ this.entries.delete(capId);
135
+ return true;
136
+ }
137
+ return false;
138
+ }
139
+
140
+ delete(capId: number): boolean {
141
+ if (capId < FIRST_USER_CAP_ID) return false;
142
+ return this.entries.delete(capId);
143
+ }
144
+
145
+ clear(): void {
146
+ this.entries.clear();
147
+ this.nextCapId = FIRST_USER_CAP_ID;
148
+ }
149
+
150
+ size(): number {
151
+ return this.entries.size;
152
+ }
153
+
154
+ values(): IterableIterator<CapTableEntry> {
155
+ return this.entries.values();
156
+ }
157
+ }
158
+
159
+ export interface Transport {
160
+ send(frame: Frame): void;
161
+ setReceive(handler: (frame: Frame) => void): void;
162
+ close(): void;
163
+ }
164
+
165
+ export type Policy = (name: string, attestation: Attestation) => boolean | Promise<boolean>;
166
+
167
+ export type IfExists = "throw" | "replace" | "skip";
168
+
169
+ export interface ServeHandle extends Disposable {
170
+ readonly names: readonly string[];
171
+ }
172
+
173
+ export interface ConnectionEvents {
174
+ bootstrap: { name: string; version?: string; attestation: Attestation; result: "ok" | "denied" | "not_found" | "version_mismatch" | "invalid_argument" | "resource_exhausted" | "internal"; capId?: number };
175
+ call: { capId: number; capName?: string; method: string; callId: number; durationMs?: number; result: "ok" | "cancelled" | IpcCode };
176
+ stream: { capId: number; capName?: string; method: string; callId: number; event: "start" | "end" | "cancel" | "error"; count?: number };
177
+ revoke: { capIds: number[]; reason: "unserve" | "replace" };
178
+ error: { phase: string; error: Error };
179
+ }
180
+
181
+ export interface Connection {
182
+ bootstrap<C extends CapDef<any, any>>(cap: C): Promise<ClientOf<C>>;
183
+ bootstrap<R extends SchemaRoots>(schema: Schema<R>): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
184
+ serve<C extends CapDef<any, any>>(cap: C, impl: ImplOf<C>, opts?: { ifExists?: IfExists }): ServeHandle;
185
+ serveAll<R extends SchemaRoots>(schema: Schema<R>, impls: ImplsOf<R>, opts?: { ifExists?: IfExists }): ServeHandle;
186
+ unserve(target: CapDef<any, any> | ServeHandle): void;
187
+ replace<C extends CapDef<any, any>>(cap: C, impl: ImplOf<C>): void;
188
+ runtime(): ClientOf<typeof RuntimeCap>;
189
+ releaseRef(proxy: unknown): void;
190
+ on<K extends keyof ConnectionEvents>(event: K, handler: (e: ConnectionEvents[K]) => void): () => void;
191
+ onClose(handler: () => void): () => void;
192
+ readonly closed: boolean;
193
+ }
194
+
195
+ export interface ConnectionOptions {
196
+ transport: Transport;
197
+ mode: "native" | "web";
198
+ origin: string;
199
+ features?: string[];
200
+ maxBytes?: number;
201
+ capLimit?: number;
202
+ maxInFlightCalls?: number;
203
+ peerId?: string;
204
+ attestation?: Attestation;
205
+ runtime?: ImplOf<typeof RuntimeCap>;
206
+ policy?: Policy;
207
+ }
208
+
209
+ export interface PendingCall {
210
+ resolve(value: unknown): void;
211
+ reject(error: Error): void;
212
+ abort: AbortController;
213
+ decodeReturn?: ReturnDecoder;
214
+ timer?: ReturnType<typeof setTimeout>;
215
+ startedAt?: number;
216
+ capId?: number;
217
+ capName?: string;
218
+ method?: string;
219
+ kind?: "call" | "stream";
220
+ }
221
+
222
+ type ReturnDecoder = (raw: unknown) => unknown;
223
+
224
+ // Default is conservative: "untrusted" until the host explicitly hands a real attestation
225
+ // (engine-mined for native, derived from req origin for web). Policy hooks should treat absence as deny-able.
226
+ const DEFAULT_ATTESTATION: Attestation = {
227
+ origin: "",
228
+ topOrigin: "",
229
+ partition: "default",
230
+ isAppRes: false,
231
+ isMainFrame: false,
232
+ userGesture: false,
233
+ level: "untrusted",
234
+ };
235
+
236
+ const EXPORTED_CAP_BRAND = Symbol("bunite.rpc.ExportedCap");
237
+ const CAP_PROXY_META = Symbol("bunite.rpc.CapProxyMeta");
238
+
239
+ function isExportedCap(v: unknown): v is ExportedCap<any> {
240
+ return typeof v === "object" && v !== null && (v as any)[EXPORTED_CAP_BRAND] === true;
241
+ }
242
+
243
+ const proxyFinalizers = typeof FinalizationRegistry !== "undefined"
244
+ ? new FinalizationRegistry<{ connRef: WeakRef<ConnectionImpl>; capId: number; dropped: () => boolean }>((held) => {
245
+ if (held.dropped()) return;
246
+ const conn = held.connRef.deref();
247
+ if (!conn || conn.closed) return;
248
+ conn._dropFromFinalizer(held.capId);
249
+ })
250
+ : ({ register: () => {} } as { register: (target: object, held: unknown) => void });
251
+
252
+ interface ServerStreamCtx {
253
+ iter: AsyncIterator<unknown> | null;
254
+ abort: AbortController;
255
+ cancelled: boolean;
256
+ credit: number;
257
+ creditWaker: (() => void) | null;
258
+ capId: number;
259
+ capName?: string;
260
+ method: string;
261
+ callId: number;
262
+ count: number;
263
+ }
264
+
265
+ interface ClientStreamCtx {
266
+ capId: number;
267
+ push(chunk: unknown): void;
268
+ end(): void;
269
+ fail(error: IpcError): void;
270
+ }
271
+
272
+ interface RegistryEntry {
273
+ cap: CapDef<any, any>;
274
+ impl: unknown;
275
+ version?: string;
276
+ }
277
+
278
+ class ConnectionImpl implements Connection {
279
+ private readonly transport: Transport;
280
+ private readonly capTable: CapTable;
281
+ private readonly pending = new Map<number, PendingCall>();
282
+ private readonly clientStreams = new Map<number, ClientStreamCtx>();
283
+ private readonly serverStreams = new Map<number, ServerStreamCtx>();
284
+ private readonly serverCallChildren = new Map<number, { parentId: number }>();
285
+ private readonly serverActiveCalls = new Map<number, { ctrl: AbortController; capId: number; capName: string; method: string; startedAt: number }>();
286
+ /** Server-side: name → registry entry. */
287
+ private readonly registry = new Map<string, RegistryEntry>();
288
+ /** Server-side: name → instance cap-id (cached bootstrap result). */
289
+ private readonly rootInstances = new Map<string, number>();
290
+ /** Client-side: cap-ids that the server revoked via cap_revoked. */
291
+ private readonly revokedCapIds = new Set<number>();
292
+ private readonly closeHandlers = new Set<() => void>();
293
+ private readonly observers: { [K in keyof ConnectionEvents]?: Set<(e: ConnectionEvents[K]) => void> } = {};
294
+ private nextCallId = 1;
295
+ private remoteHello: HelloFrame | null = null;
296
+ private readonly remoteReady: Promise<HelloFrame>;
297
+ private resolveRemoteReady!: (h: HelloFrame) => void;
298
+ private rejectRemoteReady!: (e: Error) => void;
299
+ private closed_ = false;
300
+ private readonly maxBytes: number;
301
+ private readonly maxInFlightCalls: number;
302
+ private readonly mode: "native" | "web";
303
+ private readonly origin: string;
304
+ private readonly features: string[];
305
+ private readonly attestation: Attestation;
306
+ private readonly peerId: string;
307
+ private readonly policy: Policy | undefined;
308
+
309
+ constructor(opts: ConnectionOptions) {
310
+ this.transport = opts.transport;
311
+ this.mode = opts.mode;
312
+ this.origin = opts.origin;
313
+ this.features = opts.features ?? [];
314
+ this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
315
+ this.maxInFlightCalls = opts.maxInFlightCalls ?? MAX_IN_FLIGHT_CALLS_PER_CONNECTION;
316
+ this.attestation = opts.attestation ?? DEFAULT_ATTESTATION;
317
+ this.peerId = opts.peerId ?? "peer";
318
+ this.policy = opts.policy;
319
+ this.capTable = new CapTable(opts.capLimit ?? MAX_CAPS_PER_CONNECTION);
320
+
321
+ // cap-id 0 = bootstrap dispatcher (no cap def — special-cased in handleCall)
322
+ this.capTable.install(USER_ROOTS_CAP_ID, {
323
+ typeId: USER_ROOTS_TYPE_ID,
324
+ cap: null,
325
+ impl: null,
326
+ refCount: 1,
327
+ });
328
+ // cap-id 1 = Runtime instance (framework-stable, pre-installed)
329
+ this.capTable.install(RUNTIME_CAP_ID, {
330
+ typeId: RUNTIME_TYPE_ID,
331
+ cap: RuntimeCap,
332
+ impl: opts.runtime ?? null,
333
+ refCount: 1,
334
+ });
335
+
336
+ this.remoteReady = new Promise<HelloFrame>((res, rej) => {
337
+ this.resolveRemoteReady = res;
338
+ this.rejectRemoteReady = rej;
339
+ });
340
+ this.remoteReady.catch(() => {});
341
+
342
+ this.transport.setReceive((frame) => this.handleFrame(frame));
343
+ this.transport.send({
344
+ op: "hello",
345
+ v: PROTOCOL_VERSION,
346
+ mode: this.mode,
347
+ features: this.features,
348
+ maxBytes: this.maxBytes,
349
+ origin: this.origin,
350
+ });
351
+ }
352
+
353
+ get closed(): boolean {
354
+ return this.closed_;
355
+ }
356
+
357
+ onClose(handler: () => void): () => void {
358
+ this.closeHandlers.add(handler);
359
+ return () => this.closeHandlers.delete(handler);
360
+ }
361
+
362
+ on<K extends keyof ConnectionEvents>(event: K, handler: (e: ConnectionEvents[K]) => void): () => void {
363
+ let set = this.observers[event] as Set<(e: ConnectionEvents[K]) => void> | undefined;
364
+ if (!set) {
365
+ set = new Set();
366
+ (this.observers as Record<string, Set<unknown>>)[event] = set as unknown as Set<unknown>;
367
+ }
368
+ set.add(handler);
369
+ return () => { set!.delete(handler); };
370
+ }
371
+
372
+ private emitObs<K extends keyof ConnectionEvents>(event: K, data: ConnectionEvents[K]): void {
373
+ const set = this.observers[event] as Set<(e: ConnectionEvents[K]) => void> | undefined;
374
+ if (!set || set.size === 0) return;
375
+ for (const h of set) {
376
+ try { h(data); } catch { /* swallow */ }
377
+ }
378
+ }
379
+
380
+ // ---- serve / unserve / replace ----
381
+
382
+ serve<C extends CapDef<any, any>>(cap: C, impl: ImplOf<C>, opts?: { ifExists?: IfExists }): ServeHandle {
383
+ this.assertNotFrameworkName(cap.name);
384
+ const ifExists = opts?.ifExists ?? "throw";
385
+ const existing = this.registry.get(cap.name);
386
+ if (existing) {
387
+ switch (ifExists) {
388
+ case "throw":
389
+ throw new IpcError({
390
+ code: "already_exists",
391
+ message: `cap "${cap.name}" already served`,
392
+ details: { reason: "name_collision" as AlreadyExistsReason },
393
+ });
394
+ case "skip":
395
+ // Empty handle: skipping means we are not the owner — unserve must not revoke someone else's registration.
396
+ return this.makeHandle([]);
397
+ case "replace":
398
+ this.replace(cap, impl);
399
+ return this.makeHandle([cap.name]);
400
+ }
401
+ }
402
+ this.registry.set(cap.name, { cap, impl, version: cap.version });
403
+ return this.makeHandle([cap.name]);
404
+ }
405
+
406
+ private makeHandle(names: string[]): ServeHandle {
407
+ const handle: ServeHandle = {
408
+ names,
409
+ [Symbol.dispose]: () => this.unserve(handle),
410
+ };
411
+ return handle;
412
+ }
413
+
414
+ serveAll<R extends SchemaRoots>(schema: Schema<R>, impls: ImplsOf<R>, opts?: { ifExists?: IfExists }): ServeHandle {
415
+ const ifExists = opts?.ifExists ?? "throw";
416
+ // Pre-validate everything that can fail across all modes before mutating any state (atomicity).
417
+ for (const k of Object.keys(schema.roots)) {
418
+ const c = schema.roots[k];
419
+ this.assertNotFrameworkName(c.name);
420
+ const existing = this.registry.get(c.name);
421
+ if (existing) {
422
+ if (ifExists === "throw") {
423
+ throw new IpcError({
424
+ code: "already_exists",
425
+ message: `cap "${c.name}" already served`,
426
+ details: { reason: "name_collision" as AlreadyExistsReason },
427
+ });
428
+ }
429
+ if (ifExists === "replace" && existing.version !== c.version) {
430
+ throw new IpcError({
431
+ code: "failed_precondition",
432
+ message: `version mismatch on replace for "${c.name}" (current "${existing.version}", new "${c.version}")`,
433
+ details: { reason: "version_mismatch" as FailedPreconditionReason },
434
+ });
435
+ }
436
+ }
437
+ }
438
+ // Mutations below cannot fail (pre-validation already cleared collision/version/prefix paths).
439
+ const names: string[] = [];
440
+ for (const k of Object.keys(schema.roots)) {
441
+ const c = schema.roots[k];
442
+ const i = (impls as Record<string, unknown>)[k];
443
+ const h = this.serve(c, i as ImplOf<typeof c>, { ifExists });
444
+ // serve(...) returns empty names[] only for skipped entries — exclude from the aggregate handle.
445
+ if (h.names.length > 0) names.push(c.name);
446
+ }
447
+ return this.makeHandle(names);
448
+ }
449
+
450
+ unserve(target: CapDef<any, any> | ServeHandle): void {
451
+ const names = isCapDef(target) ? [target.name] : Array.from((target as ServeHandle).names);
452
+ const revoked: number[] = [];
453
+ for (const name of names) {
454
+ if (!this.registry.delete(name)) continue;
455
+ const instId = this.rootInstances.get(name);
456
+ if (instId !== undefined) {
457
+ const entry = this.capTable.get(instId);
458
+ if (entry) this.invokeServerDisposal(entry);
459
+ this.rootInstances.delete(name);
460
+ this.capTable.delete(instId);
461
+ revoked.push(instId);
462
+ }
463
+ }
464
+ if (revoked.length > 0) {
465
+ this.transport.send({ op: "cap_revoked", capIds: revoked });
466
+ this.emitObs("revoke", { capIds: revoked, reason: "unserve" });
467
+ }
468
+ }
469
+
470
+ replace<C extends CapDef<any, any>>(cap: C, impl: ImplOf<C>): void {
471
+ const entry = this.registry.get(cap.name);
472
+ if (!entry) throw new IpcError({ code: "not_found", message: `cap "${cap.name}" not served` });
473
+ if (entry.version !== cap.version) {
474
+ throw new IpcError({
475
+ code: "failed_precondition",
476
+ message: `version mismatch (current "${entry.version}", new "${cap.version}")`,
477
+ details: { reason: "version_mismatch" as FailedPreconditionReason },
478
+ });
479
+ }
480
+ entry.impl = impl;
481
+ entry.cap = cap;
482
+ const instId = this.rootInstances.get(cap.name);
483
+ if (instId !== undefined) {
484
+ const e = this.capTable.get(instId);
485
+ if (e) { e.impl = impl; e.cap = cap; }
486
+ }
487
+ this.emitObs("revoke", { capIds: instId !== undefined ? [instId] : [], reason: "replace" });
488
+ }
489
+
490
+ private assertNotFrameworkName(name: string): void {
491
+ if (name.startsWith(FRAMEWORK_NAME_PREFIX)) {
492
+ throw new IpcError({
493
+ code: "already_exists",
494
+ message: `cap name "${name}" uses reserved prefix "${FRAMEWORK_NAME_PREFIX}"`,
495
+ details: { reason: "reserved_namespace" as AlreadyExistsReason },
496
+ });
497
+ }
498
+ }
499
+
500
+ // ---- runtime / bootstrap ----
501
+
502
+ private runtimeProxy: ClientOf<typeof RuntimeCap> | null = null;
503
+
504
+ runtime(): ClientOf<typeof RuntimeCap> {
505
+ if (!this.runtimeProxy) {
506
+ this.runtimeProxy = this.makeCapProxy(RuntimeCap, RUNTIME_CAP_ID);
507
+ }
508
+ return this.runtimeProxy;
509
+ }
510
+
511
+ bootstrap<C extends CapDef<any, any>>(cap: C): Promise<ClientOf<C>>;
512
+ bootstrap<R extends SchemaRoots>(schema: Schema<R>): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
513
+ async bootstrap(target: CapDef<any, any> | Schema<any>): Promise<unknown> {
514
+ if (isCapDef(target)) return this._bootstrapCap(target);
515
+ if (isSchema(target)) return this._bootstrapSchema(target);
516
+ throw new IpcError({ code: "invalid_argument", message: "bootstrap target must be CapDef or Schema" });
517
+ }
518
+
519
+ private async _bootstrapCap<C extends CapDef<any, any>>(cap: C): Promise<ClientOf<C>> {
520
+ await this.remoteReady;
521
+ const args: { name: string; version?: string } = { name: cap.name };
522
+ if (cap.version != null) args.version = cap.version;
523
+ const raw = await this.sendCallTyped(USER_ROOTS_CAP_ID, BOOTSTRAP_METHOD, args, undefined);
524
+ if (!(raw instanceof CapRef)) {
525
+ throw new IpcError({ code: "invalid_argument", message: "bootstrap did not return a CapRef" });
526
+ }
527
+ return this.makeCapProxy(cap, raw.capId) as ClientOf<C>;
528
+ }
529
+
530
+ private async _bootstrapSchema<R extends SchemaRoots>(
531
+ schema: Schema<R>
532
+ ): Promise<{ [K in keyof R]: ClientOf<R[K]> }> {
533
+ const keys = Object.keys(schema.roots) as (keyof R & string)[];
534
+ const settled = await Promise.allSettled(keys.map((k) => this._bootstrapCap(schema.roots[k])));
535
+ const rejected = settled.find((r): r is PromiseRejectedResult => r.status === "rejected");
536
+ if (rejected) {
537
+ // Release server refCount on the roots that succeeded — otherwise their cap-table entries linger until connection close.
538
+ for (const r of settled) {
539
+ if (r.status === "fulfilled") {
540
+ try { this.releaseRef(r.value); } catch { /* swallow */ }
541
+ }
542
+ }
543
+ throw rejected.reason;
544
+ }
545
+ const out = {} as { [K in keyof R]: ClientOf<R[K]> };
546
+ for (let i = 0; i < keys.length; i++) {
547
+ (out as Record<string, unknown>)[keys[i]] = (settled[i] as PromiseFulfilledResult<unknown>).value;
548
+ }
549
+ return out;
550
+ }
551
+
552
+ // ---- frame dispatch ----
553
+
554
+ private handleFrame(frame: Frame): void {
555
+ if (this.closed_) return;
556
+ switch (frame.op) {
557
+ case "hello":
558
+ this.handleHello(frame);
559
+ return;
560
+ case "call":
561
+ void this.handleCall(frame);
562
+ return;
563
+ case "result":
564
+ this.handleResult(frame);
565
+ return;
566
+ case "cancel":
567
+ this.handleCancel(frame);
568
+ return;
569
+ case "stream":
570
+ this.handleStreamFrame(frame);
571
+ return;
572
+ case "drop":
573
+ this.handleDrop(frame);
574
+ return;
575
+ case "cap_revoked":
576
+ this.handleCapRevoked(frame);
577
+ return;
578
+ case "goaway":
579
+ this.handleGoaway(frame);
580
+ return;
581
+ default:
582
+ this.handleUnknownFrame(frame);
583
+ return;
584
+ }
585
+ }
586
+
587
+ private handleUnknownFrame(frame: unknown): void {
588
+ const id = (frame as { id?: unknown })?.id;
589
+ if (typeof id === "number") {
590
+ this.transport.send({ op: "result", id, ok: false, error: { code: "invalid_argument", message: "unknown opcode" } });
591
+ return;
592
+ }
593
+ this.transport.send({ op: "goaway", reason: "invalid_argument", error: { code: "invalid_argument", message: "unknown opcode" } });
594
+ this.shutdown("invalid_argument");
595
+ }
596
+
597
+ private handleHello(frame: HelloFrame): void {
598
+ this.remoteHello = frame;
599
+ this.resolveRemoteReady(frame);
600
+ }
601
+
602
+ private handleGoaway(frame: Extract<Frame, { op: "goaway" }>): void {
603
+ this.rejectRemoteReady(
604
+ new IpcError(frame.error ?? { code: "unavailable", message: frame.reason ?? "peer goaway" })
605
+ );
606
+ this.shutdown(frame.reason ?? "remote goaway");
607
+ }
608
+
609
+ private handleCapRevoked(frame: CapRevokedFrame): void {
610
+ for (const capId of frame.capIds) {
611
+ this.revokedCapIds.add(capId);
612
+ const err = new IpcError({
613
+ code: "failed_precondition",
614
+ message: "cap revoked",
615
+ details: { reason: "revoked" as FailedPreconditionReason },
616
+ });
617
+ // Fail pending calls targeting this cap.
618
+ for (const [id, p] of this.pending) {
619
+ if (p.capId === capId) {
620
+ this.pending.delete(id);
621
+ if (p.timer) clearTimeout(p.timer);
622
+ p.reject(err);
623
+ }
624
+ }
625
+ // Fail active client streams targeting this cap — "revoked wins" symmetric with calls.
626
+ for (const [id, s] of this.clientStreams) {
627
+ if (s.capId === capId) {
628
+ this.clientStreams.delete(id);
629
+ s.fail(err);
630
+ }
631
+ }
632
+ }
633
+ }
634
+
635
+ private async handleCall(frame: CallFrame): Promise<void> {
636
+ if (frame.target.id === USER_ROOTS_CAP_ID && frame.method === BOOTSTRAP_METHOD) {
637
+ await this.handleBootstrap(frame);
638
+ return;
639
+ }
640
+
641
+ const entry = this.capTable.get(frame.target.id);
642
+ if (!entry) {
643
+ this.emitObs("call", { capId: frame.target.id, method: frame.method, callId: frame.id, result: "not_found" });
644
+ return this.sendError(frame.id, "not_found", `cap-id ${frame.target.id} not found`);
645
+ }
646
+
647
+ const cap = entry.cap;
648
+ if (!cap || !entry.impl) {
649
+ this.emitObs("call", { capId: frame.target.id, method: frame.method, callId: frame.id, result: "not_found" });
650
+ return this.sendError(frame.id, "not_found", "cap has no impl");
651
+ }
652
+
653
+ const methodDef = cap.methods[frame.method] as MethodDef | undefined;
654
+ if (!methodDef) {
655
+ this.emitObs("call", { capId: frame.target.id, capName: cap.name, method: frame.method, callId: frame.id, result: "not_found" });
656
+ return this.sendError(frame.id, "not_found", `method "${frame.method}" on cap "${cap.name}"`);
657
+ }
658
+ const impl = (entry.impl as Record<string, unknown>)[frame.method];
659
+ if (typeof impl !== "function") {
660
+ this.emitObs("call", { capId: frame.target.id, capName: cap.name, method: frame.method, callId: frame.id, result: "not_found" });
661
+ return this.sendError(frame.id, "not_found", `method "${frame.method}" has no handler`);
662
+ }
663
+
664
+ // Bound inbound work — symmetric with sendCallTyped's outbound check.
665
+ if (this.serverActiveCalls.size + this.serverStreams.size >= this.maxInFlightCalls) {
666
+ this.emitObs("call", { capId: frame.target.id, capName: cap.name, method: frame.method, callId: frame.id, result: "resource_exhausted" });
667
+ return this.sendError(
668
+ frame.id,
669
+ "resource_exhausted",
670
+ `in-flight calls limit ${this.maxInFlightCalls}`,
671
+ { reason: "max_concurrent_calls" as ResourceExhaustedReason },
672
+ );
673
+ }
674
+
675
+ await this.invokeServerMethod(frame, cap, methodDef, impl as (params: unknown, ctx: CallCtx) => unknown);
676
+ }
677
+
678
+ private async handleBootstrap(frame: CallFrame): Promise<void> {
679
+ const args = (frame.args ?? {}) as { name?: unknown; version?: unknown };
680
+ const name = args.name;
681
+ if (typeof name !== "string") {
682
+ this.emitObs("bootstrap", { name: String(name), attestation: this.attestation, result: "invalid_argument" });
683
+ return this.sendError(frame.id, "invalid_argument", "bootstrap requires {name: string}");
684
+ }
685
+ const clientVersion = args.version != null ? String(args.version) : undefined;
686
+
687
+ // Framework caps (cap-id 1 Runtime is pre-installed and accessed via .runtime(),
688
+ // not via bootstrap). For user-facing bootstrap, only the registry is consulted.
689
+ const entry = this.registry.get(name);
690
+ if (!entry) {
691
+ this.emitObs("bootstrap", { name, version: clientVersion, attestation: this.attestation, result: "not_found" });
692
+ return this.sendError(frame.id, "not_found", `cap "${name}" not served`);
693
+ }
694
+
695
+ const serverVersion = entry.version;
696
+ if (serverVersion != null && clientVersion != null && serverVersion !== clientVersion) {
697
+ this.emitObs("bootstrap", { name, version: clientVersion, attestation: this.attestation, result: "version_mismatch" });
698
+ return this.sendError(
699
+ frame.id,
700
+ "failed_precondition",
701
+ `version mismatch (server "${serverVersion}", client "${clientVersion}")`,
702
+ { reason: "version_mismatch" as FailedPreconditionReason }
703
+ );
704
+ }
705
+
706
+ if (this.policy) {
707
+ try {
708
+ const allowed = await this.policy(name, this.attestation);
709
+ if (!allowed) {
710
+ this.emitObs("bootstrap", { name, version: clientVersion, attestation: this.attestation, result: "denied" });
711
+ return this.sendError(
712
+ frame.id,
713
+ "failed_precondition",
714
+ "policy denied",
715
+ { reason: "unauthorized" as FailedPreconditionReason }
716
+ );
717
+ }
718
+ } catch (err) {
719
+ this.emitObs("error", { phase: "policy", error: err instanceof Error ? err : new Error(String(err)) });
720
+ this.emitObs("bootstrap", { name, version: clientVersion, attestation: this.attestation, result: "internal" });
721
+ return this.sendError(frame.id, "internal", "policy threw");
722
+ }
723
+ }
724
+
725
+ let capId = this.rootInstances.get(name);
726
+ if (capId !== undefined) {
727
+ const cached = this.capTable.get(capId);
728
+ if (cached) {
729
+ cached.refCount += 1;
730
+ } else {
731
+ this.rootInstances.delete(name);
732
+ capId = undefined;
733
+ }
734
+ }
735
+ if (capId === undefined) {
736
+ try {
737
+ const allocated = this.capTable.allocate({
738
+ typeId: frameworkTypeIdOf(entry.cap) ?? FIRST_USER_TYPE_ID,
739
+ cap: entry.cap,
740
+ impl: entry.impl,
741
+ refCount: 1,
742
+ });
743
+ capId = allocated.capId;
744
+ this.rootInstances.set(name, capId);
745
+ } catch (err) {
746
+ if (err instanceof IpcError) {
747
+ const result = err.code === "resource_exhausted" ? "resource_exhausted" : "internal";
748
+ this.emitObs("bootstrap", { name, version: clientVersion, attestation: this.attestation, result });
749
+ return this.sendError(frame.id, err.code, err.message, err.details as Record<string, unknown> | undefined);
750
+ }
751
+ throw err;
752
+ }
753
+ }
754
+ this.emitObs("bootstrap", { name, version: clientVersion, attestation: this.attestation, result: "ok", capId });
755
+ this.transport.send({ op: "result", id: frame.id, ok: true, value: new CapRef(capId) });
756
+ }
757
+
758
+ private async invokeServerMethod(
759
+ frame: CallFrame,
760
+ cap: CapDef<any, any>,
761
+ methodDef: MethodDef,
762
+ impl: (params: unknown, ctx: CallCtx) => unknown
763
+ ): Promise<void> {
764
+ const ctx = this.makeCallCtx(frame);
765
+
766
+ if (isStreamDef(methodDef)) {
767
+ const hintBudget = resolveStreamBudget(methodDef.hint);
768
+ try {
769
+ const runStream = () => impl(frame.args, ctx) as StreamType<unknown>;
770
+ const als = callContextStorage;
771
+ const scoped = als
772
+ ? () => als.run({ callId: frame.id }, runStream)
773
+ : runStream;
774
+ const stream = scoped();
775
+ this.runServerStream(frame.id, stream, ctx, hintBudget, cap, frame.method, frame.target.id);
776
+ } catch (err) {
777
+ this.emitObs("stream", { capId: frame.target.id, capName: cap.name, method: frame.method, callId: frame.id, event: "error" });
778
+ this.transport.send({ op: "stream", id: frame.id, ev: "error", error: toStatus(err) });
779
+ }
780
+ return;
781
+ }
782
+
783
+ if (isCallDef(methodDef)) {
784
+ const startedAt = performance.now();
785
+ const deadlineTimer = methodDef.idempotent ? undefined : this.armServerDeadline(frame, ctx);
786
+ this.serverActiveCalls.set(frame.id, {
787
+ ctrl: (ctx as unknown as { _ctrl: AbortController })._ctrl,
788
+ capId: frame.target.id,
789
+ capName: cap.name,
790
+ method: frame.method,
791
+ startedAt,
792
+ });
793
+ const encoder = makeReturnEncoder(methodDef.returns);
794
+ const runImpl = () => Promise.resolve(impl(frame.args, ctx));
795
+ const als = callContextStorage;
796
+ const scoped = als
797
+ ? () => als.run({ callId: frame.id }, runImpl)
798
+ : runImpl;
799
+ try {
800
+ const result = await scoped();
801
+ if (deadlineTimer) clearTimeout(deadlineTimer);
802
+ if (!this.serverActiveCalls.delete(frame.id)) return;
803
+ const encoded = encoder(result);
804
+ this.transport.send({ op: "result", id: frame.id, ok: true, value: encoded });
805
+ this.emitObs("call", { capId: frame.target.id, capName: cap.name, method: frame.method, callId: frame.id, durationMs: performance.now() - startedAt, result: "ok" });
806
+ } catch (err) {
807
+ if (deadlineTimer) clearTimeout(deadlineTimer);
808
+ if (!this.serverActiveCalls.delete(frame.id)) return;
809
+ const status = toStatus(err);
810
+ this.sendErrorFromStatus(frame.id, status);
811
+ this.emitObs("call", { capId: frame.target.id, capName: cap.name, method: frame.method, callId: frame.id, durationMs: performance.now() - startedAt, result: status.code });
812
+ }
813
+ return;
814
+ }
815
+
816
+ this.sendError(frame.id, "not_found", "unknown method kind");
817
+ }
818
+
819
+ private armServerDeadline(frame: CallFrame, ctx: CallCtx): ReturnType<typeof setTimeout> | undefined {
820
+ const ms = frame.meta?.deadlineMs;
821
+ if (!ms) return;
822
+ return setTimeout(() => {
823
+ (ctx as unknown as { _ctrl: AbortController })._ctrl.abort();
824
+ }, ms);
825
+ }
826
+
827
+ private makeCallCtx(frame: CallFrame): CallCtx {
828
+ const ctrl = new AbortController();
829
+ const ctx: CallCtx & { _ctrl: AbortController } = {
830
+ callId: frame.id,
831
+ peerId: this.peerId,
832
+ attestation: this.attestation,
833
+ signal: ctrl.signal,
834
+ deadline: frame.meta?.deadlineMs,
835
+ context: frame.meta?.context,
836
+ _ctrl: ctrl,
837
+ exportCap: (capDef, impl) => this.exportCap(capDef, impl),
838
+ };
839
+ return ctx;
840
+ }
841
+
842
+ private exportCap<C extends CapDef<any, any>>(capDef: C, impl: unknown): ExportedCap<C> {
843
+ const typeId = frameworkTypeIdOf(capDef) ?? FIRST_USER_TYPE_ID;
844
+ const entry = this.capTable.allocate({
845
+ typeId,
846
+ cap: capDef,
847
+ impl,
848
+ refCount: 1,
849
+ });
850
+ return {
851
+ [EXPORTED_CAP_BRAND]: true,
852
+ cap: capDef,
853
+ capId: entry.capId,
854
+ typeId: entry.typeId,
855
+ } as unknown as ExportedCap<C>;
856
+ }
857
+
858
+ private runServerStream(
859
+ callId: number,
860
+ stream: StreamType<unknown>,
861
+ ctx: CallCtx,
862
+ initialCredit: number,
863
+ cap: CapDef<any, any>,
864
+ method: string,
865
+ capId: number,
866
+ ): void {
867
+ const iter = (stream as AsyncIterable<unknown>)[Symbol.asyncIterator]();
868
+ const abort = (ctx as unknown as { _ctrl: AbortController })._ctrl;
869
+ const slot: ServerStreamCtx = {
870
+ iter,
871
+ abort,
872
+ cancelled: false,
873
+ credit: initialCredit,
874
+ creditWaker: null,
875
+ capId,
876
+ capName: cap.name,
877
+ method,
878
+ callId,
879
+ count: 0,
880
+ };
881
+ this.serverStreams.set(callId, slot);
882
+ this.emitObs("stream", { capId, capName: cap.name, method, callId, event: "start" });
883
+
884
+ const waitForCredit = (): Promise<void> => {
885
+ if (slot.credit > 0 || slot.cancelled) return Promise.resolve();
886
+ return new Promise<void>((resolve) => { slot.creditWaker = resolve; });
887
+ };
888
+
889
+ const drain = async () => {
890
+ let errored = false;
891
+ try {
892
+ while (!slot.cancelled) {
893
+ if (slot.credit === 0) await waitForCredit();
894
+ if (slot.cancelled) break;
895
+ const { done, value } = await iter.next();
896
+ if (done) break;
897
+ if (slot.cancelled) break;
898
+ slot.credit -= 1;
899
+ slot.count += 1;
900
+ this.transport.send({ op: "stream", id: callId, ev: "next", value });
901
+ }
902
+ } catch (err) {
903
+ errored = true;
904
+ this.transport.send({ op: "stream", id: callId, ev: "error", error: toStatus(err) });
905
+ this.emitObs("stream", { capId, capName: cap.name, method, callId, event: "error", count: slot.count });
906
+ } finally {
907
+ if (!errored) {
908
+ this.transport.send({ op: "stream", id: callId, ev: "end" });
909
+ this.emitObs("stream", { capId, capName: cap.name, method, callId, event: slot.cancelled ? "cancel" : "end", count: slot.count });
910
+ }
911
+ this.serverStreams.delete(callId);
912
+ }
913
+ };
914
+ void drain();
915
+ }
916
+
917
+ private handleResult(frame: ResultFrame): void {
918
+ const pending = this.pending.get(frame.id);
919
+ if (pending) {
920
+ this.pending.delete(frame.id);
921
+ this.serverCallChildren.delete(frame.id);
922
+ if (pending.timer) clearTimeout(pending.timer);
923
+ if (frame.ok) {
924
+ const value = pending.decodeReturn ? pending.decodeReturn(frame.value) : frame.value;
925
+ pending.resolve(value);
926
+ } else {
927
+ pending.reject(new IpcError(frame.error));
928
+ }
929
+ return;
930
+ }
931
+ const stream = this.clientStreams.get(frame.id);
932
+ if (stream) {
933
+ this.clientStreams.delete(frame.id);
934
+ const err = frame.ok
935
+ ? new IpcError({ code: "invalid_argument", message: "stream method returned result frame" })
936
+ : new IpcError(frame.error);
937
+ stream.fail(err);
938
+ }
939
+ }
940
+
941
+ private handleCancel(frame: CancelFrame): void {
942
+ const stream = this.serverStreams.get(frame.id);
943
+ if (stream) {
944
+ stream.cancelled = true;
945
+ stream.abort.abort();
946
+ void stream.iter?.return?.();
947
+ this.serverStreams.delete(frame.id);
948
+ }
949
+ const active = this.serverActiveCalls.get(frame.id);
950
+ if (active) {
951
+ this.serverActiveCalls.delete(frame.id);
952
+ active.ctrl.abort();
953
+ this.transport.send({
954
+ op: "result",
955
+ id: frame.id,
956
+ ok: false,
957
+ error: { code: "cancelled", message: frame.reason },
958
+ });
959
+ this.emitObs("call", {
960
+ capId: active.capId,
961
+ capName: active.capName,
962
+ method: active.method,
963
+ callId: frame.id,
964
+ durationMs: performance.now() - active.startedAt,
965
+ result: "cancelled",
966
+ });
967
+ }
968
+ for (const [childId, child] of this.serverCallChildren) {
969
+ if (child.parentId !== frame.id) continue;
970
+ this.serverCallChildren.delete(childId);
971
+ if (this.pending.has(childId)) {
972
+ this.transport.send({ op: "cancel", id: childId, reason: frame.reason });
973
+ }
974
+ }
975
+ }
976
+
977
+ releaseRef(proxy: unknown): void {
978
+ if (typeof proxy !== "object" || proxy === null) return;
979
+ const meta = (proxy as Record<symbol, unknown>)[CAP_PROXY_META] as { capId: number; dropped: boolean } | undefined;
980
+ if (!meta || meta.dropped) return;
981
+ meta.dropped = true;
982
+ this.sendDrop(meta.capId);
983
+ }
984
+
985
+ private sendDrop(capId: number): void {
986
+ if (this.closed_) return;
987
+ if (capId < FIRST_USER_CAP_ID) return;
988
+ this.transport.send({ op: "drop", caps: [{ id: capId, delta: 1 }] });
989
+ }
990
+
991
+ private handleStreamFrame(frame: StreamFrame): void {
992
+ if (frame.ev === "credit") {
993
+ const slot = this.serverStreams.get(frame.id);
994
+ if (!slot) return;
995
+ slot.credit += frame.credit?.messages ?? 0;
996
+ const waker = slot.creditWaker;
997
+ slot.creditWaker = null;
998
+ waker?.();
999
+ return;
1000
+ }
1001
+ const stream = this.clientStreams.get(frame.id);
1002
+ if (!stream) return;
1003
+ switch (frame.ev) {
1004
+ case "next":
1005
+ stream.push(frame.value);
1006
+ return;
1007
+ case "end":
1008
+ stream.end();
1009
+ this.clientStreams.delete(frame.id);
1010
+ return;
1011
+ case "error":
1012
+ stream.fail(new IpcError(frame.error));
1013
+ this.clientStreams.delete(frame.id);
1014
+ return;
1015
+ }
1016
+ }
1017
+
1018
+ private handleDrop(frame: DropFrame): void {
1019
+ for (const { id, delta } of frame.caps) {
1020
+ const released = this.capTable.release(id, delta);
1021
+ if (released) {
1022
+ for (const [rootName, capId] of this.rootInstances) {
1023
+ if (capId === id) { this.rootInstances.delete(rootName); break; }
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+
1029
+ private sendError(callId: number, code: IpcCode, message?: string, details?: unknown): void {
1030
+ const error: IpcStatus = { code, message };
1031
+ if (details !== undefined) error.details = details;
1032
+ this.transport.send({ op: "result", id: callId, ok: false, error });
1033
+ }
1034
+
1035
+ private sendErrorFromStatus(callId: number, status: IpcStatus): void {
1036
+ this.transport.send({ op: "result", id: callId, ok: false, error: status });
1037
+ }
1038
+
1039
+ private nextId(): number {
1040
+ return this.nextCallId++;
1041
+ }
1042
+
1043
+ sendCallTyped(
1044
+ capId: number,
1045
+ method: string,
1046
+ args: unknown,
1047
+ decodeReturn: ReturnDecoder | undefined,
1048
+ meta?: CallMeta,
1049
+ capName?: string,
1050
+ ): Promise<unknown> {
1051
+ if (this.closed_) return Promise.reject(new IpcError({ code: "unavailable", message: "connection closed" }));
1052
+ if (this.revokedCapIds.has(capId)) {
1053
+ return Promise.reject(new IpcError({
1054
+ code: "failed_precondition",
1055
+ message: "cap revoked",
1056
+ details: { reason: "revoked" as FailedPreconditionReason },
1057
+ }));
1058
+ }
1059
+ if (this.pending.size >= this.maxInFlightCalls) {
1060
+ return Promise.reject(new IpcError({
1061
+ code: "resource_exhausted",
1062
+ message: `in-flight calls limit ${this.maxInFlightCalls}`,
1063
+ details: { reason: "max_concurrent_calls" as ResourceExhaustedReason },
1064
+ }));
1065
+ }
1066
+ const id = this.nextId();
1067
+ return new Promise((resolve, reject) => {
1068
+ const abort = new AbortController();
1069
+ const pending: PendingCall = {
1070
+ resolve, reject, abort, decodeReturn,
1071
+ startedAt: performance.now(),
1072
+ capId, method, capName,
1073
+ kind: "call",
1074
+ };
1075
+ if (meta?.deadlineMs) {
1076
+ pending.timer = setTimeout(() => {
1077
+ if (this.pending.delete(id)) {
1078
+ this.transport.send({ op: "cancel", id, reason: "deadline_exceeded" });
1079
+ reject(new IpcError({ code: "deadline_exceeded" }));
1080
+ }
1081
+ }, meta.deadlineMs + DEFAULT_DEADLINE_GRACE_MS);
1082
+ }
1083
+ this.pending.set(id, pending);
1084
+ let finalMeta = meta;
1085
+ if ((finalMeta?.parentCallId === undefined) && callContextStorage) {
1086
+ const scope = callContextStorage.getStore();
1087
+ if (scope) finalMeta = { ...(finalMeta ?? {}), parentCallId: scope.callId };
1088
+ }
1089
+ if (finalMeta?.parentCallId !== undefined) {
1090
+ this.serverCallChildren.set(id, { parentId: finalMeta.parentCallId });
1091
+ }
1092
+ this.transport.send({ op: "call", id, target: { kind: "cap", id: capId }, method, args, meta: finalMeta });
1093
+ });
1094
+ }
1095
+
1096
+ openClientStream<T>(
1097
+ capId: number,
1098
+ method: string,
1099
+ args: unknown,
1100
+ meta?: CallMeta,
1101
+ capName?: string,
1102
+ ): StreamType<T> {
1103
+ if (this.closed_) {
1104
+ const empty = makeClientStream<T>(capId, 0, () => {}, () => {});
1105
+ empty.fail(new IpcError({ code: "unavailable", message: "connection closed" }));
1106
+ return empty.stream;
1107
+ }
1108
+ if (this.revokedCapIds.has(capId)) {
1109
+ const empty = makeClientStream<T>(capId, 0, () => {}, () => {});
1110
+ empty.fail(new IpcError({
1111
+ code: "failed_precondition",
1112
+ message: "cap revoked",
1113
+ details: { reason: "revoked" as FailedPreconditionReason },
1114
+ }));
1115
+ return empty.stream;
1116
+ }
1117
+ const id = this.nextId();
1118
+ const cancel = () => {
1119
+ if (this.clientStreams.delete(id)) {
1120
+ this.transport.send({ op: "cancel", id, reason: "client_cancel" });
1121
+ }
1122
+ };
1123
+ const sendCredit = (messages: number) => {
1124
+ if (this.closed_ || !this.clientStreams.has(id)) return;
1125
+ this.transport.send({ op: "stream", id, ev: "credit", credit: { messages } });
1126
+ };
1127
+ const ctx = makeClientStream<T>(capId, id, cancel, sendCredit);
1128
+ this.clientStreams.set(id, ctx);
1129
+ this.transport.send({ op: "call", id, target: { kind: "cap", id: capId }, method, args, meta });
1130
+ void capName;
1131
+ return ctx.stream;
1132
+ }
1133
+
1134
+ makeCapProxy<C extends CapDef<any, any>>(capDef: C, capId: number): ClientOf<C> {
1135
+ const proxy: Record<string | symbol, unknown> = {};
1136
+ const meta = { capId, dropped: false };
1137
+ proxy[CAP_PROXY_META] = meta;
1138
+ const drop = () => {
1139
+ if (meta.dropped) return;
1140
+ meta.dropped = true;
1141
+ this.sendDrop(capId);
1142
+ };
1143
+
1144
+ for (const name of Object.keys(capDef.methods)) {
1145
+ const def = capDef.methods[name] as MethodDef;
1146
+ if (isStreamDef(def)) {
1147
+ proxy[name] = (params?: unknown) => this.openClientStream(capId, name, params, undefined, capDef.name);
1148
+ } else if (isCallDef(def)) {
1149
+ const decoder = makeReturnDecoder(def.returns, (cd, cid) => this.makeCapProxy(cd, cid));
1150
+ proxy[name] = (params?: unknown) => this.sendCallTyped(capId, name, params, decoder, undefined, capDef.name);
1151
+ }
1152
+ }
1153
+
1154
+ const disposal = capDef.disposal as DisposalSpec | undefined;
1155
+ if (disposal) {
1156
+ const conn = this;
1157
+ proxy[Symbol.dispose] = () => {
1158
+ try {
1159
+ const r = (proxy[disposal.method] as (() => unknown) | undefined)?.();
1160
+ if (r && typeof (r as Promise<unknown>).then === "function") {
1161
+ (r as Promise<unknown>).catch((err) =>
1162
+ conn.emitObs("error", { phase: "dispose", error: err instanceof Error ? err : new Error(String(err)) })
1163
+ );
1164
+ }
1165
+ } catch (err) {
1166
+ conn.emitObs("error", { phase: "dispose", error: err instanceof Error ? err : new Error(String(err)) });
1167
+ }
1168
+ drop();
1169
+ };
1170
+ }
1171
+
1172
+ if (typeof FinalizationRegistry !== "undefined") {
1173
+ proxyFinalizers.register(proxy as object, {
1174
+ connRef: new WeakRef(this),
1175
+ capId,
1176
+ dropped: () => meta.dropped,
1177
+ });
1178
+ }
1179
+
1180
+ return proxy as ClientOf<C>;
1181
+ }
1182
+
1183
+ _dropFromFinalizer(capId: number): void {
1184
+ try { this.sendDrop(capId); } catch { /* swallow */ }
1185
+ }
1186
+
1187
+ shutdown(reason: string): void {
1188
+ if (this.closed_) return;
1189
+ this.closed_ = true;
1190
+ for (const pending of this.pending.values()) {
1191
+ if (pending.timer) clearTimeout(pending.timer);
1192
+ pending.reject(new IpcError({ code: "unavailable", message: reason }));
1193
+ }
1194
+ this.pending.clear();
1195
+ for (const stream of this.clientStreams.values()) {
1196
+ stream.fail(new IpcError({ code: "unavailable", message: reason }));
1197
+ }
1198
+ this.clientStreams.clear();
1199
+ for (const slot of this.serverStreams.values()) {
1200
+ slot.cancelled = true;
1201
+ slot.abort.abort();
1202
+ void slot.iter?.return?.();
1203
+ }
1204
+ this.serverStreams.clear();
1205
+ for (const a of this.serverActiveCalls.values()) {
1206
+ a.ctrl.abort();
1207
+ }
1208
+ this.serverActiveCalls.clear();
1209
+ for (const entry of this.capTable.values()) {
1210
+ this.invokeServerDisposal(entry);
1211
+ }
1212
+ this.capTable.clear();
1213
+ if (!this.remoteHello) {
1214
+ this.rejectRemoteReady(new IpcError({ code: "unavailable", message: reason }));
1215
+ }
1216
+ for (const fn of this.closeHandlers) {
1217
+ try { fn(); } catch { /* swallow */ }
1218
+ }
1219
+ this.closeHandlers.clear();
1220
+ try { this.transport.close(); } catch { /* swallow */ }
1221
+ }
1222
+
1223
+ private invokeServerDisposal(entry: CapTableEntry): void {
1224
+ const cap = entry.cap;
1225
+ if (!cap) return;
1226
+ const disposal = cap.disposal as DisposalSpec | undefined;
1227
+ if (!disposal) return;
1228
+ const impl = entry.impl as Record<string, unknown> | null;
1229
+ const fn = impl?.[disposal.method] as ((p: unknown, c?: unknown) => unknown) | undefined;
1230
+ if (!fn) return;
1231
+ try { void fn.call(impl, undefined, undefined); } catch { /* swallow */ }
1232
+ }
1233
+ }
1234
+
1235
+ function makeReturnEncoder(returns: AnyCapToken | undefined): (v: unknown) => unknown {
1236
+ if (!returns) return (v) => v;
1237
+ const expectExportedCap = (v: unknown, expectedCap: CapDef<any, any>): CapRef => {
1238
+ if (!isExportedCap(v)) {
1239
+ throw new IpcError({
1240
+ code: "failed_precondition",
1241
+ message: "expected ctx.exportCap return for cap method",
1242
+ details: { reason: "unregistered_cap_return" as FailedPreconditionReason },
1243
+ });
1244
+ }
1245
+ if (v.cap !== expectedCap) {
1246
+ throw new IpcError({
1247
+ code: "failed_precondition",
1248
+ message: "exported cap type mismatch with method returns",
1249
+ details: { reason: "unregistered_cap_return" as FailedPreconditionReason },
1250
+ });
1251
+ }
1252
+ return new CapRef(v.capId);
1253
+ };
1254
+ if (isCapRef(returns)) return (v) => expectExportedCap(v, returns.cap);
1255
+ if (isCapArray(returns)) {
1256
+ return (v) => {
1257
+ if (!Array.isArray(v)) {
1258
+ throw new IpcError({ code: "invalid_argument", message: "expected ExportedCap[] for cap.array method" });
1259
+ }
1260
+ return v.map((item) => expectExportedCap(item, returns.cap));
1261
+ };
1262
+ }
1263
+ if (isCapRecord(returns)) {
1264
+ return (v) => {
1265
+ if (!v || typeof v !== "object") {
1266
+ throw new IpcError({ code: "invalid_argument", message: "expected Record<string, ExportedCap> for cap.record method" });
1267
+ }
1268
+ const out: Record<string, CapRef> = {};
1269
+ for (const k of Object.keys(v as object)) {
1270
+ out[k] = expectExportedCap((v as Record<string, unknown>)[k], returns.cap);
1271
+ }
1272
+ return out;
1273
+ };
1274
+ }
1275
+ return (v) => v;
1276
+ }
1277
+
1278
+ function makeReturnDecoder(
1279
+ returns: { cap?: CapDef<any, any> } | undefined,
1280
+ spawn: (cap: CapDef<any, any>, capId: number) => unknown
1281
+ ): ReturnDecoder | undefined {
1282
+ if (!returns) return undefined;
1283
+ if (isCapRef(returns)) {
1284
+ const inner = returns.cap;
1285
+ return (raw) => {
1286
+ if (!(raw instanceof CapRef)) throw new IpcError({ code: "invalid_argument", message: "expected CapRef" });
1287
+ return spawn(inner, raw.capId);
1288
+ };
1289
+ }
1290
+ if (isCapArray(returns)) {
1291
+ const inner = returns.cap;
1292
+ const disposal = inner.disposal as DisposalSpec | undefined;
1293
+ return (raw) => {
1294
+ if (!Array.isArray(raw)) throw new IpcError({ code: "invalid_argument", message: "expected array" });
1295
+ const proxies = raw.map((item) => {
1296
+ if (!(item instanceof CapRef)) throw new IpcError({ code: "invalid_argument", message: "expected CapRef in array" });
1297
+ return spawn(inner, item.capId);
1298
+ });
1299
+ attachArrayDisposal(proxies, disposal);
1300
+ return proxies;
1301
+ };
1302
+ }
1303
+ if (isCapRecord(returns)) {
1304
+ const inner = returns.cap;
1305
+ return (raw) => {
1306
+ if (!raw || typeof raw !== "object") throw new IpcError({ code: "invalid_argument", message: "expected record" });
1307
+ const out: Record<string, unknown> = {};
1308
+ for (const k of Object.keys(raw as object)) {
1309
+ const item = (raw as Record<string, unknown>)[k];
1310
+ if (!(item instanceof CapRef)) throw new IpcError({ code: "invalid_argument", message: "expected CapRef in record" });
1311
+ out[k] = spawn(inner, item.capId);
1312
+ }
1313
+ return out;
1314
+ };
1315
+ }
1316
+ return undefined;
1317
+ }
1318
+
1319
+ function attachArrayDisposal(arr: unknown[], disposal: DisposalSpec | undefined): void {
1320
+ if (!disposal) return;
1321
+ (arr as any)[Symbol.dispose] = () => {
1322
+ for (const proxy of arr) {
1323
+ const fn = (proxy as Record<symbol, unknown>)[Symbol.dispose] as (() => void) | undefined;
1324
+ fn?.call(proxy);
1325
+ }
1326
+ };
1327
+ }
1328
+
1329
+ function toStatus(err: unknown): IpcStatus {
1330
+ if (err instanceof IpcError) return err.toStatus();
1331
+ if (err instanceof Error) return { code: "internal", message: err.message };
1332
+ return { code: "internal", message: String(err) };
1333
+ }
1334
+
1335
+ interface ClientStreamHandle<T> {
1336
+ capId: number;
1337
+ stream: StreamType<T>;
1338
+ push(chunk: unknown): void;
1339
+ end(): void;
1340
+ fail(error: IpcError): void;
1341
+ }
1342
+
1343
+ function makeClientStream<T>(
1344
+ capId: number,
1345
+ streamId: number,
1346
+ cancel: () => void,
1347
+ sendCredit: (messages: number) => void
1348
+ ): ClientStreamHandle<T> {
1349
+ const buffer: T[] = [];
1350
+ const waiters: Array<{ resolve(r: IteratorResult<T>): void; reject(e: Error): void }> = [];
1351
+ let ended = false;
1352
+ let failure: IpcError | null = null;
1353
+ let cancelled = false;
1354
+ let consumedSinceCredit = 0;
1355
+
1356
+ function tally() {
1357
+ consumedSinceCredit += 1;
1358
+ if (consumedSinceCredit >= DEFAULT_STREAM_CREDIT_BATCH) {
1359
+ sendCredit(consumedSinceCredit);
1360
+ consumedSinceCredit = 0;
1361
+ }
1362
+ }
1363
+
1364
+ function pump(): IteratorResult<T> | null {
1365
+ if (buffer.length > 0) {
1366
+ const value = buffer.shift()!;
1367
+ tally();
1368
+ return { value, done: false };
1369
+ }
1370
+ if (failure) return null;
1371
+ if (ended || cancelled) return { value: undefined as unknown as T, done: true };
1372
+ return null;
1373
+ }
1374
+
1375
+ function next(): Promise<IteratorResult<T>> {
1376
+ const immediate = pump();
1377
+ if (immediate) return Promise.resolve(immediate);
1378
+ if (failure) return Promise.reject(failure);
1379
+ return new Promise((resolve, reject) => waiters.push({ resolve, reject }));
1380
+ }
1381
+
1382
+ function drainWaiters(): void {
1383
+ while (waiters.length > 0) {
1384
+ const w = waiters.shift()!;
1385
+ const r = pump();
1386
+ if (r) { w.resolve(r); continue; }
1387
+ if (failure) { w.reject(failure); continue; }
1388
+ waiters.unshift(w);
1389
+ break;
1390
+ }
1391
+ }
1392
+
1393
+ function doCancel(): void {
1394
+ if (cancelled || ended || failure) return;
1395
+ cancelled = true;
1396
+ cancel();
1397
+ drainWaiters();
1398
+ }
1399
+
1400
+ const stream: StreamType<T> = {
1401
+ [Symbol.asyncIterator]: () => ({
1402
+ next,
1403
+ return: () => {
1404
+ doCancel();
1405
+ return Promise.resolve({ value: undefined as unknown as T, done: true });
1406
+ },
1407
+ throw: (err: unknown) => {
1408
+ doCancel();
1409
+ return Promise.reject(err);
1410
+ },
1411
+ }),
1412
+ [Symbol.dispose]: doCancel,
1413
+ cancel: doCancel,
1414
+ } as StreamType<T>;
1415
+ void streamId;
1416
+
1417
+ return {
1418
+ capId,
1419
+ stream,
1420
+ push(chunk) {
1421
+ if (cancelled || ended || failure) return;
1422
+ buffer.push(chunk as T);
1423
+ drainWaiters();
1424
+ },
1425
+ end() {
1426
+ ended = true;
1427
+ drainWaiters();
1428
+ },
1429
+ fail(error) {
1430
+ failure = error;
1431
+ drainWaiters();
1432
+ },
1433
+ };
1434
+ }
1435
+
1436
+ export function createConnection(opts: ConnectionOptions): Connection & ConnectionImpl {
1437
+ return new ConnectionImpl(opts) as Connection & ConnectionImpl;
1438
+ }