@peerbit/stream 4.6.0 → 5.0.0-2d88223

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
@@ -29,6 +29,7 @@ import {
29
29
  import type { SignatureWithKey } from "@peerbit/crypto";
30
30
  import {
31
31
  ACK,
32
+ AcknowledgeAnyWhere,
32
33
  AcknowledgeDelivery,
33
34
  AnyWhere,
34
35
  DataMessage,
@@ -39,7 +40,6 @@ import {
39
40
  MessageHeader,
40
41
  MultiAddrinfo,
41
42
  NotStartedError,
42
- SeekDelivery,
43
43
  SilentDelivery,
44
44
  TracedDelivery,
45
45
  coercePeerRefsToHashes,
@@ -47,6 +47,7 @@ import {
47
47
  getMsgId,
48
48
  } from "@peerbit/stream-interface";
49
49
  import type {
50
+ DirectStreamAckRouteHint,
50
51
  IdOptions,
51
52
  PeerRefs,
52
53
  PriorityOptions,
@@ -69,6 +70,10 @@ import { type Pushable, pushable } from "it-pushable";
69
70
  import pDefer, { type DeferredPromise } from "p-defer";
70
71
  import Queue from "p-queue";
71
72
  import { Uint8ArrayList } from "uint8arraylist";
73
+ import {
74
+ computeSeekAckRouteUpdate,
75
+ shouldAcknowledgeDataMessage,
76
+ } from "./core/seek-routing.js";
72
77
  import { logger } from "./logger.js";
73
78
  import { type PushableLanes, pushableLanes } from "./pushable-lanes.js";
74
79
  import { MAX_ROUTE_DISTANCE, Routes } from "./routes.js";
@@ -167,8 +172,6 @@ const DEFAULT_MAX_CONNECTIONS = 300;
167
172
 
168
173
  const DEFAULT_PRUNED_CONNNECTIONS_TIMEOUT = 30 * 1000;
169
174
 
170
- const ROUTE_UPDATE_DELAY_FACTOR = 3e4;
171
-
172
175
  const DEFAULT_CREATE_OUTBOUND_STREAM_TIMEOUT = 30_000;
173
176
 
174
177
  const PRIORITY_LANES = 4;
@@ -261,6 +264,19 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
261
264
  public _getActiveOutboundPushable(): PushableLanes<Uint8Array> | undefined {
262
265
  return this.outboundStreams[0]?.pushable;
263
266
  }
267
+ public getOutboundQueuedBytes(): number {
268
+ return this._getActiveOutboundPushable()?.readableLength ?? 0;
269
+ }
270
+
271
+ public getOutboundQueuedBytesByLane(): number[] {
272
+ const p = this._getActiveOutboundPushable();
273
+ if (!p) return Array(PRIORITY_LANES).fill(0);
274
+ const out: number[] = [];
275
+ for (let lane = 0; lane < PRIORITY_LANES; lane++) {
276
+ out.push(p.getReadableLength(lane));
277
+ }
278
+ return out;
279
+ }
264
280
  public _getOutboundCount() {
265
281
  return this.outboundStreams.length;
266
282
  }
@@ -433,10 +449,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
433
449
  }
434
450
 
435
451
  try {
436
- c.pushable.push(
437
- payload,
438
- getLaneFromPriority(priority),
439
- );
452
+ c.pushable.push(payload, getLaneFromPriority(priority));
440
453
  successes++;
441
454
  } catch (e) {
442
455
  failures.push(e);
@@ -498,7 +511,10 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
498
511
  return;
499
512
  }
500
513
 
501
- const timeoutMs = 3_000;
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;
502
518
 
503
519
  await new Promise<void>((resolve, reject) => {
504
520
  const onOutbound = () => {
@@ -783,21 +799,29 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
783
799
  this.outboundAbortController.abort();
784
800
  }
785
801
 
786
- // End inbound streams
787
- if (this.inboundStreams.length) {
788
- for (const inbound of this.inboundStreams) {
789
- try {
790
- inbound.abortController.abort();
791
- } catch {
792
- logger.error("Failed to abort inbound stream");
793
- }
794
- try {
795
- await inbound.raw.close?.();
796
- } catch {
797
- logger.error("Failed to close inbound stream");
802
+ // End inbound streams
803
+ if (this.inboundStreams.length) {
804
+ for (const inbound of this.inboundStreams) {
805
+ try {
806
+ inbound.abortController.abort();
807
+ } catch {
808
+ logger.error("Failed to abort inbound stream");
809
+ }
810
+ try {
811
+ // Best-effort shutdown: on some transports (notably websockets),
812
+ // awaiting a graceful close can hang indefinitely if the remote is
813
+ // concurrently stopping. Abort immediately and do not await close.
814
+ try {
815
+ inbound.raw.abort?.(new AbortError("Closed"));
816
+ } catch {
817
+ // ignore
818
+ }
819
+ void Promise.resolve(inbound.raw.close?.()).catch(() => {});
820
+ } catch {
821
+ logger.error("Failed to close inbound stream");
822
+ }
798
823
  }
799
824
  }
800
- }
801
825
 
802
826
  this.usedBandWidthTracker.stop();
803
827
 
@@ -841,9 +865,27 @@ export type DirectStreamOptions = {
841
865
  maxOutboundStreams?: number;
842
866
  inboundIdleTimeout?: number; // override PeerStreams.INBOUND_IDLE_MS
843
867
  connectionManager?: ConnectionManagerArguments;
844
- routeSeekInterval?: number;
845
868
  seekTimeout?: number;
846
869
  routeMaxRetentionPeriod?: number;
870
+ /**
871
+ * Best-effort bounds for the per-process route cache. These exist to prevent
872
+ * unbounded memory growth in large networks/simulations.
873
+ */
874
+ routeCacheMaxFromEntries?: number;
875
+ routeCacheMaxTargetsPerFrom?: number;
876
+ routeCacheMaxRelaysPerTarget?: number;
877
+ /**
878
+ * Share node-level routing/session state across DirectStream instances created
879
+ * from the same libp2p private key.
880
+ *
881
+ * This reduces duplicated topology knowledge when multiple protocols run on
882
+ * the same node (e.g. pubsub + fanout overlays).
883
+ *
884
+ * Defaults to `true`.
885
+ */
886
+ sharedRouting?: boolean;
887
+ seenCacheMax?: number;
888
+ seenCacheTtlMs?: number;
847
889
  };
848
890
 
849
891
  type ConnectionManagerLike = {
@@ -873,16 +915,25 @@ export interface DirectStreamComponents {
873
915
  privateKey: PrivateKey;
874
916
  }
875
917
 
918
+ type SharedRoutingState = {
919
+ session: number;
920
+ routes: Routes;
921
+ controller: AbortController;
922
+ refs: number;
923
+ };
924
+
925
+ const sharedRoutingByPrivateKey = new WeakMap<PrivateKey, SharedRoutingState>();
926
+
876
927
  export type PublishOptions = (WithMode | WithTo) &
877
928
  PriorityOptions &
878
929
  WithExtraSigners;
879
930
 
880
931
  export abstract class DirectStream<
881
- Events extends { [s: string]: any } = StreamEvents,
882
- >
883
- extends TypedEventEmitter<Events>
884
- implements WaitForPeer, PublicKeyFromHashResolver
885
- {
932
+ Events extends { [s: string]: any } = StreamEvents,
933
+ >
934
+ extends TypedEventEmitter<Events>
935
+ implements WaitForPeer, PublicKeyFromHashResolver
936
+ {
886
937
  public peerId: PeerId;
887
938
  public publicKey: PublicSignKey;
888
939
  public publicKeyHash: string;
@@ -908,26 +959,35 @@ export abstract class DirectStream<
908
959
  public multicodecs: string[];
909
960
  public seenCache: Cache<number>;
910
961
  private _registrarTopologyIds: string[] | undefined;
962
+ private _peerConnectListener?: (ev: any) => void;
963
+ private _peerDisconnectListener?: (ev: any) => void;
911
964
  private readonly maxInboundStreams?: number;
912
965
  private readonly maxOutboundStreams?: number;
913
966
  connectionManagerOptions: ConnectionManagerOptions;
914
967
  private recentDials?: Cache<string>;
915
968
  private healthChecks: Map<string, ReturnType<typeof setTimeout>>;
916
- private pruneConnectionsTimeout: ReturnType<typeof setInterval>;
917
- private prunedConnectionsCache?: Cache<string>;
918
- private routeMaxRetentionPeriod: number;
969
+ private pruneConnectionsTimeout: ReturnType<typeof setInterval>;
970
+ private prunedConnectionsCache?: Cache<string>;
971
+ private pruneToLimitsInFlight?: Promise<void>;
972
+ private _startInFlight?: Promise<void>;
973
+ private routeMaxRetentionPeriod: number;
974
+ private routeCacheMaxFromEntries?: number;
975
+ private routeCacheMaxTargetsPerFrom?: number;
976
+ private routeCacheMaxRelaysPerTarget?: number;
977
+ private readonly sharedRouting: boolean;
978
+ private sharedRoutingKey?: PrivateKey;
979
+ private sharedRoutingState?: SharedRoutingState;
919
980
 
920
981
  // for sequential creation of outbound streams
921
- public outboundInflightQueue: Pushable<{
922
- connection: Connection;
923
- peerId: PeerId;
924
- }>;
982
+ public outboundInflightQueue: Pushable<{
983
+ connection: Connection;
984
+ peerId: PeerId;
985
+ }>;
925
986
 
926
- routeSeekInterval: number;
927
- seekTimeout: number;
928
- closeController: AbortController;
929
- session: number;
930
- _outboundPump: ReturnType<typeof pipe> | undefined;
987
+ seekTimeout: number;
988
+ closeController: AbortController;
989
+ session: number;
990
+ _outboundPump: ReturnType<typeof pipe> | undefined;
931
991
 
932
992
  private _ackCallbacks: Map<
933
993
  string,
@@ -951,14 +1011,19 @@ export abstract class DirectStream<
951
1011
  const {
952
1012
  canRelayMessage = true,
953
1013
  messageProcessingConcurrency = 10,
954
- maxInboundStreams,
955
- maxOutboundStreams,
956
- connectionManager,
957
- routeSeekInterval = ROUTE_UPDATE_DELAY_FACTOR,
958
- seekTimeout = SEEK_DELIVERY_TIMEOUT,
959
- routeMaxRetentionPeriod = ROUTE_MAX_RETANTION_PERIOD,
960
- inboundIdleTimeout,
961
- } = options || {};
1014
+ maxInboundStreams,
1015
+ maxOutboundStreams,
1016
+ connectionManager,
1017
+ seekTimeout = SEEK_DELIVERY_TIMEOUT,
1018
+ routeMaxRetentionPeriod = ROUTE_MAX_RETANTION_PERIOD,
1019
+ routeCacheMaxFromEntries,
1020
+ routeCacheMaxTargetsPerFrom,
1021
+ routeCacheMaxRelaysPerTarget,
1022
+ sharedRouting = true,
1023
+ seenCacheMax = 1e6,
1024
+ seenCacheTtlMs = 10 * 60 * 1e3,
1025
+ inboundIdleTimeout,
1026
+ } = options || {};
962
1027
 
963
1028
  const signKey = getKeypairFromPrivateKey(components.privateKey);
964
1029
  this.seekTimeout = seekTimeout;
@@ -972,19 +1037,25 @@ export abstract class DirectStream<
972
1037
  this.publicKeyHash = signKey.publicKey.hashcode();
973
1038
  this.multicodecs = multicodecs;
974
1039
  this.started = false;
975
- this.peers = new Map<string, PeerStreams>();
976
- this.canRelayMessage = canRelayMessage;
977
- this.healthChecks = new Map();
978
- this.routeSeekInterval = routeSeekInterval;
979
- this.queue = new Queue({ concurrency: messageProcessingConcurrency });
980
- this.maxInboundStreams = maxInboundStreams;
981
- this.maxOutboundStreams = maxOutboundStreams;
982
- this.seenCache = new Cache({ max: 1e6, ttl: 10 * 60 * 1e3 });
983
- this.routeMaxRetentionPeriod = routeMaxRetentionPeriod;
984
- this.peerKeyHashToPublicKey = new Map();
985
- this._onIncomingStream = this._onIncomingStream.bind(this);
986
- this.onPeerConnected = this.onPeerConnected.bind(this);
987
- this.onPeerDisconnected = this.onPeerDisconnected.bind(this);
1040
+ this.peers = new Map<string, PeerStreams>();
1041
+ this.canRelayMessage = canRelayMessage;
1042
+ this.healthChecks = new Map();
1043
+ this.queue = new Queue({ concurrency: messageProcessingConcurrency });
1044
+ this.maxInboundStreams = maxInboundStreams;
1045
+ this.maxOutboundStreams = maxOutboundStreams;
1046
+ this.seenCache = new Cache({
1047
+ max: Math.max(1, Math.floor(seenCacheMax)),
1048
+ ttl: Math.max(1, Math.floor(seenCacheTtlMs)),
1049
+ });
1050
+ this.routeMaxRetentionPeriod = routeMaxRetentionPeriod;
1051
+ this.routeCacheMaxFromEntries = routeCacheMaxFromEntries;
1052
+ this.routeCacheMaxTargetsPerFrom = routeCacheMaxTargetsPerFrom;
1053
+ this.routeCacheMaxRelaysPerTarget = routeCacheMaxRelaysPerTarget;
1054
+ this.sharedRouting = sharedRouting !== false;
1055
+ this.peerKeyHashToPublicKey = new Map();
1056
+ this._onIncomingStream = this._onIncomingStream.bind(this);
1057
+ this.onPeerConnected = this.onPeerConnected.bind(this);
1058
+ this.onPeerDisconnected = this.onPeerDisconnected.bind(this);
988
1059
 
989
1060
  this._ackCallbacks = new Map();
990
1061
 
@@ -1025,23 +1096,60 @@ export abstract class DirectStream<
1025
1096
  })
1026
1097
  : undefined;
1027
1098
 
1028
- this.prunedConnectionsCache = this.connectionManagerOptions.pruner
1029
- ? new Cache({
1030
- max: 1e6,
1031
- ttl: this.connectionManagerOptions.pruner.connectionTimeout,
1032
- })
1033
- : undefined;
1034
- }
1099
+ this.prunedConnectionsCache = this.connectionManagerOptions.pruner
1100
+ ? new Cache({
1101
+ max: 1e6,
1102
+ ttl: this.connectionManagerOptions.pruner.connectionTimeout,
1103
+ })
1104
+ : undefined;
1105
+ }
1035
1106
 
1036
- async start() {
1037
- if (this.started) {
1038
- return;
1107
+ private pruneConnectionsToLimits(): Promise<void> {
1108
+ if (this.pruneToLimitsInFlight) {
1109
+ return this.pruneToLimitsInFlight;
1110
+ }
1111
+ this.pruneToLimitsInFlight = (async () => {
1112
+ // Respect minConnections as a hard floor.
1113
+ const maxConnections = Math.max(
1114
+ this.connectionManagerOptions.minConnections,
1115
+ this.connectionManagerOptions.maxConnections,
1116
+ );
1117
+ if (this.peers.size <= maxConnections) {
1118
+ return;
1119
+ }
1120
+
1121
+ // Prune in batches so we can quickly recover from join storms without waiting
1122
+ // for repeated pruner ticks. Bound work per run to avoid starving the event loop.
1123
+ const maxPrunes = Math.min(256, Math.max(1, this.peers.size - maxConnections));
1124
+ for (let i = 0; i < maxPrunes; i++) {
1125
+ if (this.peers.size <= maxConnections) break;
1126
+ const before = this.peers.size;
1127
+ await this.pruneConnections();
1128
+ if (this.peers.size >= before) break; // nothing prunable
1129
+ }
1130
+ })().finally(() => {
1131
+ this.pruneToLimitsInFlight = undefined;
1132
+ });
1133
+ return this.pruneToLimitsInFlight;
1039
1134
  }
1040
1135
 
1041
- this.session = +new Date();
1042
- await ready;
1136
+ async start() {
1137
+ if (this.started) return;
1138
+ if (this._startInFlight) return this._startInFlight;
1139
+ this._startInFlight = this._startImpl().finally(() => {
1140
+ this._startInFlight = undefined;
1141
+ });
1142
+ return this._startInFlight;
1143
+ }
1144
+
1145
+ private async _startImpl() {
1146
+ if (this.started) {
1147
+ return;
1148
+ }
1149
+
1150
+ await ready;
1043
1151
 
1044
- this.closeController = new AbortController();
1152
+ this.closeController = new AbortController();
1045
1153
 
1046
1154
  this.outboundInflightQueue = pushable({ objectMode: true });
1047
1155
 
@@ -1119,10 +1227,47 @@ export abstract class DirectStream<
1119
1227
  this.outboundInflightQueue.return();
1120
1228
  });
1121
1229
 
1122
- this.routes = new Routes(this.publicKeyHash, {
1123
- routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1124
- signal: this.closeController.signal,
1125
- });
1230
+ if (this.sharedRouting) {
1231
+ const key = this.components.privateKey;
1232
+ this.sharedRoutingKey = key;
1233
+ let state = sharedRoutingByPrivateKey.get(key);
1234
+ if (!state) {
1235
+ const controller = new AbortController();
1236
+ state = {
1237
+ session: Date.now(),
1238
+ controller,
1239
+ routes: new Routes(this.publicKeyHash, {
1240
+ routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1241
+ signal: controller.signal,
1242
+ maxFromEntries: this.routeCacheMaxFromEntries,
1243
+ maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
1244
+ maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
1245
+ }),
1246
+ refs: 0,
1247
+ };
1248
+ sharedRoutingByPrivateKey.set(key, state);
1249
+ } else {
1250
+ // Best-effort: prefer the strictest cleanup policy among co-located protocols.
1251
+ state.routes.routeMaxRetentionPeriod = Math.min(
1252
+ state.routes.routeMaxRetentionPeriod,
1253
+ this.routeMaxRetentionPeriod,
1254
+ );
1255
+ }
1256
+
1257
+ state.refs += 1;
1258
+ this.sharedRoutingState = state;
1259
+ this.session = state.session;
1260
+ this.routes = state.routes;
1261
+ } else {
1262
+ this.session = Date.now();
1263
+ this.routes = new Routes(this.publicKeyHash, {
1264
+ routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1265
+ signal: this.closeController.signal,
1266
+ maxFromEntries: this.routeCacheMaxFromEntries,
1267
+ maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
1268
+ maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
1269
+ });
1270
+ }
1126
1271
 
1127
1272
  this.started = true;
1128
1273
  this.stopping = false;
@@ -1152,6 +1297,46 @@ export abstract class DirectStream<
1152
1297
  ),
1153
1298
  );
1154
1299
 
1300
+ // Best-effort fallback: topology callbacks can depend on identify/protocol
1301
+ // discovery. Some test environments connect peers without triggering the
1302
+ // per-protocol topology immediately. Listening to peer connection events
1303
+ // ensures we still attempt to open outbound streams opportunistically.
1304
+ this._peerConnectListener = (ev: any) => {
1305
+ if (this.stopping || !this.started) return;
1306
+ const peerId = (ev as any)?.detail ?? (ev as any)?.peerId;
1307
+ if (!peerId) return;
1308
+ const conns = this.components.connectionManager.getConnections(peerId);
1309
+ if (!conns || conns.length === 0) return;
1310
+ let conn = conns[0]!;
1311
+ for (const c of conns) {
1312
+ if (!isWebsocketConnection(c)) {
1313
+ conn = c;
1314
+ break;
1315
+ }
1316
+ }
1317
+ void this.onPeerConnected(peerId, conn);
1318
+ };
1319
+ this._peerDisconnectListener = (ev: any) => {
1320
+ if (this.stopping || !this.started) return;
1321
+ const peerId = (ev as any)?.detail ?? (ev as any)?.peerId;
1322
+ if (!peerId) return;
1323
+ const conns = this.components.connectionManager.getConnections(peerId);
1324
+ const conn = conns && conns.length > 0 ? conns[0] : undefined;
1325
+ void this.onPeerDisconnected(peerId, conn);
1326
+ };
1327
+ try {
1328
+ this.components.events.addEventListener(
1329
+ "peer:connect",
1330
+ this._peerConnectListener as any,
1331
+ );
1332
+ this.components.events.addEventListener(
1333
+ "peer:disconnect",
1334
+ this._peerDisconnectListener as any,
1335
+ );
1336
+ } catch {
1337
+ // ignore unsupported event targets
1338
+ }
1339
+
1155
1340
  // All existing connections are like new ones for us. To deduplication on remotes so we only resuse one connection for this protocol (we could be connected with many connections)
1156
1341
  const peerToConnections: Map<string, Connection[]> = new Map();
1157
1342
  const connections = this.components.connectionManager.getConnections();
@@ -1201,7 +1386,28 @@ export abstract class DirectStream<
1201
1386
  return;
1202
1387
  }
1203
1388
 
1389
+ const sharedState = this.sharedRoutingState;
1390
+ const sharedKey = this.sharedRoutingKey;
1391
+
1204
1392
  clearTimeout(this.pruneConnectionsTimeout);
1393
+ try {
1394
+ if (this._peerConnectListener) {
1395
+ this.components.events.removeEventListener(
1396
+ "peer:connect",
1397
+ this._peerConnectListener as any,
1398
+ );
1399
+ }
1400
+ if (this._peerDisconnectListener) {
1401
+ this.components.events.removeEventListener(
1402
+ "peer:disconnect",
1403
+ this._peerDisconnectListener as any,
1404
+ );
1405
+ }
1406
+ } catch {
1407
+ // ignore unsupported event targets
1408
+ }
1409
+ this._peerConnectListener = undefined;
1410
+ this._peerDisconnectListener = undefined;
1205
1411
 
1206
1412
  await Promise.all(
1207
1413
  this.multicodecs.map((x) => this.components.registrar.unhandle(x)),
@@ -1235,7 +1441,11 @@ export abstract class DirectStream<
1235
1441
  this.queue.clear();
1236
1442
  this.peers.clear();
1237
1443
  this.seenCache.clear();
1238
- this.routes.clear();
1444
+ // When routing is shared across co-located protocols, only clear once the last
1445
+ // instance stops. Otherwise we'd wipe routes still in use by other services.
1446
+ if (!sharedState) {
1447
+ this.routes.clear();
1448
+ }
1239
1449
  this.peerKeyHashToPublicKey.clear();
1240
1450
 
1241
1451
  for (const [_k, v] of this._ackCallbacks) {
@@ -1243,6 +1453,24 @@ export abstract class DirectStream<
1243
1453
  }
1244
1454
 
1245
1455
  this._ackCallbacks.clear();
1456
+ this.sharedRoutingState = undefined;
1457
+ this.sharedRoutingKey = undefined;
1458
+ if (sharedState && sharedKey) {
1459
+ sharedState.refs = Math.max(0, sharedState.refs - 1);
1460
+ if (sharedState.refs === 0) {
1461
+ try {
1462
+ sharedState.routes.clear();
1463
+ } catch {
1464
+ // ignore
1465
+ }
1466
+ try {
1467
+ sharedState.controller.abort();
1468
+ } catch {
1469
+ // ignore
1470
+ }
1471
+ sharedRoutingByPrivateKey.delete(sharedKey);
1472
+ }
1473
+ }
1246
1474
  logger.trace("stopped");
1247
1475
  this.stopping = false;
1248
1476
  }
@@ -1439,7 +1667,8 @@ export abstract class DirectStream<
1439
1667
  ).catch(dontThrowIfDeliveryError);
1440
1668
  }
1441
1669
 
1442
- this.checkIsAlive([peerKeyHash]);
1670
+ // Best-effort liveness probe; never let background probe failures crash callers.
1671
+ void this.checkIsAlive([peerKeyHash]).catch(() => false);
1443
1672
  }
1444
1673
 
1445
1674
  logger.trace("connection ended:" + peerKey.toString());
@@ -1464,27 +1693,31 @@ export abstract class DirectStream<
1464
1693
  distance: number,
1465
1694
  session: number,
1466
1695
  remoteSession: number,
1467
- ) {
1468
- const targetHash = typeof target === "string" ? target : target.hashcode();
1696
+ ) {
1697
+ const targetHash = typeof target === "string" ? target : target.hashcode();
1698
+ // Best-effort: keep a hash -> public key map for any routed targets so
1699
+ // peer:unreachable events can always carry a PublicSignKey when we have seen it.
1700
+ if (typeof target !== "string") {
1701
+ this.peerKeyHashToPublicKey.set(targetHash, target);
1702
+ }
1469
1703
 
1470
- const update = this.routes.add(
1471
- from,
1472
- neighbour,
1473
- targetHash,
1704
+ const update = this.routes.add(
1705
+ from,
1706
+ neighbour,
1707
+ targetHash,
1474
1708
  distance,
1475
1709
  session,
1476
1710
  remoteSession,
1477
1711
  );
1478
1712
 
1479
- // second condition is that we don't want to emit 'reachable' events for routes where we act only as a relay
1480
- // in this case, from is != this.publicKeyhash
1481
- if (from === this.publicKeyHash) {
1482
- if (update === "new") {
1483
- this.peerKeyHashToPublicKey.set(target.hashcode(), target);
1484
- this.onPeerReachable(target);
1713
+ // second condition is that we don't want to emit 'reachable' events for routes where we act only as a relay
1714
+ // in this case, from is != this.publicKeyhash
1715
+ if (from === this.publicKeyHash) {
1716
+ if (update === "new") {
1717
+ this.onPeerReachable(target);
1718
+ }
1485
1719
  }
1486
1720
  }
1487
- }
1488
1721
 
1489
1722
  public onPeerReachable(publicKey: PublicSignKey) {
1490
1723
  // override this fn
@@ -1493,16 +1726,19 @@ export abstract class DirectStream<
1493
1726
  );
1494
1727
  }
1495
1728
 
1496
- public onPeerUnreachable(hash: string) {
1497
- // override this fns
1498
-
1499
- this.dispatchEvent(
1500
- // TODO types
1501
- new CustomEvent("peer:unreachable", {
1502
- detail: this.peerKeyHashToPublicKey.get(hash)!,
1503
- }),
1504
- );
1505
- }
1729
+ public onPeerUnreachable(hash: string) {
1730
+ // override this fns
1731
+ const key = this.peerKeyHashToPublicKey.get(hash);
1732
+ if (!key) {
1733
+ // Best-effort: we may only have the hash (no public key) for some routes.
1734
+ // Avoid crashing downstream listeners that assume `detail` is a PublicSignKey.
1735
+ return;
1736
+ }
1737
+ this.dispatchEvent(
1738
+ // TODO types
1739
+ new CustomEvent("peer:unreachable", { detail: key }),
1740
+ );
1741
+ }
1506
1742
 
1507
1743
  public updateSession(key: PublicSignKey, session?: number) {
1508
1744
  if (this.routes.updateSession(key.hashcode(), session)) {
@@ -1513,6 +1749,20 @@ export abstract class DirectStream<
1513
1749
  this.routes.updateSession(key, undefined);
1514
1750
  }
1515
1751
 
1752
+ public getRouteHints(
1753
+ target: string,
1754
+ from: string = this.publicKeyHash,
1755
+ ): DirectStreamAckRouteHint[] {
1756
+ return this.routes.getRouteHints(from, target);
1757
+ }
1758
+
1759
+ public getBestRouteHint(
1760
+ target: string,
1761
+ from: string = this.publicKeyHash,
1762
+ ): DirectStreamAckRouteHint | undefined {
1763
+ return this.routes.getBestRouteHint(from, target);
1764
+ }
1765
+
1516
1766
  public onPeerSession(key: PublicSignKey, session: number) {
1517
1767
  this.dispatchEvent(
1518
1768
  // TODO types
@@ -1525,11 +1775,11 @@ export abstract class DirectStream<
1525
1775
  /**
1526
1776
  * Notifies the router that a peer has been connected
1527
1777
  */
1528
- addPeer(
1529
- peerId: PeerId,
1530
- publicKey: PublicSignKey,
1531
- protocol: string,
1532
- connId: string,
1778
+ addPeer(
1779
+ peerId: PeerId,
1780
+ publicKey: PublicSignKey,
1781
+ protocol: string,
1782
+ connId: string,
1533
1783
  ): PeerStreams {
1534
1784
  const publicKeyHash = publicKey.hashcode();
1535
1785
 
@@ -1577,17 +1827,23 @@ export abstract class DirectStream<
1577
1827
  { once: true },
1578
1828
  );
1579
1829
 
1580
- this.addRouteConnection(
1581
- this.publicKeyHash,
1582
- publicKey.hashcode(),
1583
- publicKey,
1584
- -1,
1585
- +new Date(),
1586
- -1,
1587
- );
1830
+ this.addRouteConnection(
1831
+ this.publicKeyHash,
1832
+ publicKey.hashcode(),
1833
+ publicKey,
1834
+ -1,
1835
+ +new Date(),
1836
+ -1,
1837
+ );
1588
1838
 
1589
- return peerStreams;
1590
- }
1839
+ // Enforce connection manager limits eagerly when new peers are added. Without this,
1840
+ // join storms can create large temporary peer sets and OOM in single-process sims.
1841
+ if (this.peers.size > this.connectionManagerOptions.maxConnections) {
1842
+ void this.pruneConnectionsToLimits().catch(() => {});
1843
+ }
1844
+
1845
+ return peerStreams;
1846
+ }
1591
1847
 
1592
1848
  /**
1593
1849
  * Notifies the router that a peer has been disconnected
@@ -1730,24 +1986,25 @@ export abstract class DirectStream<
1730
1986
  }
1731
1987
 
1732
1988
  public shouldIgnore(message: DataMessage, seenBefore: number) {
1733
- const fromMe = message.header.signatures?.publicKeys.find((x) =>
1734
- x.equals(this.publicKey),
1735
- );
1989
+ const signedBySelf =
1990
+ message.header.signatures?.publicKeys.some((x) =>
1991
+ x.equals(this.publicKey),
1992
+ ) ?? false;
1736
1993
 
1737
- if (fromMe) {
1994
+ if (signedBySelf) {
1738
1995
  return true;
1739
1996
  }
1740
1997
 
1998
+ // For acknowledged modes, allow limited duplicate forwarding so that we can
1999
+ // discover and maintain multiple candidate routes (distance=seenCounter).
1741
2000
  if (
1742
- (seenBefore > 0 &&
1743
- message.header.mode instanceof SeekDelivery === false) ||
1744
- (message.header.mode instanceof SeekDelivery &&
1745
- seenBefore >= message.header.mode.redundancy)
2001
+ message.header.mode instanceof AcknowledgeDelivery ||
2002
+ message.header.mode instanceof AcknowledgeAnyWhere
1746
2003
  ) {
1747
- return true;
2004
+ return seenBefore >= message.header.mode.redundancy;
1748
2005
  }
1749
2006
 
1750
- return false;
2007
+ return seenBefore > 0;
1751
2008
  }
1752
2009
 
1753
2010
  public async onDataMessage(
@@ -1761,14 +2018,19 @@ export abstract class DirectStream<
1761
2018
  }
1762
2019
 
1763
2020
  let isForMe = false;
1764
- if (message.header.mode instanceof AnyWhere) {
2021
+ if (
2022
+ message.header.mode instanceof AnyWhere ||
2023
+ message.header.mode instanceof AcknowledgeAnyWhere
2024
+ ) {
1765
2025
  isForMe = true;
1766
2026
  } else {
1767
2027
  const isFromSelf = this.publicKey.equals(from);
1768
- if (!isFromSelf) {
1769
- isForMe =
1770
- message.header.mode.to == null ||
1771
- message.header.mode.to.find((x) => x === this.publicKeyHash) != null;
2028
+ if (
2029
+ !isFromSelf &&
2030
+ (message.header.mode instanceof SilentDelivery ||
2031
+ message.header.mode instanceof AcknowledgeDelivery)
2032
+ ) {
2033
+ isForMe = message.header.mode.to.includes(this.publicKeyHash);
1772
2034
  }
1773
2035
  }
1774
2036
 
@@ -1804,25 +2066,17 @@ export abstract class DirectStream<
1804
2066
  }
1805
2067
  }
1806
2068
 
1807
- // Forward
1808
- if (message.header.mode instanceof SeekDelivery || seenBefore === 0) {
1809
- // DONT await this since it might introduce a dead-lock
1810
- if (message.header.mode instanceof SeekDelivery) {
1811
- if (seenBefore < message.header.mode.redundancy) {
1812
- const to = [...this.peers.values()].filter(
1813
- (x) =>
1814
- !message.header.signatures?.publicKeys.find((y) =>
1815
- y.equals(x.publicKey),
1816
- ) && x !== peerStream,
1817
- );
1818
- if (to.length > 0) {
1819
- this.relayMessage(from, message, to);
1820
- }
1821
- }
1822
- } else {
2069
+ // Forward
2070
+ const shouldForward =
2071
+ seenBefore === 0 ||
2072
+ ((message.header.mode instanceof AcknowledgeDelivery ||
2073
+ message.header.mode instanceof AcknowledgeAnyWhere) &&
2074
+ seenBefore < message.header.mode.redundancy);
2075
+
2076
+ if (shouldForward) {
2077
+ // DONT await this since it might introduce a dead-lock
1823
2078
  this.relayMessage(from, message);
1824
2079
  }
1825
- }
1826
2080
  }
1827
2081
 
1828
2082
  public async verifyAndProcess(message: Message<any>) {
@@ -1846,14 +2100,20 @@ export abstract class DirectStream<
1846
2100
  seenBefore: number,
1847
2101
  ) {
1848
2102
  if (
1849
- (message.header.mode instanceof SeekDelivery ||
1850
- message.header.mode instanceof AcknowledgeDelivery) &&
1851
- seenBefore < message.header.mode.redundancy
2103
+ message.header.mode instanceof AcknowledgeDelivery ||
2104
+ message.header.mode instanceof AcknowledgeAnyWhere
1852
2105
  ) {
1853
- const shouldAcknowldege =
1854
- message.header.mode.to == null ||
1855
- message.header.mode.to.includes(this.publicKeyHash);
1856
- if (!shouldAcknowldege) {
2106
+ const isRecipient =
2107
+ message.header.mode instanceof AcknowledgeAnyWhere
2108
+ ? true
2109
+ : message.header.mode.to.includes(this.publicKeyHash);
2110
+ if (
2111
+ !shouldAcknowledgeDataMessage({
2112
+ isRecipient,
2113
+ seenBefore,
2114
+ redundancy: message.header.mode.redundancy,
2115
+ })
2116
+ ) {
1857
2117
  return;
1858
2118
  }
1859
2119
  const signers = message.header.signatures!.publicKeys.map((x) =>
@@ -1870,14 +2130,18 @@ export abstract class DirectStream<
1870
2130
  mode: new TracedDelivery(signers),
1871
2131
  session: this.session,
1872
2132
 
1873
- // include our origin if message is SeekDelivery and we have not recently pruned a connection to this peer
1874
- origin:
1875
- message.header.mode instanceof SeekDelivery &&
1876
- !message.header.signatures!.publicKeys.find((x) =>
1877
- this.prunedConnectionsCache?.has(x.hashcode()),
1878
- )
1879
- ? new MultiAddrinfo(
1880
- this.components.addressManager
2133
+ // include our origin for route-learning/dialer hints (best-effort privacy/anti-spam control):
2134
+ // only include once (seenBefore=0) and only if we have not recently pruned
2135
+ // a connection to any signer in the path
2136
+ origin:
2137
+ (message.header.mode instanceof AcknowledgeAnyWhere ||
2138
+ message.header.mode instanceof AcknowledgeDelivery) &&
2139
+ seenBefore === 0 &&
2140
+ !message.header.signatures!.publicKeys.find((x) =>
2141
+ this.prunedConnectionsCache?.has(x.hashcode()),
2142
+ )
2143
+ ? new MultiAddrinfo(
2144
+ this.components.addressManager
1881
2145
  .getAddresses()
1882
2146
  .map((x) => x.toString()),
1883
2147
  )
@@ -2020,33 +2284,42 @@ export abstract class DirectStream<
2020
2284
  for (const remote of remotes) {
2021
2285
  this.invalidateSession(remote);
2022
2286
  }
2023
- this.checkIsAlive(remotes);
2287
+ // Best-effort liveness probe; never let background probe failures crash callers.
2288
+ void this.checkIsAlive(remotes).catch(() => false);
2024
2289
  }
2025
2290
  private async checkIsAlive(remotes: string[]) {
2026
2291
  if (this.peers.size === 0) {
2027
2292
  return false;
2028
2293
  }
2029
2294
  if (remotes.length > 0) {
2030
- return this.publish(undefined, {
2031
- mode: new SeekDelivery({
2032
- to: remotes,
2033
- redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
2034
- }),
2035
- })
2036
- .then(() => true)
2037
- .catch((e) => {
2038
- if (e instanceof DeliveryError) {
2039
- return false;
2040
- } else if (e instanceof NotStartedError) {
2041
- return false;
2042
- } else if (e instanceof TimeoutError) {
2043
- return false;
2044
- } else if (e instanceof AbortError) {
2045
- return false;
2046
- } else {
2047
- throw e;
2048
- }
2049
- }); // this will remove the target if it is still not reable
2295
+ try {
2296
+ // Keepalive probes are best-effort, but must still require ACKs so we can
2297
+ // conclusively prune stale routes when peers actually went away.
2298
+ await this.publish(undefined, {
2299
+ mode: new AcknowledgeDelivery({
2300
+ to: remotes,
2301
+ redundancy: DEFAULT_SILENT_MESSAGE_REDUDANCY,
2302
+ }),
2303
+ });
2304
+ return true;
2305
+ } catch (e: any) {
2306
+ const errorName =
2307
+ e?.name ?? e?.constructor?.name ?? "UnknownError";
2308
+ if (
2309
+ e instanceof DeliveryError ||
2310
+ e instanceof NotStartedError ||
2311
+ e instanceof TimeoutError ||
2312
+ e instanceof AbortError ||
2313
+ errorName === "DeliveryError" ||
2314
+ errorName === "NotStartedError" ||
2315
+ errorName === "TimeoutError" ||
2316
+ errorName === "AbortError"
2317
+ ) {
2318
+ return false;
2319
+ }
2320
+ warn(`checkIsAlive unexpected error: ${errorName}`);
2321
+ return false;
2322
+ }
2050
2323
  }
2051
2324
  return false;
2052
2325
  }
@@ -2059,7 +2332,11 @@ export abstract class DirectStream<
2059
2332
  ) {
2060
2333
  // dispatch the event if we are interested
2061
2334
 
2062
- let mode: SilentDelivery | SeekDelivery | AcknowledgeDelivery | AnyWhere = (
2335
+ let mode:
2336
+ | SilentDelivery
2337
+ | AcknowledgeDelivery
2338
+ | AcknowledgeAnyWhere
2339
+ | AnyWhere = (
2063
2340
  options as WithMode
2064
2341
  ).mode
2065
2342
  ? (options as WithMode).mode!
@@ -2070,8 +2347,7 @@ export abstract class DirectStream<
2070
2347
 
2071
2348
  if (
2072
2349
  mode instanceof AcknowledgeDelivery ||
2073
- mode instanceof SilentDelivery ||
2074
- mode instanceof SeekDelivery
2350
+ mode instanceof SilentDelivery
2075
2351
  ) {
2076
2352
  if (mode.to) {
2077
2353
  let preLength = mode.to.length;
@@ -2083,7 +2359,7 @@ export abstract class DirectStream<
2083
2359
  );
2084
2360
  }
2085
2361
 
2086
- if (mode.to.length === 0 && mode instanceof SeekDelivery === false) {
2362
+ if (mode.to.length === 0) {
2087
2363
  throw new InvalidMessageError(
2088
2364
  "Unexpected to deliver message with mode: " +
2089
2365
  mode.constructor.name +
@@ -2094,28 +2370,6 @@ export abstract class DirectStream<
2094
2370
  }
2095
2371
  }
2096
2372
 
2097
- if (mode instanceof AcknowledgeDelivery || mode instanceof SilentDelivery) {
2098
- const now = +new Date();
2099
- for (const hash of mode.to) {
2100
- const neighbourRoutes = this.routes.routes
2101
- .get(this.publicKeyHash)
2102
- ?.get(hash);
2103
-
2104
- if (
2105
- !neighbourRoutes ||
2106
- now - neighbourRoutes.session >
2107
- neighbourRoutes.list.length * this.routeSeekInterval ||
2108
- !this.routes.isUpToDate(hash, neighbourRoutes)
2109
- ) {
2110
- mode = new SeekDelivery({
2111
- to: mode.to,
2112
- redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
2113
- });
2114
- break;
2115
- }
2116
- }
2117
- }
2118
-
2119
2373
  const message = new DataMessage({
2120
2374
  data: data instanceof Uint8ArrayList ? data.subarray() : data,
2121
2375
  header: new MessageHeader({
@@ -2142,7 +2396,9 @@ export abstract class DirectStream<
2142
2396
  async publish(
2143
2397
  data: Uint8Array | Uint8ArrayList | undefined,
2144
2398
  options: PublishOptions = {
2145
- mode: new SeekDelivery({ redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY }),
2399
+ mode: new AcknowledgeAnyWhere({
2400
+ redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
2401
+ }),
2146
2402
  },
2147
2403
  ): Promise<Uint8Array | undefined> {
2148
2404
  if (!this.started) {
@@ -2187,7 +2443,7 @@ export abstract class DirectStream<
2187
2443
  if (message instanceof DataMessage) {
2188
2444
  if (
2189
2445
  message.header.mode instanceof AcknowledgeDelivery ||
2190
- message.header.mode instanceof SeekDelivery
2446
+ message.header.mode instanceof AcknowledgeAnyWhere
2191
2447
  ) {
2192
2448
  await message.sign(this.sign);
2193
2449
  }
@@ -2215,6 +2471,37 @@ export abstract class DirectStream<
2215
2471
  this.healthChecks.delete(to);
2216
2472
  }
2217
2473
 
2474
+ private formatDeliveryDebugState(
2475
+ targets: Set<string>,
2476
+ fastestNodesReached: Map<string, number[]>,
2477
+ ): string {
2478
+ const sampleSize = 16;
2479
+ const sampledTargets = [...targets].slice(0, sampleSize);
2480
+ const sampled = sampledTargets.map((target) => {
2481
+ const route = this.routes.findNeighbor(this.publicKeyHash, target);
2482
+ const routeSample =
2483
+ route?.list
2484
+ .slice(0, 3)
2485
+ .map((r) => `${r.hash}:${r.distance}${r.expireAt ? ":old" : ""}`)
2486
+ .join(",") ?? "-";
2487
+ const reachable = this.routes.isReachable(this.publicKeyHash, target);
2488
+ const stream = this.peers.get(target);
2489
+ let openConnections = 0;
2490
+ if (stream) {
2491
+ try {
2492
+ openConnections = this.components.connectionManager.getConnections(
2493
+ stream.peerId,
2494
+ ).length;
2495
+ } catch {
2496
+ openConnections = stream.isReadable || stream.isWritable ? 1 : 0;
2497
+ }
2498
+ }
2499
+ return `${target}{reachable=${reachable ? 1 : 0},ack=${fastestNodesReached.has(target) ? 1 : 0},stream=${stream ? 1 : 0},conns=${openConnections},routes=${routeSample}}`;
2500
+ });
2501
+ const more = targets.size - sampledTargets.length;
2502
+ return `deliveryState peers=${this.peers.size} routesLocal=${this.routes.count()} routesAll=${this.routes.countAll()} targets=[${sampled.join(";")}]${more > 0 ? ` (+${more} more)` : ""}`;
2503
+ }
2504
+
2218
2505
  private async createDeliveryPromise(
2219
2506
  from: PublicSignKey,
2220
2507
  message: DataMessage | Goodbye,
@@ -2238,11 +2525,9 @@ export abstract class DirectStream<
2238
2525
 
2239
2526
  const fastestNodesReached = new Map<string, number[]>();
2240
2527
  const messageToSet: Set<string> = new Set();
2241
- if (message.header.mode.to) {
2528
+ if (deliveryModeHasReceiver(message.header.mode)) {
2242
2529
  for (const to of message.header.mode.to) {
2243
- if (to === from.hashcode()) {
2244
- continue;
2245
- }
2530
+ if (to === from.hashcode()) continue;
2246
2531
  messageToSet.add(to);
2247
2532
 
2248
2533
  if (!relayed && !this.healthChecks.has(to)) {
@@ -2276,29 +2561,25 @@ export abstract class DirectStream<
2276
2561
  const willGetAllAcknowledgements = !relayed; // Only the origin will get all acks
2277
2562
 
2278
2563
  // Expected to receive at least 'filterMessageForSeenCounter' acknowledgements from each peer
2279
- const filterMessageForSeenCounter = relayed
2280
- ? undefined
2281
- : message.header.mode instanceof SeekDelivery
2282
- ? Math.min(this.peers.size, message.header.mode.redundancy)
2283
- : 1; /* message.deliveryMode instanceof SeekDelivery ? Math.min(this.peers.size - (relayed ? 1 : 0), message.deliveryMode.redundancy) : 1 */
2564
+ const filterMessageForSeenCounter = relayed ? undefined : 1;
2284
2565
 
2285
2566
  const uniqueAcks = new Set();
2286
2567
  const session = +new Date();
2287
2568
 
2288
- const onUnreachable =
2289
- !relayed &&
2290
- ((ev: any) => {
2291
- const deletedReceiver = messageToSet.delete(ev.detail.hashcode());
2292
- if (deletedReceiver) {
2293
- // Only reject if we are the sender
2294
- clear();
2295
- deliveryDeferredPromise.reject(
2296
- new DeliveryError(
2297
- `At least one recipent became unreachable while delivering messsage of type ${message.constructor.name}} to ${ev.detail.hashcode()}`,
2298
- ),
2299
- );
2300
- }
2301
- });
2569
+ const onUnreachable =
2570
+ !relayed &&
2571
+ ((ev: any) => {
2572
+ const target = ev.detail.hashcode();
2573
+ if (messageToSet.has(target)) {
2574
+ // A peer can transiently appear unreachable while routes are being updated.
2575
+ // Keep waiting for ACK/timeout instead of failing delivery immediately.
2576
+ logger.trace(
2577
+ "peer unreachable during delivery (msg=%s, target=%s); waiting for ACK/timeout",
2578
+ idString,
2579
+ target,
2580
+ );
2581
+ }
2582
+ });
2302
2583
 
2303
2584
  onUnreachable && this.addEventListener("peer:unreachable", onUnreachable);
2304
2585
 
@@ -2311,8 +2592,8 @@ export abstract class DirectStream<
2311
2592
  onAbort && signal?.removeEventListener("abort", onAbort);
2312
2593
  };
2313
2594
 
2314
- const timeout = setTimeout(async () => {
2315
- clear();
2595
+ const timeout = setTimeout(async () => {
2596
+ clear();
2316
2597
 
2317
2598
  let hasAll = true;
2318
2599
 
@@ -2330,22 +2611,31 @@ export abstract class DirectStream<
2330
2611
  }
2331
2612
  }
2332
2613
 
2333
- if (!hasAll && willGetAllAcknowledgements) {
2334
- deliveryDeferredPromise.reject(
2335
- new DeliveryError(
2336
- `Failed to get message ${idString} ${filterMessageForSeenCounter} ${[
2337
- ...messageToSet,
2338
- ]} delivery acknowledges from all nodes (${
2339
- fastestNodesReached.size
2340
- }/${messageToSet.size}). Mode: ${
2341
- message.header.mode.constructor.name
2342
- }. Redundancy: ${(message.header.mode as any)["redundancy"]}`,
2343
- ),
2344
- );
2614
+ if (!hasAll && willGetAllAcknowledgements) {
2615
+ const debugState = this.formatDeliveryDebugState(
2616
+ messageToSet,
2617
+ fastestNodesReached,
2618
+ );
2619
+ const msgMeta = `msgType=${message.constructor.name} dataBytes=${
2620
+ message instanceof DataMessage
2621
+ ? (message.data?.byteLength ?? 0)
2622
+ : 0
2623
+ } relayed=${relayed ? 1 : 0}`;
2624
+ deliveryDeferredPromise.reject(
2625
+ new DeliveryError(
2626
+ `Failed to get message ${idString} ${filterMessageForSeenCounter} ${[
2627
+ ...messageToSet,
2628
+ ]} delivery acknowledges from all nodes (${
2629
+ fastestNodesReached.size
2630
+ }/${messageToSet.size}). Mode: ${
2631
+ message.header.mode.constructor.name
2632
+ }. Redundancy: ${(message.header.mode as any)["redundancy"]}. ${msgMeta}. ${debugState}`,
2633
+ ),
2634
+ );
2345
2635
  } else {
2346
2636
  deliveryDeferredPromise.resolve();
2347
2637
  }
2348
- }, this.seekTimeout);
2638
+ }, this.seekTimeout);
2349
2639
 
2350
2640
  if (signal) {
2351
2641
  onAbort = () => {
@@ -2359,22 +2649,33 @@ export abstract class DirectStream<
2359
2649
  }
2360
2650
  }
2361
2651
 
2362
- const checkDone = () => {
2363
- // This if clause should never enter for relayed connections, since we don't
2364
- // know how many ACKs we will get
2365
- if (
2366
- filterMessageForSeenCounter != null &&
2367
- uniqueAcks.size >= messageToSet.size * filterMessageForSeenCounter
2368
- ) {
2369
- if (haveReceivers) {
2370
- // this statement exist beacuse if we do SEEK and have to = [], then it means we try to reach as many as possible hence we never want to delete this ACK callback
2371
- // only remove callback function if we actually expected a expected amount of responses
2372
- clear();
2373
- }
2652
+ const checkDone = () => {
2653
+ // This if clause should never enter for relayed connections, since we don't
2654
+ // know how many ACKs we will get
2655
+ if (
2656
+ filterMessageForSeenCounter != null &&
2657
+ uniqueAcks.size >= messageToSet.size * filterMessageForSeenCounter
2658
+ ) {
2659
+ const shouldKeepCallbackForRouteLearning =
2660
+ !relayed &&
2661
+ message.header.mode instanceof AcknowledgeDelivery &&
2662
+ message.header.mode.redundancy > 1;
2663
+ if (haveReceivers && !shouldKeepCallbackForRouteLearning) {
2664
+ // If we have an explicit recipient list we can clear the ACK callback once we
2665
+ // got the expected acknowledgements.
2666
+ clear();
2667
+ } else if (haveReceivers && shouldKeepCallbackForRouteLearning) {
2668
+ // Resolve delivery early, but keep ACK callbacks alive until timeout so we can
2669
+ // learn additional redundant routes (seenCounter=1..redundancy-1).
2670
+ onUnreachable &&
2671
+ this.removeEventListener("peer:unreachable", onUnreachable);
2672
+ onAbort && signal?.removeEventListener("abort", onAbort);
2673
+ onAbort = undefined;
2674
+ }
2374
2675
 
2375
- deliveryDeferredPromise.resolve();
2376
- return true;
2377
- }
2676
+ deliveryDeferredPromise.resolve();
2677
+ return true;
2678
+ }
2378
2679
  return false;
2379
2680
  };
2380
2681
 
@@ -2388,26 +2689,39 @@ export abstract class DirectStream<
2388
2689
  // remove the automatic removal of route timeout since we have observed lifesigns of a peer
2389
2690
  this.clearHealthcheckTimer(messageTargetHash);
2390
2691
 
2391
- // if the target is not inside the original message to, we still ad the target to our routes
2392
- // this because a relay might modify the 'to' list and we might receive more answers than initially set
2393
- if (message.header.mode instanceof SeekDelivery) {
2692
+ // If the target is not inside the original message `to`, we can still add the target to our routes.
2693
+ // This matters because relays may modify `to`, and because "ack-anywhere" style probes intentionally
2694
+ // do not provide an explicit recipient list.
2695
+ if (
2696
+ message.header.mode instanceof AcknowledgeDelivery ||
2697
+ message.header.mode instanceof AcknowledgeAnyWhere
2698
+ ) {
2699
+ const upstreamHash = messageFrom?.publicKey.hashcode();
2700
+
2701
+ const routeUpdate = computeSeekAckRouteUpdate({
2702
+ current: this.publicKeyHash,
2703
+ upstream: upstreamHash,
2704
+ downstream: messageThrough.publicKey.hashcode(),
2705
+ target: messageTargetHash,
2706
+ // Route "distance" is based on recipient-seen order (0 = fastest). This is relied upon by
2707
+ // `Routes.getFanout(...)` which uses `distance < redundancy` to select redundant next-hops.
2708
+ distance: seenCounter,
2709
+ });
2710
+
2394
2711
  this.addRouteConnection(
2395
- messageFrom?.publicKey.hashcode() || this.publicKeyHash,
2396
- messageThrough.publicKey.hashcode(),
2712
+ routeUpdate.from,
2713
+ routeUpdate.neighbour,
2397
2714
  messageTarget,
2398
- seenCounter,
2715
+ routeUpdate.distance,
2399
2716
  session,
2400
2717
  Number(ack.header.session),
2401
- ); // we assume the seenCounter = distance. The more the message has been seen by the target the longer the path is to the target
2718
+ );
2402
2719
  }
2403
2720
 
2404
- if (messageToSet.has(messageTargetHash)) {
2405
- // Only keep track of relevant acks
2406
- if (
2407
- filterMessageForSeenCounter == null ||
2408
- seenCounter < filterMessageForSeenCounter
2409
- ) {
2410
- // TODO set limit correctly
2721
+ if (messageToSet.has(messageTargetHash)) {
2722
+ // Any ACK from the target should satisfy delivery semantics.
2723
+ // Relying on only seenCounter=0 can fail under churn if the first ACK
2724
+ // is lost but a later ACK (seenCounter>0) arrives.
2411
2725
  if (seenCounter < MAX_ROUTE_DISTANCE) {
2412
2726
  let arr = fastestNodesReached.get(messageTargetHash);
2413
2727
  if (!arr) {
@@ -2416,10 +2730,9 @@ export abstract class DirectStream<
2416
2730
  }
2417
2731
  arr.push(seenCounter);
2418
2732
 
2419
- uniqueAcks.add(messageTargetHash + seenCounter);
2733
+ uniqueAcks.add(messageTargetHash);
2420
2734
  }
2421
2735
  }
2422
- }
2423
2736
 
2424
2737
  checkDone();
2425
2738
  },
@@ -2443,7 +2756,9 @@ export abstract class DirectStream<
2443
2756
  throw new NotStartedError();
2444
2757
  }
2445
2758
 
2759
+ const isRelayed = relayed ?? from.hashcode() !== this.publicKeyHash;
2446
2760
  let delivereyPromise: Promise<void> | undefined = undefined as any;
2761
+ let ackCallbackId: string | undefined;
2447
2762
 
2448
2763
  if (
2449
2764
  (!message.header.signatures ||
@@ -2458,123 +2773,182 @@ export abstract class DirectStream<
2458
2773
  * Logic for handling acknowledge messages when we receive them (later)
2459
2774
  */
2460
2775
 
2461
- if (
2462
- message instanceof DataMessage &&
2463
- message.header.mode instanceof SeekDelivery &&
2464
- !relayed
2465
- ) {
2466
- to = this.peers; // seek delivery will not work unless we try all possible paths
2467
- }
2468
-
2469
- if (message.header.mode instanceof AcknowledgeDelivery) {
2470
- to = undefined;
2471
- }
2472
-
2473
2776
  if (
2474
2777
  (message instanceof DataMessage || message instanceof Goodbye) &&
2475
- (message.header.mode instanceof SeekDelivery ||
2476
- message.header.mode instanceof AcknowledgeDelivery)
2778
+ (message.header.mode instanceof AcknowledgeDelivery ||
2779
+ message.header.mode instanceof AcknowledgeAnyWhere)
2477
2780
  ) {
2478
2781
  const deliveryDeferredPromise = await this.createDeliveryPromise(
2479
2782
  from,
2480
2783
  message,
2481
- relayed,
2784
+ isRelayed,
2482
2785
  signal,
2483
2786
  );
2484
2787
  delivereyPromise = deliveryDeferredPromise.promise;
2788
+ ackCallbackId = toBase64(message.id);
2485
2789
  }
2486
2790
 
2487
- const bytes = message.bytes();
2791
+ try {
2792
+ const bytes = message.bytes();
2488
2793
 
2489
- if (!relayed) {
2490
- const bytesArray = bytes instanceof Uint8Array ? bytes : bytes.subarray();
2491
- await this.modifySeenCache(bytesArray);
2492
- }
2794
+ if (!isRelayed) {
2795
+ const bytesArray = bytes instanceof Uint8Array ? bytes : bytes.subarray();
2796
+ await this.modifySeenCache(bytesArray);
2797
+ }
2493
2798
 
2494
- /**
2495
- * For non SEEKing message delivery modes, use routing
2496
- */
2799
+ /**
2800
+ * For non SEEKing message delivery modes, use routing
2801
+ */
2497
2802
 
2498
- if (message instanceof DataMessage) {
2499
- if (
2500
- (message.header.mode instanceof AcknowledgeDelivery ||
2501
- message.header.mode instanceof SilentDelivery) &&
2502
- !to
2503
- ) {
2504
- if (message.header.mode.to.length === 0) {
2505
- return delivereyPromise; // we defintely know that we should not forward the message anywhere
2506
- }
2803
+ if (message instanceof DataMessage) {
2804
+ if (
2805
+ (message.header.mode instanceof AcknowledgeDelivery ||
2806
+ message.header.mode instanceof SilentDelivery) &&
2807
+ !to
2808
+ ) {
2809
+ if (message.header.mode.to.length === 0) {
2810
+ // we definitely know that we should not forward the message anywhere
2811
+ return delivereyPromise;
2812
+ }
2507
2813
 
2508
- const fanout = this.routes.getFanout(
2509
- from.hashcode(),
2510
- message.header.mode.to,
2511
- message.header.mode.redundancy,
2512
- );
2814
+ const fanout = this.routes.getFanout(
2815
+ from.hashcode(),
2816
+ message.header.mode.to,
2817
+ message.header.mode.redundancy,
2818
+ );
2513
2819
 
2514
- if (fanout) {
2515
- if (fanout.size > 0) {
2820
+ // If we have explicit routing information, send only along the chosen next-hops.
2821
+ // If `fanout` is empty (no route info yet), fall through to the flooding logic below
2822
+ // so acknowledged deliveries can discover/repair routes instead of timing out.
2823
+ if (fanout && fanout.size > 0) {
2516
2824
  const promises: Promise<any>[] = [];
2825
+ const usedNeighbours = new Set<string>();
2826
+ const originalTo = message.header.mode.to;
2517
2827
  for (const [neighbour, _distantPeers] of fanout) {
2518
2828
  const stream = this.peers.get(neighbour);
2519
- stream &&
2829
+ if (!stream) continue;
2830
+ if (message.header.mode instanceof SilentDelivery) {
2831
+ message.header.mode.to = [..._distantPeers.keys()];
2520
2832
  promises.push(
2521
- stream.waitForWrite(bytes, message.header.priority),
2833
+ stream.waitForWrite(message.bytes(), message.header.priority),
2522
2834
  );
2835
+ } else {
2836
+ promises.push(stream.waitForWrite(bytes, message.header.priority));
2837
+ }
2838
+ usedNeighbours.add(neighbour);
2839
+ }
2840
+ if (message.header.mode instanceof SilentDelivery) {
2841
+ message.header.mode.to = originalTo;
2842
+ }
2843
+
2844
+ // If the sender requested redundancy but we don't yet have enough distinct
2845
+ // next-hops for the target(s), opportunistically probe additional neighbours.
2846
+ // This replaces the previous "greedy fanout" probing behavior without needing
2847
+ // a separate delivery mode.
2848
+ if (
2849
+ !isRelayed &&
2850
+ message.header.mode instanceof AcknowledgeDelivery &&
2851
+ usedNeighbours.size < message.header.mode.redundancy
2852
+ ) {
2853
+ for (const [neighbour, stream] of this.peers) {
2854
+ if (usedNeighbours.size >= message.header.mode.redundancy) {
2855
+ break;
2856
+ }
2857
+ if (usedNeighbours.has(neighbour)) continue;
2858
+ usedNeighbours.add(neighbour);
2859
+ promises.push(stream.waitForWrite(bytes, message.header.priority));
2860
+ }
2523
2861
  }
2862
+
2524
2863
  await Promise.all(promises);
2525
- return delivereyPromise; // we are done sending the message in all direction with updates 'to' lists
2864
+ return delivereyPromise;
2526
2865
  }
2527
2866
 
2528
- return; // we defintely know that we should not forward the message anywhere
2867
+ // If we don't have routing information:
2868
+ // - For acknowledged deliveries, fall through to flooding (route discovery / repair).
2869
+ // - For silent deliveries, relays should not flood (prevents unnecessary fanout); origin may still flood.
2870
+ // We still allow direct neighbour delivery to explicit recipients (if connected).
2871
+ if (isRelayed && message.header.mode instanceof SilentDelivery) {
2872
+ const promises: Promise<any>[] = [];
2873
+ const originalTo = message.header.mode.to;
2874
+ for (const recipient of originalTo) {
2875
+ if (recipient === this.publicKeyHash) continue;
2876
+ if (recipient === from.hashcode()) continue; // never send back to previous hop
2877
+ const stream = this.peers.get(recipient);
2878
+ if (!stream) continue;
2879
+ if (
2880
+ message.header.signatures?.publicKeys.find(
2881
+ (x) => x.hashcode() === recipient,
2882
+ )
2883
+ ) {
2884
+ continue; // recipient already signed/seen this message
2885
+ }
2886
+ message.header.mode.to = [recipient];
2887
+ promises.push(
2888
+ stream.waitForWrite(message.bytes(), message.header.priority),
2889
+ );
2890
+ }
2891
+ message.header.mode.to = originalTo;
2892
+ if (promises.length > 0) {
2893
+ await Promise.all(promises);
2894
+ }
2895
+ return delivereyPromise;
2896
+ }
2529
2897
  }
2898
+ }
2530
2899
 
2531
- // we end up here because we don't have enough information yet in how to send data to the peer (TODO test this codepath)
2532
- if (relayed) {
2533
- return;
2534
- }
2535
- } // else send to all (fallthrough to code below)
2536
- }
2900
+ // We fail to send the message directly, instead fallback to floodsub
2901
+ const peers: PeerStreams[] | Map<string, PeerStreams> = to || this.peers;
2902
+ if (
2903
+ peers == null ||
2904
+ (Array.isArray(peers) && peers.length === 0) ||
2905
+ (peers instanceof Map && peers.size === 0)
2906
+ ) {
2907
+ logger.trace("No peers to send to");
2908
+ return delivereyPromise;
2909
+ }
2537
2910
 
2538
- // We fils to send the message directly, instead fallback to floodsub
2539
- const peers: PeerStreams[] | Map<string, PeerStreams> = to || this.peers;
2540
- if (
2541
- peers == null ||
2542
- (Array.isArray(peers) && peers.length === 0) ||
2543
- (peers instanceof Map && peers.size === 0)
2544
- ) {
2545
- logger.trace("No peers to send to");
2546
- return delivereyPromise;
2547
- }
2911
+ let sentOnce = false;
2912
+ const promises: Promise<any>[] = [];
2913
+ for (const stream of peers.values()) {
2914
+ const id = stream as PeerStreams;
2548
2915
 
2549
- let sentOnce = false;
2550
- const promises: Promise<any>[] = [];
2551
- for (const stream of peers.values()) {
2552
- const id = stream as PeerStreams;
2916
+ // Dont sent back to the sender
2917
+ if (id.publicKey.equals(from)) {
2918
+ continue;
2919
+ }
2920
+ // Dont send message back to any of the signers (they have already seen the message)
2921
+ if (
2922
+ message.header.signatures?.publicKeys.find((x) =>
2923
+ x.equals(id.publicKey),
2924
+ )
2925
+ ) {
2926
+ continue;
2927
+ }
2553
2928
 
2554
- // Dont sent back to the sender
2555
- if (id.publicKey.equals(from)) {
2556
- continue;
2557
- }
2558
- // Dont send message back to any of the signers (they have already seen the message)
2559
- if (
2560
- message.header.signatures?.publicKeys.find((x) =>
2561
- x.equals(id.publicKey),
2562
- )
2563
- ) {
2564
- continue;
2929
+ sentOnce = true;
2930
+ promises.push(id.waitForWrite(bytes, message.header.priority));
2565
2931
  }
2932
+ await Promise.all(promises);
2566
2933
 
2567
- sentOnce = true;
2568
- promises.push(id.waitForWrite(bytes, message.header.priority));
2569
- }
2570
- await Promise.all(promises);
2934
+ if (!sentOnce) {
2935
+ // If the caller provided an explicit peer list, treat "no valid receivers" as an error
2936
+ // even when forwarding. This catches programming mistakes early and matches test expectations.
2937
+ if (!isRelayed || to != null) {
2938
+ throw new DeliveryError("Message did not have any valid receivers");
2939
+ }
2940
+ }
2571
2941
 
2572
- if (!sentOnce) {
2573
- if (!relayed) {
2574
- throw new DeliveryError("Message did not have any valid receivers");
2942
+ return delivereyPromise;
2943
+ } catch (error) {
2944
+ // If message fanout/write fails before publishMessage returns its delivery
2945
+ // promise, clear any ACK callback to avoid late timer rejections leaking as
2946
+ // unhandled rejections in fire-and-forget call paths.
2947
+ if (ackCallbackId) {
2948
+ this._ackCallbacks.get(ackCallbackId)?.clear();
2575
2949
  }
2950
+ throw error;
2576
2951
  }
2577
- return delivereyPromise;
2578
2952
  }
2579
2953
 
2580
2954
  async maybeConnectDirectly(toHash: string, origin: MultiAddrinfo) {
@@ -2697,8 +3071,11 @@ export abstract class DirectStream<
2697
3071
  const wins = new Set<string>();
2698
3072
  for (const h of admitted) if (reached(h, target)) wins.add(h);
2699
3073
 
2700
- if (settle === "any" && wins.size > 0) return [...wins];
2701
- if (settle === "all" && wins.size === admitted.length) return [...wins];
3074
+ // Preserve input order in the returned list (important for deterministic callers/tests).
3075
+ const orderedWins = () => admitted.filter((h) => wins.has(h));
3076
+
3077
+ if (settle === "any" && wins.size > 0) return orderedWins();
3078
+ if (settle === "all" && wins.size === admitted.length) return orderedWins();
2702
3079
 
2703
3080
  // Abort/timeout
2704
3081
  const abortSignals = [this.closeController.signal];
@@ -2714,143 +3091,62 @@ export abstract class DirectStream<
2714
3091
  return defer.resolve();
2715
3092
  };
2716
3093
 
2717
- try {
2718
- await waitForEvent(this, eventsFor(target), check, {
2719
- signals: abortSignals,
2720
- timeout,
2721
- });
2722
- return [...wins];
2723
- } catch (e) {
2724
- const abortSignal = abortSignals.find((s) => s.aborted);
2725
- if (abortSignal) {
2726
- if (abortSignal.reason instanceof Error) {
2727
- throw abortSignal.reason;
2728
- }
2729
- throw new AbortError(
2730
- "Aborted waiting for peers: " + abortSignal.reason,
2731
- );
2732
- }
2733
- if (e instanceof Error) {
2734
- throw e;
2735
- }
2736
- if (settle === "all") throw new TimeoutError("Timeout waiting for peers");
2737
- return [...wins]; // settle:any: return whatever successes we got
2738
- }
2739
- }
2740
-
2741
- /* async waitFor(
2742
- peer: PeerId | PublicSignKey | string,
2743
- options?: {
2744
- timeout?: number;
2745
- signal?: AbortSignal;
2746
- neighbour?: boolean;
2747
- inflight?: boolean;
2748
- },
2749
- ) {
2750
- const hash =
2751
- typeof peer === "string"
2752
- ? peer
2753
- : (peer instanceof PublicSignKey
2754
- ? peer
2755
- : getPublicKeyFromPeerId(peer)
2756
- ).hashcode();
2757
- if (hash === this.publicKeyHash) {
2758
- return; // TODO throw error instead?
2759
- }
2760
-
2761
- if (options?.inflight) {
2762
- // if peer is not in active connections or dialQueue, return silenty
2763
- if (
2764
- !this.peers.has(hash) &&
2765
- !this.components.connectionManager
2766
- .getDialQueue()
2767
- .some((x) => getPublicKeyFromPeerId(x.peerId).hashcode() === hash) &&
2768
- !this.components.connectionManager
2769
- .getConnections()
2770
- .some((x) => getPublicKeyFromPeerId(x.remotePeer).hashcode() === hash)
2771
- ) {
2772
- return;
2773
- }
2774
- }
2775
-
2776
- const checkIsReachable = (deferred: DeferredPromise<void>) => {
2777
- if (options?.neighbour && !this.peers.has(hash)) {
2778
- return;
2779
- }
2780
-
2781
- if (!this.routes.isReachable(this.publicKeyHash, hash, 0)) {
2782
- return;
2783
- }
2784
-
2785
- deferred.resolve();
2786
- };
2787
- const abortSignals = [this.closeController.signal];
2788
- if (options?.signal) {
2789
- abortSignals.push(options.signal);
2790
- }
2791
-
2792
- try {
2793
- await waitForEvent(this, ["peer:reachable"], checkIsReachable, {
2794
- signals: abortSignals,
2795
- timeout: options?.timeout,
2796
- });
2797
- } catch (error) {
2798
- throw new Error(
2799
- "Stream to " +
2800
- hash +
2801
- " from " +
2802
- this.publicKeyHash +
2803
- " does not exist. Connection exist: " +
2804
- this.peers.has(hash) +
2805
- ". Route exist: " +
2806
- this.routes.isReachable(this.publicKeyHash, hash, 0),
2807
- );
2808
- }
2809
-
2810
- if (options?.neighbour) {
2811
- const stream = this.peers.get(hash)!;
2812
3094
  try {
2813
- let checkIsWritable = (pDefer: DeferredPromise<void>) => {
2814
- if (stream.isReadable && stream.isWritable) {
2815
- pDefer.resolve();
3095
+ await waitForEvent(this, eventsFor(target), check, {
3096
+ signals: abortSignals,
3097
+ timeout,
3098
+ });
3099
+ return orderedWins();
3100
+ } catch (e) {
3101
+ const abortSignal = abortSignals.find((s) => s.aborted);
3102
+ if (abortSignal) {
3103
+ if (abortSignal.reason instanceof Error) {
3104
+ throw abortSignal.reason;
2816
3105
  }
2817
- };
2818
- await waitForEvent(
2819
- stream,
2820
- ["stream:outbound", "stream:inbound"],
2821
- checkIsWritable,
2822
- {
2823
- signals: abortSignals,
2824
- timeout: options?.timeout,
2825
- },
2826
- );
2827
- } catch (error) {
2828
- throw new Error(
2829
- "Stream to " +
2830
- stream.publicKey.hashcode() +
2831
- " not ready. Readable: " +
2832
- stream.isReadable +
2833
- ". Writable " +
2834
- stream.isWritable,
2835
- );
3106
+ throw new AbortError(
3107
+ "Aborted waiting for peers: " + abortSignal.reason,
3108
+ );
3109
+ }
3110
+ if (e instanceof TimeoutError) {
3111
+ if (settle === "any") {
3112
+ if (wins.size > 0) return orderedWins();
3113
+ throw new TimeoutError(
3114
+ `Timeout waiting for peers (target=${target}, seek=${seek}, missing=${admitted.length}/${admitted.length})`,
3115
+ );
3116
+ }
3117
+ const missing = admitted.filter((h) => !wins.has(h));
3118
+ const preview = missing.slice(0, 5).join(", ");
3119
+ throw new TimeoutError(
3120
+ `Timeout waiting for peers (target=${target}, seek=${seek}, missing=${missing.length}/${admitted.length}${
3121
+ preview ? `, e.g. ${preview}` : ""
3122
+ })`,
3123
+ );
3124
+ }
3125
+ if (e instanceof Error) throw e;
3126
+ if (settle === "all") throw new TimeoutError("Timeout waiting for peers");
3127
+ return orderedWins(); // settle:any: return whatever successes we got
2836
3128
  }
2837
3129
  }
2838
- } */
2839
3130
 
2840
- getPublicKey(hash: string): PublicSignKey | undefined {
2841
- return this.peerKeyHashToPublicKey.get(hash);
2842
- }
3131
+ getPublicKey(hash: string): PublicSignKey | undefined {
3132
+ return this.peerKeyHashToPublicKey.get(hash);
3133
+ }
2843
3134
 
2844
3135
  get pending(): boolean {
2845
3136
  return this._ackCallbacks.size > 0;
2846
3137
  }
2847
3138
 
2848
3139
  // make this into a job? run every few ms
2849
- maybePruneConnections(): Promise<void> {
2850
- if (this.connectionManagerOptions.pruner) {
2851
- if (this.connectionManagerOptions.pruner.bandwidth != null) {
2852
- let usedBandwidth = 0;
2853
- for (const [_k, v] of this.peers) {
3140
+ maybePruneConnections(): Promise<void> {
3141
+ // Hard cap on peer streams: treat as a primary pruning signal.
3142
+ if (this.peers.size > this.connectionManagerOptions.maxConnections) {
3143
+ return this.pruneConnectionsToLimits();
3144
+ }
3145
+
3146
+ if (this.connectionManagerOptions.pruner) {
3147
+ if (this.connectionManagerOptions.pruner.bandwidth != null) {
3148
+ let usedBandwidth = 0;
3149
+ for (const [_k, v] of this.peers) {
2854
3150
  usedBandwidth += v.usedBandwidth;
2855
3151
  }
2856
3152
  usedBandwidth /= this.peers.size;
@@ -2859,17 +3155,17 @@ export abstract class DirectStream<
2859
3155
  // prune
2860
3156
  return this.pruneConnections();
2861
3157
  }
2862
- } else if (this.connectionManagerOptions.pruner.maxBuffer != null) {
2863
- const queuedBytes = this.getQueuedBytes();
2864
- if (queuedBytes > this.connectionManagerOptions.pruner.maxBuffer) {
2865
- // prune
2866
- return this.pruneConnections();
3158
+ } else if (this.connectionManagerOptions.pruner.maxBuffer != null) {
3159
+ const queuedBytes = this.getQueuedBytes();
3160
+ if (queuedBytes > this.connectionManagerOptions.pruner.maxBuffer) {
3161
+ // prune
3162
+ return this.pruneConnections();
3163
+ }
2867
3164
  }
2868
3165
  }
2869
- }
2870
3166
 
2871
- return Promise.resolve();
2872
- }
3167
+ return Promise.resolve();
3168
+ }
2873
3169
 
2874
3170
  async pruneConnections(): Promise<void> {
2875
3171
  // TODO sort by bandwidth
@@ -2880,17 +3176,17 @@ export abstract class DirectStream<
2880
3176
  const sorted = [...this.peers.values()]
2881
3177
  .sort((x, y) => x.usedBandwidth - y.usedBandwidth)
2882
3178
  .map((x) => x.publicKey.hashcode());
2883
- const prunables = this.routes.getPrunable(sorted);
2884
- if (prunables.length === 0) {
2885
- return;
2886
- }
3179
+ const prunables = this.routes.getPrunable(sorted);
3180
+ if (prunables.length === 0) {
3181
+ return;
3182
+ }
2887
3183
 
2888
- const stream = this.peers.get(prunables[0])!;
2889
- this.prunedConnectionsCache!.add(stream.publicKey.hashcode());
3184
+ const stream = this.peers.get(prunables[0])!;
3185
+ this.prunedConnectionsCache?.add(stream.publicKey.hashcode());
2890
3186
 
2891
- await this.onPeerDisconnected(stream.peerId);
2892
- return this.components.connectionManager.closeConnections(stream.peerId);
2893
- }
3187
+ await this.onPeerDisconnected(stream.peerId);
3188
+ return this.components.connectionManager.closeConnections(stream.peerId);
3189
+ }
2894
3190
 
2895
3191
  getQueuedBytes(): number {
2896
3192
  let sum = 0;