@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.
@@ -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
- if (!this.transport?.connected || !this.isRunning) {
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 transport = this.transport;
404
- const id = `req_${++this.requestCounter}`;
405
- const request: IManagementRequest = { id, method, params };
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
- return new Promise<TCommands[K]['result']>((resolve, reject) => {
417
- const pending: IPendingRequest = {
418
- resolve,
419
- reject,
420
- timer: null,
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
- abortSignal: options?.signal,
423
- };
424
- this.pendingRequests.set(id, pending);
441
+ options?.signal,
442
+ transport,
443
+ lifecycleGeneration,
444
+ );
445
+ if (availabilityError) {
446
+ throw availabilityError;
447
+ }
425
448
 
426
- if (options?.signal) {
427
- pending.abortHandler = () => {
428
- this.settlePendingRequest(id, {
429
- outcome: 'reject',
430
- error: new RustBridgeRequestError(
431
- `Command '${method}' was aborted`,
432
- {
433
- code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
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
- options.signal.addEventListener('abort', pending.abortHandler, { once: true });
442
- }
458
+ this.pendingRequests.set(id, pending);
459
+ reservationTransferred = true;
443
460
 
444
- pending.timer = setTimeout(() => {
445
- this.settlePendingRequest(id, {
446
- outcome: 'reject',
447
- error: new RustBridgeRequestError(
448
- `Command '${method}' timed out after ${timeoutMs}ms`,
449
- {
450
- code: 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT',
451
- requestId: id,
452
- method,
453
- },
454
- ),
455
- });
456
- }, timeoutMs);
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
- try {
459
- void transport.write(json + '\n').catch((err: unknown) => {
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: new RustBridgeRequestError(
463
- `Failed to write to transport: ${this.getErrorMessage(err)}`,
464
- {
465
- code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
466
- requestId: id,
467
- method,
468
- cause: err,
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
- } catch (err: unknown) {
474
- this.settlePendingRequest(id, {
475
- outcome: 'reject',
476
- error: new RustBridgeRequestError(
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
- if (!this.transport?.connected || !this.isRunning) {
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 id = `req_${++this.requestCounter}`;
507
- const request: IManagementRequest = { id, method, params };
508
- const json = JSON.stringify(request);
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
- const byteLength = Buffer.byteLength(json, 'utf8');
511
- if (byteLength > this.options.maxPayloadSize) {
512
- streaming.fail(
513
- new Error(`Outbound message exceeds maxPayloadSize (${byteLength} > ${this.options.maxPayloadSize})`)
587
+ const availabilityError = this.getRequestAvailabilityError(
588
+ method,
589
+ undefined,
590
+ transport,
591
+ lifecycleGeneration,
514
592
  );
515
- return streaming;
516
- }
593
+ if (availabilityError) {
594
+ throw availabilityError;
595
+ }
517
596
 
518
- const timeoutMs = this.options.streamTimeoutMs ?? this.options.requestTimeoutMs;
519
- const timer = setTimeout(() => {
520
- this.settlePendingRequest(id, {
521
- outcome: 'reject',
522
- error: new Error(`Streaming command '${method}' timed out after ${timeoutMs}ms`),
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
- }, timeoutMs);
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
- try {
535
- void this.transport!.write(json + '\n').catch((err: unknown) => {
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: new Error(`Failed to write to transport: ${this.getErrorMessage(err)}`),
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.settlePendingRequest(id, {
543
- outcome: 'reject',
544
- error: new Error(`Failed to write to transport: ${this.getErrorMessage(err)}`),
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 response
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
- const canContinue = generation.socket.write(data, 'utf8', (error) => {
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
- const canContinue = generation.childProcess.stdin.write(data, 'utf8', (error) => {
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
 
@@ -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
  /**