@push.rocks/smartrust 1.7.0 → 1.8.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 +236 -89
- package/dist_ts/classes.rustbridgerequesterror.d.ts +3 -3
- package/dist_ts/classes.rustbridgerequesterror.js +3 -3
- package/dist_ts/classes.sockettransport.d.ts +1 -3
- package/dist_ts/classes.sockettransport.js +6 -13
- package/dist_ts/classes.stdiotransport.d.ts +1 -3
- package/dist_ts/classes.stdiotransport.js +37 -13
- package/dist_ts/interfaces/config.d.ts +6 -0
- package/package.json +1 -1
- package/readme.md +24 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.rustbridge.ts +319 -108
- package/ts/classes.rustbridgerequesterror.ts +3 -2
- package/ts/classes.sockettransport.ts +4 -12
- package/ts/classes.stdiotransport.ts +42 -12
- package/ts/interfaces/config.ts +6 -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,15 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
67
74
|
|
|
68
75
|
constructor(options: IRustBridgeOptions) {
|
|
69
76
|
super();
|
|
77
|
+
if (options.maxPendingRequestsByMethod) {
|
|
78
|
+
for (const [method, limit] of Object.entries(options.maxPendingRequestsByMethod)) {
|
|
79
|
+
if (!method || !Number.isSafeInteger(limit) || limit <= 0) {
|
|
80
|
+
throw new RangeError(
|
|
81
|
+
'RustBridge maxPendingRequestsByMethod values must be positive safe integers',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
70
86
|
this.logger = options.logger || defaultLogger;
|
|
71
87
|
this.options = {
|
|
72
88
|
cliArgs: ['--management'],
|
|
@@ -75,6 +91,9 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
75
91
|
readyEventName: 'ready',
|
|
76
92
|
maxPayloadSize: 50 * 1024 * 1024,
|
|
77
93
|
...options,
|
|
94
|
+
maxPendingRequestsByMethod: options.maxPendingRequestsByMethod
|
|
95
|
+
? { ...options.maxPendingRequestsByMethod }
|
|
96
|
+
: undefined,
|
|
78
97
|
};
|
|
79
98
|
this.locator = new RustBinaryLocator(options, this.logger);
|
|
80
99
|
}
|
|
@@ -396,95 +415,137 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
396
415
|
);
|
|
397
416
|
}
|
|
398
417
|
|
|
399
|
-
|
|
418
|
+
const transport = this.transport;
|
|
419
|
+
if (!transport?.connected || !this.isRunning) {
|
|
400
420
|
throw new Error(`${this.options.binaryName} bridge is not running`);
|
|
401
421
|
}
|
|
402
422
|
|
|
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
|
-
}
|
|
423
|
+
const lifecycleGeneration = this.lifecycleGeneration;
|
|
424
|
+
const admissionReservation = this.reservePendingRequest(method);
|
|
425
|
+
let reservationTransferred = false;
|
|
415
426
|
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
427
|
+
try {
|
|
428
|
+
const id = `req_${++this.requestCounter}`;
|
|
429
|
+
const request: IManagementRequest = { id, method, params };
|
|
430
|
+
const json = JSON.stringify(request);
|
|
431
|
+
|
|
432
|
+
const byteLength = Buffer.byteLength(json, 'utf8');
|
|
433
|
+
if (byteLength > this.options.maxPayloadSize) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
`Outbound message exceeds maxPayloadSize (${byteLength} > ${this.options.maxPayloadSize})`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const availabilityError = this.getRequestAvailabilityError(
|
|
421
440
|
method,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
441
|
+
options?.signal,
|
|
442
|
+
transport,
|
|
443
|
+
lifecycleGeneration,
|
|
444
|
+
);
|
|
445
|
+
if (availabilityError) {
|
|
446
|
+
throw availabilityError;
|
|
447
|
+
}
|
|
425
448
|
|
|
426
|
-
|
|
427
|
-
pending
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
requestId: id,
|
|
435
|
-
method,
|
|
436
|
-
cause: options.signal?.reason,
|
|
437
|
-
},
|
|
438
|
-
),
|
|
439
|
-
});
|
|
449
|
+
return new Promise<TCommands[K]['result']>((resolve, reject) => {
|
|
450
|
+
const pending: IPendingRequest = {
|
|
451
|
+
resolve,
|
|
452
|
+
reject,
|
|
453
|
+
timer: null,
|
|
454
|
+
method,
|
|
455
|
+
abortSignal: options?.signal,
|
|
456
|
+
admissionReservation,
|
|
440
457
|
};
|
|
441
|
-
|
|
442
|
-
|
|
458
|
+
this.pendingRequests.set(id, pending);
|
|
459
|
+
reservationTransferred = true;
|
|
443
460
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
461
|
+
try {
|
|
462
|
+
if (options?.signal) {
|
|
463
|
+
pending.abortHandler = () => {
|
|
464
|
+
this.settlePendingRequest(id, {
|
|
465
|
+
outcome: 'reject',
|
|
466
|
+
error: new RustBridgeRequestError(
|
|
467
|
+
`Command '${method}' was aborted`,
|
|
468
|
+
{
|
|
469
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
|
|
470
|
+
requestId: id,
|
|
471
|
+
method,
|
|
472
|
+
cause: options.signal?.reason,
|
|
473
|
+
},
|
|
474
|
+
),
|
|
475
|
+
});
|
|
476
|
+
};
|
|
477
|
+
options.signal.addEventListener('abort', pending.abortHandler, { once: true });
|
|
478
|
+
if (options.signal.aborted) {
|
|
479
|
+
pending.abortHandler();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
457
483
|
|
|
458
|
-
|
|
459
|
-
|
|
484
|
+
pending.timer = setTimeout(() => {
|
|
485
|
+
this.settlePendingRequest(id, {
|
|
486
|
+
outcome: 'reject',
|
|
487
|
+
error: new RustBridgeRequestError(
|
|
488
|
+
`Command '${method}' timed out after ${timeoutMs}ms`,
|
|
489
|
+
{
|
|
490
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT',
|
|
491
|
+
requestId: id,
|
|
492
|
+
method,
|
|
493
|
+
},
|
|
494
|
+
),
|
|
495
|
+
});
|
|
496
|
+
}, timeoutMs);
|
|
497
|
+
|
|
498
|
+
const currentAvailabilityError = this.getRequestAvailabilityError(
|
|
499
|
+
method,
|
|
500
|
+
options?.signal,
|
|
501
|
+
transport,
|
|
502
|
+
lifecycleGeneration,
|
|
503
|
+
id,
|
|
504
|
+
);
|
|
505
|
+
if (currentAvailabilityError) {
|
|
506
|
+
this.settlePendingRequest(id, {
|
|
507
|
+
outcome: 'reject',
|
|
508
|
+
error: currentAvailabilityError,
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
void transport.write(json + '\n').catch((err: unknown) => {
|
|
514
|
+
this.settlePendingRequest(id, {
|
|
515
|
+
outcome: 'reject',
|
|
516
|
+
error: new RustBridgeRequestError(
|
|
517
|
+
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
518
|
+
{
|
|
519
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
520
|
+
requestId: id,
|
|
521
|
+
method,
|
|
522
|
+
cause: err,
|
|
523
|
+
},
|
|
524
|
+
),
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
} catch (err: unknown) {
|
|
460
528
|
this.settlePendingRequest(id, {
|
|
461
529
|
outcome: 'reject',
|
|
462
|
-
error:
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
530
|
+
error: err instanceof RustBridgeRequestError
|
|
531
|
+
? err
|
|
532
|
+
: new RustBridgeRequestError(
|
|
533
|
+
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
534
|
+
{
|
|
535
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
536
|
+
requestId: id,
|
|
537
|
+
method,
|
|
538
|
+
cause: err,
|
|
539
|
+
},
|
|
540
|
+
),
|
|
471
541
|
});
|
|
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
|
-
});
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
} finally {
|
|
545
|
+
if (!reservationTransferred) {
|
|
546
|
+
this.releasePendingRequestReservation(admissionReservation);
|
|
486
547
|
}
|
|
487
|
-
}
|
|
548
|
+
}
|
|
488
549
|
}
|
|
489
550
|
|
|
490
551
|
/**
|
|
@@ -498,51 +559,120 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
498
559
|
): StreamingResponse<TExtractChunk<TCommands[K]>, TCommands[K]['result']> {
|
|
499
560
|
const streaming = new StreamingResponse<TExtractChunk<TCommands[K]>, TCommands[K]['result']>();
|
|
500
561
|
|
|
501
|
-
|
|
562
|
+
const transport = this.transport;
|
|
563
|
+
if (!transport?.connected || !this.isRunning) {
|
|
502
564
|
streaming.fail(new Error(`${this.options.binaryName} bridge is not running`));
|
|
503
565
|
return streaming;
|
|
504
566
|
}
|
|
505
567
|
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
568
|
+
const lifecycleGeneration = this.lifecycleGeneration;
|
|
569
|
+
let admissionReservation: IRequestAdmissionReservation | undefined;
|
|
570
|
+
let reservationTransferred = false;
|
|
571
|
+
let requestId: string | undefined;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
admissionReservation = this.reservePendingRequest(method);
|
|
575
|
+
const id = `req_${++this.requestCounter}`;
|
|
576
|
+
requestId = id;
|
|
577
|
+
const request: IManagementRequest = { id, method, params };
|
|
578
|
+
const json = JSON.stringify(request);
|
|
579
|
+
|
|
580
|
+
const byteLength = Buffer.byteLength(json, 'utf8');
|
|
581
|
+
if (byteLength > this.options.maxPayloadSize) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`Outbound message exceeds maxPayloadSize (${byteLength} > ${this.options.maxPayloadSize})`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
509
586
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
587
|
+
const availabilityError = this.getRequestAvailabilityError(
|
|
588
|
+
method,
|
|
589
|
+
undefined,
|
|
590
|
+
transport,
|
|
591
|
+
lifecycleGeneration,
|
|
514
592
|
);
|
|
515
|
-
|
|
516
|
-
|
|
593
|
+
if (availabilityError) {
|
|
594
|
+
throw availabilityError;
|
|
595
|
+
}
|
|
517
596
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
597
|
+
this.pendingRequests.set(id, {
|
|
598
|
+
resolve: (result: any) => streaming.finish(result),
|
|
599
|
+
reject: (error: Error) => streaming.fail(error),
|
|
600
|
+
timer: null,
|
|
601
|
+
method,
|
|
602
|
+
streaming,
|
|
603
|
+
admissionReservation,
|
|
523
604
|
});
|
|
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
|
-
});
|
|
605
|
+
reservationTransferred = true;
|
|
533
606
|
|
|
534
|
-
|
|
535
|
-
|
|
607
|
+
try {
|
|
608
|
+
const timeoutMs = this.options.streamTimeoutMs ?? this.options.requestTimeoutMs;
|
|
609
|
+
const pending = this.pendingRequests.get(id)!;
|
|
610
|
+
pending.timer = setTimeout(() => {
|
|
611
|
+
this.settlePendingRequest(id, {
|
|
612
|
+
outcome: 'reject',
|
|
613
|
+
error: new Error(`Streaming command '${method}' timed out after ${timeoutMs}ms`),
|
|
614
|
+
});
|
|
615
|
+
}, timeoutMs);
|
|
616
|
+
|
|
617
|
+
const currentAvailabilityError = this.getRequestAvailabilityError(
|
|
618
|
+
method,
|
|
619
|
+
undefined,
|
|
620
|
+
transport,
|
|
621
|
+
lifecycleGeneration,
|
|
622
|
+
id,
|
|
623
|
+
);
|
|
624
|
+
if (currentAvailabilityError) {
|
|
625
|
+
this.settlePendingRequest(id, {
|
|
626
|
+
outcome: 'reject',
|
|
627
|
+
error: currentAvailabilityError,
|
|
628
|
+
});
|
|
629
|
+
return streaming;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
void transport.write(json + '\n').catch((err: unknown) => {
|
|
633
|
+
this.settlePendingRequest(id, {
|
|
634
|
+
outcome: 'reject',
|
|
635
|
+
error: new RustBridgeRequestError(
|
|
636
|
+
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
637
|
+
{
|
|
638
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
639
|
+
requestId: id,
|
|
640
|
+
method,
|
|
641
|
+
cause: err,
|
|
642
|
+
},
|
|
643
|
+
),
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
} catch (err: unknown) {
|
|
536
647
|
this.settlePendingRequest(id, {
|
|
537
648
|
outcome: 'reject',
|
|
538
|
-
error:
|
|
649
|
+
error: err instanceof RustBridgeRequestError
|
|
650
|
+
? err
|
|
651
|
+
: new RustBridgeRequestError(
|
|
652
|
+
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
653
|
+
{
|
|
654
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
655
|
+
requestId: id,
|
|
656
|
+
method,
|
|
657
|
+
cause: err,
|
|
658
|
+
},
|
|
659
|
+
),
|
|
539
660
|
});
|
|
540
|
-
}
|
|
661
|
+
}
|
|
541
662
|
} catch (err: unknown) {
|
|
542
|
-
this.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
663
|
+
const error = err instanceof Error ? err : new Error(this.getErrorMessage(err));
|
|
664
|
+
if (reservationTransferred && requestId) {
|
|
665
|
+
this.settlePendingRequest(requestId, {
|
|
666
|
+
outcome: 'reject',
|
|
667
|
+
error,
|
|
668
|
+
});
|
|
669
|
+
} else {
|
|
670
|
+
streaming.fail(error);
|
|
671
|
+
}
|
|
672
|
+
} finally {
|
|
673
|
+
if (!reservationTransferred) {
|
|
674
|
+
this.releasePendingRequestReservation(admissionReservation);
|
|
675
|
+
}
|
|
546
676
|
}
|
|
547
677
|
|
|
548
678
|
return streaming;
|
|
@@ -746,6 +876,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
746
876
|
}
|
|
747
877
|
|
|
748
878
|
this.pendingRequests.delete(id);
|
|
879
|
+
this.releasePendingRequestReservation(pending.admissionReservation);
|
|
749
880
|
if (pending.timer) {
|
|
750
881
|
clearTimeout(pending.timer);
|
|
751
882
|
pending.timer = null;
|
|
@@ -782,6 +913,86 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
782
913
|
}
|
|
783
914
|
}
|
|
784
915
|
|
|
916
|
+
private reservePendingRequest(method: string): IRequestAdmissionReservation | undefined {
|
|
917
|
+
const configuredLimits = this.options.maxPendingRequestsByMethod;
|
|
918
|
+
const limit = configuredLimits
|
|
919
|
+
&& Object.prototype.hasOwnProperty.call(configuredLimits, method)
|
|
920
|
+
? configuredLimits[method]
|
|
921
|
+
: undefined;
|
|
922
|
+
if (limit === undefined) {
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const currentCount = this.pendingRequestReservationsByMethod.get(method) || 0;
|
|
927
|
+
if (currentCount >= limit) {
|
|
928
|
+
throw new RustBridgeRequestError(
|
|
929
|
+
`Command '${method}' reached its pending request limit of ${limit}`,
|
|
930
|
+
{
|
|
931
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_LIMIT',
|
|
932
|
+
method,
|
|
933
|
+
},
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
this.pendingRequestReservationsByMethod.set(method, currentCount + 1);
|
|
938
|
+
return { method, released: false };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
private releasePendingRequestReservation(
|
|
942
|
+
reservation: IRequestAdmissionReservation | undefined,
|
|
943
|
+
): void {
|
|
944
|
+
if (!reservation || reservation.released) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
reservation.released = true;
|
|
948
|
+
|
|
949
|
+
const currentCount = this.pendingRequestReservationsByMethod.get(reservation.method) || 0;
|
|
950
|
+
if (currentCount <= 1) {
|
|
951
|
+
this.pendingRequestReservationsByMethod.delete(reservation.method);
|
|
952
|
+
} else {
|
|
953
|
+
this.pendingRequestReservationsByMethod.set(reservation.method, currentCount - 1);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private getRequestAvailabilityError(
|
|
958
|
+
method: string,
|
|
959
|
+
signal: AbortSignal | undefined,
|
|
960
|
+
transport: TConfirmableRustTransport,
|
|
961
|
+
lifecycleGeneration: number,
|
|
962
|
+
requestId?: string,
|
|
963
|
+
): RustBridgeRequestError | null {
|
|
964
|
+
if (signal?.aborted) {
|
|
965
|
+
return new RustBridgeRequestError(
|
|
966
|
+
`Command '${method}' was aborted before it was sent`,
|
|
967
|
+
{
|
|
968
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
|
|
969
|
+
requestId,
|
|
970
|
+
method,
|
|
971
|
+
cause: signal.reason,
|
|
972
|
+
},
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (
|
|
977
|
+
this.lifecycleState !== 'ready'
|
|
978
|
+
|| !this.isRunning
|
|
979
|
+
|| this.lifecycleGeneration !== lifecycleGeneration
|
|
980
|
+
|| this.transport !== transport
|
|
981
|
+
|| !transport.connected
|
|
982
|
+
) {
|
|
983
|
+
return new RustBridgeRequestError(
|
|
984
|
+
`Command '${method}' could not be sent because the bridge lifecycle changed`,
|
|
985
|
+
{
|
|
986
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_TRANSPORT',
|
|
987
|
+
requestId,
|
|
988
|
+
method,
|
|
989
|
+
},
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
|
|
785
996
|
private getErrorMessage(error: unknown): string {
|
|
786
997
|
return error instanceof Error ? error.message : String(error);
|
|
787
998
|
}
|
|
@@ -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;
|
|
@@ -320,9 +320,7 @@ export class SocketTransport extends plugins.events.EventEmitter implements IRus
|
|
|
320
320
|
}, delay);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
/**
|
|
324
|
-
* Write data to the socket with backpressure support.
|
|
325
|
-
*/
|
|
323
|
+
/** Write data to the socket and resolve after the stream callback confirms it. */
|
|
326
324
|
public write(data: string): Promise<void> {
|
|
327
325
|
return new Promise<void>((resolve, reject) => {
|
|
328
326
|
const generation = this.generation;
|
|
@@ -336,19 +334,13 @@ export class SocketTransport extends plugins.events.EventEmitter implements IRus
|
|
|
336
334
|
return;
|
|
337
335
|
}
|
|
338
336
|
|
|
339
|
-
|
|
337
|
+
generation.socket.write(data, 'utf8', (error) => {
|
|
340
338
|
if (error) {
|
|
341
339
|
reject(error);
|
|
340
|
+
} else {
|
|
341
|
+
resolve();
|
|
342
342
|
}
|
|
343
343
|
});
|
|
344
|
-
|
|
345
|
-
if (canContinue) {
|
|
346
|
-
resolve();
|
|
347
|
-
} else {
|
|
348
|
-
generation.socket.once('drain', () => {
|
|
349
|
-
resolve();
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
344
|
});
|
|
353
345
|
}
|
|
354
346
|
|
|
@@ -20,6 +20,7 @@ export interface IStdioTransportOptions {
|
|
|
20
20
|
interface IStdioTransportGeneration {
|
|
21
21
|
childProcess: plugins.childProcess.ChildProcess;
|
|
22
22
|
stderrRemainder: string;
|
|
23
|
+
stdinError: Error | null;
|
|
23
24
|
exited: boolean;
|
|
24
25
|
closed: boolean;
|
|
25
26
|
closePromise: Promise<void>;
|
|
@@ -85,6 +86,7 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
85
86
|
const generation: IStdioTransportGeneration = {
|
|
86
87
|
childProcess,
|
|
87
88
|
stderrRemainder: '',
|
|
89
|
+
stdinError: null,
|
|
88
90
|
exited: false,
|
|
89
91
|
closed: false,
|
|
90
92
|
closePromise,
|
|
@@ -100,6 +102,42 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
100
102
|
this.generation = generation;
|
|
101
103
|
this.childProcess = childProcess;
|
|
102
104
|
|
|
105
|
+
childProcess.stdin?.on('error', (error: Error) => {
|
|
106
|
+
if (generation.stdinError) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
generation.stdinError = error;
|
|
110
|
+
if (this.generation !== generation || generation.closed) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this._connected = false;
|
|
115
|
+
// Let the write callback settle its request before the bridge rejects all
|
|
116
|
+
// other pending work through the transport-wide error path.
|
|
117
|
+
queueMicrotask(() => {
|
|
118
|
+
if (this.generation !== generation || generation.closed) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
void this.disconnectAndWait().catch((disconnectError: unknown) => {
|
|
122
|
+
this.options.logger.log(
|
|
123
|
+
'error',
|
|
124
|
+
'Failed to clean up child after stdin error: '
|
|
125
|
+
+ this.getErrorMessage(disconnectError),
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
if (this.listenerCount('error') > 0) {
|
|
129
|
+
this.emit('error', error);
|
|
130
|
+
} else if (generation.disconnectPromise) {
|
|
131
|
+
this.options.logger.log(
|
|
132
|
+
'error',
|
|
133
|
+
'Stdio transport error during disconnect: ' + error.message,
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
this.emit('error', error);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
103
141
|
// Handle stderr with cross-chunk buffering.
|
|
104
142
|
childProcess.stderr?.on('data', (data: Buffer) => {
|
|
105
143
|
if (this.generation !== generation || generation.closed) {
|
|
@@ -209,9 +247,7 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
209
247
|
return Promise.resolve();
|
|
210
248
|
}
|
|
211
249
|
|
|
212
|
-
/**
|
|
213
|
-
* Write data to stdin with backpressure support.
|
|
214
|
-
*/
|
|
250
|
+
/** Write data to stdin and resolve after the stream callback confirms it. */
|
|
215
251
|
public write(data: string): Promise<void> {
|
|
216
252
|
return new Promise<void>((resolve, reject) => {
|
|
217
253
|
const generation = this.generation;
|
|
@@ -226,19 +262,13 @@ export class StdioTransport extends plugins.events.EventEmitter implements IRust
|
|
|
226
262
|
return;
|
|
227
263
|
}
|
|
228
264
|
|
|
229
|
-
|
|
265
|
+
generation.childProcess.stdin.write(data, 'utf8', (error) => {
|
|
230
266
|
if (error) {
|
|
231
267
|
reject(error);
|
|
268
|
+
} else {
|
|
269
|
+
resolve();
|
|
232
270
|
}
|
|
233
271
|
});
|
|
234
|
-
|
|
235
|
-
if (canContinue) {
|
|
236
|
-
resolve();
|
|
237
|
-
} else {
|
|
238
|
-
generation.childProcess.stdin.once('drain', () => {
|
|
239
|
-
resolve();
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
272
|
});
|
|
243
273
|
}
|
|
244
274
|
|
package/ts/interfaces/config.ts
CHANGED
|
@@ -63,6 +63,12 @@ export interface IRustBridgeOptions extends IBinaryLocatorOptions {
|
|
|
63
63
|
/** Inactivity timeout for streaming commands in ms (default: same as requestTimeoutMs).
|
|
64
64
|
* Resets on each chunk received. */
|
|
65
65
|
streamTimeoutMs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Optional per-method limits for locally pending requests. A saturated
|
|
68
|
+
* method fails before serialization or transport writes, while other
|
|
69
|
+
* methods remain available for control and callback traffic.
|
|
70
|
+
*/
|
|
71
|
+
maxPendingRequestsByMethod?: Record<string, number>;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
/**
|