@push.rocks/smartrust 1.7.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.rustbridge.d.ts +4 -0
- package/dist_ts/classes.rustbridge.js +250 -89
- package/dist_ts/classes.rustbridgerequesterror.d.ts +3 -3
- package/dist_ts/classes.rustbridgerequesterror.js +3 -3
- package/dist_ts/classes.stdiotransport.d.ts +1 -0
- package/dist_ts/classes.stdiotransport.js +8 -2
- package/dist_ts/interfaces/config.d.ts +13 -0
- package/package.json +1 -1
- package/readme.md +57 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.rustbridge.ts +337 -108
- package/ts/classes.rustbridgerequesterror.ts +3 -2
- package/ts/classes.stdiotransport.ts +8 -1
- package/ts/interfaces/config.ts +13 -0
package/ts/classes.rustbridge.ts
CHANGED
|
@@ -32,6 +32,12 @@ interface IPendingRequest {
|
|
|
32
32
|
abortSignal?: AbortSignal;
|
|
33
33
|
abortHandler?: () => void;
|
|
34
34
|
streaming?: StreamingResponse<any, any>;
|
|
35
|
+
admissionReservation?: IRequestAdmissionReservation;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface IRequestAdmissionReservation {
|
|
39
|
+
method: string;
|
|
40
|
+
released: boolean;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
type TPendingRequestSettlement =
|
|
@@ -57,6 +63,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
57
63
|
private logger: IRustBridgeLogger;
|
|
58
64
|
private transport: TConfirmableRustTransport | null = null;
|
|
59
65
|
private pendingRequests = new Map<string, IPendingRequest>();
|
|
66
|
+
private pendingRequestReservationsByMethod = new Map<string, number>();
|
|
60
67
|
private requestCounter = 0;
|
|
61
68
|
private isRunning = false;
|
|
62
69
|
private binaryPath: string | null = null;
|
|
@@ -67,6 +74,24 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
67
74
|
|
|
68
75
|
constructor(options: IRustBridgeOptions) {
|
|
69
76
|
super();
|
|
77
|
+
if (options.inheritedFileDescriptors) {
|
|
78
|
+
for (const descriptor of options.inheritedFileDescriptors) {
|
|
79
|
+
if (!Number.isSafeInteger(descriptor) || descriptor < 0) {
|
|
80
|
+
throw new RangeError(
|
|
81
|
+
'RustBridge inheritedFileDescriptors values must be non-negative safe integers',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (options.maxPendingRequestsByMethod) {
|
|
87
|
+
for (const [method, limit] of Object.entries(options.maxPendingRequestsByMethod)) {
|
|
88
|
+
if (!method || !Number.isSafeInteger(limit) || limit <= 0) {
|
|
89
|
+
throw new RangeError(
|
|
90
|
+
'RustBridge maxPendingRequestsByMethod values must be positive safe integers',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
70
95
|
this.logger = options.logger || defaultLogger;
|
|
71
96
|
this.options = {
|
|
72
97
|
cliArgs: ['--management'],
|
|
@@ -75,6 +100,12 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
75
100
|
readyEventName: 'ready',
|
|
76
101
|
maxPayloadSize: 50 * 1024 * 1024,
|
|
77
102
|
...options,
|
|
103
|
+
inheritedFileDescriptors: options.inheritedFileDescriptors
|
|
104
|
+
? [...options.inheritedFileDescriptors]
|
|
105
|
+
: undefined,
|
|
106
|
+
maxPendingRequestsByMethod: options.maxPendingRequestsByMethod
|
|
107
|
+
? { ...options.maxPendingRequestsByMethod }
|
|
108
|
+
: undefined,
|
|
78
109
|
};
|
|
79
110
|
this.locator = new RustBinaryLocator(options, this.logger);
|
|
80
111
|
}
|
|
@@ -98,6 +129,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
98
129
|
binaryPath,
|
|
99
130
|
cliArgs: this.options.cliArgs,
|
|
100
131
|
env: this.options.env,
|
|
132
|
+
inheritedFileDescriptors: this.options.inheritedFileDescriptors,
|
|
101
133
|
maxPayloadSize: this.options.maxPayloadSize,
|
|
102
134
|
logger: this.logger,
|
|
103
135
|
});
|
|
@@ -112,6 +144,11 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
112
144
|
* @param socketOptions - Optional socket connection options (reconnect, etc.)
|
|
113
145
|
*/
|
|
114
146
|
public connect(socketPath: string, socketOptions?: ISocketConnectOptions): Promise<boolean> {
|
|
147
|
+
if (this.options.inheritedFileDescriptors?.length) {
|
|
148
|
+
return Promise.reject(
|
|
149
|
+
new Error('RustBridge inheritedFileDescriptors are available only with spawn()'),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
115
152
|
return this.activate(async () => new SocketTransport({
|
|
116
153
|
socketPath,
|
|
117
154
|
maxPayloadSize: this.options.maxPayloadSize,
|
|
@@ -396,95 +433,137 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
396
433
|
);
|
|
397
434
|
}
|
|
398
435
|
|
|
399
|
-
|
|
436
|
+
const transport = this.transport;
|
|
437
|
+
if (!transport?.connected || !this.isRunning) {
|
|
400
438
|
throw new Error(`${this.options.binaryName} bridge is not running`);
|
|
401
439
|
}
|
|
402
440
|
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
const json = JSON.stringify(request);
|
|
407
|
-
|
|
408
|
-
// Check outbound payload size
|
|
409
|
-
const byteLength = Buffer.byteLength(json, 'utf8');
|
|
410
|
-
if (byteLength > this.options.maxPayloadSize) {
|
|
411
|
-
throw new Error(
|
|
412
|
-
`Outbound message exceeds maxPayloadSize (${byteLength} > ${this.options.maxPayloadSize})`
|
|
413
|
-
);
|
|
414
|
-
}
|
|
441
|
+
const lifecycleGeneration = this.lifecycleGeneration;
|
|
442
|
+
const admissionReservation = this.reservePendingRequest(method);
|
|
443
|
+
let reservationTransferred = false;
|
|
415
444
|
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
445
|
+
try {
|
|
446
|
+
const id = `req_${++this.requestCounter}`;
|
|
447
|
+
const request: IManagementRequest = { id, method, params };
|
|
448
|
+
const json = JSON.stringify(request);
|
|
449
|
+
|
|
450
|
+
const byteLength = Buffer.byteLength(json, 'utf8');
|
|
451
|
+
if (byteLength > this.options.maxPayloadSize) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
`Outbound message exceeds maxPayloadSize (${byteLength} > ${this.options.maxPayloadSize})`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const availabilityError = this.getRequestAvailabilityError(
|
|
421
458
|
method,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
459
|
+
options?.signal,
|
|
460
|
+
transport,
|
|
461
|
+
lifecycleGeneration,
|
|
462
|
+
);
|
|
463
|
+
if (availabilityError) {
|
|
464
|
+
throw availabilityError;
|
|
465
|
+
}
|
|
425
466
|
|
|
426
|
-
|
|
427
|
-
pending
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
requestId: id,
|
|
435
|
-
method,
|
|
436
|
-
cause: options.signal?.reason,
|
|
437
|
-
},
|
|
438
|
-
),
|
|
439
|
-
});
|
|
467
|
+
return new Promise<TCommands[K]['result']>((resolve, reject) => {
|
|
468
|
+
const pending: IPendingRequest = {
|
|
469
|
+
resolve,
|
|
470
|
+
reject,
|
|
471
|
+
timer: null,
|
|
472
|
+
method,
|
|
473
|
+
abortSignal: options?.signal,
|
|
474
|
+
admissionReservation,
|
|
440
475
|
};
|
|
441
|
-
|
|
442
|
-
|
|
476
|
+
this.pendingRequests.set(id, pending);
|
|
477
|
+
reservationTransferred = true;
|
|
443
478
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
479
|
+
try {
|
|
480
|
+
if (options?.signal) {
|
|
481
|
+
pending.abortHandler = () => {
|
|
482
|
+
this.settlePendingRequest(id, {
|
|
483
|
+
outcome: 'reject',
|
|
484
|
+
error: new RustBridgeRequestError(
|
|
485
|
+
`Command '${method}' was aborted`,
|
|
486
|
+
{
|
|
487
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
|
|
488
|
+
requestId: id,
|
|
489
|
+
method,
|
|
490
|
+
cause: options.signal?.reason,
|
|
491
|
+
},
|
|
492
|
+
),
|
|
493
|
+
});
|
|
494
|
+
};
|
|
495
|
+
options.signal.addEventListener('abort', pending.abortHandler, { once: true });
|
|
496
|
+
if (options.signal.aborted) {
|
|
497
|
+
pending.abortHandler();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
457
501
|
|
|
458
|
-
|
|
459
|
-
|
|
502
|
+
pending.timer = setTimeout(() => {
|
|
503
|
+
this.settlePendingRequest(id, {
|
|
504
|
+
outcome: 'reject',
|
|
505
|
+
error: new RustBridgeRequestError(
|
|
506
|
+
`Command '${method}' timed out after ${timeoutMs}ms`,
|
|
507
|
+
{
|
|
508
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT',
|
|
509
|
+
requestId: id,
|
|
510
|
+
method,
|
|
511
|
+
},
|
|
512
|
+
),
|
|
513
|
+
});
|
|
514
|
+
}, timeoutMs);
|
|
515
|
+
|
|
516
|
+
const currentAvailabilityError = this.getRequestAvailabilityError(
|
|
517
|
+
method,
|
|
518
|
+
options?.signal,
|
|
519
|
+
transport,
|
|
520
|
+
lifecycleGeneration,
|
|
521
|
+
id,
|
|
522
|
+
);
|
|
523
|
+
if (currentAvailabilityError) {
|
|
524
|
+
this.settlePendingRequest(id, {
|
|
525
|
+
outcome: 'reject',
|
|
526
|
+
error: currentAvailabilityError,
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
void transport.write(json + '\n').catch((err: unknown) => {
|
|
532
|
+
this.settlePendingRequest(id, {
|
|
533
|
+
outcome: 'reject',
|
|
534
|
+
error: new RustBridgeRequestError(
|
|
535
|
+
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
536
|
+
{
|
|
537
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
538
|
+
requestId: id,
|
|
539
|
+
method,
|
|
540
|
+
cause: err,
|
|
541
|
+
},
|
|
542
|
+
),
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
} catch (err: unknown) {
|
|
460
546
|
this.settlePendingRequest(id, {
|
|
461
547
|
outcome: 'reject',
|
|
462
|
-
error:
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
548
|
+
error: err instanceof RustBridgeRequestError
|
|
549
|
+
? err
|
|
550
|
+
: new RustBridgeRequestError(
|
|
551
|
+
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
552
|
+
{
|
|
553
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
554
|
+
requestId: id,
|
|
555
|
+
method,
|
|
556
|
+
cause: err,
|
|
557
|
+
},
|
|
558
|
+
),
|
|
471
559
|
});
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
478
|
-
{
|
|
479
|
-
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
480
|
-
requestId: id,
|
|
481
|
-
method,
|
|
482
|
-
cause: err,
|
|
483
|
-
},
|
|
484
|
-
),
|
|
485
|
-
});
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
} finally {
|
|
563
|
+
if (!reservationTransferred) {
|
|
564
|
+
this.releasePendingRequestReservation(admissionReservation);
|
|
486
565
|
}
|
|
487
|
-
}
|
|
566
|
+
}
|
|
488
567
|
}
|
|
489
568
|
|
|
490
569
|
/**
|
|
@@ -498,51 +577,120 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
498
577
|
): StreamingResponse<TExtractChunk<TCommands[K]>, TCommands[K]['result']> {
|
|
499
578
|
const streaming = new StreamingResponse<TExtractChunk<TCommands[K]>, TCommands[K]['result']>();
|
|
500
579
|
|
|
501
|
-
|
|
580
|
+
const transport = this.transport;
|
|
581
|
+
if (!transport?.connected || !this.isRunning) {
|
|
502
582
|
streaming.fail(new Error(`${this.options.binaryName} bridge is not running`));
|
|
503
583
|
return streaming;
|
|
504
584
|
}
|
|
505
585
|
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
586
|
+
const lifecycleGeneration = this.lifecycleGeneration;
|
|
587
|
+
let admissionReservation: IRequestAdmissionReservation | undefined;
|
|
588
|
+
let reservationTransferred = false;
|
|
589
|
+
let requestId: string | undefined;
|
|
509
590
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
591
|
+
try {
|
|
592
|
+
admissionReservation = this.reservePendingRequest(method);
|
|
593
|
+
const id = `req_${++this.requestCounter}`;
|
|
594
|
+
requestId = id;
|
|
595
|
+
const request: IManagementRequest = { id, method, params };
|
|
596
|
+
const json = JSON.stringify(request);
|
|
597
|
+
|
|
598
|
+
const byteLength = Buffer.byteLength(json, 'utf8');
|
|
599
|
+
if (byteLength > this.options.maxPayloadSize) {
|
|
600
|
+
throw new Error(
|
|
601
|
+
`Outbound message exceeds maxPayloadSize (${byteLength} > ${this.options.maxPayloadSize})`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const availabilityError = this.getRequestAvailabilityError(
|
|
606
|
+
method,
|
|
607
|
+
undefined,
|
|
608
|
+
transport,
|
|
609
|
+
lifecycleGeneration,
|
|
514
610
|
);
|
|
515
|
-
|
|
516
|
-
|
|
611
|
+
if (availabilityError) {
|
|
612
|
+
throw availabilityError;
|
|
613
|
+
}
|
|
517
614
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
615
|
+
this.pendingRequests.set(id, {
|
|
616
|
+
resolve: (result: any) => streaming.finish(result),
|
|
617
|
+
reject: (error: Error) => streaming.fail(error),
|
|
618
|
+
timer: null,
|
|
619
|
+
method,
|
|
620
|
+
streaming,
|
|
621
|
+
admissionReservation,
|
|
523
622
|
});
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
this.pendingRequests.set(id, {
|
|
527
|
-
resolve: (result: any) => streaming.finish(result),
|
|
528
|
-
reject: (error: Error) => streaming.fail(error),
|
|
529
|
-
timer,
|
|
530
|
-
method,
|
|
531
|
-
streaming,
|
|
532
|
-
});
|
|
623
|
+
reservationTransferred = true;
|
|
533
624
|
|
|
534
|
-
|
|
535
|
-
|
|
625
|
+
try {
|
|
626
|
+
const timeoutMs = this.options.streamTimeoutMs ?? this.options.requestTimeoutMs;
|
|
627
|
+
const pending = this.pendingRequests.get(id)!;
|
|
628
|
+
pending.timer = setTimeout(() => {
|
|
629
|
+
this.settlePendingRequest(id, {
|
|
630
|
+
outcome: 'reject',
|
|
631
|
+
error: new Error(`Streaming command '${method}' timed out after ${timeoutMs}ms`),
|
|
632
|
+
});
|
|
633
|
+
}, timeoutMs);
|
|
634
|
+
|
|
635
|
+
const currentAvailabilityError = this.getRequestAvailabilityError(
|
|
636
|
+
method,
|
|
637
|
+
undefined,
|
|
638
|
+
transport,
|
|
639
|
+
lifecycleGeneration,
|
|
640
|
+
id,
|
|
641
|
+
);
|
|
642
|
+
if (currentAvailabilityError) {
|
|
643
|
+
this.settlePendingRequest(id, {
|
|
644
|
+
outcome: 'reject',
|
|
645
|
+
error: currentAvailabilityError,
|
|
646
|
+
});
|
|
647
|
+
return streaming;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
void transport.write(json + '\n').catch((err: unknown) => {
|
|
651
|
+
this.settlePendingRequest(id, {
|
|
652
|
+
outcome: 'reject',
|
|
653
|
+
error: new RustBridgeRequestError(
|
|
654
|
+
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
655
|
+
{
|
|
656
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
657
|
+
requestId: id,
|
|
658
|
+
method,
|
|
659
|
+
cause: err,
|
|
660
|
+
},
|
|
661
|
+
),
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
} catch (err: unknown) {
|
|
536
665
|
this.settlePendingRequest(id, {
|
|
537
666
|
outcome: 'reject',
|
|
538
|
-
error:
|
|
667
|
+
error: err instanceof RustBridgeRequestError
|
|
668
|
+
? err
|
|
669
|
+
: new RustBridgeRequestError(
|
|
670
|
+
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
671
|
+
{
|
|
672
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
673
|
+
requestId: id,
|
|
674
|
+
method,
|
|
675
|
+
cause: err,
|
|
676
|
+
},
|
|
677
|
+
),
|
|
539
678
|
});
|
|
540
|
-
}
|
|
679
|
+
}
|
|
541
680
|
} catch (err: unknown) {
|
|
542
|
-
this.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
681
|
+
const error = err instanceof Error ? err : new Error(this.getErrorMessage(err));
|
|
682
|
+
if (reservationTransferred && requestId) {
|
|
683
|
+
this.settlePendingRequest(requestId, {
|
|
684
|
+
outcome: 'reject',
|
|
685
|
+
error,
|
|
686
|
+
});
|
|
687
|
+
} else {
|
|
688
|
+
streaming.fail(error);
|
|
689
|
+
}
|
|
690
|
+
} finally {
|
|
691
|
+
if (!reservationTransferred) {
|
|
692
|
+
this.releasePendingRequestReservation(admissionReservation);
|
|
693
|
+
}
|
|
546
694
|
}
|
|
547
695
|
|
|
548
696
|
return streaming;
|
|
@@ -746,6 +894,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
746
894
|
}
|
|
747
895
|
|
|
748
896
|
this.pendingRequests.delete(id);
|
|
897
|
+
this.releasePendingRequestReservation(pending.admissionReservation);
|
|
749
898
|
if (pending.timer) {
|
|
750
899
|
clearTimeout(pending.timer);
|
|
751
900
|
pending.timer = null;
|
|
@@ -782,6 +931,86 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
782
931
|
}
|
|
783
932
|
}
|
|
784
933
|
|
|
934
|
+
private reservePendingRequest(method: string): IRequestAdmissionReservation | undefined {
|
|
935
|
+
const configuredLimits = this.options.maxPendingRequestsByMethod;
|
|
936
|
+
const limit = configuredLimits
|
|
937
|
+
&& Object.prototype.hasOwnProperty.call(configuredLimits, method)
|
|
938
|
+
? configuredLimits[method]
|
|
939
|
+
: undefined;
|
|
940
|
+
if (limit === undefined) {
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const currentCount = this.pendingRequestReservationsByMethod.get(method) || 0;
|
|
945
|
+
if (currentCount >= limit) {
|
|
946
|
+
throw new RustBridgeRequestError(
|
|
947
|
+
`Command '${method}' reached its pending request limit of ${limit}`,
|
|
948
|
+
{
|
|
949
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_LIMIT',
|
|
950
|
+
method,
|
|
951
|
+
},
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
this.pendingRequestReservationsByMethod.set(method, currentCount + 1);
|
|
956
|
+
return { method, released: false };
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
private releasePendingRequestReservation(
|
|
960
|
+
reservation: IRequestAdmissionReservation | undefined,
|
|
961
|
+
): void {
|
|
962
|
+
if (!reservation || reservation.released) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
reservation.released = true;
|
|
966
|
+
|
|
967
|
+
const currentCount = this.pendingRequestReservationsByMethod.get(reservation.method) || 0;
|
|
968
|
+
if (currentCount <= 1) {
|
|
969
|
+
this.pendingRequestReservationsByMethod.delete(reservation.method);
|
|
970
|
+
} else {
|
|
971
|
+
this.pendingRequestReservationsByMethod.set(reservation.method, currentCount - 1);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
private getRequestAvailabilityError(
|
|
976
|
+
method: string,
|
|
977
|
+
signal: AbortSignal | undefined,
|
|
978
|
+
transport: TConfirmableRustTransport,
|
|
979
|
+
lifecycleGeneration: number,
|
|
980
|
+
requestId?: string,
|
|
981
|
+
): RustBridgeRequestError | null {
|
|
982
|
+
if (signal?.aborted) {
|
|
983
|
+
return new RustBridgeRequestError(
|
|
984
|
+
`Command '${method}' was aborted before it was sent`,
|
|
985
|
+
{
|
|
986
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
|
|
987
|
+
requestId,
|
|
988
|
+
method,
|
|
989
|
+
cause: signal.reason,
|
|
990
|
+
},
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (
|
|
995
|
+
this.lifecycleState !== 'ready'
|
|
996
|
+
|| !this.isRunning
|
|
997
|
+
|| this.lifecycleGeneration !== lifecycleGeneration
|
|
998
|
+
|| this.transport !== transport
|
|
999
|
+
|| !transport.connected
|
|
1000
|
+
) {
|
|
1001
|
+
return new RustBridgeRequestError(
|
|
1002
|
+
`Command '${method}' could not be sent because the bridge lifecycle changed`,
|
|
1003
|
+
{
|
|
1004
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_TRANSPORT',
|
|
1005
|
+
requestId,
|
|
1006
|
+
method,
|
|
1007
|
+
},
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
785
1014
|
private getErrorMessage(error: unknown): string {
|
|
786
1015
|
return error instanceof Error ? error.message : String(error);
|
|
787
1016
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export type TRustBridgeRequestErrorCode =
|
|
5
5
|
| 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT'
|
|
6
6
|
| 'ERR_RUST_BRIDGE_REQUEST_ABORTED'
|
|
7
|
+
| 'ERR_RUST_BRIDGE_REQUEST_LIMIT'
|
|
7
8
|
| 'ERR_RUST_BRIDGE_REQUEST_WRITE'
|
|
8
9
|
| 'ERR_RUST_BRIDGE_REQUEST_TRANSPORT'
|
|
9
10
|
| 'ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE';
|
|
@@ -20,8 +21,8 @@ export interface IRustBridgeRequestErrorOptions {
|
|
|
20
21
|
* Error raised while preparing or waiting for a Rust bridge request.
|
|
21
22
|
*
|
|
22
23
|
* Existing error messages are preserved while `code` provides a stable way to
|
|
23
|
-
* distinguish timeout, cancellation, transport, write, and Rust
|
|
24
|
-
* failures.
|
|
24
|
+
* distinguish admission, timeout, cancellation, transport, write, and Rust
|
|
25
|
+
* response failures.
|
|
25
26
|
*/
|
|
26
27
|
export class RustBridgeRequestError extends Error {
|
|
27
28
|
public readonly code: TRustBridgeRequestErrorCode;
|
|
@@ -13,6 +13,7 @@ export interface IStdioTransportOptions {
|
|
|
13
13
|
binaryPath: string;
|
|
14
14
|
cliArgs: string[];
|
|
15
15
|
env?: Record<string, string>;
|
|
16
|
+
inheritedFileDescriptors?: readonly number[];
|
|
16
17
|
maxPayloadSize: number;
|
|
17
18
|
logger: IRustBridgeLogger;
|
|
18
19
|
}
|
|
@@ -70,10 +71,16 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
70
71
|
|
|
71
72
|
let childProcess: plugins.childProcess.ChildProcess;
|
|
72
73
|
try {
|
|
74
|
+
const stdio: plugins.childProcess.StdioOptions = [
|
|
75
|
+
'pipe',
|
|
76
|
+
'pipe',
|
|
77
|
+
'pipe',
|
|
78
|
+
...(this.options.inheritedFileDescriptors ?? []),
|
|
79
|
+
];
|
|
73
80
|
childProcess = plugins.childProcess.spawn(
|
|
74
81
|
this.options.binaryPath,
|
|
75
82
|
this.options.cliArgs,
|
|
76
|
-
{ stdio
|
|
83
|
+
{ stdio, env },
|
|
77
84
|
);
|
|
78
85
|
} catch (error: unknown) {
|
|
79
86
|
return Promise.reject(this.toError(error, 'Failed to spawn child process'));
|
package/ts/interfaces/config.ts
CHANGED
|
@@ -48,6 +48,13 @@ export interface IBinaryLocatorOptions {
|
|
|
48
48
|
export interface IRustBridgeOptions extends IBinaryLocatorOptions {
|
|
49
49
|
/** CLI arguments passed to the binary (default: ['--management']) */
|
|
50
50
|
cliArgs?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Caller-owned descriptors inherited only by stdio children. The first
|
|
53
|
+
* source descriptor is duplicated to child fd 3, the next to fd 4, and so
|
|
54
|
+
* on. Sources must remain valid through each spawn call. Smartrust never
|
|
55
|
+
* closes the caller's source descriptors.
|
|
56
|
+
*/
|
|
57
|
+
inheritedFileDescriptors?: readonly number[];
|
|
51
58
|
/** Timeout for individual requests in ms (default: 30000) */
|
|
52
59
|
requestTimeoutMs?: number;
|
|
53
60
|
/** Timeout for the ready event during spawn in ms (default: 10000) */
|
|
@@ -63,6 +70,12 @@ export interface IRustBridgeOptions extends IBinaryLocatorOptions {
|
|
|
63
70
|
/** Inactivity timeout for streaming commands in ms (default: same as requestTimeoutMs).
|
|
64
71
|
* Resets on each chunk received. */
|
|
65
72
|
streamTimeoutMs?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Optional per-method limits for locally pending requests. A saturated
|
|
75
|
+
* method fails before serialization or transport writes, while other
|
|
76
|
+
* methods remain available for control and callback traffic.
|
|
77
|
+
*/
|
|
78
|
+
maxPendingRequestsByMethod?: Record<string, number>;
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
/**
|