bunite-core 0.17.1 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/host/core/App.ts +28 -18
- package/src/host/core/BrowserView.ts +333 -116
- package/src/host/core/BrowserWindow.ts +44 -22
- package/src/host/core/Socket.ts +26 -9
- package/src/host/core/SurfaceBrowserIPC.ts +15 -5
- package/src/host/core/SurfaceManager.ts +345 -158
- package/src/host/core/SurfaceRegistry.ts +1 -1
- package/src/host/core/inputDispatch.ts +71 -42
- package/src/host/core/singleInstanceLock.ts +10 -2
- package/src/host/core/windowCap.ts +42 -23
- package/src/host/encryptedPipe.ts +12 -3
- package/src/host/events/appEvents.ts +1 -1
- package/src/host/events/eventEmitter.ts +6 -6
- package/src/host/events/webviewEvents.ts +11 -4
- package/src/host/events/windowEvents.ts +1 -2
- package/src/host/index.ts +9 -17
- package/src/host/log.ts +6 -4
- package/src/host/native.ts +271 -163
- package/src/host/paths.ts +18 -15
- package/src/host/platform.ts +2 -4
- package/src/host/preloadBundle.ts +1 -1
- package/src/host/serveWeb.ts +18 -11
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +61 -24
- package/src/rpc/encrypt.ts +15 -4
- package/src/rpc/error.ts +2 -7
- package/src/rpc/framework.ts +105 -35
- package/src/rpc/index.ts +129 -140
- package/src/rpc/peer.ts +430 -159
- package/src/rpc/renderer.ts +15 -9
- package/src/rpc/schema.ts +86 -61
- package/src/rpc/stream.ts +12 -3
- package/src/rpc/transport.ts +28 -9
- package/src/rpc/wire.ts +23 -10
- package/src/webview/native.ts +150 -73
- package/src/webview/polyfill.ts +184 -48
package/src/rpc/peer.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type
|
|
3
|
-
type
|
|
4
|
-
type
|
|
2
|
+
type AlreadyExistsReason,
|
|
3
|
+
type FailedPreconditionReason,
|
|
4
|
+
type IpcCode,
|
|
5
|
+
IpcError,
|
|
6
|
+
type IpcStatus,
|
|
7
|
+
type ResourceExhaustedReason,
|
|
8
|
+
} from "./error";
|
|
9
|
+
import { frameworkTypeIdOf, RuntimeCap } from "./framework";
|
|
10
|
+
import {
|
|
11
|
+
type AnyCapDef,
|
|
12
|
+
type AnyCapToken,
|
|
13
|
+
type Attestation,
|
|
14
|
+
type CallCtx,
|
|
5
15
|
type ClientOf,
|
|
16
|
+
type DisposalSpec,
|
|
17
|
+
type ExportedCap,
|
|
6
18
|
type ImplOf,
|
|
7
19
|
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
20
|
isCallDef,
|
|
16
|
-
isStreamDef,
|
|
17
|
-
isCapRef,
|
|
18
21
|
isCapArray,
|
|
19
|
-
isCapRecord,
|
|
20
22
|
isCapDef,
|
|
23
|
+
isCapRecord,
|
|
24
|
+
isCapRef,
|
|
21
25
|
isSchema,
|
|
26
|
+
isStreamDef,
|
|
27
|
+
type MethodDef,
|
|
28
|
+
type Schema,
|
|
29
|
+
type SchemaRoots,
|
|
30
|
+
type Stream as StreamType,
|
|
22
31
|
} from "./schema";
|
|
23
|
-
import { RuntimeCap, frameworkTypeIdOf } from "./framework";
|
|
24
32
|
import {
|
|
25
|
-
|
|
33
|
+
BOOTSTRAP_METHOD,
|
|
26
34
|
type CallFrame,
|
|
27
|
-
type ResultFrame,
|
|
28
|
-
type StreamFrame,
|
|
29
|
-
type CancelFrame,
|
|
30
|
-
type DropFrame,
|
|
31
|
-
type HelloFrame,
|
|
32
|
-
type CapRevokedFrame,
|
|
33
35
|
type CallMeta,
|
|
36
|
+
type CancelFrame,
|
|
34
37
|
CapRef,
|
|
38
|
+
type CapRevokedFrame,
|
|
35
39
|
DEFAULT_MAX_BYTES,
|
|
36
|
-
|
|
40
|
+
type DropFrame,
|
|
37
41
|
FRAMEWORK_NAME_PREFIX,
|
|
38
|
-
|
|
42
|
+
type Frame,
|
|
43
|
+
type HelloFrame,
|
|
44
|
+
PROTOCOL_VERSION,
|
|
45
|
+
type ResultFrame,
|
|
46
|
+
type StreamFrame,
|
|
39
47
|
} 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
48
|
|
|
49
49
|
export const USER_ROOTS_CAP_ID = 0;
|
|
50
50
|
export const RUNTIME_CAP_ID = 1;
|
|
@@ -90,7 +90,7 @@ export function _setCallContextStorage(als: CallContextStorage | null): void {
|
|
|
90
90
|
export interface CapTableEntry {
|
|
91
91
|
capId: number;
|
|
92
92
|
typeId: number;
|
|
93
|
-
cap:
|
|
93
|
+
cap: AnyCapDef | null;
|
|
94
94
|
impl: unknown;
|
|
95
95
|
refCount: number;
|
|
96
96
|
}
|
|
@@ -173,23 +173,57 @@ export interface ServeHandle extends Disposable {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
export interface ConnectionEvents {
|
|
176
|
-
bootstrap: {
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
bootstrap: {
|
|
177
|
+
name: string;
|
|
178
|
+
version?: string;
|
|
179
|
+
attestation: Attestation;
|
|
180
|
+
result:
|
|
181
|
+
| "ok"
|
|
182
|
+
| "denied"
|
|
183
|
+
| "not_found"
|
|
184
|
+
| "version_mismatch"
|
|
185
|
+
| "invalid_argument"
|
|
186
|
+
| "resource_exhausted"
|
|
187
|
+
| "internal";
|
|
188
|
+
capId?: number;
|
|
189
|
+
};
|
|
190
|
+
call: {
|
|
191
|
+
capId: number;
|
|
192
|
+
capName?: string;
|
|
193
|
+
method: string;
|
|
194
|
+
callId: number;
|
|
195
|
+
durationMs?: number;
|
|
196
|
+
result: "ok" | "cancelled" | IpcCode;
|
|
197
|
+
};
|
|
198
|
+
stream: {
|
|
199
|
+
capId: number;
|
|
200
|
+
capName?: string;
|
|
201
|
+
method: string;
|
|
202
|
+
callId: number;
|
|
203
|
+
event: "start" | "end" | "cancel" | "error";
|
|
204
|
+
count?: number;
|
|
205
|
+
};
|
|
179
206
|
revoke: { capIds: number[]; reason: "unserve" | "replace" };
|
|
180
207
|
error: { phase: string; error: Error };
|
|
181
208
|
}
|
|
182
209
|
|
|
183
210
|
export interface Connection {
|
|
184
|
-
bootstrap<C extends
|
|
211
|
+
bootstrap<C extends AnyCapDef>(cap: C): Promise<ClientOf<C>>;
|
|
185
212
|
bootstrap<R extends SchemaRoots>(schema: Schema<R>): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
|
|
186
|
-
serve<C extends
|
|
187
|
-
serveAll<R extends SchemaRoots>(
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
serve<C extends AnyCapDef>(cap: C, impl: ImplOf<C>, opts?: { ifExists?: IfExists }): ServeHandle;
|
|
214
|
+
serveAll<R extends SchemaRoots>(
|
|
215
|
+
schema: Schema<R>,
|
|
216
|
+
impls: ImplsOf<R>,
|
|
217
|
+
opts?: { ifExists?: IfExists },
|
|
218
|
+
): ServeHandle;
|
|
219
|
+
unserve(target: AnyCapDef | ServeHandle): void;
|
|
220
|
+
replace<C extends AnyCapDef>(cap: C, impl: ImplOf<C>): void;
|
|
190
221
|
runtime(): ClientOf<typeof RuntimeCap>;
|
|
191
222
|
releaseRef(proxy: unknown): void;
|
|
192
|
-
on<K extends keyof ConnectionEvents>(
|
|
223
|
+
on<K extends keyof ConnectionEvents>(
|
|
224
|
+
event: K,
|
|
225
|
+
handler: (e: ConnectionEvents[K]) => void,
|
|
226
|
+
): () => void;
|
|
193
227
|
onClose(handler: () => void): () => void;
|
|
194
228
|
/** Tear down the connection — rejects pending, fires onClose, closes transport. Reliable signal for application lifecycle. */
|
|
195
229
|
shutdown(reason?: string): void;
|
|
@@ -244,14 +278,19 @@ function isExportedCap(v: unknown): v is ExportedCap<any> {
|
|
|
244
278
|
return typeof v === "object" && v !== null && (v as any)[EXPORTED_CAP_BRAND] === true;
|
|
245
279
|
}
|
|
246
280
|
|
|
247
|
-
const proxyFinalizers =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
281
|
+
const proxyFinalizers =
|
|
282
|
+
typeof FinalizationRegistry !== "undefined"
|
|
283
|
+
? new FinalizationRegistry<{
|
|
284
|
+
connRef: WeakRef<ConnectionImpl>;
|
|
285
|
+
capId: number;
|
|
286
|
+
dropped: () => boolean;
|
|
287
|
+
}>((held) => {
|
|
288
|
+
if (held.dropped()) return;
|
|
289
|
+
const conn = held.connRef.deref();
|
|
290
|
+
if (!conn || conn.closed) return;
|
|
291
|
+
conn._dropFromFinalizer(held.capId);
|
|
292
|
+
})
|
|
293
|
+
: ({ register: () => {} } as { register: (target: object, held: unknown) => void });
|
|
255
294
|
|
|
256
295
|
interface ServerStreamCtx {
|
|
257
296
|
iter: AsyncIterator<unknown> | null;
|
|
@@ -274,7 +313,7 @@ interface ClientStreamCtx {
|
|
|
274
313
|
}
|
|
275
314
|
|
|
276
315
|
interface RegistryEntry {
|
|
277
|
-
cap:
|
|
316
|
+
cap: AnyCapDef;
|
|
278
317
|
impl: unknown;
|
|
279
318
|
version?: string;
|
|
280
319
|
}
|
|
@@ -286,7 +325,10 @@ class ConnectionImpl implements Connection {
|
|
|
286
325
|
private readonly clientStreams = new Map<number, ClientStreamCtx>();
|
|
287
326
|
private readonly serverStreams = new Map<number, ServerStreamCtx>();
|
|
288
327
|
private readonly serverCallChildren = new Map<number, { parentId: number }>();
|
|
289
|
-
private readonly serverActiveCalls = new Map<
|
|
328
|
+
private readonly serverActiveCalls = new Map<
|
|
329
|
+
number,
|
|
330
|
+
{ ctrl: AbortController; capId: number; capName: string; method: string; startedAt: number }
|
|
331
|
+
>();
|
|
290
332
|
/** Server-side: name → registry entry. */
|
|
291
333
|
private readonly registry = new Map<string, RegistryEntry>();
|
|
292
334
|
/** Server-side: name → instance cap-id (cached bootstrap result). */
|
|
@@ -294,7 +336,9 @@ class ConnectionImpl implements Connection {
|
|
|
294
336
|
/** Client-side: cap-ids that the server revoked via cap_revoked. */
|
|
295
337
|
private readonly revokedCapIds = new Set<number>();
|
|
296
338
|
private readonly closeHandlers = new Set<() => void>();
|
|
297
|
-
private readonly observers: {
|
|
339
|
+
private readonly observers: {
|
|
340
|
+
[K in keyof ConnectionEvents]?: Set<(e: ConnectionEvents[K]) => void>;
|
|
341
|
+
} = {};
|
|
298
342
|
private nextCallId = 1;
|
|
299
343
|
private remoteHello: HelloFrame | null = null;
|
|
300
344
|
private readonly remoteReady: Promise<HelloFrame>;
|
|
@@ -363,21 +407,30 @@ class ConnectionImpl implements Connection {
|
|
|
363
407
|
return () => this.closeHandlers.delete(handler);
|
|
364
408
|
}
|
|
365
409
|
|
|
366
|
-
on<K extends keyof ConnectionEvents>(
|
|
410
|
+
on<K extends keyof ConnectionEvents>(
|
|
411
|
+
event: K,
|
|
412
|
+
handler: (e: ConnectionEvents[K]) => void,
|
|
413
|
+
): () => void {
|
|
367
414
|
let set = this.observers[event] as Set<(e: ConnectionEvents[K]) => void> | undefined;
|
|
368
415
|
if (!set) {
|
|
369
416
|
set = new Set();
|
|
370
417
|
(this.observers as Record<string, Set<unknown>>)[event] = set as unknown as Set<unknown>;
|
|
371
418
|
}
|
|
372
419
|
set.add(handler);
|
|
373
|
-
return () => {
|
|
420
|
+
return () => {
|
|
421
|
+
set!.delete(handler);
|
|
422
|
+
};
|
|
374
423
|
}
|
|
375
424
|
|
|
376
425
|
private emitObs<K extends keyof ConnectionEvents>(event: K, data: ConnectionEvents[K]): void {
|
|
377
426
|
const set = this.observers[event] as Set<(e: ConnectionEvents[K]) => void> | undefined;
|
|
378
427
|
if (!set || set.size === 0) return;
|
|
379
428
|
for (const h of set) {
|
|
380
|
-
try {
|
|
429
|
+
try {
|
|
430
|
+
h(data);
|
|
431
|
+
} catch {
|
|
432
|
+
/* swallow */
|
|
433
|
+
}
|
|
381
434
|
}
|
|
382
435
|
}
|
|
383
436
|
|
|
@@ -393,7 +446,7 @@ class ConnectionImpl implements Connection {
|
|
|
393
446
|
|
|
394
447
|
// ---- serve / unserve / replace ----
|
|
395
448
|
|
|
396
|
-
serve<C extends
|
|
449
|
+
serve<C extends AnyCapDef>(cap: C, impl: ImplOf<C>, opts?: { ifExists?: IfExists }): ServeHandle {
|
|
397
450
|
this.assertNotFrameworkName(cap.name);
|
|
398
451
|
const ifExists = opts?.ifExists ?? "throw";
|
|
399
452
|
const existing = this.registry.get(cap.name);
|
|
@@ -425,7 +478,11 @@ class ConnectionImpl implements Connection {
|
|
|
425
478
|
return handle;
|
|
426
479
|
}
|
|
427
480
|
|
|
428
|
-
serveAll<R extends SchemaRoots>(
|
|
481
|
+
serveAll<R extends SchemaRoots>(
|
|
482
|
+
schema: Schema<R>,
|
|
483
|
+
impls: ImplsOf<R>,
|
|
484
|
+
opts?: { ifExists?: IfExists },
|
|
485
|
+
): ServeHandle {
|
|
429
486
|
const ifExists = opts?.ifExists ?? "throw";
|
|
430
487
|
// Pre-validate everything that can fail across all modes before mutating any state (atomicity).
|
|
431
488
|
for (const k of Object.keys(schema.roots)) {
|
|
@@ -461,7 +518,7 @@ class ConnectionImpl implements Connection {
|
|
|
461
518
|
return this.makeHandle(names);
|
|
462
519
|
}
|
|
463
520
|
|
|
464
|
-
unserve(target:
|
|
521
|
+
unserve(target: AnyCapDef | ServeHandle): void {
|
|
465
522
|
const names = isCapDef(target) ? [target.name] : Array.from((target as ServeHandle).names);
|
|
466
523
|
const revoked: number[] = [];
|
|
467
524
|
for (const name of names) {
|
|
@@ -481,7 +538,7 @@ class ConnectionImpl implements Connection {
|
|
|
481
538
|
}
|
|
482
539
|
}
|
|
483
540
|
|
|
484
|
-
replace<C extends
|
|
541
|
+
replace<C extends AnyCapDef>(cap: C, impl: ImplOf<C>): void {
|
|
485
542
|
const entry = this.registry.get(cap.name);
|
|
486
543
|
if (!entry) throw new IpcError({ code: "not_found", message: `cap "${cap.name}" not served` });
|
|
487
544
|
if (entry.version !== cap.version) {
|
|
@@ -496,7 +553,10 @@ class ConnectionImpl implements Connection {
|
|
|
496
553
|
const instId = this.rootInstances.get(cap.name);
|
|
497
554
|
if (instId !== undefined) {
|
|
498
555
|
const e = this.capTable.get(instId);
|
|
499
|
-
if (e) {
|
|
556
|
+
if (e) {
|
|
557
|
+
e.impl = impl;
|
|
558
|
+
e.cap = cap;
|
|
559
|
+
}
|
|
500
560
|
}
|
|
501
561
|
this.emitObs("revoke", { capIds: instId !== undefined ? [instId] : [], reason: "replace" });
|
|
502
562
|
}
|
|
@@ -522,27 +582,33 @@ class ConnectionImpl implements Connection {
|
|
|
522
582
|
return this.runtimeProxy;
|
|
523
583
|
}
|
|
524
584
|
|
|
525
|
-
bootstrap<C extends
|
|
585
|
+
bootstrap<C extends AnyCapDef>(cap: C): Promise<ClientOf<C>>;
|
|
526
586
|
bootstrap<R extends SchemaRoots>(schema: Schema<R>): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
|
|
527
|
-
async bootstrap(target:
|
|
587
|
+
async bootstrap(target: AnyCapDef | Schema<any>): Promise<unknown> {
|
|
528
588
|
if (isCapDef(target)) return this._bootstrapCap(target);
|
|
529
589
|
if (isSchema(target)) return this._bootstrapSchema(target);
|
|
530
|
-
throw new IpcError({
|
|
590
|
+
throw new IpcError({
|
|
591
|
+
code: "invalid_argument",
|
|
592
|
+
message: "bootstrap target must be CapDef or Schema",
|
|
593
|
+
});
|
|
531
594
|
}
|
|
532
595
|
|
|
533
|
-
private async _bootstrapCap<C extends
|
|
596
|
+
private async _bootstrapCap<C extends AnyCapDef>(cap: C): Promise<ClientOf<C>> {
|
|
534
597
|
await this.remoteReady;
|
|
535
598
|
const args: { name: string; version?: string } = { name: cap.name };
|
|
536
599
|
if (cap.version != null) args.version = cap.version;
|
|
537
600
|
const raw = await this.sendCallTyped(USER_ROOTS_CAP_ID, BOOTSTRAP_METHOD, args, undefined);
|
|
538
601
|
if (!(raw instanceof CapRef)) {
|
|
539
|
-
throw new IpcError({
|
|
602
|
+
throw new IpcError({
|
|
603
|
+
code: "invalid_argument",
|
|
604
|
+
message: "bootstrap did not return a CapRef",
|
|
605
|
+
});
|
|
540
606
|
}
|
|
541
607
|
return this.makeCapProxy(cap, raw.capId) as ClientOf<C>;
|
|
542
608
|
}
|
|
543
609
|
|
|
544
610
|
private async _bootstrapSchema<R extends SchemaRoots>(
|
|
545
|
-
schema: Schema<R
|
|
611
|
+
schema: Schema<R>,
|
|
546
612
|
): Promise<{ [K in keyof R]: ClientOf<R[K]> }> {
|
|
547
613
|
const keys = Object.keys(schema.roots) as (keyof R & string)[];
|
|
548
614
|
const settled = await Promise.allSettled(keys.map((k) => this._bootstrapCap(schema.roots[k])));
|
|
@@ -551,14 +617,20 @@ class ConnectionImpl implements Connection {
|
|
|
551
617
|
// Release server refCount on the roots that succeeded — otherwise their cap-table entries linger until connection close.
|
|
552
618
|
for (const r of settled) {
|
|
553
619
|
if (r.status === "fulfilled") {
|
|
554
|
-
try {
|
|
620
|
+
try {
|
|
621
|
+
this.releaseRef(r.value);
|
|
622
|
+
} catch {
|
|
623
|
+
/* swallow */
|
|
624
|
+
}
|
|
555
625
|
}
|
|
556
626
|
}
|
|
557
627
|
throw rejected.reason;
|
|
558
628
|
}
|
|
559
629
|
const out = {} as { [K in keyof R]: ClientOf<R[K]> };
|
|
560
630
|
for (let i = 0; i < keys.length; i++) {
|
|
561
|
-
(out as Record<string, unknown>)[keys[i]] = (
|
|
631
|
+
(out as Record<string, unknown>)[keys[i]] = (
|
|
632
|
+
settled[i] as PromiseFulfilledResult<unknown>
|
|
633
|
+
).value;
|
|
562
634
|
}
|
|
563
635
|
return out;
|
|
564
636
|
}
|
|
@@ -601,10 +673,19 @@ class ConnectionImpl implements Connection {
|
|
|
601
673
|
private handleUnknownFrame(frame: unknown): void {
|
|
602
674
|
const id = (frame as { id?: unknown })?.id;
|
|
603
675
|
if (typeof id === "number") {
|
|
604
|
-
this.transport.send({
|
|
676
|
+
this.transport.send({
|
|
677
|
+
op: "result",
|
|
678
|
+
id,
|
|
679
|
+
ok: false,
|
|
680
|
+
error: { code: "invalid_argument", message: "unknown opcode" },
|
|
681
|
+
});
|
|
605
682
|
return;
|
|
606
683
|
}
|
|
607
|
-
this.transport.send({
|
|
684
|
+
this.transport.send({
|
|
685
|
+
op: "goaway",
|
|
686
|
+
reason: "invalid_argument",
|
|
687
|
+
error: { code: "invalid_argument", message: "unknown opcode" },
|
|
688
|
+
});
|
|
608
689
|
this.shutdown("invalid_argument");
|
|
609
690
|
}
|
|
610
691
|
|
|
@@ -615,7 +696,7 @@ class ConnectionImpl implements Connection {
|
|
|
615
696
|
|
|
616
697
|
private handleGoaway(frame: Extract<Frame, { op: "goaway" }>): void {
|
|
617
698
|
this.rejectRemoteReady(
|
|
618
|
-
new IpcError(frame.error ?? { code: "unavailable", message: frame.reason ?? "peer goaway" })
|
|
699
|
+
new IpcError(frame.error ?? { code: "unavailable", message: frame.reason ?? "peer goaway" }),
|
|
619
700
|
);
|
|
620
701
|
this.shutdown(frame.reason ?? "remote goaway");
|
|
621
702
|
}
|
|
@@ -654,30 +735,58 @@ class ConnectionImpl implements Connection {
|
|
|
654
735
|
|
|
655
736
|
const entry = this.capTable.get(frame.target.id);
|
|
656
737
|
if (!entry) {
|
|
657
|
-
this.emitObs("call", {
|
|
738
|
+
this.emitObs("call", {
|
|
739
|
+
capId: frame.target.id,
|
|
740
|
+
method: frame.method,
|
|
741
|
+
callId: frame.id,
|
|
742
|
+
result: "not_found",
|
|
743
|
+
});
|
|
658
744
|
return this.sendError(frame.id, "not_found", `cap-id ${frame.target.id} not found`);
|
|
659
745
|
}
|
|
660
746
|
|
|
661
747
|
const cap = entry.cap;
|
|
662
748
|
if (!cap || !entry.impl) {
|
|
663
|
-
this.emitObs("call", {
|
|
749
|
+
this.emitObs("call", {
|
|
750
|
+
capId: frame.target.id,
|
|
751
|
+
method: frame.method,
|
|
752
|
+
callId: frame.id,
|
|
753
|
+
result: "not_found",
|
|
754
|
+
});
|
|
664
755
|
return this.sendError(frame.id, "not_found", "cap has no impl");
|
|
665
756
|
}
|
|
666
757
|
|
|
667
758
|
const methodDef = cap.methods[frame.method] as MethodDef | undefined;
|
|
668
759
|
if (!methodDef) {
|
|
669
|
-
this.emitObs("call", {
|
|
760
|
+
this.emitObs("call", {
|
|
761
|
+
capId: frame.target.id,
|
|
762
|
+
capName: cap.name,
|
|
763
|
+
method: frame.method,
|
|
764
|
+
callId: frame.id,
|
|
765
|
+
result: "not_found",
|
|
766
|
+
});
|
|
670
767
|
return this.sendError(frame.id, "not_found", `method "${frame.method}" on cap "${cap.name}"`);
|
|
671
768
|
}
|
|
672
769
|
const impl = (entry.impl as Record<string, unknown>)[frame.method];
|
|
673
770
|
if (typeof impl !== "function") {
|
|
674
|
-
this.emitObs("call", {
|
|
771
|
+
this.emitObs("call", {
|
|
772
|
+
capId: frame.target.id,
|
|
773
|
+
capName: cap.name,
|
|
774
|
+
method: frame.method,
|
|
775
|
+
callId: frame.id,
|
|
776
|
+
result: "not_found",
|
|
777
|
+
});
|
|
675
778
|
return this.sendError(frame.id, "not_found", `method "${frame.method}" has no handler`);
|
|
676
779
|
}
|
|
677
780
|
|
|
678
781
|
// Bound inbound work — symmetric with sendCallTyped's outbound check.
|
|
679
782
|
if (this.serverActiveCalls.size + this.serverStreams.size >= this.maxInFlightCalls) {
|
|
680
|
-
this.emitObs("call", {
|
|
783
|
+
this.emitObs("call", {
|
|
784
|
+
capId: frame.target.id,
|
|
785
|
+
capName: cap.name,
|
|
786
|
+
method: frame.method,
|
|
787
|
+
callId: frame.id,
|
|
788
|
+
result: "resource_exhausted",
|
|
789
|
+
});
|
|
681
790
|
return this.sendError(
|
|
682
791
|
frame.id,
|
|
683
792
|
"resource_exhausted",
|
|
@@ -686,14 +795,23 @@ class ConnectionImpl implements Connection {
|
|
|
686
795
|
);
|
|
687
796
|
}
|
|
688
797
|
|
|
689
|
-
await this.invokeServerMethod(
|
|
798
|
+
await this.invokeServerMethod(
|
|
799
|
+
frame,
|
|
800
|
+
cap,
|
|
801
|
+
methodDef,
|
|
802
|
+
impl as (params: unknown, ctx: CallCtx) => unknown,
|
|
803
|
+
);
|
|
690
804
|
}
|
|
691
805
|
|
|
692
806
|
private async handleBootstrap(frame: CallFrame): Promise<void> {
|
|
693
807
|
const args = (frame.args ?? {}) as { name?: unknown; version?: unknown };
|
|
694
808
|
const name = args.name;
|
|
695
809
|
if (typeof name !== "string") {
|
|
696
|
-
this.emitObs("bootstrap", {
|
|
810
|
+
this.emitObs("bootstrap", {
|
|
811
|
+
name: String(name),
|
|
812
|
+
attestation: this.attestation,
|
|
813
|
+
result: "invalid_argument",
|
|
814
|
+
});
|
|
697
815
|
return this.sendError(frame.id, "invalid_argument", "bootstrap requires {name: string}");
|
|
698
816
|
}
|
|
699
817
|
const clientVersion = args.version != null ? String(args.version) : undefined;
|
|
@@ -702,18 +820,28 @@ class ConnectionImpl implements Connection {
|
|
|
702
820
|
// not via bootstrap). For user-facing bootstrap, only the registry is consulted.
|
|
703
821
|
const entry = this.registry.get(name);
|
|
704
822
|
if (!entry) {
|
|
705
|
-
this.emitObs("bootstrap", {
|
|
823
|
+
this.emitObs("bootstrap", {
|
|
824
|
+
name,
|
|
825
|
+
version: clientVersion,
|
|
826
|
+
attestation: this.attestation,
|
|
827
|
+
result: "not_found",
|
|
828
|
+
});
|
|
706
829
|
return this.sendError(frame.id, "not_found", `cap "${name}" not served`);
|
|
707
830
|
}
|
|
708
831
|
|
|
709
832
|
const serverVersion = entry.version;
|
|
710
833
|
if (serverVersion != null && clientVersion != null && serverVersion !== clientVersion) {
|
|
711
|
-
this.emitObs("bootstrap", {
|
|
834
|
+
this.emitObs("bootstrap", {
|
|
835
|
+
name,
|
|
836
|
+
version: clientVersion,
|
|
837
|
+
attestation: this.attestation,
|
|
838
|
+
result: "version_mismatch",
|
|
839
|
+
});
|
|
712
840
|
return this.sendError(
|
|
713
841
|
frame.id,
|
|
714
842
|
"failed_precondition",
|
|
715
843
|
`version mismatch (server "${serverVersion}", client "${clientVersion}")`,
|
|
716
|
-
{ reason: "version_mismatch" as FailedPreconditionReason }
|
|
844
|
+
{ reason: "version_mismatch" as FailedPreconditionReason },
|
|
717
845
|
);
|
|
718
846
|
}
|
|
719
847
|
|
|
@@ -722,24 +850,42 @@ class ConnectionImpl implements Connection {
|
|
|
722
850
|
try {
|
|
723
851
|
allowed = await this.policy(name, this.attestation);
|
|
724
852
|
} catch (err) {
|
|
725
|
-
this.emitObs("error", {
|
|
726
|
-
|
|
853
|
+
this.emitObs("error", {
|
|
854
|
+
phase: "policy",
|
|
855
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
856
|
+
});
|
|
857
|
+
this.emitObs("bootstrap", {
|
|
858
|
+
name,
|
|
859
|
+
version: clientVersion,
|
|
860
|
+
attestation: this.attestation,
|
|
861
|
+
result: "internal",
|
|
862
|
+
});
|
|
727
863
|
return this.sendError(frame.id, "internal", "policy threw");
|
|
728
864
|
}
|
|
729
865
|
if (typeof allowed !== "boolean") {
|
|
730
866
|
// Non-boolean return = programming bug; surface explicitly (not silent deny).
|
|
731
|
-
this.emitObs("error", {
|
|
732
|
-
|
|
867
|
+
this.emitObs("error", {
|
|
868
|
+
phase: "policy",
|
|
869
|
+
error: new Error(`policy must return boolean (got ${typeof allowed})`),
|
|
870
|
+
});
|
|
871
|
+
this.emitObs("bootstrap", {
|
|
872
|
+
name,
|
|
873
|
+
version: clientVersion,
|
|
874
|
+
attestation: this.attestation,
|
|
875
|
+
result: "internal",
|
|
876
|
+
});
|
|
733
877
|
return this.sendError(frame.id, "internal", "policy returned non-boolean");
|
|
734
878
|
}
|
|
735
879
|
if (!allowed) {
|
|
736
|
-
this.emitObs("bootstrap", {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
"
|
|
741
|
-
|
|
742
|
-
|
|
880
|
+
this.emitObs("bootstrap", {
|
|
881
|
+
name,
|
|
882
|
+
version: clientVersion,
|
|
883
|
+
attestation: this.attestation,
|
|
884
|
+
result: "denied",
|
|
885
|
+
});
|
|
886
|
+
return this.sendError(frame.id, "failed_precondition", "policy denied", {
|
|
887
|
+
reason: "unauthorized" as FailedPreconditionReason,
|
|
888
|
+
});
|
|
743
889
|
}
|
|
744
890
|
}
|
|
745
891
|
|
|
@@ -766,21 +912,37 @@ class ConnectionImpl implements Connection {
|
|
|
766
912
|
} catch (err) {
|
|
767
913
|
if (err instanceof IpcError) {
|
|
768
914
|
const result = err.code === "resource_exhausted" ? "resource_exhausted" : "internal";
|
|
769
|
-
this.emitObs("bootstrap", {
|
|
770
|
-
|
|
915
|
+
this.emitObs("bootstrap", {
|
|
916
|
+
name,
|
|
917
|
+
version: clientVersion,
|
|
918
|
+
attestation: this.attestation,
|
|
919
|
+
result,
|
|
920
|
+
});
|
|
921
|
+
return this.sendError(
|
|
922
|
+
frame.id,
|
|
923
|
+
err.code,
|
|
924
|
+
err.message,
|
|
925
|
+
err.details as Record<string, unknown> | undefined,
|
|
926
|
+
);
|
|
771
927
|
}
|
|
772
928
|
throw err;
|
|
773
929
|
}
|
|
774
930
|
}
|
|
775
|
-
this.emitObs("bootstrap", {
|
|
931
|
+
this.emitObs("bootstrap", {
|
|
932
|
+
name,
|
|
933
|
+
version: clientVersion,
|
|
934
|
+
attestation: this.attestation,
|
|
935
|
+
result: "ok",
|
|
936
|
+
capId,
|
|
937
|
+
});
|
|
776
938
|
this.transport.send({ op: "result", id: frame.id, ok: true, value: new CapRef(capId) });
|
|
777
939
|
}
|
|
778
940
|
|
|
779
941
|
private async invokeServerMethod(
|
|
780
942
|
frame: CallFrame,
|
|
781
|
-
cap:
|
|
943
|
+
cap: AnyCapDef,
|
|
782
944
|
methodDef: MethodDef,
|
|
783
|
-
impl: (params: unknown, ctx: CallCtx) => unknown
|
|
945
|
+
impl: (params: unknown, ctx: CallCtx) => unknown,
|
|
784
946
|
): Promise<void> {
|
|
785
947
|
const ctx = this.makeCallCtx(frame);
|
|
786
948
|
|
|
@@ -789,13 +951,17 @@ class ConnectionImpl implements Connection {
|
|
|
789
951
|
try {
|
|
790
952
|
const runStream = () => impl(frame.args, ctx) as StreamType<unknown>;
|
|
791
953
|
const als = callContextStorage;
|
|
792
|
-
const scoped = als
|
|
793
|
-
? () => als.run({ callId: frame.id }, runStream)
|
|
794
|
-
: runStream;
|
|
954
|
+
const scoped = als ? () => als.run({ callId: frame.id }, runStream) : runStream;
|
|
795
955
|
const stream = scoped();
|
|
796
956
|
this.runServerStream(frame.id, stream, ctx, hintBudget, cap, frame.method, frame.target.id);
|
|
797
957
|
} catch (err) {
|
|
798
|
-
this.emitObs("stream", {
|
|
958
|
+
this.emitObs("stream", {
|
|
959
|
+
capId: frame.target.id,
|
|
960
|
+
capName: cap.name,
|
|
961
|
+
method: frame.method,
|
|
962
|
+
callId: frame.id,
|
|
963
|
+
event: "error",
|
|
964
|
+
});
|
|
799
965
|
this.transport.send({ op: "stream", id: frame.id, ev: "error", error: toStatus(err) });
|
|
800
966
|
}
|
|
801
967
|
return;
|
|
@@ -814,22 +980,34 @@ class ConnectionImpl implements Connection {
|
|
|
814
980
|
const encoder = makeReturnEncoder(methodDef.returns);
|
|
815
981
|
const runImpl = () => Promise.resolve(impl(frame.args, ctx));
|
|
816
982
|
const als = callContextStorage;
|
|
817
|
-
const scoped = als
|
|
818
|
-
? () => als.run({ callId: frame.id }, runImpl)
|
|
819
|
-
: runImpl;
|
|
983
|
+
const scoped = als ? () => als.run({ callId: frame.id }, runImpl) : runImpl;
|
|
820
984
|
try {
|
|
821
985
|
const result = await scoped();
|
|
822
986
|
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
823
987
|
if (!this.serverActiveCalls.delete(frame.id)) return;
|
|
824
988
|
const encoded = encoder(result);
|
|
825
989
|
this.transport.send({ op: "result", id: frame.id, ok: true, value: encoded });
|
|
826
|
-
this.emitObs("call", {
|
|
990
|
+
this.emitObs("call", {
|
|
991
|
+
capId: frame.target.id,
|
|
992
|
+
capName: cap.name,
|
|
993
|
+
method: frame.method,
|
|
994
|
+
callId: frame.id,
|
|
995
|
+
durationMs: performance.now() - startedAt,
|
|
996
|
+
result: "ok",
|
|
997
|
+
});
|
|
827
998
|
} catch (err) {
|
|
828
999
|
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
829
1000
|
if (!this.serverActiveCalls.delete(frame.id)) return;
|
|
830
1001
|
const status = toStatus(err);
|
|
831
1002
|
this.sendErrorFromStatus(frame.id, status);
|
|
832
|
-
this.emitObs("call", {
|
|
1003
|
+
this.emitObs("call", {
|
|
1004
|
+
capId: frame.target.id,
|
|
1005
|
+
capName: cap.name,
|
|
1006
|
+
method: frame.method,
|
|
1007
|
+
callId: frame.id,
|
|
1008
|
+
durationMs: performance.now() - startedAt,
|
|
1009
|
+
result: status.code,
|
|
1010
|
+
});
|
|
833
1011
|
}
|
|
834
1012
|
return;
|
|
835
1013
|
}
|
|
@@ -837,7 +1015,10 @@ class ConnectionImpl implements Connection {
|
|
|
837
1015
|
this.sendError(frame.id, "not_found", "unknown method kind");
|
|
838
1016
|
}
|
|
839
1017
|
|
|
840
|
-
private armServerDeadline(
|
|
1018
|
+
private armServerDeadline(
|
|
1019
|
+
frame: CallFrame,
|
|
1020
|
+
ctx: CallCtx,
|
|
1021
|
+
): ReturnType<typeof setTimeout> | undefined {
|
|
841
1022
|
const ms = frame.meta?.deadlineMs;
|
|
842
1023
|
if (!ms) return;
|
|
843
1024
|
return setTimeout(() => {
|
|
@@ -859,7 +1040,7 @@ class ConnectionImpl implements Connection {
|
|
|
859
1040
|
return ctx;
|
|
860
1041
|
}
|
|
861
1042
|
|
|
862
|
-
private exportCap<C extends
|
|
1043
|
+
private exportCap<C extends AnyCapDef>(capDef: C, impl: unknown): ExportedCap<C> {
|
|
863
1044
|
const typeId = frameworkTypeIdOf(capDef) ?? FIRST_USER_TYPE_ID;
|
|
864
1045
|
const entry = this.capTable.allocate({
|
|
865
1046
|
typeId,
|
|
@@ -880,7 +1061,7 @@ class ConnectionImpl implements Connection {
|
|
|
880
1061
|
stream: StreamType<unknown>,
|
|
881
1062
|
ctx: CallCtx,
|
|
882
1063
|
initialCredit: number,
|
|
883
|
-
cap:
|
|
1064
|
+
cap: AnyCapDef,
|
|
884
1065
|
method: string,
|
|
885
1066
|
capId: number,
|
|
886
1067
|
): void {
|
|
@@ -903,7 +1084,9 @@ class ConnectionImpl implements Connection {
|
|
|
903
1084
|
|
|
904
1085
|
const waitForCredit = (): Promise<void> => {
|
|
905
1086
|
if (slot.credit > 0 || slot.cancelled) return Promise.resolve();
|
|
906
|
-
return new Promise<void>((resolve) => {
|
|
1087
|
+
return new Promise<void>((resolve) => {
|
|
1088
|
+
slot.creditWaker = resolve;
|
|
1089
|
+
});
|
|
907
1090
|
};
|
|
908
1091
|
|
|
909
1092
|
const drain = async () => {
|
|
@@ -922,11 +1105,25 @@ class ConnectionImpl implements Connection {
|
|
|
922
1105
|
} catch (err) {
|
|
923
1106
|
errored = true;
|
|
924
1107
|
this.transport.send({ op: "stream", id: callId, ev: "error", error: toStatus(err) });
|
|
925
|
-
this.emitObs("stream", {
|
|
1108
|
+
this.emitObs("stream", {
|
|
1109
|
+
capId,
|
|
1110
|
+
capName: cap.name,
|
|
1111
|
+
method,
|
|
1112
|
+
callId,
|
|
1113
|
+
event: "error",
|
|
1114
|
+
count: slot.count,
|
|
1115
|
+
});
|
|
926
1116
|
} finally {
|
|
927
1117
|
if (!errored) {
|
|
928
1118
|
this.transport.send({ op: "stream", id: callId, ev: "end" });
|
|
929
|
-
this.emitObs("stream", {
|
|
1119
|
+
this.emitObs("stream", {
|
|
1120
|
+
capId,
|
|
1121
|
+
capName: cap.name,
|
|
1122
|
+
method,
|
|
1123
|
+
callId,
|
|
1124
|
+
event: slot.cancelled ? "cancel" : "end",
|
|
1125
|
+
count: slot.count,
|
|
1126
|
+
});
|
|
930
1127
|
}
|
|
931
1128
|
this.serverStreams.delete(callId);
|
|
932
1129
|
}
|
|
@@ -996,7 +1193,9 @@ class ConnectionImpl implements Connection {
|
|
|
996
1193
|
|
|
997
1194
|
releaseRef(proxy: unknown): void {
|
|
998
1195
|
if (typeof proxy !== "object" || proxy === null) return;
|
|
999
|
-
const meta = (proxy as Record<symbol, unknown>)[CAP_PROXY_META] as
|
|
1196
|
+
const meta = (proxy as Record<symbol, unknown>)[CAP_PROXY_META] as
|
|
1197
|
+
| { capId: number; dropped: boolean }
|
|
1198
|
+
| undefined;
|
|
1000
1199
|
if (!meta || meta.dropped) return;
|
|
1001
1200
|
meta.dropped = true;
|
|
1002
1201
|
this.sendDrop(meta.capId);
|
|
@@ -1040,7 +1239,10 @@ class ConnectionImpl implements Connection {
|
|
|
1040
1239
|
const released = this.capTable.release(id, delta);
|
|
1041
1240
|
if (released) {
|
|
1042
1241
|
for (const [rootName, capId] of this.rootInstances) {
|
|
1043
|
-
if (capId === id) {
|
|
1242
|
+
if (capId === id) {
|
|
1243
|
+
this.rootInstances.delete(rootName);
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1044
1246
|
}
|
|
1045
1247
|
}
|
|
1046
1248
|
}
|
|
@@ -1068,28 +1270,38 @@ class ConnectionImpl implements Connection {
|
|
|
1068
1270
|
meta?: CallMeta,
|
|
1069
1271
|
capName?: string,
|
|
1070
1272
|
): Promise<unknown> {
|
|
1071
|
-
if (this.closed_)
|
|
1273
|
+
if (this.closed_)
|
|
1274
|
+
return Promise.reject(new IpcError({ code: "unavailable", message: "connection closed" }));
|
|
1072
1275
|
if (this.revokedCapIds.has(capId)) {
|
|
1073
|
-
return Promise.reject(
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1276
|
+
return Promise.reject(
|
|
1277
|
+
new IpcError({
|
|
1278
|
+
code: "failed_precondition",
|
|
1279
|
+
message: "cap revoked",
|
|
1280
|
+
details: { reason: "revoked" as FailedPreconditionReason },
|
|
1281
|
+
}),
|
|
1282
|
+
);
|
|
1078
1283
|
}
|
|
1079
1284
|
if (this.pending.size >= this.maxInFlightCalls) {
|
|
1080
|
-
return Promise.reject(
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1285
|
+
return Promise.reject(
|
|
1286
|
+
new IpcError({
|
|
1287
|
+
code: "resource_exhausted",
|
|
1288
|
+
message: `in-flight calls limit ${this.maxInFlightCalls}`,
|
|
1289
|
+
details: { reason: "max_concurrent_calls" as ResourceExhaustedReason },
|
|
1290
|
+
}),
|
|
1291
|
+
);
|
|
1085
1292
|
}
|
|
1086
1293
|
const id = this.nextId();
|
|
1087
1294
|
return new Promise((resolve, reject) => {
|
|
1088
1295
|
const abort = new AbortController();
|
|
1089
1296
|
const pending: PendingCall = {
|
|
1090
|
-
resolve,
|
|
1297
|
+
resolve,
|
|
1298
|
+
reject,
|
|
1299
|
+
abort,
|
|
1300
|
+
decodeReturn,
|
|
1091
1301
|
startedAt: performance.now(),
|
|
1092
|
-
capId,
|
|
1302
|
+
capId,
|
|
1303
|
+
method,
|
|
1304
|
+
capName,
|
|
1093
1305
|
kind: "call",
|
|
1094
1306
|
};
|
|
1095
1307
|
if (meta?.deadlineMs) {
|
|
@@ -1102,14 +1314,21 @@ class ConnectionImpl implements Connection {
|
|
|
1102
1314
|
}
|
|
1103
1315
|
this.pending.set(id, pending);
|
|
1104
1316
|
let finalMeta = meta;
|
|
1105
|
-
if (
|
|
1317
|
+
if (finalMeta?.parentCallId === undefined && callContextStorage) {
|
|
1106
1318
|
const scope = callContextStorage.getStore();
|
|
1107
1319
|
if (scope) finalMeta = { ...(finalMeta ?? {}), parentCallId: scope.callId };
|
|
1108
1320
|
}
|
|
1109
1321
|
if (finalMeta?.parentCallId !== undefined) {
|
|
1110
1322
|
this.serverCallChildren.set(id, { parentId: finalMeta.parentCallId });
|
|
1111
1323
|
}
|
|
1112
|
-
this.transport.send({
|
|
1324
|
+
this.transport.send({
|
|
1325
|
+
op: "call",
|
|
1326
|
+
id,
|
|
1327
|
+
target: { kind: "cap", id: capId },
|
|
1328
|
+
method,
|
|
1329
|
+
args,
|
|
1330
|
+
meta: finalMeta,
|
|
1331
|
+
});
|
|
1113
1332
|
});
|
|
1114
1333
|
}
|
|
1115
1334
|
|
|
@@ -1121,17 +1340,29 @@ class ConnectionImpl implements Connection {
|
|
|
1121
1340
|
capName?: string,
|
|
1122
1341
|
): StreamType<T> {
|
|
1123
1342
|
if (this.closed_) {
|
|
1124
|
-
const empty = makeClientStream<T>(
|
|
1343
|
+
const empty = makeClientStream<T>(
|
|
1344
|
+
capId,
|
|
1345
|
+
0,
|
|
1346
|
+
() => {},
|
|
1347
|
+
() => {},
|
|
1348
|
+
);
|
|
1125
1349
|
empty.fail(new IpcError({ code: "unavailable", message: "connection closed" }));
|
|
1126
1350
|
return empty.stream;
|
|
1127
1351
|
}
|
|
1128
1352
|
if (this.revokedCapIds.has(capId)) {
|
|
1129
|
-
const empty = makeClientStream<T>(
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1353
|
+
const empty = makeClientStream<T>(
|
|
1354
|
+
capId,
|
|
1355
|
+
0,
|
|
1356
|
+
() => {},
|
|
1357
|
+
() => {},
|
|
1358
|
+
);
|
|
1359
|
+
empty.fail(
|
|
1360
|
+
new IpcError({
|
|
1361
|
+
code: "failed_precondition",
|
|
1362
|
+
message: "cap revoked",
|
|
1363
|
+
details: { reason: "revoked" as FailedPreconditionReason },
|
|
1364
|
+
}),
|
|
1365
|
+
);
|
|
1135
1366
|
return empty.stream;
|
|
1136
1367
|
}
|
|
1137
1368
|
const id = this.nextId();
|
|
@@ -1151,7 +1382,7 @@ class ConnectionImpl implements Connection {
|
|
|
1151
1382
|
return ctx.stream;
|
|
1152
1383
|
}
|
|
1153
1384
|
|
|
1154
|
-
makeCapProxy<C extends
|
|
1385
|
+
makeCapProxy<C extends AnyCapDef>(capDef: C, capId: number): ClientOf<C> {
|
|
1155
1386
|
const proxy: Record<string | symbol, unknown> = {};
|
|
1156
1387
|
const meta = { capId, dropped: false };
|
|
1157
1388
|
proxy[CAP_PROXY_META] = meta;
|
|
@@ -1164,26 +1395,33 @@ class ConnectionImpl implements Connection {
|
|
|
1164
1395
|
for (const name of Object.keys(capDef.methods)) {
|
|
1165
1396
|
const def = capDef.methods[name] as MethodDef;
|
|
1166
1397
|
if (isStreamDef(def)) {
|
|
1167
|
-
proxy[name] = (params?: unknown) =>
|
|
1398
|
+
proxy[name] = (params?: unknown) =>
|
|
1399
|
+
this.openClientStream(capId, name, params, undefined, capDef.name);
|
|
1168
1400
|
} else if (isCallDef(def)) {
|
|
1169
1401
|
const decoder = makeReturnDecoder(def.returns, (cd, cid) => this.makeCapProxy(cd, cid));
|
|
1170
|
-
proxy[name] = (params?: unknown) =>
|
|
1402
|
+
proxy[name] = (params?: unknown) =>
|
|
1403
|
+
this.sendCallTyped(capId, name, params, decoder, undefined, capDef.name);
|
|
1171
1404
|
}
|
|
1172
1405
|
}
|
|
1173
1406
|
|
|
1174
1407
|
const disposal = capDef.disposal as DisposalSpec | undefined;
|
|
1175
1408
|
if (disposal) {
|
|
1176
|
-
const conn = this;
|
|
1177
1409
|
proxy[Symbol.dispose] = () => {
|
|
1178
1410
|
try {
|
|
1179
1411
|
const r = (proxy[disposal.method] as (() => unknown) | undefined)?.();
|
|
1180
1412
|
if (r && typeof (r as Promise<unknown>).then === "function") {
|
|
1181
1413
|
(r as Promise<unknown>).catch((err) =>
|
|
1182
|
-
|
|
1414
|
+
this.emitObs("error", {
|
|
1415
|
+
phase: "dispose",
|
|
1416
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1417
|
+
}),
|
|
1183
1418
|
);
|
|
1184
1419
|
}
|
|
1185
1420
|
} catch (err) {
|
|
1186
|
-
|
|
1421
|
+
this.emitObs("error", {
|
|
1422
|
+
phase: "dispose",
|
|
1423
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1424
|
+
});
|
|
1187
1425
|
}
|
|
1188
1426
|
drop();
|
|
1189
1427
|
};
|
|
@@ -1201,7 +1439,11 @@ class ConnectionImpl implements Connection {
|
|
|
1201
1439
|
}
|
|
1202
1440
|
|
|
1203
1441
|
_dropFromFinalizer(capId: number): void {
|
|
1204
|
-
try {
|
|
1442
|
+
try {
|
|
1443
|
+
this.sendDrop(capId);
|
|
1444
|
+
} catch {
|
|
1445
|
+
/* swallow */
|
|
1446
|
+
}
|
|
1205
1447
|
}
|
|
1206
1448
|
|
|
1207
1449
|
shutdown(reason: string = "shutdown"): void {
|
|
@@ -1234,10 +1476,18 @@ class ConnectionImpl implements Connection {
|
|
|
1234
1476
|
this.rejectRemoteReady(new IpcError({ code: "unavailable", message: reason }));
|
|
1235
1477
|
}
|
|
1236
1478
|
for (const fn of this.closeHandlers) {
|
|
1237
|
-
try {
|
|
1479
|
+
try {
|
|
1480
|
+
fn();
|
|
1481
|
+
} catch {
|
|
1482
|
+
/* swallow */
|
|
1483
|
+
}
|
|
1238
1484
|
}
|
|
1239
1485
|
this.closeHandlers.clear();
|
|
1240
|
-
try {
|
|
1486
|
+
try {
|
|
1487
|
+
this.transport.close();
|
|
1488
|
+
} catch {
|
|
1489
|
+
/* swallow */
|
|
1490
|
+
}
|
|
1241
1491
|
}
|
|
1242
1492
|
|
|
1243
1493
|
private invokeServerDisposal(entry: CapTableEntry): void {
|
|
@@ -1248,13 +1498,17 @@ class ConnectionImpl implements Connection {
|
|
|
1248
1498
|
const impl = entry.impl as Record<string, unknown> | null;
|
|
1249
1499
|
const fn = impl?.[disposal.method] as ((p: unknown, c?: unknown) => unknown) | undefined;
|
|
1250
1500
|
if (!fn) return;
|
|
1251
|
-
try {
|
|
1501
|
+
try {
|
|
1502
|
+
void fn.call(impl, undefined, undefined);
|
|
1503
|
+
} catch {
|
|
1504
|
+
/* swallow */
|
|
1505
|
+
}
|
|
1252
1506
|
}
|
|
1253
1507
|
}
|
|
1254
1508
|
|
|
1255
1509
|
function makeReturnEncoder(returns: AnyCapToken | undefined): (v: unknown) => unknown {
|
|
1256
1510
|
if (!returns) return (v) => v;
|
|
1257
|
-
const expectExportedCap = (v: unknown, expectedCap:
|
|
1511
|
+
const expectExportedCap = (v: unknown, expectedCap: AnyCapDef): CapRef => {
|
|
1258
1512
|
if (!isExportedCap(v)) {
|
|
1259
1513
|
throw new IpcError({
|
|
1260
1514
|
code: "failed_precondition",
|
|
@@ -1275,7 +1529,10 @@ function makeReturnEncoder(returns: AnyCapToken | undefined): (v: unknown) => un
|
|
|
1275
1529
|
if (isCapArray(returns)) {
|
|
1276
1530
|
return (v) => {
|
|
1277
1531
|
if (!Array.isArray(v)) {
|
|
1278
|
-
throw new IpcError({
|
|
1532
|
+
throw new IpcError({
|
|
1533
|
+
code: "invalid_argument",
|
|
1534
|
+
message: "expected ExportedCap[] for cap.array method",
|
|
1535
|
+
});
|
|
1279
1536
|
}
|
|
1280
1537
|
return v.map((item) => expectExportedCap(item, returns.cap));
|
|
1281
1538
|
};
|
|
@@ -1283,7 +1540,10 @@ function makeReturnEncoder(returns: AnyCapToken | undefined): (v: unknown) => un
|
|
|
1283
1540
|
if (isCapRecord(returns)) {
|
|
1284
1541
|
return (v) => {
|
|
1285
1542
|
if (!v || typeof v !== "object") {
|
|
1286
|
-
throw new IpcError({
|
|
1543
|
+
throw new IpcError({
|
|
1544
|
+
code: "invalid_argument",
|
|
1545
|
+
message: "expected Record<string, ExportedCap> for cap.record method",
|
|
1546
|
+
});
|
|
1287
1547
|
}
|
|
1288
1548
|
const out: Record<string, CapRef> = {};
|
|
1289
1549
|
for (const k of Object.keys(v as object)) {
|
|
@@ -1296,14 +1556,15 @@ function makeReturnEncoder(returns: AnyCapToken | undefined): (v: unknown) => un
|
|
|
1296
1556
|
}
|
|
1297
1557
|
|
|
1298
1558
|
function makeReturnDecoder(
|
|
1299
|
-
returns: { cap?:
|
|
1300
|
-
spawn: (cap:
|
|
1559
|
+
returns: { cap?: AnyCapDef } | undefined,
|
|
1560
|
+
spawn: (cap: AnyCapDef, capId: number) => unknown,
|
|
1301
1561
|
): ReturnDecoder | undefined {
|
|
1302
1562
|
if (!returns) return undefined;
|
|
1303
1563
|
if (isCapRef(returns)) {
|
|
1304
1564
|
const inner = returns.cap;
|
|
1305
1565
|
return (raw) => {
|
|
1306
|
-
if (!(raw instanceof CapRef))
|
|
1566
|
+
if (!(raw instanceof CapRef))
|
|
1567
|
+
throw new IpcError({ code: "invalid_argument", message: "expected CapRef" });
|
|
1307
1568
|
return spawn(inner, raw.capId);
|
|
1308
1569
|
};
|
|
1309
1570
|
}
|
|
@@ -1311,9 +1572,11 @@ function makeReturnDecoder(
|
|
|
1311
1572
|
const inner = returns.cap;
|
|
1312
1573
|
const disposal = inner.disposal as DisposalSpec | undefined;
|
|
1313
1574
|
return (raw) => {
|
|
1314
|
-
if (!Array.isArray(raw))
|
|
1575
|
+
if (!Array.isArray(raw))
|
|
1576
|
+
throw new IpcError({ code: "invalid_argument", message: "expected array" });
|
|
1315
1577
|
const proxies = raw.map((item) => {
|
|
1316
|
-
if (!(item instanceof CapRef))
|
|
1578
|
+
if (!(item instanceof CapRef))
|
|
1579
|
+
throw new IpcError({ code: "invalid_argument", message: "expected CapRef in array" });
|
|
1317
1580
|
return spawn(inner, item.capId);
|
|
1318
1581
|
});
|
|
1319
1582
|
attachArrayDisposal(proxies, disposal);
|
|
@@ -1323,11 +1586,13 @@ function makeReturnDecoder(
|
|
|
1323
1586
|
if (isCapRecord(returns)) {
|
|
1324
1587
|
const inner = returns.cap;
|
|
1325
1588
|
return (raw) => {
|
|
1326
|
-
if (!raw || typeof raw !== "object")
|
|
1589
|
+
if (!raw || typeof raw !== "object")
|
|
1590
|
+
throw new IpcError({ code: "invalid_argument", message: "expected record" });
|
|
1327
1591
|
const out: Record<string, unknown> = {};
|
|
1328
1592
|
for (const k of Object.keys(raw as object)) {
|
|
1329
1593
|
const item = (raw as Record<string, unknown>)[k];
|
|
1330
|
-
if (!(item instanceof CapRef))
|
|
1594
|
+
if (!(item instanceof CapRef))
|
|
1595
|
+
throw new IpcError({ code: "invalid_argument", message: "expected CapRef in record" });
|
|
1331
1596
|
out[k] = spawn(inner, item.capId);
|
|
1332
1597
|
}
|
|
1333
1598
|
return out;
|
|
@@ -1364,7 +1629,7 @@ function makeClientStream<T>(
|
|
|
1364
1629
|
capId: number,
|
|
1365
1630
|
streamId: number,
|
|
1366
1631
|
cancel: () => void,
|
|
1367
|
-
sendCredit: (messages: number) => void
|
|
1632
|
+
sendCredit: (messages: number) => void,
|
|
1368
1633
|
): ClientStreamHandle<T> {
|
|
1369
1634
|
const buffer: T[] = [];
|
|
1370
1635
|
const waiters: Array<{ resolve(r: IteratorResult<T>): void; reject(e: Error): void }> = [];
|
|
@@ -1403,8 +1668,14 @@ function makeClientStream<T>(
|
|
|
1403
1668
|
while (waiters.length > 0) {
|
|
1404
1669
|
const w = waiters.shift()!;
|
|
1405
1670
|
const r = pump();
|
|
1406
|
-
if (r) {
|
|
1407
|
-
|
|
1671
|
+
if (r) {
|
|
1672
|
+
w.resolve(r);
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (failure) {
|
|
1676
|
+
w.reject(failure);
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1408
1679
|
waiters.unshift(w);
|
|
1409
1680
|
break;
|
|
1410
1681
|
}
|