@peerbit/stream 5.0.0 → 5.0.1-5d63b4f

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/src/index.ts CHANGED
@@ -48,10 +48,12 @@ import {
48
48
  } from "@peerbit/stream-interface";
49
49
  import type {
50
50
  DirectStreamAckRouteHint,
51
+ ExpiresAtOptions,
51
52
  IdOptions,
52
53
  PeerRefs,
53
54
  PriorityOptions,
54
55
  PublicKeyFromHashResolver,
56
+ ResponsePriorityOptions,
55
57
  StreamEvents,
56
58
  WaitForAnyOpts,
57
59
  WaitForBaseOpts,
@@ -144,6 +146,7 @@ export interface PeerStreamsInit {
144
146
  publicKey: PublicSignKey;
145
147
  protocol: string;
146
148
  connId: string;
149
+ outboundQueue?: PeerOutboundQueueOptions;
147
150
  }
148
151
  const DEFAULT_SEEK_MESSAGE_REDUDANCY = 2;
149
152
  const DEFAULT_SILENT_MESSAGE_REDUDANCY = 1;
@@ -156,6 +159,7 @@ const isWebsocketConnection = (c: Connection) =>
156
159
  export interface PeerStreamEvents {
157
160
  "stream:inbound": CustomEvent<never>;
158
161
  "stream:outbound": CustomEvent<never>;
162
+ "queue:outbound": CustomEvent<never>;
159
163
  close: CustomEvent<never>;
160
164
  }
161
165
 
@@ -165,6 +169,8 @@ const MAX_DATA_LENGTH_IN = 15e6 + 1000; // 15 mb and some metadata
165
169
  const MAX_DATA_LENGTH_OUT = 1e7 + 1000; // 10 mb and some metadata
166
170
 
167
171
  const MAX_QUEUED_BYTES = MAX_DATA_LENGTH_IN * 50;
172
+ const DEFAULT_OUTBOUND_QUEUE_MAX_BYTES = MAX_DATA_LENGTH_OUT * 2;
173
+ const DEFAULT_OUTBOUND_QUEUE_RESERVED_PRIORITY_BYTES = 1024 * 1024;
168
174
 
169
175
  const DEFAULT_PRUNE_CONNECTIONS_INTERVAL = 2e4;
170
176
  const DEFAULT_MIN_CONNECTIONS = 2;
@@ -186,6 +192,16 @@ const getLaneFromPriority = (priority: number) => {
186
192
  const clampedPriority = Math.max(0, Math.min(maxLane, Math.floor(priority)));
187
193
  return maxLane - clampedPriority;
188
194
  };
195
+ type OutboundQueueOptions = {
196
+ maxBufferedBytes: number;
197
+ reservedPriorityBytes: number;
198
+ maxTotalBufferedBytes: number;
199
+ reservedTotalPriorityBytes: number;
200
+ };
201
+ type PeerOutboundQueueOptions = Pick<
202
+ OutboundQueueOptions,
203
+ "maxBufferedBytes" | "reservedPriorityBytes"
204
+ >;
189
205
  interface OutboundCandidate {
190
206
  raw: Stream;
191
207
  pushable: PushableLanes<Uint8Array>;
@@ -202,6 +218,36 @@ export interface InboundStreamRecord {
202
218
  lastActivity: number;
203
219
  bytesReceived: number;
204
220
  }
221
+
222
+ export class BackpressureError extends Error {
223
+ readonly scope: "peer" | "node";
224
+ readonly peerId: PeerId;
225
+ readonly priority: number;
226
+ readonly limitBytes: number;
227
+ readonly currentBufferedBytes: number;
228
+ readonly attemptedBytes: number;
229
+
230
+ constructor(options: {
231
+ scope: "peer" | "node";
232
+ peerId: PeerId;
233
+ priority: number;
234
+ limitBytes: number;
235
+ currentBufferedBytes: number;
236
+ attemptedBytes: number;
237
+ }) {
238
+ super(
239
+ `Outbound ${options.scope} queue full for ${options.peerId.toString()} on priority ${options.priority}: ` +
240
+ `${options.currentBufferedBytes} buffered + ${options.attemptedBytes} attempted > ${options.limitBytes} limit`,
241
+ );
242
+ this.name = "BackpressureError";
243
+ this.scope = options.scope;
244
+ this.peerId = options.peerId;
245
+ this.priority = options.priority;
246
+ this.limitBytes = options.limitBytes;
247
+ this.currentBufferedBytes = options.currentBufferedBytes;
248
+ this.attemptedBytes = options.attemptedBytes;
249
+ }
250
+ }
205
251
  // Hook for tests to override queued length measurement (peerStreams, default impl)
206
252
  export let measureOutboundQueuedBytes: (
207
253
  ps: PeerStreams, // return queued bytes for active outbound (all lanes) or 0 if none
@@ -254,6 +300,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
254
300
  public seekedOnce: boolean;
255
301
 
256
302
  private usedBandWidthTracker: BandwidthTracker;
303
+ private readonly outboundQueue?: PeerOutboundQueueOptions;
257
304
 
258
305
  // Unified outbound streams list (during grace may contain >1; after pruning length==1)
259
306
  private outboundStreams: OutboundCandidate[] = [];
@@ -306,6 +353,11 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
306
353
  if (existing) return existing;
307
354
  const pushableInst = pushableLanes<Uint8Array>({
308
355
  lanes: PRIORITY_LANES,
356
+ maxBufferedBytes: this.outboundQueue?.maxBufferedBytes,
357
+ overflow: "throw",
358
+ onBufferSize: () => {
359
+ this.dispatchEvent(new CustomEvent("queue:outbound"));
360
+ },
309
361
  onPush: (val: Uint8Array) => {
310
362
  candidate.bytesDelivered += val.length || val.byteLength || 0;
311
363
  },
@@ -397,6 +449,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
397
449
  this.connId = init.connId;
398
450
  this.usedBandWidthTracker = new BandwidthTracker(10);
399
451
  this.usedBandWidthTracker.start();
452
+ this.outboundQueue = init.outboundQueue;
400
453
  }
401
454
 
402
455
  /**
@@ -417,6 +470,65 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
417
470
  return this.usedBandWidthTracker.value;
418
471
  }
419
472
 
473
+ private getQueueAdmissionLimitBytes(lane: number): number | undefined {
474
+ if (!this.outboundQueue) {
475
+ return undefined;
476
+ }
477
+ if (lane === getLaneFromPriority(0)) {
478
+ return Math.max(
479
+ 0,
480
+ this.outboundQueue.maxBufferedBytes -
481
+ this.outboundQueue.reservedPriorityBytes,
482
+ );
483
+ }
484
+ return this.outboundQueue.maxBufferedBytes;
485
+ }
486
+
487
+ private assertQueueCapacity(
488
+ candidate: OutboundCandidate,
489
+ payloadBytes: number,
490
+ priority: number,
491
+ ) {
492
+ const lane = getLaneFromPriority(priority);
493
+ const limitBytes = this.getQueueAdmissionLimitBytes(lane);
494
+ if (limitBytes == null) {
495
+ return;
496
+ }
497
+ const currentBufferedBytes = candidate.pushable.getReadableLength();
498
+ if (currentBufferedBytes + payloadBytes > limitBytes) {
499
+ throw new BackpressureError({
500
+ scope: "peer",
501
+ peerId: this.peerId,
502
+ priority,
503
+ limitBytes,
504
+ currentBufferedBytes,
505
+ attemptedBytes: payloadBytes,
506
+ });
507
+ }
508
+ }
509
+
510
+ private async waitForQueueCapacity(
511
+ payloadBytes: number,
512
+ priority: number,
513
+ signal?: AbortSignal,
514
+ ) {
515
+ const lane = getLaneFromPriority(priority);
516
+ const limitBytes = this.getQueueAdmissionLimitBytes(lane);
517
+ if (limitBytes == null) {
518
+ return;
519
+ }
520
+ const threshold = Math.max(0, limitBytes - payloadBytes);
521
+ const waiters = this.outboundStreams
522
+ .filter((candidate) => !candidate.aborted)
523
+ .map((candidate) =>
524
+ candidate.pushable.onBufferedBelow(threshold, { signal }),
525
+ );
526
+ if (waiters.length === 0) {
527
+ throw new Error("No writable connection to " + this.peerId.toString());
528
+ }
529
+ await Promise.race(waiters);
530
+ }
531
+
420
532
  /**
421
533
  * Send a message to this peer.
422
534
  * Throws if there is no `stream` to write to available.
@@ -434,8 +546,6 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
434
546
  throw new Error("No writable connection to " + this.peerId.toString());
435
547
  }
436
548
 
437
- this.usedBandWidthTracker.add(data.byteLength);
438
-
439
549
  // Write to all current outbound streams (normally 1, but >1 during grace)
440
550
  const payload = data instanceof Uint8Array ? data : data.subarray();
441
551
  let successes = 0;
@@ -449,6 +559,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
449
559
  }
450
560
 
451
561
  try {
562
+ this.assertQueueCapacity(c, payload.byteLength, priority);
452
563
  c.pushable.push(payload, getLaneFromPriority(priority));
453
564
  successes++;
454
565
  } catch (e) {
@@ -458,6 +569,12 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
458
569
  }
459
570
  }
460
571
  if (successes === 0) {
572
+ const backpressureFailures = failures.filter(
573
+ (f) => f instanceof BackpressureError,
574
+ );
575
+ if (backpressureFailures.length === failures.length) {
576
+ throw backpressureFailures[0];
577
+ }
461
578
  throw new Error(
462
579
  "All outbound writes failed (" +
463
580
  failures.map((f) => f?.message).join(", ") +
@@ -489,6 +606,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
489
606
  else this.dispatchEvent(new CustomEvent("stream:outbound"));
490
607
  }
491
608
  }
609
+ this.usedBandWidthTracker.add(payload.byteLength);
492
610
  }
493
611
 
494
612
  /**
@@ -506,54 +624,63 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
506
624
  return;
507
625
  }
508
626
 
509
- if (this.isWritable) {
510
- this.write(bytes, priority);
511
- return;
512
- }
513
-
514
- // Outbound stream negotiation can legitimately take several seconds in CI
515
- // (identify/protocol discovery, resource contention, etc). Keep this fairly
516
- // generous so control-plane messages (joins/subscriptions) don't flap.
517
- const timeoutMs = 10_000;
627
+ while (true) {
628
+ if (!this.isWritable) {
629
+ // Outbound stream negotiation can legitimately take several seconds in CI
630
+ // (identify/protocol discovery, resource contention, etc). Keep this fairly
631
+ // generous so control-plane messages (joins/subscriptions) don't flap.
632
+ const timeoutMs = 10_000;
633
+
634
+ await new Promise<void>((resolve, reject) => {
635
+ const onOutbound = () => {
636
+ cleanup();
637
+ resolve();
638
+ };
639
+
640
+ const onAbortOrClose = () => {
641
+ cleanup();
642
+ reject(new AbortError("Closed"));
643
+ };
644
+
645
+ const onTimeout = () => {
646
+ cleanup();
647
+ reject(
648
+ new TimeoutError("Failed to deliver message, never reachable"),
649
+ );
650
+ };
518
651
 
519
- await new Promise<void>((resolve, reject) => {
520
- const onOutbound = () => {
521
- cleanup();
522
- resolve();
523
- };
652
+ const timerId = setTimeout(onTimeout, timeoutMs);
524
653
 
525
- const onAbortOrClose = () => {
526
- cleanup();
527
- reject(new AbortError("Closed"));
528
- };
529
-
530
- const onTimeout = () => {
531
- cleanup();
532
- reject(new TimeoutError("Failed to deliver message, never reachable"));
533
- };
654
+ const cleanup = () => {
655
+ clearTimeout(timerId);
656
+ this.removeEventListener("stream:outbound", onOutbound);
657
+ this.removeEventListener("close", onAbortOrClose);
658
+ signal?.removeEventListener("abort", onAbortOrClose);
659
+ };
534
660
 
535
- const timerId = setTimeout(onTimeout, timeoutMs);
536
-
537
- const cleanup = () => {
538
- clearTimeout(timerId);
539
- this.removeEventListener("stream:outbound", onOutbound);
540
- this.removeEventListener("close", onAbortOrClose);
541
- signal?.removeEventListener("abort", onAbortOrClose);
542
- };
661
+ this.addEventListener("stream:outbound", onOutbound, { once: true });
662
+ this.addEventListener("close", onAbortOrClose, { once: true });
663
+ if (signal?.aborted) {
664
+ onAbortOrClose();
665
+ } else {
666
+ signal?.addEventListener("abort", onAbortOrClose, { once: true });
667
+ }
543
668
 
544
- this.addEventListener("stream:outbound", onOutbound, { once: true });
545
- this.addEventListener("close", onAbortOrClose, { once: true });
546
- if (signal?.aborted) {
547
- onAbortOrClose();
548
- } else {
549
- signal?.addEventListener("abort", onAbortOrClose, { once: true });
669
+ // Catch a race where writability flips after the first check.
670
+ if (this.isWritable) onOutbound();
671
+ });
550
672
  }
551
673
 
552
- // Catch a race where writability flips after the first check.
553
- if (this.isWritable) onOutbound();
554
- });
555
-
556
- this.write(bytes, priority);
674
+ try {
675
+ this.write(bytes, priority);
676
+ return;
677
+ } catch (error) {
678
+ if (!(error instanceof BackpressureError)) {
679
+ throw error;
680
+ }
681
+ await this.waitForQueueCapacity(bytes.byteLength, priority, signal);
682
+ }
683
+ }
557
684
  }
558
685
 
559
686
  /**
@@ -858,6 +985,15 @@ export type ConnectionManagerArguments =
858
985
  } & { dialer?: Partial<DialerOptions> | false })
859
986
  | false;
860
987
 
988
+ type OutboundQueueArguments =
989
+ | {
990
+ maxBufferedBytes?: number;
991
+ reservedPriorityBytes?: number;
992
+ maxTotalBufferedBytes?: number;
993
+ reservedTotalPriorityBytes?: number;
994
+ }
995
+ | false;
996
+
861
997
  export type DirectStreamOptions = {
862
998
  canRelayMessage?: boolean;
863
999
  messageProcessingConcurrency?: number;
@@ -886,6 +1022,7 @@ export type DirectStreamOptions = {
886
1022
  sharedRouting?: boolean;
887
1023
  seenCacheMax?: number;
888
1024
  seenCacheTtlMs?: number;
1025
+ outboundQueue?: OutboundQueueArguments;
889
1026
  };
890
1027
 
891
1028
  type ConnectionManagerLike = {
@@ -926,6 +1063,8 @@ const sharedRoutingByPrivateKey = new WeakMap<PrivateKey, SharedRoutingState>();
926
1063
 
927
1064
  export type PublishOptions = (WithMode | WithTo) &
928
1065
  PriorityOptions &
1066
+ ResponsePriorityOptions &
1067
+ ExpiresAtOptions &
929
1068
  WithExtraSigners;
930
1069
 
931
1070
  export abstract class DirectStream<
@@ -975,6 +1114,11 @@ export abstract class DirectStream<
975
1114
  private routeCacheMaxTargetsPerFrom?: number;
976
1115
  private routeCacheMaxRelaysPerTarget?: number;
977
1116
  private readonly sharedRouting: boolean;
1117
+ private readonly outboundQueueOptions?: OutboundQueueOptions;
1118
+ private readonly totalOutboundQueueWaiters: Set<{
1119
+ limitBytes: number;
1120
+ deferred: DeferredPromise<void>;
1121
+ }> = new Set();
978
1122
  private sharedRoutingKey?: PrivateKey;
979
1123
  private sharedRoutingState?: SharedRoutingState;
980
1124
 
@@ -1023,6 +1167,7 @@ export abstract class DirectStream<
1023
1167
  seenCacheMax = 1e6,
1024
1168
  seenCacheTtlMs = 10 * 60 * 1e3,
1025
1169
  inboundIdleTimeout,
1170
+ outboundQueue,
1026
1171
  } = options || {};
1027
1172
 
1028
1173
  const signKey = getKeypairFromPrivateKey(components.privateKey);
@@ -1088,6 +1233,50 @@ export abstract class DirectStream<
1088
1233
  : undefined,
1089
1234
  };
1090
1235
  }
1236
+ if (outboundQueue === false) {
1237
+ this.outboundQueueOptions = undefined;
1238
+ } else {
1239
+ const maxBufferedBytes = Math.max(
1240
+ MAX_DATA_LENGTH_OUT,
1241
+ Math.floor(
1242
+ outboundQueue?.maxBufferedBytes ?? DEFAULT_OUTBOUND_QUEUE_MAX_BYTES,
1243
+ ),
1244
+ );
1245
+ const reservedPriorityBytes = Math.max(
1246
+ 0,
1247
+ Math.min(
1248
+ maxBufferedBytes,
1249
+ Math.floor(
1250
+ outboundQueue?.reservedPriorityBytes ??
1251
+ DEFAULT_OUTBOUND_QUEUE_RESERVED_PRIORITY_BYTES,
1252
+ ),
1253
+ ),
1254
+ );
1255
+ const maxTotalBufferedBytes = Math.max(
1256
+ maxBufferedBytes,
1257
+ Math.floor(
1258
+ outboundQueue?.maxTotalBufferedBytes ??
1259
+ this.connectionManagerOptions.pruner?.maxBuffer ??
1260
+ MAX_QUEUED_BYTES,
1261
+ ),
1262
+ );
1263
+ const reservedTotalPriorityBytes = Math.max(
1264
+ 0,
1265
+ Math.min(
1266
+ maxTotalBufferedBytes,
1267
+ Math.floor(
1268
+ outboundQueue?.reservedTotalPriorityBytes ??
1269
+ DEFAULT_OUTBOUND_QUEUE_RESERVED_PRIORITY_BYTES,
1270
+ ),
1271
+ ),
1272
+ );
1273
+ this.outboundQueueOptions = {
1274
+ maxBufferedBytes,
1275
+ reservedPriorityBytes,
1276
+ maxTotalBufferedBytes,
1277
+ reservedTotalPriorityBytes,
1278
+ };
1279
+ }
1091
1280
 
1092
1281
  this.recentDials = this.connectionManagerOptions.dialer
1093
1282
  ? new Cache({
@@ -1802,30 +1991,35 @@ export abstract class DirectStream<
1802
1991
  publicKey,
1803
1992
  protocol,
1804
1993
  connId,
1994
+ outboundQueue: this.outboundQueueOptions,
1805
1995
  });
1806
1996
 
1807
1997
  this.peers.set(publicKeyHash, peerStreams);
1808
1998
  this.updateSession(publicKey, -1);
1809
1999
 
1810
2000
  // Propagate per-peer stream readiness events to the parent emitter
1811
- const forwardOutbound = () =>
1812
- this.dispatchEvent(new CustomEvent("stream:outbound"));
1813
- const forwardInbound = () =>
1814
- this.dispatchEvent(new CustomEvent("stream:inbound"));
1815
- peerStreams.addEventListener("stream:outbound", forwardOutbound);
1816
- peerStreams.addEventListener("stream:inbound", forwardInbound);
1817
-
1818
- peerStreams.addEventListener("close", () => this._removePeer(publicKey), {
1819
- once: true,
2001
+ const forwardOutbound = () =>
2002
+ this.dispatchEvent(new CustomEvent("stream:outbound"));
2003
+ const forwardInbound = () =>
2004
+ this.dispatchEvent(new CustomEvent("stream:inbound"));
2005
+ const forwardQueue = () => this.notifyTotalOutboundQueueWaiters();
2006
+ peerStreams.addEventListener("stream:outbound", forwardOutbound);
2007
+ peerStreams.addEventListener("stream:inbound", forwardInbound);
2008
+ peerStreams.addEventListener("queue:outbound", forwardQueue);
2009
+
2010
+ peerStreams.addEventListener("close", () => this._removePeer(publicKey), {
2011
+ once: true,
1820
2012
  });
1821
2013
  peerStreams.addEventListener(
1822
- "close",
1823
- () => {
1824
- peerStreams.removeEventListener("stream:outbound", forwardOutbound);
1825
- peerStreams.removeEventListener("stream:inbound", forwardInbound);
1826
- },
1827
- { once: true },
1828
- );
2014
+ "close",
2015
+ () => {
2016
+ peerStreams.removeEventListener("stream:outbound", forwardOutbound);
2017
+ peerStreams.removeEventListener("stream:inbound", forwardInbound);
2018
+ peerStreams.removeEventListener("queue:outbound", forwardQueue);
2019
+ this.notifyTotalOutboundQueueWaiters();
2020
+ },
2021
+ { once: true },
2022
+ );
1829
2023
 
1830
2024
  this.addRouteConnection(
1831
2025
  this.publicKeyHash,
@@ -2228,7 +2422,6 @@ export abstract class DirectStream<
2228
2422
  }
2229
2423
  }
2230
2424
  }
2231
-
2232
2425
  async onGoodBye(
2233
2426
  publicKey: PublicSignKey,
2234
2427
  peerStream: PeerStreams,
@@ -2328,6 +2521,8 @@ export abstract class DirectStream<
2328
2521
  data: Uint8Array | Uint8ArrayList | undefined,
2329
2522
  options: (WithTo | WithMode) &
2330
2523
  PriorityOptions &
2524
+ ResponsePriorityOptions &
2525
+ ExpiresAtOptions &
2331
2526
  IdOptions & { skipRecipientValidation?: boolean } & WithExtraSigners,
2332
2527
  ) {
2333
2528
  // dispatch the event if we are interested
@@ -2377,6 +2572,8 @@ export abstract class DirectStream<
2377
2572
  mode,
2378
2573
  session: this.session,
2379
2574
  priority: options.priority,
2575
+ responsePriority: options.responsePriority,
2576
+ expires: options.expiresAt,
2380
2577
  }),
2381
2578
  });
2382
2579
 
@@ -2827,14 +3024,24 @@ export abstract class DirectStream<
2827
3024
  for (const [neighbour, _distantPeers] of fanout) {
2828
3025
  const stream = this.peers.get(neighbour);
2829
3026
  if (!stream) continue;
2830
- if (message.header.mode instanceof SilentDelivery) {
2831
- message.header.mode.to = [..._distantPeers.keys()];
2832
- promises.push(
2833
- stream.waitForWrite(message.bytes(), message.header.priority),
2834
- );
2835
- } else {
2836
- promises.push(stream.waitForWrite(bytes, message.header.priority));
2837
- }
3027
+ if (message.header.mode instanceof SilentDelivery) {
3028
+ message.header.mode.to = [..._distantPeers.keys()];
3029
+ promises.push(
3030
+ this.waitForPeerWrite(
3031
+ stream,
3032
+ message.bytes(),
3033
+ message.header.priority,
3034
+ ),
3035
+ );
3036
+ } else {
3037
+ promises.push(
3038
+ this.waitForPeerWrite(
3039
+ stream,
3040
+ bytes,
3041
+ message.header.priority,
3042
+ ),
3043
+ );
3044
+ }
2838
3045
  usedNeighbours.add(neighbour);
2839
3046
  }
2840
3047
  if (message.header.mode instanceof SilentDelivery) {
@@ -2853,12 +3060,18 @@ export abstract class DirectStream<
2853
3060
  for (const [neighbour, stream] of this.peers) {
2854
3061
  if (usedNeighbours.size >= message.header.mode.redundancy) {
2855
3062
  break;
3063
+ }
3064
+ if (usedNeighbours.has(neighbour)) continue;
3065
+ usedNeighbours.add(neighbour);
3066
+ promises.push(
3067
+ this.waitForPeerWrite(
3068
+ stream,
3069
+ bytes,
3070
+ message.header.priority,
3071
+ ),
3072
+ );
2856
3073
  }
2857
- if (usedNeighbours.has(neighbour)) continue;
2858
- usedNeighbours.add(neighbour);
2859
- promises.push(stream.waitForWrite(bytes, message.header.priority));
2860
3074
  }
2861
- }
2862
3075
 
2863
3076
  await Promise.all(promises);
2864
3077
  return delivereyPromise;
@@ -2882,12 +3095,16 @@ export abstract class DirectStream<
2882
3095
  )
2883
3096
  ) {
2884
3097
  continue; // recipient already signed/seen this message
3098
+ }
3099
+ message.header.mode.to = [recipient];
3100
+ promises.push(
3101
+ this.waitForPeerWrite(
3102
+ stream,
3103
+ message.bytes(),
3104
+ message.header.priority,
3105
+ ),
3106
+ );
2885
3107
  }
2886
- message.header.mode.to = [recipient];
2887
- promises.push(
2888
- stream.waitForWrite(message.bytes(), message.header.priority),
2889
- );
2890
- }
2891
3108
  message.header.mode.to = originalTo;
2892
3109
  if (promises.length > 0) {
2893
3110
  await Promise.all(promises);
@@ -2924,11 +3141,11 @@ export abstract class DirectStream<
2924
3141
  )
2925
3142
  ) {
2926
3143
  continue;
2927
- }
3144
+ }
2928
3145
 
2929
- sentOnce = true;
2930
- promises.push(id.waitForWrite(bytes, message.header.priority));
2931
- }
3146
+ sentOnce = true;
3147
+ promises.push(this.waitForPeerWrite(id, bytes, message.header.priority));
3148
+ }
2932
3149
  await Promise.all(promises);
2933
3150
 
2934
3151
  if (!sentOnce) {
@@ -3188,6 +3405,121 @@ export abstract class DirectStream<
3188
3405
  return this.components.connectionManager.closeConnections(stream.peerId);
3189
3406
  }
3190
3407
 
3408
+ private getTotalQueueAdmissionLimitBytes(
3409
+ priority: number,
3410
+ ): number | undefined {
3411
+ const limitBytes = this.outboundQueueOptions?.maxTotalBufferedBytes;
3412
+ if (limitBytes == null) {
3413
+ return undefined;
3414
+ }
3415
+ if (getLaneFromPriority(priority) === getLaneFromPriority(0)) {
3416
+ return Math.max(
3417
+ 0,
3418
+ limitBytes - this.outboundQueueOptions.reservedTotalPriorityBytes,
3419
+ );
3420
+ }
3421
+ return limitBytes;
3422
+ }
3423
+
3424
+ private assertTotalQueueCapacity(
3425
+ peerId: PeerId,
3426
+ payloadBytes: number,
3427
+ priority: number,
3428
+ ) {
3429
+ const limitBytes = this.getTotalQueueAdmissionLimitBytes(priority);
3430
+ if (limitBytes == null) {
3431
+ return;
3432
+ }
3433
+ const currentBufferedBytes = this.getQueuedBytes();
3434
+ if (currentBufferedBytes + payloadBytes > limitBytes) {
3435
+ throw new BackpressureError({
3436
+ scope: "node",
3437
+ peerId,
3438
+ priority,
3439
+ limitBytes,
3440
+ currentBufferedBytes,
3441
+ attemptedBytes: payloadBytes,
3442
+ });
3443
+ }
3444
+ }
3445
+
3446
+ private notifyTotalOutboundQueueWaiters() {
3447
+ if (this.totalOutboundQueueWaiters.size === 0) {
3448
+ return;
3449
+ }
3450
+ const queuedBytes = this.getQueuedBytes();
3451
+ for (const waiter of [...this.totalOutboundQueueWaiters]) {
3452
+ if (queuedBytes <= waiter.limitBytes) {
3453
+ this.totalOutboundQueueWaiters.delete(waiter);
3454
+ waiter.deferred.resolve();
3455
+ }
3456
+ }
3457
+ }
3458
+
3459
+ private async waitForTotalQueueCapacity(
3460
+ payloadBytes: number,
3461
+ priority: number,
3462
+ signal?: AbortSignal,
3463
+ ) {
3464
+ const limitBytes = this.getTotalQueueAdmissionLimitBytes(priority);
3465
+ if (limitBytes == null) {
3466
+ return;
3467
+ }
3468
+ const threshold = Math.max(0, limitBytes - payloadBytes);
3469
+ if (this.getQueuedBytes() <= threshold) {
3470
+ return;
3471
+ }
3472
+ const waiter = {
3473
+ limitBytes: threshold,
3474
+ deferred: pDefer<void>(),
3475
+ };
3476
+ this.totalOutboundQueueWaiters.add(waiter);
3477
+
3478
+ let cancel: Promise<void> | undefined;
3479
+ let listener: (() => void) | undefined;
3480
+
3481
+ if (signal != null) {
3482
+ cancel = new Promise<void>((_resolve, reject) => {
3483
+ listener = () => {
3484
+ this.totalOutboundQueueWaiters.delete(waiter);
3485
+ reject(new AbortError());
3486
+ };
3487
+ signal.addEventListener("abort", listener!);
3488
+ });
3489
+ }
3490
+
3491
+ try {
3492
+ await Promise.race(
3493
+ cancel != null ? [waiter.deferred.promise, cancel] : [waiter.deferred.promise],
3494
+ );
3495
+ } finally {
3496
+ this.totalOutboundQueueWaiters.delete(waiter);
3497
+ if (listener != null) {
3498
+ signal?.removeEventListener("abort", listener);
3499
+ }
3500
+ }
3501
+ }
3502
+
3503
+ protected async waitForPeerWrite(
3504
+ stream: PeerStreams,
3505
+ bytes: Uint8Array | Uint8ArrayList,
3506
+ priority = 0,
3507
+ signal?: AbortSignal,
3508
+ ) {
3509
+ while (true) {
3510
+ try {
3511
+ this.assertTotalQueueCapacity(stream.peerId, bytes.byteLength, priority);
3512
+ await stream.waitForWrite(bytes, priority, signal);
3513
+ return;
3514
+ } catch (error) {
3515
+ if (!(error instanceof BackpressureError) || error.scope !== "node") {
3516
+ throw error;
3517
+ }
3518
+ await this.waitForTotalQueueCapacity(bytes.byteLength, priority, signal);
3519
+ }
3520
+ }
3521
+ }
3522
+
3191
3523
  getQueuedBytes(): number {
3192
3524
  let sum = 0;
3193
3525
  for (const [_k, ps] of this.peers) {