bunite-core 0.17.1 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/host/core/App.ts +28 -18
- package/src/host/core/BrowserView.ts +333 -116
- package/src/host/core/BrowserWindow.ts +44 -22
- package/src/host/core/Socket.ts +26 -9
- package/src/host/core/SurfaceBrowserIPC.ts +15 -5
- package/src/host/core/SurfaceManager.ts +345 -158
- package/src/host/core/SurfaceRegistry.ts +1 -1
- package/src/host/core/inputDispatch.ts +71 -42
- package/src/host/core/singleInstanceLock.ts +10 -2
- package/src/host/core/windowCap.ts +42 -23
- package/src/host/encryptedPipe.ts +20 -3
- package/src/host/events/appEvents.ts +1 -1
- package/src/host/events/eventEmitter.ts +6 -6
- package/src/host/events/webviewEvents.ts +11 -4
- package/src/host/events/windowEvents.ts +1 -2
- package/src/host/index.ts +9 -17
- package/src/host/log.ts +6 -4
- package/src/host/native.ts +271 -163
- package/src/host/paths.ts +18 -15
- package/src/host/platform.ts +2 -4
- package/src/host/preloadBundle.ts +1 -1
- package/src/host/serveWeb.ts +45 -30
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +66 -25
- package/src/rpc/encrypt.ts +23 -4
- package/src/rpc/error.ts +2 -7
- package/src/rpc/framework.ts +105 -35
- package/src/rpc/index.ts +130 -140
- package/src/rpc/peer.ts +442 -164
- package/src/rpc/renderer.ts +29 -11
- package/src/rpc/schema.ts +86 -61
- package/src/rpc/stream.ts +12 -3
- package/src/rpc/transport.ts +76 -12
- package/src/rpc/wire.ts +23 -10
- package/src/webview/native.ts +150 -73
- package/src/webview/polyfill.ts +184 -48
package/src/rpc/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
|
}
|
|
@@ -158,10 +158,16 @@ export class CapTable {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
export interface CloseInfo {
|
|
162
|
+
code?: number;
|
|
163
|
+
reason?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
161
166
|
export interface Transport {
|
|
162
167
|
send(frame: Frame): void;
|
|
163
168
|
setReceive(handler: (frame: Frame) => void): void;
|
|
164
169
|
close(): void;
|
|
170
|
+
onClose?(handler: (info?: CloseInfo) => void): void;
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
export type Policy = (name: string, attestation: Attestation) => boolean | Promise<boolean>;
|
|
@@ -173,26 +179,60 @@ export interface ServeHandle extends Disposable {
|
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
export interface ConnectionEvents {
|
|
176
|
-
bootstrap: {
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
bootstrap: {
|
|
183
|
+
name: string;
|
|
184
|
+
version?: string;
|
|
185
|
+
attestation: Attestation;
|
|
186
|
+
result:
|
|
187
|
+
| "ok"
|
|
188
|
+
| "denied"
|
|
189
|
+
| "not_found"
|
|
190
|
+
| "version_mismatch"
|
|
191
|
+
| "invalid_argument"
|
|
192
|
+
| "resource_exhausted"
|
|
193
|
+
| "internal";
|
|
194
|
+
capId?: number;
|
|
195
|
+
};
|
|
196
|
+
call: {
|
|
197
|
+
capId: number;
|
|
198
|
+
capName?: string;
|
|
199
|
+
method: string;
|
|
200
|
+
callId: number;
|
|
201
|
+
durationMs?: number;
|
|
202
|
+
result: "ok" | "cancelled" | IpcCode;
|
|
203
|
+
};
|
|
204
|
+
stream: {
|
|
205
|
+
capId: number;
|
|
206
|
+
capName?: string;
|
|
207
|
+
method: string;
|
|
208
|
+
callId: number;
|
|
209
|
+
event: "start" | "end" | "cancel" | "error";
|
|
210
|
+
count?: number;
|
|
211
|
+
};
|
|
179
212
|
revoke: { capIds: number[]; reason: "unserve" | "replace" };
|
|
180
213
|
error: { phase: string; error: Error };
|
|
181
214
|
}
|
|
182
215
|
|
|
183
216
|
export interface Connection {
|
|
184
|
-
bootstrap<C extends
|
|
217
|
+
bootstrap<C extends AnyCapDef>(cap: C): Promise<ClientOf<C>>;
|
|
185
218
|
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
|
-
|
|
219
|
+
serve<C extends AnyCapDef>(cap: C, impl: ImplOf<C>, opts?: { ifExists?: IfExists }): ServeHandle;
|
|
220
|
+
serveAll<R extends SchemaRoots>(
|
|
221
|
+
schema: Schema<R>,
|
|
222
|
+
impls: ImplsOf<R>,
|
|
223
|
+
opts?: { ifExists?: IfExists },
|
|
224
|
+
): ServeHandle;
|
|
225
|
+
unserve(target: AnyCapDef | ServeHandle): void;
|
|
226
|
+
replace<C extends AnyCapDef>(cap: C, impl: ImplOf<C>): void;
|
|
190
227
|
runtime(): ClientOf<typeof RuntimeCap>;
|
|
191
228
|
releaseRef(proxy: unknown): void;
|
|
192
|
-
on<K extends keyof ConnectionEvents>(
|
|
193
|
-
|
|
229
|
+
on<K extends keyof ConnectionEvents>(
|
|
230
|
+
event: K,
|
|
231
|
+
handler: (e: ConnectionEvents[K]) => void,
|
|
232
|
+
): () => void;
|
|
233
|
+
onClose(handler: (info?: CloseInfo) => void): () => void;
|
|
194
234
|
/** Tear down the connection — rejects pending, fires onClose, closes transport. Reliable signal for application lifecycle. */
|
|
195
|
-
shutdown(reason?: string): void;
|
|
235
|
+
shutdown(reason?: string, info?: CloseInfo): void;
|
|
196
236
|
readonly closed: boolean;
|
|
197
237
|
}
|
|
198
238
|
|
|
@@ -244,14 +284,19 @@ function isExportedCap(v: unknown): v is ExportedCap<any> {
|
|
|
244
284
|
return typeof v === "object" && v !== null && (v as any)[EXPORTED_CAP_BRAND] === true;
|
|
245
285
|
}
|
|
246
286
|
|
|
247
|
-
const proxyFinalizers =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
287
|
+
const proxyFinalizers =
|
|
288
|
+
typeof FinalizationRegistry !== "undefined"
|
|
289
|
+
? new FinalizationRegistry<{
|
|
290
|
+
connRef: WeakRef<ConnectionImpl>;
|
|
291
|
+
capId: number;
|
|
292
|
+
dropped: () => boolean;
|
|
293
|
+
}>((held) => {
|
|
294
|
+
if (held.dropped()) return;
|
|
295
|
+
const conn = held.connRef.deref();
|
|
296
|
+
if (!conn || conn.closed) return;
|
|
297
|
+
conn._dropFromFinalizer(held.capId);
|
|
298
|
+
})
|
|
299
|
+
: ({ register: () => {} } as { register: (target: object, held: unknown) => void });
|
|
255
300
|
|
|
256
301
|
interface ServerStreamCtx {
|
|
257
302
|
iter: AsyncIterator<unknown> | null;
|
|
@@ -274,7 +319,7 @@ interface ClientStreamCtx {
|
|
|
274
319
|
}
|
|
275
320
|
|
|
276
321
|
interface RegistryEntry {
|
|
277
|
-
cap:
|
|
322
|
+
cap: AnyCapDef;
|
|
278
323
|
impl: unknown;
|
|
279
324
|
version?: string;
|
|
280
325
|
}
|
|
@@ -286,15 +331,20 @@ class ConnectionImpl implements Connection {
|
|
|
286
331
|
private readonly clientStreams = new Map<number, ClientStreamCtx>();
|
|
287
332
|
private readonly serverStreams = new Map<number, ServerStreamCtx>();
|
|
288
333
|
private readonly serverCallChildren = new Map<number, { parentId: number }>();
|
|
289
|
-
private readonly serverActiveCalls = new Map<
|
|
334
|
+
private readonly serverActiveCalls = new Map<
|
|
335
|
+
number,
|
|
336
|
+
{ ctrl: AbortController; capId: number; capName: string; method: string; startedAt: number }
|
|
337
|
+
>();
|
|
290
338
|
/** Server-side: name → registry entry. */
|
|
291
339
|
private readonly registry = new Map<string, RegistryEntry>();
|
|
292
340
|
/** Server-side: name → instance cap-id (cached bootstrap result). */
|
|
293
341
|
private readonly rootInstances = new Map<string, number>();
|
|
294
342
|
/** Client-side: cap-ids that the server revoked via cap_revoked. */
|
|
295
343
|
private readonly revokedCapIds = new Set<number>();
|
|
296
|
-
private readonly closeHandlers = new Set<() => void>();
|
|
297
|
-
private readonly observers: {
|
|
344
|
+
private readonly closeHandlers = new Set<(info?: CloseInfo) => void>();
|
|
345
|
+
private readonly observers: {
|
|
346
|
+
[K in keyof ConnectionEvents]?: Set<(e: ConnectionEvents[K]) => void>;
|
|
347
|
+
} = {};
|
|
298
348
|
private nextCallId = 1;
|
|
299
349
|
private remoteHello: HelloFrame | null = null;
|
|
300
350
|
private readonly remoteReady: Promise<HelloFrame>;
|
|
@@ -344,6 +394,7 @@ class ConnectionImpl implements Connection {
|
|
|
344
394
|
this.remoteReady.catch(() => {});
|
|
345
395
|
|
|
346
396
|
this.transport.setReceive((frame) => this.handleFrame(frame));
|
|
397
|
+
this.transport.onClose?.((info) => this.shutdown(info?.reason || "transport_closed", info));
|
|
347
398
|
this.transport.send({
|
|
348
399
|
op: "hello",
|
|
349
400
|
v: PROTOCOL_VERSION,
|
|
@@ -358,26 +409,35 @@ class ConnectionImpl implements Connection {
|
|
|
358
409
|
return this.closed_;
|
|
359
410
|
}
|
|
360
411
|
|
|
361
|
-
onClose(handler: () => void): () => void {
|
|
412
|
+
onClose(handler: (info?: CloseInfo) => void): () => void {
|
|
362
413
|
this.closeHandlers.add(handler);
|
|
363
414
|
return () => this.closeHandlers.delete(handler);
|
|
364
415
|
}
|
|
365
416
|
|
|
366
|
-
on<K extends keyof ConnectionEvents>(
|
|
417
|
+
on<K extends keyof ConnectionEvents>(
|
|
418
|
+
event: K,
|
|
419
|
+
handler: (e: ConnectionEvents[K]) => void,
|
|
420
|
+
): () => void {
|
|
367
421
|
let set = this.observers[event] as Set<(e: ConnectionEvents[K]) => void> | undefined;
|
|
368
422
|
if (!set) {
|
|
369
423
|
set = new Set();
|
|
370
424
|
(this.observers as Record<string, Set<unknown>>)[event] = set as unknown as Set<unknown>;
|
|
371
425
|
}
|
|
372
426
|
set.add(handler);
|
|
373
|
-
return () => {
|
|
427
|
+
return () => {
|
|
428
|
+
set!.delete(handler);
|
|
429
|
+
};
|
|
374
430
|
}
|
|
375
431
|
|
|
376
432
|
private emitObs<K extends keyof ConnectionEvents>(event: K, data: ConnectionEvents[K]): void {
|
|
377
433
|
const set = this.observers[event] as Set<(e: ConnectionEvents[K]) => void> | undefined;
|
|
378
434
|
if (!set || set.size === 0) return;
|
|
379
435
|
for (const h of set) {
|
|
380
|
-
try {
|
|
436
|
+
try {
|
|
437
|
+
h(data);
|
|
438
|
+
} catch {
|
|
439
|
+
/* swallow */
|
|
440
|
+
}
|
|
381
441
|
}
|
|
382
442
|
}
|
|
383
443
|
|
|
@@ -393,7 +453,7 @@ class ConnectionImpl implements Connection {
|
|
|
393
453
|
|
|
394
454
|
// ---- serve / unserve / replace ----
|
|
395
455
|
|
|
396
|
-
serve<C extends
|
|
456
|
+
serve<C extends AnyCapDef>(cap: C, impl: ImplOf<C>, opts?: { ifExists?: IfExists }): ServeHandle {
|
|
397
457
|
this.assertNotFrameworkName(cap.name);
|
|
398
458
|
const ifExists = opts?.ifExists ?? "throw";
|
|
399
459
|
const existing = this.registry.get(cap.name);
|
|
@@ -425,7 +485,11 @@ class ConnectionImpl implements Connection {
|
|
|
425
485
|
return handle;
|
|
426
486
|
}
|
|
427
487
|
|
|
428
|
-
serveAll<R extends SchemaRoots>(
|
|
488
|
+
serveAll<R extends SchemaRoots>(
|
|
489
|
+
schema: Schema<R>,
|
|
490
|
+
impls: ImplsOf<R>,
|
|
491
|
+
opts?: { ifExists?: IfExists },
|
|
492
|
+
): ServeHandle {
|
|
429
493
|
const ifExists = opts?.ifExists ?? "throw";
|
|
430
494
|
// Pre-validate everything that can fail across all modes before mutating any state (atomicity).
|
|
431
495
|
for (const k of Object.keys(schema.roots)) {
|
|
@@ -461,7 +525,7 @@ class ConnectionImpl implements Connection {
|
|
|
461
525
|
return this.makeHandle(names);
|
|
462
526
|
}
|
|
463
527
|
|
|
464
|
-
unserve(target:
|
|
528
|
+
unserve(target: AnyCapDef | ServeHandle): void {
|
|
465
529
|
const names = isCapDef(target) ? [target.name] : Array.from((target as ServeHandle).names);
|
|
466
530
|
const revoked: number[] = [];
|
|
467
531
|
for (const name of names) {
|
|
@@ -481,7 +545,7 @@ class ConnectionImpl implements Connection {
|
|
|
481
545
|
}
|
|
482
546
|
}
|
|
483
547
|
|
|
484
|
-
replace<C extends
|
|
548
|
+
replace<C extends AnyCapDef>(cap: C, impl: ImplOf<C>): void {
|
|
485
549
|
const entry = this.registry.get(cap.name);
|
|
486
550
|
if (!entry) throw new IpcError({ code: "not_found", message: `cap "${cap.name}" not served` });
|
|
487
551
|
if (entry.version !== cap.version) {
|
|
@@ -496,7 +560,10 @@ class ConnectionImpl implements Connection {
|
|
|
496
560
|
const instId = this.rootInstances.get(cap.name);
|
|
497
561
|
if (instId !== undefined) {
|
|
498
562
|
const e = this.capTable.get(instId);
|
|
499
|
-
if (e) {
|
|
563
|
+
if (e) {
|
|
564
|
+
e.impl = impl;
|
|
565
|
+
e.cap = cap;
|
|
566
|
+
}
|
|
500
567
|
}
|
|
501
568
|
this.emitObs("revoke", { capIds: instId !== undefined ? [instId] : [], reason: "replace" });
|
|
502
569
|
}
|
|
@@ -522,27 +589,33 @@ class ConnectionImpl implements Connection {
|
|
|
522
589
|
return this.runtimeProxy;
|
|
523
590
|
}
|
|
524
591
|
|
|
525
|
-
bootstrap<C extends
|
|
592
|
+
bootstrap<C extends AnyCapDef>(cap: C): Promise<ClientOf<C>>;
|
|
526
593
|
bootstrap<R extends SchemaRoots>(schema: Schema<R>): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
|
|
527
|
-
async bootstrap(target:
|
|
594
|
+
async bootstrap(target: AnyCapDef | Schema<any>): Promise<unknown> {
|
|
528
595
|
if (isCapDef(target)) return this._bootstrapCap(target);
|
|
529
596
|
if (isSchema(target)) return this._bootstrapSchema(target);
|
|
530
|
-
throw new IpcError({
|
|
597
|
+
throw new IpcError({
|
|
598
|
+
code: "invalid_argument",
|
|
599
|
+
message: "bootstrap target must be CapDef or Schema",
|
|
600
|
+
});
|
|
531
601
|
}
|
|
532
602
|
|
|
533
|
-
private async _bootstrapCap<C extends
|
|
603
|
+
private async _bootstrapCap<C extends AnyCapDef>(cap: C): Promise<ClientOf<C>> {
|
|
534
604
|
await this.remoteReady;
|
|
535
605
|
const args: { name: string; version?: string } = { name: cap.name };
|
|
536
606
|
if (cap.version != null) args.version = cap.version;
|
|
537
607
|
const raw = await this.sendCallTyped(USER_ROOTS_CAP_ID, BOOTSTRAP_METHOD, args, undefined);
|
|
538
608
|
if (!(raw instanceof CapRef)) {
|
|
539
|
-
throw new IpcError({
|
|
609
|
+
throw new IpcError({
|
|
610
|
+
code: "invalid_argument",
|
|
611
|
+
message: "bootstrap did not return a CapRef",
|
|
612
|
+
});
|
|
540
613
|
}
|
|
541
614
|
return this.makeCapProxy(cap, raw.capId) as ClientOf<C>;
|
|
542
615
|
}
|
|
543
616
|
|
|
544
617
|
private async _bootstrapSchema<R extends SchemaRoots>(
|
|
545
|
-
schema: Schema<R
|
|
618
|
+
schema: Schema<R>,
|
|
546
619
|
): Promise<{ [K in keyof R]: ClientOf<R[K]> }> {
|
|
547
620
|
const keys = Object.keys(schema.roots) as (keyof R & string)[];
|
|
548
621
|
const settled = await Promise.allSettled(keys.map((k) => this._bootstrapCap(schema.roots[k])));
|
|
@@ -551,14 +624,20 @@ class ConnectionImpl implements Connection {
|
|
|
551
624
|
// Release server refCount on the roots that succeeded — otherwise their cap-table entries linger until connection close.
|
|
552
625
|
for (const r of settled) {
|
|
553
626
|
if (r.status === "fulfilled") {
|
|
554
|
-
try {
|
|
627
|
+
try {
|
|
628
|
+
this.releaseRef(r.value);
|
|
629
|
+
} catch {
|
|
630
|
+
/* swallow */
|
|
631
|
+
}
|
|
555
632
|
}
|
|
556
633
|
}
|
|
557
634
|
throw rejected.reason;
|
|
558
635
|
}
|
|
559
636
|
const out = {} as { [K in keyof R]: ClientOf<R[K]> };
|
|
560
637
|
for (let i = 0; i < keys.length; i++) {
|
|
561
|
-
(out as Record<string, unknown>)[keys[i]] = (
|
|
638
|
+
(out as Record<string, unknown>)[keys[i]] = (
|
|
639
|
+
settled[i] as PromiseFulfilledResult<unknown>
|
|
640
|
+
).value;
|
|
562
641
|
}
|
|
563
642
|
return out;
|
|
564
643
|
}
|
|
@@ -601,10 +680,19 @@ class ConnectionImpl implements Connection {
|
|
|
601
680
|
private handleUnknownFrame(frame: unknown): void {
|
|
602
681
|
const id = (frame as { id?: unknown })?.id;
|
|
603
682
|
if (typeof id === "number") {
|
|
604
|
-
this.transport.send({
|
|
683
|
+
this.transport.send({
|
|
684
|
+
op: "result",
|
|
685
|
+
id,
|
|
686
|
+
ok: false,
|
|
687
|
+
error: { code: "invalid_argument", message: "unknown opcode" },
|
|
688
|
+
});
|
|
605
689
|
return;
|
|
606
690
|
}
|
|
607
|
-
this.transport.send({
|
|
691
|
+
this.transport.send({
|
|
692
|
+
op: "goaway",
|
|
693
|
+
reason: "invalid_argument",
|
|
694
|
+
error: { code: "invalid_argument", message: "unknown opcode" },
|
|
695
|
+
});
|
|
608
696
|
this.shutdown("invalid_argument");
|
|
609
697
|
}
|
|
610
698
|
|
|
@@ -615,7 +703,7 @@ class ConnectionImpl implements Connection {
|
|
|
615
703
|
|
|
616
704
|
private handleGoaway(frame: Extract<Frame, { op: "goaway" }>): void {
|
|
617
705
|
this.rejectRemoteReady(
|
|
618
|
-
new IpcError(frame.error ?? { code: "unavailable", message: frame.reason ?? "peer goaway" })
|
|
706
|
+
new IpcError(frame.error ?? { code: "unavailable", message: frame.reason ?? "peer goaway" }),
|
|
619
707
|
);
|
|
620
708
|
this.shutdown(frame.reason ?? "remote goaway");
|
|
621
709
|
}
|
|
@@ -654,30 +742,58 @@ class ConnectionImpl implements Connection {
|
|
|
654
742
|
|
|
655
743
|
const entry = this.capTable.get(frame.target.id);
|
|
656
744
|
if (!entry) {
|
|
657
|
-
this.emitObs("call", {
|
|
745
|
+
this.emitObs("call", {
|
|
746
|
+
capId: frame.target.id,
|
|
747
|
+
method: frame.method,
|
|
748
|
+
callId: frame.id,
|
|
749
|
+
result: "not_found",
|
|
750
|
+
});
|
|
658
751
|
return this.sendError(frame.id, "not_found", `cap-id ${frame.target.id} not found`);
|
|
659
752
|
}
|
|
660
753
|
|
|
661
754
|
const cap = entry.cap;
|
|
662
755
|
if (!cap || !entry.impl) {
|
|
663
|
-
this.emitObs("call", {
|
|
756
|
+
this.emitObs("call", {
|
|
757
|
+
capId: frame.target.id,
|
|
758
|
+
method: frame.method,
|
|
759
|
+
callId: frame.id,
|
|
760
|
+
result: "not_found",
|
|
761
|
+
});
|
|
664
762
|
return this.sendError(frame.id, "not_found", "cap has no impl");
|
|
665
763
|
}
|
|
666
764
|
|
|
667
765
|
const methodDef = cap.methods[frame.method] as MethodDef | undefined;
|
|
668
766
|
if (!methodDef) {
|
|
669
|
-
this.emitObs("call", {
|
|
767
|
+
this.emitObs("call", {
|
|
768
|
+
capId: frame.target.id,
|
|
769
|
+
capName: cap.name,
|
|
770
|
+
method: frame.method,
|
|
771
|
+
callId: frame.id,
|
|
772
|
+
result: "not_found",
|
|
773
|
+
});
|
|
670
774
|
return this.sendError(frame.id, "not_found", `method "${frame.method}" on cap "${cap.name}"`);
|
|
671
775
|
}
|
|
672
776
|
const impl = (entry.impl as Record<string, unknown>)[frame.method];
|
|
673
777
|
if (typeof impl !== "function") {
|
|
674
|
-
this.emitObs("call", {
|
|
778
|
+
this.emitObs("call", {
|
|
779
|
+
capId: frame.target.id,
|
|
780
|
+
capName: cap.name,
|
|
781
|
+
method: frame.method,
|
|
782
|
+
callId: frame.id,
|
|
783
|
+
result: "not_found",
|
|
784
|
+
});
|
|
675
785
|
return this.sendError(frame.id, "not_found", `method "${frame.method}" has no handler`);
|
|
676
786
|
}
|
|
677
787
|
|
|
678
788
|
// Bound inbound work — symmetric with sendCallTyped's outbound check.
|
|
679
789
|
if (this.serverActiveCalls.size + this.serverStreams.size >= this.maxInFlightCalls) {
|
|
680
|
-
this.emitObs("call", {
|
|
790
|
+
this.emitObs("call", {
|
|
791
|
+
capId: frame.target.id,
|
|
792
|
+
capName: cap.name,
|
|
793
|
+
method: frame.method,
|
|
794
|
+
callId: frame.id,
|
|
795
|
+
result: "resource_exhausted",
|
|
796
|
+
});
|
|
681
797
|
return this.sendError(
|
|
682
798
|
frame.id,
|
|
683
799
|
"resource_exhausted",
|
|
@@ -686,14 +802,23 @@ class ConnectionImpl implements Connection {
|
|
|
686
802
|
);
|
|
687
803
|
}
|
|
688
804
|
|
|
689
|
-
await this.invokeServerMethod(
|
|
805
|
+
await this.invokeServerMethod(
|
|
806
|
+
frame,
|
|
807
|
+
cap,
|
|
808
|
+
methodDef,
|
|
809
|
+
impl as (params: unknown, ctx: CallCtx) => unknown,
|
|
810
|
+
);
|
|
690
811
|
}
|
|
691
812
|
|
|
692
813
|
private async handleBootstrap(frame: CallFrame): Promise<void> {
|
|
693
814
|
const args = (frame.args ?? {}) as { name?: unknown; version?: unknown };
|
|
694
815
|
const name = args.name;
|
|
695
816
|
if (typeof name !== "string") {
|
|
696
|
-
this.emitObs("bootstrap", {
|
|
817
|
+
this.emitObs("bootstrap", {
|
|
818
|
+
name: String(name),
|
|
819
|
+
attestation: this.attestation,
|
|
820
|
+
result: "invalid_argument",
|
|
821
|
+
});
|
|
697
822
|
return this.sendError(frame.id, "invalid_argument", "bootstrap requires {name: string}");
|
|
698
823
|
}
|
|
699
824
|
const clientVersion = args.version != null ? String(args.version) : undefined;
|
|
@@ -702,18 +827,28 @@ class ConnectionImpl implements Connection {
|
|
|
702
827
|
// not via bootstrap). For user-facing bootstrap, only the registry is consulted.
|
|
703
828
|
const entry = this.registry.get(name);
|
|
704
829
|
if (!entry) {
|
|
705
|
-
this.emitObs("bootstrap", {
|
|
830
|
+
this.emitObs("bootstrap", {
|
|
831
|
+
name,
|
|
832
|
+
version: clientVersion,
|
|
833
|
+
attestation: this.attestation,
|
|
834
|
+
result: "not_found",
|
|
835
|
+
});
|
|
706
836
|
return this.sendError(frame.id, "not_found", `cap "${name}" not served`);
|
|
707
837
|
}
|
|
708
838
|
|
|
709
839
|
const serverVersion = entry.version;
|
|
710
840
|
if (serverVersion != null && clientVersion != null && serverVersion !== clientVersion) {
|
|
711
|
-
this.emitObs("bootstrap", {
|
|
841
|
+
this.emitObs("bootstrap", {
|
|
842
|
+
name,
|
|
843
|
+
version: clientVersion,
|
|
844
|
+
attestation: this.attestation,
|
|
845
|
+
result: "version_mismatch",
|
|
846
|
+
});
|
|
712
847
|
return this.sendError(
|
|
713
848
|
frame.id,
|
|
714
849
|
"failed_precondition",
|
|
715
850
|
`version mismatch (server "${serverVersion}", client "${clientVersion}")`,
|
|
716
|
-
{ reason: "version_mismatch" as FailedPreconditionReason }
|
|
851
|
+
{ reason: "version_mismatch" as FailedPreconditionReason },
|
|
717
852
|
);
|
|
718
853
|
}
|
|
719
854
|
|
|
@@ -722,24 +857,42 @@ class ConnectionImpl implements Connection {
|
|
|
722
857
|
try {
|
|
723
858
|
allowed = await this.policy(name, this.attestation);
|
|
724
859
|
} catch (err) {
|
|
725
|
-
this.emitObs("error", {
|
|
726
|
-
|
|
860
|
+
this.emitObs("error", {
|
|
861
|
+
phase: "policy",
|
|
862
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
863
|
+
});
|
|
864
|
+
this.emitObs("bootstrap", {
|
|
865
|
+
name,
|
|
866
|
+
version: clientVersion,
|
|
867
|
+
attestation: this.attestation,
|
|
868
|
+
result: "internal",
|
|
869
|
+
});
|
|
727
870
|
return this.sendError(frame.id, "internal", "policy threw");
|
|
728
871
|
}
|
|
729
872
|
if (typeof allowed !== "boolean") {
|
|
730
873
|
// Non-boolean return = programming bug; surface explicitly (not silent deny).
|
|
731
|
-
this.emitObs("error", {
|
|
732
|
-
|
|
874
|
+
this.emitObs("error", {
|
|
875
|
+
phase: "policy",
|
|
876
|
+
error: new Error(`policy must return boolean (got ${typeof allowed})`),
|
|
877
|
+
});
|
|
878
|
+
this.emitObs("bootstrap", {
|
|
879
|
+
name,
|
|
880
|
+
version: clientVersion,
|
|
881
|
+
attestation: this.attestation,
|
|
882
|
+
result: "internal",
|
|
883
|
+
});
|
|
733
884
|
return this.sendError(frame.id, "internal", "policy returned non-boolean");
|
|
734
885
|
}
|
|
735
886
|
if (!allowed) {
|
|
736
|
-
this.emitObs("bootstrap", {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
"
|
|
741
|
-
|
|
742
|
-
|
|
887
|
+
this.emitObs("bootstrap", {
|
|
888
|
+
name,
|
|
889
|
+
version: clientVersion,
|
|
890
|
+
attestation: this.attestation,
|
|
891
|
+
result: "denied",
|
|
892
|
+
});
|
|
893
|
+
return this.sendError(frame.id, "failed_precondition", "policy denied", {
|
|
894
|
+
reason: "unauthorized" as FailedPreconditionReason,
|
|
895
|
+
});
|
|
743
896
|
}
|
|
744
897
|
}
|
|
745
898
|
|
|
@@ -766,21 +919,37 @@ class ConnectionImpl implements Connection {
|
|
|
766
919
|
} catch (err) {
|
|
767
920
|
if (err instanceof IpcError) {
|
|
768
921
|
const result = err.code === "resource_exhausted" ? "resource_exhausted" : "internal";
|
|
769
|
-
this.emitObs("bootstrap", {
|
|
770
|
-
|
|
922
|
+
this.emitObs("bootstrap", {
|
|
923
|
+
name,
|
|
924
|
+
version: clientVersion,
|
|
925
|
+
attestation: this.attestation,
|
|
926
|
+
result,
|
|
927
|
+
});
|
|
928
|
+
return this.sendError(
|
|
929
|
+
frame.id,
|
|
930
|
+
err.code,
|
|
931
|
+
err.message,
|
|
932
|
+
err.details as Record<string, unknown> | undefined,
|
|
933
|
+
);
|
|
771
934
|
}
|
|
772
935
|
throw err;
|
|
773
936
|
}
|
|
774
937
|
}
|
|
775
|
-
this.emitObs("bootstrap", {
|
|
938
|
+
this.emitObs("bootstrap", {
|
|
939
|
+
name,
|
|
940
|
+
version: clientVersion,
|
|
941
|
+
attestation: this.attestation,
|
|
942
|
+
result: "ok",
|
|
943
|
+
capId,
|
|
944
|
+
});
|
|
776
945
|
this.transport.send({ op: "result", id: frame.id, ok: true, value: new CapRef(capId) });
|
|
777
946
|
}
|
|
778
947
|
|
|
779
948
|
private async invokeServerMethod(
|
|
780
949
|
frame: CallFrame,
|
|
781
|
-
cap:
|
|
950
|
+
cap: AnyCapDef,
|
|
782
951
|
methodDef: MethodDef,
|
|
783
|
-
impl: (params: unknown, ctx: CallCtx) => unknown
|
|
952
|
+
impl: (params: unknown, ctx: CallCtx) => unknown,
|
|
784
953
|
): Promise<void> {
|
|
785
954
|
const ctx = this.makeCallCtx(frame);
|
|
786
955
|
|
|
@@ -789,13 +958,17 @@ class ConnectionImpl implements Connection {
|
|
|
789
958
|
try {
|
|
790
959
|
const runStream = () => impl(frame.args, ctx) as StreamType<unknown>;
|
|
791
960
|
const als = callContextStorage;
|
|
792
|
-
const scoped = als
|
|
793
|
-
? () => als.run({ callId: frame.id }, runStream)
|
|
794
|
-
: runStream;
|
|
961
|
+
const scoped = als ? () => als.run({ callId: frame.id }, runStream) : runStream;
|
|
795
962
|
const stream = scoped();
|
|
796
963
|
this.runServerStream(frame.id, stream, ctx, hintBudget, cap, frame.method, frame.target.id);
|
|
797
964
|
} catch (err) {
|
|
798
|
-
this.emitObs("stream", {
|
|
965
|
+
this.emitObs("stream", {
|
|
966
|
+
capId: frame.target.id,
|
|
967
|
+
capName: cap.name,
|
|
968
|
+
method: frame.method,
|
|
969
|
+
callId: frame.id,
|
|
970
|
+
event: "error",
|
|
971
|
+
});
|
|
799
972
|
this.transport.send({ op: "stream", id: frame.id, ev: "error", error: toStatus(err) });
|
|
800
973
|
}
|
|
801
974
|
return;
|
|
@@ -814,22 +987,34 @@ class ConnectionImpl implements Connection {
|
|
|
814
987
|
const encoder = makeReturnEncoder(methodDef.returns);
|
|
815
988
|
const runImpl = () => Promise.resolve(impl(frame.args, ctx));
|
|
816
989
|
const als = callContextStorage;
|
|
817
|
-
const scoped = als
|
|
818
|
-
? () => als.run({ callId: frame.id }, runImpl)
|
|
819
|
-
: runImpl;
|
|
990
|
+
const scoped = als ? () => als.run({ callId: frame.id }, runImpl) : runImpl;
|
|
820
991
|
try {
|
|
821
992
|
const result = await scoped();
|
|
822
993
|
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
823
994
|
if (!this.serverActiveCalls.delete(frame.id)) return;
|
|
824
995
|
const encoded = encoder(result);
|
|
825
996
|
this.transport.send({ op: "result", id: frame.id, ok: true, value: encoded });
|
|
826
|
-
this.emitObs("call", {
|
|
997
|
+
this.emitObs("call", {
|
|
998
|
+
capId: frame.target.id,
|
|
999
|
+
capName: cap.name,
|
|
1000
|
+
method: frame.method,
|
|
1001
|
+
callId: frame.id,
|
|
1002
|
+
durationMs: performance.now() - startedAt,
|
|
1003
|
+
result: "ok",
|
|
1004
|
+
});
|
|
827
1005
|
} catch (err) {
|
|
828
1006
|
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
829
1007
|
if (!this.serverActiveCalls.delete(frame.id)) return;
|
|
830
1008
|
const status = toStatus(err);
|
|
831
1009
|
this.sendErrorFromStatus(frame.id, status);
|
|
832
|
-
this.emitObs("call", {
|
|
1010
|
+
this.emitObs("call", {
|
|
1011
|
+
capId: frame.target.id,
|
|
1012
|
+
capName: cap.name,
|
|
1013
|
+
method: frame.method,
|
|
1014
|
+
callId: frame.id,
|
|
1015
|
+
durationMs: performance.now() - startedAt,
|
|
1016
|
+
result: status.code,
|
|
1017
|
+
});
|
|
833
1018
|
}
|
|
834
1019
|
return;
|
|
835
1020
|
}
|
|
@@ -837,7 +1022,10 @@ class ConnectionImpl implements Connection {
|
|
|
837
1022
|
this.sendError(frame.id, "not_found", "unknown method kind");
|
|
838
1023
|
}
|
|
839
1024
|
|
|
840
|
-
private armServerDeadline(
|
|
1025
|
+
private armServerDeadline(
|
|
1026
|
+
frame: CallFrame,
|
|
1027
|
+
ctx: CallCtx,
|
|
1028
|
+
): ReturnType<typeof setTimeout> | undefined {
|
|
841
1029
|
const ms = frame.meta?.deadlineMs;
|
|
842
1030
|
if (!ms) return;
|
|
843
1031
|
return setTimeout(() => {
|
|
@@ -859,7 +1047,7 @@ class ConnectionImpl implements Connection {
|
|
|
859
1047
|
return ctx;
|
|
860
1048
|
}
|
|
861
1049
|
|
|
862
|
-
private exportCap<C extends
|
|
1050
|
+
private exportCap<C extends AnyCapDef>(capDef: C, impl: unknown): ExportedCap<C> {
|
|
863
1051
|
const typeId = frameworkTypeIdOf(capDef) ?? FIRST_USER_TYPE_ID;
|
|
864
1052
|
const entry = this.capTable.allocate({
|
|
865
1053
|
typeId,
|
|
@@ -880,7 +1068,7 @@ class ConnectionImpl implements Connection {
|
|
|
880
1068
|
stream: StreamType<unknown>,
|
|
881
1069
|
ctx: CallCtx,
|
|
882
1070
|
initialCredit: number,
|
|
883
|
-
cap:
|
|
1071
|
+
cap: AnyCapDef,
|
|
884
1072
|
method: string,
|
|
885
1073
|
capId: number,
|
|
886
1074
|
): void {
|
|
@@ -903,7 +1091,9 @@ class ConnectionImpl implements Connection {
|
|
|
903
1091
|
|
|
904
1092
|
const waitForCredit = (): Promise<void> => {
|
|
905
1093
|
if (slot.credit > 0 || slot.cancelled) return Promise.resolve();
|
|
906
|
-
return new Promise<void>((resolve) => {
|
|
1094
|
+
return new Promise<void>((resolve) => {
|
|
1095
|
+
slot.creditWaker = resolve;
|
|
1096
|
+
});
|
|
907
1097
|
};
|
|
908
1098
|
|
|
909
1099
|
const drain = async () => {
|
|
@@ -922,11 +1112,25 @@ class ConnectionImpl implements Connection {
|
|
|
922
1112
|
} catch (err) {
|
|
923
1113
|
errored = true;
|
|
924
1114
|
this.transport.send({ op: "stream", id: callId, ev: "error", error: toStatus(err) });
|
|
925
|
-
this.emitObs("stream", {
|
|
1115
|
+
this.emitObs("stream", {
|
|
1116
|
+
capId,
|
|
1117
|
+
capName: cap.name,
|
|
1118
|
+
method,
|
|
1119
|
+
callId,
|
|
1120
|
+
event: "error",
|
|
1121
|
+
count: slot.count,
|
|
1122
|
+
});
|
|
926
1123
|
} finally {
|
|
927
1124
|
if (!errored) {
|
|
928
1125
|
this.transport.send({ op: "stream", id: callId, ev: "end" });
|
|
929
|
-
this.emitObs("stream", {
|
|
1126
|
+
this.emitObs("stream", {
|
|
1127
|
+
capId,
|
|
1128
|
+
capName: cap.name,
|
|
1129
|
+
method,
|
|
1130
|
+
callId,
|
|
1131
|
+
event: slot.cancelled ? "cancel" : "end",
|
|
1132
|
+
count: slot.count,
|
|
1133
|
+
});
|
|
930
1134
|
}
|
|
931
1135
|
this.serverStreams.delete(callId);
|
|
932
1136
|
}
|
|
@@ -996,7 +1200,9 @@ class ConnectionImpl implements Connection {
|
|
|
996
1200
|
|
|
997
1201
|
releaseRef(proxy: unknown): void {
|
|
998
1202
|
if (typeof proxy !== "object" || proxy === null) return;
|
|
999
|
-
const meta = (proxy as Record<symbol, unknown>)[CAP_PROXY_META] as
|
|
1203
|
+
const meta = (proxy as Record<symbol, unknown>)[CAP_PROXY_META] as
|
|
1204
|
+
| { capId: number; dropped: boolean }
|
|
1205
|
+
| undefined;
|
|
1000
1206
|
if (!meta || meta.dropped) return;
|
|
1001
1207
|
meta.dropped = true;
|
|
1002
1208
|
this.sendDrop(meta.capId);
|
|
@@ -1040,7 +1246,10 @@ class ConnectionImpl implements Connection {
|
|
|
1040
1246
|
const released = this.capTable.release(id, delta);
|
|
1041
1247
|
if (released) {
|
|
1042
1248
|
for (const [rootName, capId] of this.rootInstances) {
|
|
1043
|
-
if (capId === id) {
|
|
1249
|
+
if (capId === id) {
|
|
1250
|
+
this.rootInstances.delete(rootName);
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1044
1253
|
}
|
|
1045
1254
|
}
|
|
1046
1255
|
}
|
|
@@ -1068,28 +1277,38 @@ class ConnectionImpl implements Connection {
|
|
|
1068
1277
|
meta?: CallMeta,
|
|
1069
1278
|
capName?: string,
|
|
1070
1279
|
): Promise<unknown> {
|
|
1071
|
-
if (this.closed_)
|
|
1280
|
+
if (this.closed_)
|
|
1281
|
+
return Promise.reject(new IpcError({ code: "unavailable", message: "connection closed" }));
|
|
1072
1282
|
if (this.revokedCapIds.has(capId)) {
|
|
1073
|
-
return Promise.reject(
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1283
|
+
return Promise.reject(
|
|
1284
|
+
new IpcError({
|
|
1285
|
+
code: "failed_precondition",
|
|
1286
|
+
message: "cap revoked",
|
|
1287
|
+
details: { reason: "revoked" as FailedPreconditionReason },
|
|
1288
|
+
}),
|
|
1289
|
+
);
|
|
1078
1290
|
}
|
|
1079
1291
|
if (this.pending.size >= this.maxInFlightCalls) {
|
|
1080
|
-
return Promise.reject(
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1292
|
+
return Promise.reject(
|
|
1293
|
+
new IpcError({
|
|
1294
|
+
code: "resource_exhausted",
|
|
1295
|
+
message: `in-flight calls limit ${this.maxInFlightCalls}`,
|
|
1296
|
+
details: { reason: "max_concurrent_calls" as ResourceExhaustedReason },
|
|
1297
|
+
}),
|
|
1298
|
+
);
|
|
1085
1299
|
}
|
|
1086
1300
|
const id = this.nextId();
|
|
1087
1301
|
return new Promise((resolve, reject) => {
|
|
1088
1302
|
const abort = new AbortController();
|
|
1089
1303
|
const pending: PendingCall = {
|
|
1090
|
-
resolve,
|
|
1304
|
+
resolve,
|
|
1305
|
+
reject,
|
|
1306
|
+
abort,
|
|
1307
|
+
decodeReturn,
|
|
1091
1308
|
startedAt: performance.now(),
|
|
1092
|
-
capId,
|
|
1309
|
+
capId,
|
|
1310
|
+
method,
|
|
1311
|
+
capName,
|
|
1093
1312
|
kind: "call",
|
|
1094
1313
|
};
|
|
1095
1314
|
if (meta?.deadlineMs) {
|
|
@@ -1102,14 +1321,21 @@ class ConnectionImpl implements Connection {
|
|
|
1102
1321
|
}
|
|
1103
1322
|
this.pending.set(id, pending);
|
|
1104
1323
|
let finalMeta = meta;
|
|
1105
|
-
if (
|
|
1324
|
+
if (finalMeta?.parentCallId === undefined && callContextStorage) {
|
|
1106
1325
|
const scope = callContextStorage.getStore();
|
|
1107
1326
|
if (scope) finalMeta = { ...(finalMeta ?? {}), parentCallId: scope.callId };
|
|
1108
1327
|
}
|
|
1109
1328
|
if (finalMeta?.parentCallId !== undefined) {
|
|
1110
1329
|
this.serverCallChildren.set(id, { parentId: finalMeta.parentCallId });
|
|
1111
1330
|
}
|
|
1112
|
-
this.transport.send({
|
|
1331
|
+
this.transport.send({
|
|
1332
|
+
op: "call",
|
|
1333
|
+
id,
|
|
1334
|
+
target: { kind: "cap", id: capId },
|
|
1335
|
+
method,
|
|
1336
|
+
args,
|
|
1337
|
+
meta: finalMeta,
|
|
1338
|
+
});
|
|
1113
1339
|
});
|
|
1114
1340
|
}
|
|
1115
1341
|
|
|
@@ -1121,17 +1347,29 @@ class ConnectionImpl implements Connection {
|
|
|
1121
1347
|
capName?: string,
|
|
1122
1348
|
): StreamType<T> {
|
|
1123
1349
|
if (this.closed_) {
|
|
1124
|
-
const empty = makeClientStream<T>(
|
|
1350
|
+
const empty = makeClientStream<T>(
|
|
1351
|
+
capId,
|
|
1352
|
+
0,
|
|
1353
|
+
() => {},
|
|
1354
|
+
() => {},
|
|
1355
|
+
);
|
|
1125
1356
|
empty.fail(new IpcError({ code: "unavailable", message: "connection closed" }));
|
|
1126
1357
|
return empty.stream;
|
|
1127
1358
|
}
|
|
1128
1359
|
if (this.revokedCapIds.has(capId)) {
|
|
1129
|
-
const empty = makeClientStream<T>(
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1360
|
+
const empty = makeClientStream<T>(
|
|
1361
|
+
capId,
|
|
1362
|
+
0,
|
|
1363
|
+
() => {},
|
|
1364
|
+
() => {},
|
|
1365
|
+
);
|
|
1366
|
+
empty.fail(
|
|
1367
|
+
new IpcError({
|
|
1368
|
+
code: "failed_precondition",
|
|
1369
|
+
message: "cap revoked",
|
|
1370
|
+
details: { reason: "revoked" as FailedPreconditionReason },
|
|
1371
|
+
}),
|
|
1372
|
+
);
|
|
1135
1373
|
return empty.stream;
|
|
1136
1374
|
}
|
|
1137
1375
|
const id = this.nextId();
|
|
@@ -1151,7 +1389,7 @@ class ConnectionImpl implements Connection {
|
|
|
1151
1389
|
return ctx.stream;
|
|
1152
1390
|
}
|
|
1153
1391
|
|
|
1154
|
-
makeCapProxy<C extends
|
|
1392
|
+
makeCapProxy<C extends AnyCapDef>(capDef: C, capId: number): ClientOf<C> {
|
|
1155
1393
|
const proxy: Record<string | symbol, unknown> = {};
|
|
1156
1394
|
const meta = { capId, dropped: false };
|
|
1157
1395
|
proxy[CAP_PROXY_META] = meta;
|
|
@@ -1164,26 +1402,33 @@ class ConnectionImpl implements Connection {
|
|
|
1164
1402
|
for (const name of Object.keys(capDef.methods)) {
|
|
1165
1403
|
const def = capDef.methods[name] as MethodDef;
|
|
1166
1404
|
if (isStreamDef(def)) {
|
|
1167
|
-
proxy[name] = (params?: unknown) =>
|
|
1405
|
+
proxy[name] = (params?: unknown) =>
|
|
1406
|
+
this.openClientStream(capId, name, params, undefined, capDef.name);
|
|
1168
1407
|
} else if (isCallDef(def)) {
|
|
1169
1408
|
const decoder = makeReturnDecoder(def.returns, (cd, cid) => this.makeCapProxy(cd, cid));
|
|
1170
|
-
proxy[name] = (params?: unknown) =>
|
|
1409
|
+
proxy[name] = (params?: unknown) =>
|
|
1410
|
+
this.sendCallTyped(capId, name, params, decoder, undefined, capDef.name);
|
|
1171
1411
|
}
|
|
1172
1412
|
}
|
|
1173
1413
|
|
|
1174
1414
|
const disposal = capDef.disposal as DisposalSpec | undefined;
|
|
1175
1415
|
if (disposal) {
|
|
1176
|
-
const conn = this;
|
|
1177
1416
|
proxy[Symbol.dispose] = () => {
|
|
1178
1417
|
try {
|
|
1179
1418
|
const r = (proxy[disposal.method] as (() => unknown) | undefined)?.();
|
|
1180
1419
|
if (r && typeof (r as Promise<unknown>).then === "function") {
|
|
1181
1420
|
(r as Promise<unknown>).catch((err) =>
|
|
1182
|
-
|
|
1421
|
+
this.emitObs("error", {
|
|
1422
|
+
phase: "dispose",
|
|
1423
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1424
|
+
}),
|
|
1183
1425
|
);
|
|
1184
1426
|
}
|
|
1185
1427
|
} catch (err) {
|
|
1186
|
-
|
|
1428
|
+
this.emitObs("error", {
|
|
1429
|
+
phase: "dispose",
|
|
1430
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1431
|
+
});
|
|
1187
1432
|
}
|
|
1188
1433
|
drop();
|
|
1189
1434
|
};
|
|
@@ -1201,10 +1446,14 @@ class ConnectionImpl implements Connection {
|
|
|
1201
1446
|
}
|
|
1202
1447
|
|
|
1203
1448
|
_dropFromFinalizer(capId: number): void {
|
|
1204
|
-
try {
|
|
1449
|
+
try {
|
|
1450
|
+
this.sendDrop(capId);
|
|
1451
|
+
} catch {
|
|
1452
|
+
/* swallow */
|
|
1453
|
+
}
|
|
1205
1454
|
}
|
|
1206
1455
|
|
|
1207
|
-
shutdown(reason: string = "shutdown"): void {
|
|
1456
|
+
shutdown(reason: string = "shutdown", info?: CloseInfo): void {
|
|
1208
1457
|
if (this.closed_) return;
|
|
1209
1458
|
this.closed_ = true;
|
|
1210
1459
|
for (const pending of this.pending.values()) {
|
|
@@ -1234,10 +1483,18 @@ class ConnectionImpl implements Connection {
|
|
|
1234
1483
|
this.rejectRemoteReady(new IpcError({ code: "unavailable", message: reason }));
|
|
1235
1484
|
}
|
|
1236
1485
|
for (const fn of this.closeHandlers) {
|
|
1237
|
-
try {
|
|
1486
|
+
try {
|
|
1487
|
+
fn(info);
|
|
1488
|
+
} catch {
|
|
1489
|
+
/* swallow */
|
|
1490
|
+
}
|
|
1238
1491
|
}
|
|
1239
1492
|
this.closeHandlers.clear();
|
|
1240
|
-
try {
|
|
1493
|
+
try {
|
|
1494
|
+
this.transport.close();
|
|
1495
|
+
} catch {
|
|
1496
|
+
/* swallow */
|
|
1497
|
+
}
|
|
1241
1498
|
}
|
|
1242
1499
|
|
|
1243
1500
|
private invokeServerDisposal(entry: CapTableEntry): void {
|
|
@@ -1248,13 +1505,17 @@ class ConnectionImpl implements Connection {
|
|
|
1248
1505
|
const impl = entry.impl as Record<string, unknown> | null;
|
|
1249
1506
|
const fn = impl?.[disposal.method] as ((p: unknown, c?: unknown) => unknown) | undefined;
|
|
1250
1507
|
if (!fn) return;
|
|
1251
|
-
try {
|
|
1508
|
+
try {
|
|
1509
|
+
void fn.call(impl, undefined, undefined);
|
|
1510
|
+
} catch {
|
|
1511
|
+
/* swallow */
|
|
1512
|
+
}
|
|
1252
1513
|
}
|
|
1253
1514
|
}
|
|
1254
1515
|
|
|
1255
1516
|
function makeReturnEncoder(returns: AnyCapToken | undefined): (v: unknown) => unknown {
|
|
1256
1517
|
if (!returns) return (v) => v;
|
|
1257
|
-
const expectExportedCap = (v: unknown, expectedCap:
|
|
1518
|
+
const expectExportedCap = (v: unknown, expectedCap: AnyCapDef): CapRef => {
|
|
1258
1519
|
if (!isExportedCap(v)) {
|
|
1259
1520
|
throw new IpcError({
|
|
1260
1521
|
code: "failed_precondition",
|
|
@@ -1275,7 +1536,10 @@ function makeReturnEncoder(returns: AnyCapToken | undefined): (v: unknown) => un
|
|
|
1275
1536
|
if (isCapArray(returns)) {
|
|
1276
1537
|
return (v) => {
|
|
1277
1538
|
if (!Array.isArray(v)) {
|
|
1278
|
-
throw new IpcError({
|
|
1539
|
+
throw new IpcError({
|
|
1540
|
+
code: "invalid_argument",
|
|
1541
|
+
message: "expected ExportedCap[] for cap.array method",
|
|
1542
|
+
});
|
|
1279
1543
|
}
|
|
1280
1544
|
return v.map((item) => expectExportedCap(item, returns.cap));
|
|
1281
1545
|
};
|
|
@@ -1283,7 +1547,10 @@ function makeReturnEncoder(returns: AnyCapToken | undefined): (v: unknown) => un
|
|
|
1283
1547
|
if (isCapRecord(returns)) {
|
|
1284
1548
|
return (v) => {
|
|
1285
1549
|
if (!v || typeof v !== "object") {
|
|
1286
|
-
throw new IpcError({
|
|
1550
|
+
throw new IpcError({
|
|
1551
|
+
code: "invalid_argument",
|
|
1552
|
+
message: "expected Record<string, ExportedCap> for cap.record method",
|
|
1553
|
+
});
|
|
1287
1554
|
}
|
|
1288
1555
|
const out: Record<string, CapRef> = {};
|
|
1289
1556
|
for (const k of Object.keys(v as object)) {
|
|
@@ -1296,14 +1563,15 @@ function makeReturnEncoder(returns: AnyCapToken | undefined): (v: unknown) => un
|
|
|
1296
1563
|
}
|
|
1297
1564
|
|
|
1298
1565
|
function makeReturnDecoder(
|
|
1299
|
-
returns: { cap?:
|
|
1300
|
-
spawn: (cap:
|
|
1566
|
+
returns: { cap?: AnyCapDef } | undefined,
|
|
1567
|
+
spawn: (cap: AnyCapDef, capId: number) => unknown,
|
|
1301
1568
|
): ReturnDecoder | undefined {
|
|
1302
1569
|
if (!returns) return undefined;
|
|
1303
1570
|
if (isCapRef(returns)) {
|
|
1304
1571
|
const inner = returns.cap;
|
|
1305
1572
|
return (raw) => {
|
|
1306
|
-
if (!(raw instanceof CapRef))
|
|
1573
|
+
if (!(raw instanceof CapRef))
|
|
1574
|
+
throw new IpcError({ code: "invalid_argument", message: "expected CapRef" });
|
|
1307
1575
|
return spawn(inner, raw.capId);
|
|
1308
1576
|
};
|
|
1309
1577
|
}
|
|
@@ -1311,9 +1579,11 @@ function makeReturnDecoder(
|
|
|
1311
1579
|
const inner = returns.cap;
|
|
1312
1580
|
const disposal = inner.disposal as DisposalSpec | undefined;
|
|
1313
1581
|
return (raw) => {
|
|
1314
|
-
if (!Array.isArray(raw))
|
|
1582
|
+
if (!Array.isArray(raw))
|
|
1583
|
+
throw new IpcError({ code: "invalid_argument", message: "expected array" });
|
|
1315
1584
|
const proxies = raw.map((item) => {
|
|
1316
|
-
if (!(item instanceof CapRef))
|
|
1585
|
+
if (!(item instanceof CapRef))
|
|
1586
|
+
throw new IpcError({ code: "invalid_argument", message: "expected CapRef in array" });
|
|
1317
1587
|
return spawn(inner, item.capId);
|
|
1318
1588
|
});
|
|
1319
1589
|
attachArrayDisposal(proxies, disposal);
|
|
@@ -1323,11 +1593,13 @@ function makeReturnDecoder(
|
|
|
1323
1593
|
if (isCapRecord(returns)) {
|
|
1324
1594
|
const inner = returns.cap;
|
|
1325
1595
|
return (raw) => {
|
|
1326
|
-
if (!raw || typeof raw !== "object")
|
|
1596
|
+
if (!raw || typeof raw !== "object")
|
|
1597
|
+
throw new IpcError({ code: "invalid_argument", message: "expected record" });
|
|
1327
1598
|
const out: Record<string, unknown> = {};
|
|
1328
1599
|
for (const k of Object.keys(raw as object)) {
|
|
1329
1600
|
const item = (raw as Record<string, unknown>)[k];
|
|
1330
|
-
if (!(item instanceof CapRef))
|
|
1601
|
+
if (!(item instanceof CapRef))
|
|
1602
|
+
throw new IpcError({ code: "invalid_argument", message: "expected CapRef in record" });
|
|
1331
1603
|
out[k] = spawn(inner, item.capId);
|
|
1332
1604
|
}
|
|
1333
1605
|
return out;
|
|
@@ -1364,7 +1636,7 @@ function makeClientStream<T>(
|
|
|
1364
1636
|
capId: number,
|
|
1365
1637
|
streamId: number,
|
|
1366
1638
|
cancel: () => void,
|
|
1367
|
-
sendCredit: (messages: number) => void
|
|
1639
|
+
sendCredit: (messages: number) => void,
|
|
1368
1640
|
): ClientStreamHandle<T> {
|
|
1369
1641
|
const buffer: T[] = [];
|
|
1370
1642
|
const waiters: Array<{ resolve(r: IteratorResult<T>): void; reject(e: Error): void }> = [];
|
|
@@ -1403,8 +1675,14 @@ function makeClientStream<T>(
|
|
|
1403
1675
|
while (waiters.length > 0) {
|
|
1404
1676
|
const w = waiters.shift()!;
|
|
1405
1677
|
const r = pump();
|
|
1406
|
-
if (r) {
|
|
1407
|
-
|
|
1678
|
+
if (r) {
|
|
1679
|
+
w.resolve(r);
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
if (failure) {
|
|
1683
|
+
w.reject(failure);
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1408
1686
|
waiters.unshift(w);
|
|
1409
1687
|
break;
|
|
1410
1688
|
}
|