@peerbit/stream 4.6.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -3,7 +3,7 @@ import { multiaddr } from "@multiformats/multiaddr";
3
3
  import { Circuit } from "@multiformats/multiaddr-matcher";
4
4
  import { Cache } from "@peerbit/cache";
5
5
  import { PublicSignKey, getKeypairFromPrivateKey, getPublicKeyFromPeerId, ready, sha256Base64, toBase64, } from "@peerbit/crypto";
6
- import { ACK, AcknowledgeDelivery, AnyWhere, DataMessage, DeliveryError, Goodbye, InvalidMessageError, Message, MessageHeader, MultiAddrinfo, NotStartedError, SeekDelivery, SilentDelivery, TracedDelivery, coercePeerRefsToHashes, deliveryModeHasReceiver, getMsgId, } from "@peerbit/stream-interface";
6
+ import { ACK, AcknowledgeAnyWhere, AcknowledgeDelivery, AnyWhere, DataMessage, DeliveryError, Goodbye, InvalidMessageError, Message, MessageHeader, MultiAddrinfo, NotStartedError, SilentDelivery, TracedDelivery, coercePeerRefsToHashes, deliveryModeHasReceiver, getMsgId, } from "@peerbit/stream-interface";
7
7
  import { AbortError, TimeoutError, delay } from "@peerbit/time";
8
8
  import { abortableSource } from "abortable-iterator";
9
9
  import { anySignal } from "any-signal";
@@ -13,6 +13,7 @@ import { pushable } from "it-pushable";
13
13
  import pDefer, {} from "p-defer";
14
14
  import Queue from "p-queue";
15
15
  import { Uint8ArrayList } from "uint8arraylist";
16
+ import { computeSeekAckRouteUpdate, shouldAcknowledgeDataMessage, } from "./core/seek-routing.js";
16
17
  import { logger } from "./logger.js";
17
18
  import { pushableLanes } from "./pushable-lanes.js";
18
19
  import { MAX_ROUTE_DISTANCE, Routes } from "./routes.js";
@@ -82,7 +83,6 @@ const DEFAULT_PRUNE_CONNECTIONS_INTERVAL = 2e4;
82
83
  const DEFAULT_MIN_CONNECTIONS = 2;
83
84
  const DEFAULT_MAX_CONNECTIONS = 300;
84
85
  const DEFAULT_PRUNED_CONNNECTIONS_TIMEOUT = 30 * 1000;
85
- const ROUTE_UPDATE_DELAY_FACTOR = 3e4;
86
86
  const DEFAULT_CREATE_OUTBOUND_STREAM_TIMEOUT = 30_000;
87
87
  const PRIORITY_LANES = 4;
88
88
  const getLaneFromPriority = (priority) => {
@@ -148,6 +148,19 @@ export class PeerStreams extends TypedEventEmitter {
148
148
  _getActiveOutboundPushable() {
149
149
  return this.outboundStreams[0]?.pushable;
150
150
  }
151
+ getOutboundQueuedBytes() {
152
+ return this._getActiveOutboundPushable()?.readableLength ?? 0;
153
+ }
154
+ getOutboundQueuedBytesByLane() {
155
+ const p = this._getActiveOutboundPushable();
156
+ if (!p)
157
+ return Array(PRIORITY_LANES).fill(0);
158
+ const out = [];
159
+ for (let lane = 0; lane < PRIORITY_LANES; lane++) {
160
+ out.push(p.getReadableLength(lane));
161
+ }
162
+ return out;
163
+ }
151
164
  _getOutboundCount() {
152
165
  return this.outboundStreams.length;
153
166
  }
@@ -349,7 +362,10 @@ export class PeerStreams extends TypedEventEmitter {
349
362
  this.write(bytes, priority);
350
363
  return;
351
364
  }
352
- const timeoutMs = 3_000;
365
+ // Outbound stream negotiation can legitimately take several seconds in CI
366
+ // (identify/protocol discovery, resource contention, etc). Keep this fairly
367
+ // generous so control-plane messages (joins/subscriptions) don't flap.
368
+ const timeoutMs = 10_000;
353
369
  await new Promise((resolve, reject) => {
354
370
  const onOutbound = () => {
355
371
  cleanup();
@@ -626,7 +642,16 @@ export class PeerStreams extends TypedEventEmitter {
626
642
  logger.error("Failed to abort inbound stream");
627
643
  }
628
644
  try {
629
- await inbound.raw.close?.();
645
+ // Best-effort shutdown: on some transports (notably websockets),
646
+ // awaiting a graceful close can hang indefinitely if the remote is
647
+ // concurrently stopping. Abort immediately and do not await close.
648
+ try {
649
+ inbound.raw.abort?.(new AbortError("Closed"));
650
+ }
651
+ catch {
652
+ // ignore
653
+ }
654
+ void Promise.resolve(inbound.raw.close?.()).catch(() => { });
630
655
  }
631
656
  catch {
632
657
  logger.error("Failed to close inbound stream");
@@ -641,6 +666,7 @@ export class PeerStreams extends TypedEventEmitter {
641
666
  this.inboundStreams = [];
642
667
  }
643
668
  }
669
+ const sharedRoutingByPrivateKey = new WeakMap();
644
670
  export class DirectStream extends TypedEventEmitter {
645
671
  components;
646
672
  peerId;
@@ -666,6 +692,8 @@ export class DirectStream extends TypedEventEmitter {
666
692
  multicodecs;
667
693
  seenCache;
668
694
  _registrarTopologyIds;
695
+ _peerConnectListener;
696
+ _peerDisconnectListener;
669
697
  maxInboundStreams;
670
698
  maxOutboundStreams;
671
699
  connectionManagerOptions;
@@ -673,10 +701,17 @@ export class DirectStream extends TypedEventEmitter {
673
701
  healthChecks;
674
702
  pruneConnectionsTimeout;
675
703
  prunedConnectionsCache;
704
+ pruneToLimitsInFlight;
705
+ _startInFlight;
676
706
  routeMaxRetentionPeriod;
707
+ routeCacheMaxFromEntries;
708
+ routeCacheMaxTargetsPerFrom;
709
+ routeCacheMaxRelaysPerTarget;
710
+ sharedRouting;
711
+ sharedRoutingKey;
712
+ sharedRoutingState;
677
713
  // for sequential creation of outbound streams
678
714
  outboundInflightQueue;
679
- routeSeekInterval;
680
715
  seekTimeout;
681
716
  closeController;
682
717
  session;
@@ -685,7 +720,7 @@ export class DirectStream extends TypedEventEmitter {
685
720
  constructor(components, multicodecs, options) {
686
721
  super();
687
722
  this.components = components;
688
- const { canRelayMessage = true, messageProcessingConcurrency = 10, maxInboundStreams, maxOutboundStreams, connectionManager, routeSeekInterval = ROUTE_UPDATE_DELAY_FACTOR, seekTimeout = SEEK_DELIVERY_TIMEOUT, routeMaxRetentionPeriod = ROUTE_MAX_RETANTION_PERIOD, inboundIdleTimeout, } = options || {};
723
+ const { canRelayMessage = true, messageProcessingConcurrency = 10, maxInboundStreams, maxOutboundStreams, connectionManager, seekTimeout = SEEK_DELIVERY_TIMEOUT, routeMaxRetentionPeriod = ROUTE_MAX_RETANTION_PERIOD, routeCacheMaxFromEntries, routeCacheMaxTargetsPerFrom, routeCacheMaxRelaysPerTarget, sharedRouting = true, seenCacheMax = 1e6, seenCacheTtlMs = 10 * 60 * 1e3, inboundIdleTimeout, } = options || {};
689
724
  const signKey = getKeypairFromPrivateKey(components.privateKey);
690
725
  this.seekTimeout = seekTimeout;
691
726
  this.sign = signKey.sign.bind(signKey);
@@ -701,12 +736,18 @@ export class DirectStream extends TypedEventEmitter {
701
736
  this.peers = new Map();
702
737
  this.canRelayMessage = canRelayMessage;
703
738
  this.healthChecks = new Map();
704
- this.routeSeekInterval = routeSeekInterval;
705
739
  this.queue = new Queue({ concurrency: messageProcessingConcurrency });
706
740
  this.maxInboundStreams = maxInboundStreams;
707
741
  this.maxOutboundStreams = maxOutboundStreams;
708
- this.seenCache = new Cache({ max: 1e6, ttl: 10 * 60 * 1e3 });
742
+ this.seenCache = new Cache({
743
+ max: Math.max(1, Math.floor(seenCacheMax)),
744
+ ttl: Math.max(1, Math.floor(seenCacheTtlMs)),
745
+ });
709
746
  this.routeMaxRetentionPeriod = routeMaxRetentionPeriod;
747
+ this.routeCacheMaxFromEntries = routeCacheMaxFromEntries;
748
+ this.routeCacheMaxTargetsPerFrom = routeCacheMaxTargetsPerFrom;
749
+ this.routeCacheMaxRelaysPerTarget = routeCacheMaxRelaysPerTarget;
750
+ this.sharedRouting = sharedRouting !== false;
710
751
  this.peerKeyHashToPublicKey = new Map();
711
752
  this._onIncomingStream = this._onIncomingStream.bind(this);
712
753
  this.onPeerConnected = this.onPeerConnected.bind(this);
@@ -753,11 +794,46 @@ export class DirectStream extends TypedEventEmitter {
753
794
  })
754
795
  : undefined;
755
796
  }
797
+ pruneConnectionsToLimits() {
798
+ if (this.pruneToLimitsInFlight) {
799
+ return this.pruneToLimitsInFlight;
800
+ }
801
+ this.pruneToLimitsInFlight = (async () => {
802
+ // Respect minConnections as a hard floor.
803
+ const maxConnections = Math.max(this.connectionManagerOptions.minConnections, this.connectionManagerOptions.maxConnections);
804
+ if (this.peers.size <= maxConnections) {
805
+ return;
806
+ }
807
+ // Prune in batches so we can quickly recover from join storms without waiting
808
+ // for repeated pruner ticks. Bound work per run to avoid starving the event loop.
809
+ const maxPrunes = Math.min(256, Math.max(1, this.peers.size - maxConnections));
810
+ for (let i = 0; i < maxPrunes; i++) {
811
+ if (this.peers.size <= maxConnections)
812
+ break;
813
+ const before = this.peers.size;
814
+ await this.pruneConnections();
815
+ if (this.peers.size >= before)
816
+ break; // nothing prunable
817
+ }
818
+ })().finally(() => {
819
+ this.pruneToLimitsInFlight = undefined;
820
+ });
821
+ return this.pruneToLimitsInFlight;
822
+ }
756
823
  async start() {
824
+ if (this.started)
825
+ return;
826
+ if (this._startInFlight)
827
+ return this._startInFlight;
828
+ this._startInFlight = this._startImpl().finally(() => {
829
+ this._startInFlight = undefined;
830
+ });
831
+ return this._startInFlight;
832
+ }
833
+ async _startImpl() {
757
834
  if (this.started) {
758
835
  return;
759
836
  }
760
- this.session = +new Date();
761
837
  await ready;
762
838
  this.closeController = new AbortController();
763
839
  this.outboundInflightQueue = pushable({ objectMode: true });
@@ -810,10 +886,45 @@ export class DirectStream extends TypedEventEmitter {
810
886
  this.closeController.signal.addEventListener("abort", () => {
811
887
  this.outboundInflightQueue.return();
812
888
  });
813
- this.routes = new Routes(this.publicKeyHash, {
814
- routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
815
- signal: this.closeController.signal,
816
- });
889
+ if (this.sharedRouting) {
890
+ const key = this.components.privateKey;
891
+ this.sharedRoutingKey = key;
892
+ let state = sharedRoutingByPrivateKey.get(key);
893
+ if (!state) {
894
+ const controller = new AbortController();
895
+ state = {
896
+ session: Date.now(),
897
+ controller,
898
+ routes: new Routes(this.publicKeyHash, {
899
+ routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
900
+ signal: controller.signal,
901
+ maxFromEntries: this.routeCacheMaxFromEntries,
902
+ maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
903
+ maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
904
+ }),
905
+ refs: 0,
906
+ };
907
+ sharedRoutingByPrivateKey.set(key, state);
908
+ }
909
+ else {
910
+ // Best-effort: prefer the strictest cleanup policy among co-located protocols.
911
+ state.routes.routeMaxRetentionPeriod = Math.min(state.routes.routeMaxRetentionPeriod, this.routeMaxRetentionPeriod);
912
+ }
913
+ state.refs += 1;
914
+ this.sharedRoutingState = state;
915
+ this.session = state.session;
916
+ this.routes = state.routes;
917
+ }
918
+ else {
919
+ this.session = Date.now();
920
+ this.routes = new Routes(this.publicKeyHash, {
921
+ routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
922
+ signal: this.closeController.signal,
923
+ maxFromEntries: this.routeCacheMaxFromEntries,
924
+ maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
925
+ maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
926
+ });
927
+ }
817
928
  this.started = true;
818
929
  this.stopping = false;
819
930
  logger.trace("starting");
@@ -831,6 +942,45 @@ export class DirectStream extends TypedEventEmitter {
831
942
  onDisconnect: this.onPeerDisconnected.bind(this),
832
943
  notifyOnLimitedConnection: false,
833
944
  })));
945
+ // Best-effort fallback: topology callbacks can depend on identify/protocol
946
+ // discovery. Some test environments connect peers without triggering the
947
+ // per-protocol topology immediately. Listening to peer connection events
948
+ // ensures we still attempt to open outbound streams opportunistically.
949
+ this._peerConnectListener = (ev) => {
950
+ if (this.stopping || !this.started)
951
+ return;
952
+ const peerId = ev?.detail ?? ev?.peerId;
953
+ if (!peerId)
954
+ return;
955
+ const conns = this.components.connectionManager.getConnections(peerId);
956
+ if (!conns || conns.length === 0)
957
+ return;
958
+ let conn = conns[0];
959
+ for (const c of conns) {
960
+ if (!isWebsocketConnection(c)) {
961
+ conn = c;
962
+ break;
963
+ }
964
+ }
965
+ void this.onPeerConnected(peerId, conn);
966
+ };
967
+ this._peerDisconnectListener = (ev) => {
968
+ if (this.stopping || !this.started)
969
+ return;
970
+ const peerId = ev?.detail ?? ev?.peerId;
971
+ if (!peerId)
972
+ return;
973
+ const conns = this.components.connectionManager.getConnections(peerId);
974
+ const conn = conns && conns.length > 0 ? conns[0] : undefined;
975
+ void this.onPeerDisconnected(peerId, conn);
976
+ };
977
+ try {
978
+ this.components.events.addEventListener("peer:connect", this._peerConnectListener);
979
+ this.components.events.addEventListener("peer:disconnect", this._peerDisconnectListener);
980
+ }
981
+ catch {
982
+ // ignore unsupported event targets
983
+ }
834
984
  // 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)
835
985
  const peerToConnections = new Map();
836
986
  const connections = this.components.connectionManager.getConnections();
@@ -877,7 +1027,22 @@ export class DirectStream extends TypedEventEmitter {
877
1027
  if (!this.started) {
878
1028
  return;
879
1029
  }
1030
+ const sharedState = this.sharedRoutingState;
1031
+ const sharedKey = this.sharedRoutingKey;
880
1032
  clearTimeout(this.pruneConnectionsTimeout);
1033
+ try {
1034
+ if (this._peerConnectListener) {
1035
+ this.components.events.removeEventListener("peer:connect", this._peerConnectListener);
1036
+ }
1037
+ if (this._peerDisconnectListener) {
1038
+ this.components.events.removeEventListener("peer:disconnect", this._peerDisconnectListener);
1039
+ }
1040
+ }
1041
+ catch {
1042
+ // ignore unsupported event targets
1043
+ }
1044
+ this._peerConnectListener = undefined;
1045
+ this._peerDisconnectListener = undefined;
881
1046
  await Promise.all(this.multicodecs.map((x) => this.components.registrar.unhandle(x)));
882
1047
  // unregister protocol and handlers
883
1048
  if (this._registrarTopologyIds != null) {
@@ -899,12 +1064,36 @@ export class DirectStream extends TypedEventEmitter {
899
1064
  this.queue.clear();
900
1065
  this.peers.clear();
901
1066
  this.seenCache.clear();
902
- this.routes.clear();
1067
+ // When routing is shared across co-located protocols, only clear once the last
1068
+ // instance stops. Otherwise we'd wipe routes still in use by other services.
1069
+ if (!sharedState) {
1070
+ this.routes.clear();
1071
+ }
903
1072
  this.peerKeyHashToPublicKey.clear();
904
1073
  for (const [_k, v] of this._ackCallbacks) {
905
1074
  v.clear();
906
1075
  }
907
1076
  this._ackCallbacks.clear();
1077
+ this.sharedRoutingState = undefined;
1078
+ this.sharedRoutingKey = undefined;
1079
+ if (sharedState && sharedKey) {
1080
+ sharedState.refs = Math.max(0, sharedState.refs - 1);
1081
+ if (sharedState.refs === 0) {
1082
+ try {
1083
+ sharedState.routes.clear();
1084
+ }
1085
+ catch {
1086
+ // ignore
1087
+ }
1088
+ try {
1089
+ sharedState.controller.abort();
1090
+ }
1091
+ catch {
1092
+ // ignore
1093
+ }
1094
+ sharedRoutingByPrivateKey.delete(sharedKey);
1095
+ }
1096
+ }
908
1097
  logger.trace("stopped");
909
1098
  this.stopping = false;
910
1099
  }
@@ -1056,7 +1245,8 @@ export class DirectStream extends TypedEventEmitter {
1056
1245
  }),
1057
1246
  }).sign(this.sign)).catch(dontThrowIfDeliveryError);
1058
1247
  }
1059
- this.checkIsAlive([peerKeyHash]);
1248
+ // Best-effort liveness probe; never let background probe failures crash callers.
1249
+ void this.checkIsAlive([peerKeyHash]).catch(() => false);
1060
1250
  }
1061
1251
  logger.trace("connection ended:" + peerKey.toString());
1062
1252
  }
@@ -1072,12 +1262,16 @@ export class DirectStream extends TypedEventEmitter {
1072
1262
  }
1073
1263
  addRouteConnection(from, neighbour, target, distance, session, remoteSession) {
1074
1264
  const targetHash = typeof target === "string" ? target : target.hashcode();
1265
+ // Best-effort: keep a hash -> public key map for any routed targets so
1266
+ // peer:unreachable events can always carry a PublicSignKey when we have seen it.
1267
+ if (typeof target !== "string") {
1268
+ this.peerKeyHashToPublicKey.set(targetHash, target);
1269
+ }
1075
1270
  const update = this.routes.add(from, neighbour, targetHash, distance, session, remoteSession);
1076
1271
  // second condition is that we don't want to emit 'reachable' events for routes where we act only as a relay
1077
1272
  // in this case, from is != this.publicKeyhash
1078
1273
  if (from === this.publicKeyHash) {
1079
1274
  if (update === "new") {
1080
- this.peerKeyHashToPublicKey.set(target.hashcode(), target);
1081
1275
  this.onPeerReachable(target);
1082
1276
  }
1083
1277
  }
@@ -1088,11 +1282,15 @@ export class DirectStream extends TypedEventEmitter {
1088
1282
  }
1089
1283
  onPeerUnreachable(hash) {
1090
1284
  // override this fns
1285
+ const key = this.peerKeyHashToPublicKey.get(hash);
1286
+ if (!key) {
1287
+ // Best-effort: we may only have the hash (no public key) for some routes.
1288
+ // Avoid crashing downstream listeners that assume `detail` is a PublicSignKey.
1289
+ return;
1290
+ }
1091
1291
  this.dispatchEvent(
1092
1292
  // TODO types
1093
- new CustomEvent("peer:unreachable", {
1094
- detail: this.peerKeyHashToPublicKey.get(hash),
1095
- }));
1293
+ new CustomEvent("peer:unreachable", { detail: key }));
1096
1294
  }
1097
1295
  updateSession(key, session) {
1098
1296
  if (this.routes.updateSession(key.hashcode(), session)) {
@@ -1102,6 +1300,12 @@ export class DirectStream extends TypedEventEmitter {
1102
1300
  invalidateSession(key) {
1103
1301
  this.routes.updateSession(key, undefined);
1104
1302
  }
1303
+ getRouteHints(target, from = this.publicKeyHash) {
1304
+ return this.routes.getRouteHints(from, target);
1305
+ }
1306
+ getBestRouteHint(target, from = this.publicKeyHash) {
1307
+ return this.routes.getBestRouteHint(from, target);
1308
+ }
1105
1309
  onPeerSession(key, session) {
1106
1310
  this.dispatchEvent(
1107
1311
  // TODO types
@@ -1145,6 +1349,11 @@ export class DirectStream extends TypedEventEmitter {
1145
1349
  peerStreams.removeEventListener("stream:inbound", forwardInbound);
1146
1350
  }, { once: true });
1147
1351
  this.addRouteConnection(this.publicKeyHash, publicKey.hashcode(), publicKey, -1, +new Date(), -1);
1352
+ // Enforce connection manager limits eagerly when new peers are added. Without this,
1353
+ // join storms can create large temporary peer sets and OOM in single-process sims.
1354
+ if (this.peers.size > this.connectionManagerOptions.maxConnections) {
1355
+ void this.pruneConnectionsToLimits().catch(() => { });
1356
+ }
1148
1357
  return peerStreams;
1149
1358
  }
1150
1359
  /**
@@ -1260,32 +1469,33 @@ export class DirectStream extends TypedEventEmitter {
1260
1469
  }
1261
1470
  }
1262
1471
  shouldIgnore(message, seenBefore) {
1263
- const fromMe = message.header.signatures?.publicKeys.find((x) => x.equals(this.publicKey));
1264
- if (fromMe) {
1472
+ const signedBySelf = message.header.signatures?.publicKeys.some((x) => x.equals(this.publicKey)) ?? false;
1473
+ if (signedBySelf) {
1265
1474
  return true;
1266
1475
  }
1267
- if ((seenBefore > 0 &&
1268
- message.header.mode instanceof SeekDelivery === false) ||
1269
- (message.header.mode instanceof SeekDelivery &&
1270
- seenBefore >= message.header.mode.redundancy)) {
1271
- return true;
1476
+ // For acknowledged modes, allow limited duplicate forwarding so that we can
1477
+ // discover and maintain multiple candidate routes (distance=seenCounter).
1478
+ if (message.header.mode instanceof AcknowledgeDelivery ||
1479
+ message.header.mode instanceof AcknowledgeAnyWhere) {
1480
+ return seenBefore >= message.header.mode.redundancy;
1272
1481
  }
1273
- return false;
1482
+ return seenBefore > 0;
1274
1483
  }
1275
1484
  async onDataMessage(from, peerStream, message, seenBefore) {
1276
1485
  if (this.shouldIgnore(message, seenBefore)) {
1277
1486
  return false;
1278
1487
  }
1279
1488
  let isForMe = false;
1280
- if (message.header.mode instanceof AnyWhere) {
1489
+ if (message.header.mode instanceof AnyWhere ||
1490
+ message.header.mode instanceof AcknowledgeAnyWhere) {
1281
1491
  isForMe = true;
1282
1492
  }
1283
1493
  else {
1284
1494
  const isFromSelf = this.publicKey.equals(from);
1285
- if (!isFromSelf) {
1286
- isForMe =
1287
- message.header.mode.to == null ||
1288
- message.header.mode.to.find((x) => x === this.publicKeyHash) != null;
1495
+ if (!isFromSelf &&
1496
+ (message.header.mode instanceof SilentDelivery ||
1497
+ message.header.mode instanceof AcknowledgeDelivery)) {
1498
+ isForMe = message.header.mode.to.includes(this.publicKeyHash);
1289
1499
  }
1290
1500
  }
1291
1501
  if (isForMe) {
@@ -1311,19 +1521,13 @@ export class DirectStream extends TypedEventEmitter {
1311
1521
  }
1312
1522
  }
1313
1523
  // Forward
1314
- if (message.header.mode instanceof SeekDelivery || seenBefore === 0) {
1524
+ const shouldForward = seenBefore === 0 ||
1525
+ ((message.header.mode instanceof AcknowledgeDelivery ||
1526
+ message.header.mode instanceof AcknowledgeAnyWhere) &&
1527
+ seenBefore < message.header.mode.redundancy);
1528
+ if (shouldForward) {
1315
1529
  // DONT await this since it might introduce a dead-lock
1316
- if (message.header.mode instanceof SeekDelivery) {
1317
- if (seenBefore < message.header.mode.redundancy) {
1318
- const to = [...this.peers.values()].filter((x) => !message.header.signatures?.publicKeys.find((y) => y.equals(x.publicKey)) && x !== peerStream);
1319
- if (to.length > 0) {
1320
- this.relayMessage(from, message, to);
1321
- }
1322
- }
1323
- }
1324
- else {
1325
- this.relayMessage(from, message);
1326
- }
1530
+ this.relayMessage(from, message);
1327
1531
  }
1328
1532
  }
1329
1533
  async verifyAndProcess(message) {
@@ -1341,12 +1545,16 @@ export class DirectStream extends TypedEventEmitter {
1341
1545
  return true;
1342
1546
  }
1343
1547
  async maybeAcknowledgeMessage(peerStream, message, seenBefore) {
1344
- if ((message.header.mode instanceof SeekDelivery ||
1345
- message.header.mode instanceof AcknowledgeDelivery) &&
1346
- seenBefore < message.header.mode.redundancy) {
1347
- const shouldAcknowldege = message.header.mode.to == null ||
1348
- message.header.mode.to.includes(this.publicKeyHash);
1349
- if (!shouldAcknowldege) {
1548
+ if (message.header.mode instanceof AcknowledgeDelivery ||
1549
+ message.header.mode instanceof AcknowledgeAnyWhere) {
1550
+ const isRecipient = message.header.mode instanceof AcknowledgeAnyWhere
1551
+ ? true
1552
+ : message.header.mode.to.includes(this.publicKeyHash);
1553
+ if (!shouldAcknowledgeDataMessage({
1554
+ isRecipient,
1555
+ seenBefore,
1556
+ redundancy: message.header.mode.redundancy,
1557
+ })) {
1350
1558
  return;
1351
1559
  }
1352
1560
  const signers = message.header.signatures.publicKeys.map((x) => x.hashcode());
@@ -1357,8 +1565,12 @@ export class DirectStream extends TypedEventEmitter {
1357
1565
  header: new MessageHeader({
1358
1566
  mode: new TracedDelivery(signers),
1359
1567
  session: this.session,
1360
- // include our origin if message is SeekDelivery and we have not recently pruned a connection to this peer
1361
- origin: message.header.mode instanceof SeekDelivery &&
1568
+ // include our origin for route-learning/dialer hints (best-effort privacy/anti-spam control):
1569
+ // only include once (seenBefore=0) and only if we have not recently pruned
1570
+ // a connection to any signer in the path
1571
+ origin: (message.header.mode instanceof AcknowledgeAnyWhere ||
1572
+ message.header.mode instanceof AcknowledgeDelivery) &&
1573
+ seenBefore === 0 &&
1362
1574
  !message.header.signatures.publicKeys.find((x) => this.prunedConnectionsCache?.has(x.hashcode()))
1363
1575
  ? new MultiAddrinfo(this.components.addressManager
1364
1576
  .getAddresses()
@@ -1443,37 +1655,40 @@ export class DirectStream extends TypedEventEmitter {
1443
1655
  for (const remote of remotes) {
1444
1656
  this.invalidateSession(remote);
1445
1657
  }
1446
- this.checkIsAlive(remotes);
1658
+ // Best-effort liveness probe; never let background probe failures crash callers.
1659
+ void this.checkIsAlive(remotes).catch(() => false);
1447
1660
  }
1448
1661
  async checkIsAlive(remotes) {
1449
1662
  if (this.peers.size === 0) {
1450
1663
  return false;
1451
1664
  }
1452
1665
  if (remotes.length > 0) {
1453
- return this.publish(undefined, {
1454
- mode: new SeekDelivery({
1455
- to: remotes,
1456
- redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
1457
- }),
1458
- })
1459
- .then(() => true)
1460
- .catch((e) => {
1461
- if (e instanceof DeliveryError) {
1462
- return false;
1463
- }
1464
- else if (e instanceof NotStartedError) {
1465
- return false;
1466
- }
1467
- else if (e instanceof TimeoutError) {
1468
- return false;
1469
- }
1470
- else if (e instanceof AbortError) {
1666
+ try {
1667
+ // Keepalive probes are best-effort, but must still require ACKs so we can
1668
+ // conclusively prune stale routes when peers actually went away.
1669
+ await this.publish(undefined, {
1670
+ mode: new AcknowledgeDelivery({
1671
+ to: remotes,
1672
+ redundancy: DEFAULT_SILENT_MESSAGE_REDUDANCY,
1673
+ }),
1674
+ });
1675
+ return true;
1676
+ }
1677
+ catch (e) {
1678
+ const errorName = e?.name ?? e?.constructor?.name ?? "UnknownError";
1679
+ if (e instanceof DeliveryError ||
1680
+ e instanceof NotStartedError ||
1681
+ e instanceof TimeoutError ||
1682
+ e instanceof AbortError ||
1683
+ errorName === "DeliveryError" ||
1684
+ errorName === "NotStartedError" ||
1685
+ errorName === "TimeoutError" ||
1686
+ errorName === "AbortError") {
1471
1687
  return false;
1472
1688
  }
1473
- else {
1474
- throw e;
1475
- }
1476
- }); // this will remove the target if it is still not reable
1689
+ warn(`checkIsAlive unexpected error: ${errorName}`);
1690
+ return false;
1691
+ }
1477
1692
  }
1478
1693
  return false;
1479
1694
  }
@@ -1486,8 +1701,7 @@ export class DirectStream extends TypedEventEmitter {
1486
1701
  redundancy: DEFAULT_SILENT_MESSAGE_REDUDANCY,
1487
1702
  });
1488
1703
  if (mode instanceof AcknowledgeDelivery ||
1489
- mode instanceof SilentDelivery ||
1490
- mode instanceof SeekDelivery) {
1704
+ mode instanceof SilentDelivery) {
1491
1705
  if (mode.to) {
1492
1706
  let preLength = mode.to.length;
1493
1707
  mode.to = mode.to.filter((x) => x !== this.publicKeyHash);
@@ -1495,7 +1709,7 @@ export class DirectStream extends TypedEventEmitter {
1495
1709
  if (preLength > 0 && mode.to?.length === 0) {
1496
1710
  throw new InvalidMessageError("Unexpected to create a message with self as the only receiver");
1497
1711
  }
1498
- if (mode.to.length === 0 && mode instanceof SeekDelivery === false) {
1712
+ if (mode.to.length === 0) {
1499
1713
  throw new InvalidMessageError("Unexpected to deliver message with mode: " +
1500
1714
  mode.constructor.name +
1501
1715
  " without recipents");
@@ -1503,24 +1717,6 @@ export class DirectStream extends TypedEventEmitter {
1503
1717
  }
1504
1718
  }
1505
1719
  }
1506
- if (mode instanceof AcknowledgeDelivery || mode instanceof SilentDelivery) {
1507
- const now = +new Date();
1508
- for (const hash of mode.to) {
1509
- const neighbourRoutes = this.routes.routes
1510
- .get(this.publicKeyHash)
1511
- ?.get(hash);
1512
- if (!neighbourRoutes ||
1513
- now - neighbourRoutes.session >
1514
- neighbourRoutes.list.length * this.routeSeekInterval ||
1515
- !this.routes.isUpToDate(hash, neighbourRoutes)) {
1516
- mode = new SeekDelivery({
1517
- to: mode.to,
1518
- redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
1519
- });
1520
- break;
1521
- }
1522
- }
1523
- }
1524
1720
  const message = new DataMessage({
1525
1721
  data: data instanceof Uint8ArrayList ? data.subarray() : data,
1526
1722
  header: new MessageHeader({
@@ -1543,7 +1739,9 @@ export class DirectStream extends TypedEventEmitter {
1543
1739
  * Publishes messages to all peers
1544
1740
  */
1545
1741
  async publish(data, options = {
1546
- mode: new SeekDelivery({ redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY }),
1742
+ mode: new AcknowledgeAnyWhere({
1743
+ redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
1744
+ }),
1547
1745
  }) {
1548
1746
  if (!this.started) {
1549
1747
  throw new NotStartedError();
@@ -1579,7 +1777,7 @@ export class DirectStream extends TypedEventEmitter {
1579
1777
  if (this.canRelayMessage) {
1580
1778
  if (message instanceof DataMessage) {
1581
1779
  if (message.header.mode instanceof AcknowledgeDelivery ||
1582
- message.header.mode instanceof SeekDelivery) {
1780
+ message.header.mode instanceof AcknowledgeAnyWhere) {
1583
1781
  await message.sign(this.sign);
1584
1782
  }
1585
1783
  }
@@ -1600,6 +1798,31 @@ export class DirectStream extends TypedEventEmitter {
1600
1798
  clearTimeout(timer);
1601
1799
  this.healthChecks.delete(to);
1602
1800
  }
1801
+ formatDeliveryDebugState(targets, fastestNodesReached) {
1802
+ const sampleSize = 16;
1803
+ const sampledTargets = [...targets].slice(0, sampleSize);
1804
+ const sampled = sampledTargets.map((target) => {
1805
+ const route = this.routes.findNeighbor(this.publicKeyHash, target);
1806
+ const routeSample = route?.list
1807
+ .slice(0, 3)
1808
+ .map((r) => `${r.hash}:${r.distance}${r.expireAt ? ":old" : ""}`)
1809
+ .join(",") ?? "-";
1810
+ const reachable = this.routes.isReachable(this.publicKeyHash, target);
1811
+ const stream = this.peers.get(target);
1812
+ let openConnections = 0;
1813
+ if (stream) {
1814
+ try {
1815
+ openConnections = this.components.connectionManager.getConnections(stream.peerId).length;
1816
+ }
1817
+ catch {
1818
+ openConnections = stream.isReadable || stream.isWritable ? 1 : 0;
1819
+ }
1820
+ }
1821
+ return `${target}{reachable=${reachable ? 1 : 0},ack=${fastestNodesReached.has(target) ? 1 : 0},stream=${stream ? 1 : 0},conns=${openConnections},routes=${routeSample}}`;
1822
+ });
1823
+ const more = targets.size - sampledTargets.length;
1824
+ return `deliveryState peers=${this.peers.size} routesLocal=${this.routes.count()} routesAll=${this.routes.countAll()} targets=[${sampled.join(";")}]${more > 0 ? ` (+${more} more)` : ""}`;
1825
+ }
1603
1826
  async createDeliveryPromise(from, message, relayed, signal) {
1604
1827
  if (message.header.mode instanceof AnyWhere) {
1605
1828
  return {
@@ -1615,11 +1838,10 @@ export class DirectStream extends TypedEventEmitter {
1615
1838
  }
1616
1839
  const fastestNodesReached = new Map();
1617
1840
  const messageToSet = new Set();
1618
- if (message.header.mode.to) {
1841
+ if (deliveryModeHasReceiver(message.header.mode)) {
1619
1842
  for (const to of message.header.mode.to) {
1620
- if (to === from.hashcode()) {
1843
+ if (to === from.hashcode())
1621
1844
  continue;
1622
- }
1623
1845
  messageToSet.add(to);
1624
1846
  if (!relayed && !this.healthChecks.has(to)) {
1625
1847
  this.healthChecks.set(to, setTimeout(() => {
@@ -1640,20 +1862,16 @@ export class DirectStream extends TypedEventEmitter {
1640
1862
  }
1641
1863
  const willGetAllAcknowledgements = !relayed; // Only the origin will get all acks
1642
1864
  // Expected to receive at least 'filterMessageForSeenCounter' acknowledgements from each peer
1643
- const filterMessageForSeenCounter = relayed
1644
- ? undefined
1645
- : message.header.mode instanceof SeekDelivery
1646
- ? Math.min(this.peers.size, message.header.mode.redundancy)
1647
- : 1; /* message.deliveryMode instanceof SeekDelivery ? Math.min(this.peers.size - (relayed ? 1 : 0), message.deliveryMode.redundancy) : 1 */
1865
+ const filterMessageForSeenCounter = relayed ? undefined : 1;
1648
1866
  const uniqueAcks = new Set();
1649
1867
  const session = +new Date();
1650
1868
  const onUnreachable = !relayed &&
1651
1869
  ((ev) => {
1652
- const deletedReceiver = messageToSet.delete(ev.detail.hashcode());
1653
- if (deletedReceiver) {
1654
- // Only reject if we are the sender
1655
- clear();
1656
- deliveryDeferredPromise.reject(new DeliveryError(`At least one recipent became unreachable while delivering messsage of type ${message.constructor.name}} to ${ev.detail.hashcode()}`));
1870
+ const target = ev.detail.hashcode();
1871
+ if (messageToSet.has(target)) {
1872
+ // A peer can transiently appear unreachable while routes are being updated.
1873
+ // Keep waiting for ACK/timeout instead of failing delivery immediately.
1874
+ logger.trace("peer unreachable during delivery (msg=%s, target=%s); waiting for ACK/timeout", idString, target);
1657
1875
  }
1658
1876
  });
1659
1877
  onUnreachable && this.addEventListener("peer:unreachable", onUnreachable);
@@ -1680,9 +1898,13 @@ export class DirectStream extends TypedEventEmitter {
1680
1898
  }
1681
1899
  }
1682
1900
  if (!hasAll && willGetAllAcknowledgements) {
1901
+ const debugState = this.formatDeliveryDebugState(messageToSet, fastestNodesReached);
1902
+ const msgMeta = `msgType=${message.constructor.name} dataBytes=${message instanceof DataMessage
1903
+ ? (message.data?.byteLength ?? 0)
1904
+ : 0} relayed=${relayed ? 1 : 0}`;
1683
1905
  deliveryDeferredPromise.reject(new DeliveryError(`Failed to get message ${idString} ${filterMessageForSeenCounter} ${[
1684
1906
  ...messageToSet,
1685
- ]} delivery acknowledges from all nodes (${fastestNodesReached.size}/${messageToSet.size}). Mode: ${message.header.mode.constructor.name}. Redundancy: ${message.header.mode["redundancy"]}`));
1907
+ ]} delivery acknowledges from all nodes (${fastestNodesReached.size}/${messageToSet.size}). Mode: ${message.header.mode.constructor.name}. Redundancy: ${message.header.mode["redundancy"]}. ${msgMeta}. ${debugState}`));
1686
1908
  }
1687
1909
  else {
1688
1910
  deliveryDeferredPromise.resolve();
@@ -1705,11 +1927,22 @@ export class DirectStream extends TypedEventEmitter {
1705
1927
  // know how many ACKs we will get
1706
1928
  if (filterMessageForSeenCounter != null &&
1707
1929
  uniqueAcks.size >= messageToSet.size * filterMessageForSeenCounter) {
1708
- if (haveReceivers) {
1709
- // 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
1710
- // only remove callback function if we actually expected a expected amount of responses
1930
+ const shouldKeepCallbackForRouteLearning = !relayed &&
1931
+ message.header.mode instanceof AcknowledgeDelivery &&
1932
+ message.header.mode.redundancy > 1;
1933
+ if (haveReceivers && !shouldKeepCallbackForRouteLearning) {
1934
+ // If we have an explicit recipient list we can clear the ACK callback once we
1935
+ // got the expected acknowledgements.
1711
1936
  clear();
1712
1937
  }
1938
+ else if (haveReceivers && shouldKeepCallbackForRouteLearning) {
1939
+ // Resolve delivery early, but keep ACK callbacks alive until timeout so we can
1940
+ // learn additional redundant routes (seenCounter=1..redundancy-1).
1941
+ onUnreachable &&
1942
+ this.removeEventListener("peer:unreachable", onUnreachable);
1943
+ onAbort && signal?.removeEventListener("abort", onAbort);
1944
+ onAbort = undefined;
1945
+ }
1713
1946
  deliveryDeferredPromise.resolve();
1714
1947
  return true;
1715
1948
  }
@@ -1723,25 +1956,35 @@ export class DirectStream extends TypedEventEmitter {
1723
1956
  const seenCounter = ack.seenCounter;
1724
1957
  // remove the automatic removal of route timeout since we have observed lifesigns of a peer
1725
1958
  this.clearHealthcheckTimer(messageTargetHash);
1726
- // if the target is not inside the original message to, we still ad the target to our routes
1727
- // this because a relay might modify the 'to' list and we might receive more answers than initially set
1728
- if (message.header.mode instanceof SeekDelivery) {
1729
- this.addRouteConnection(messageFrom?.publicKey.hashcode() || this.publicKeyHash, messageThrough.publicKey.hashcode(), messageTarget, seenCounter, session, Number(ack.header.session)); // we assume the seenCounter = distance. The more the message has been seen by the target the longer the path is to the target
1959
+ // If the target is not inside the original message `to`, we can still add the target to our routes.
1960
+ // This matters because relays may modify `to`, and because "ack-anywhere" style probes intentionally
1961
+ // do not provide an explicit recipient list.
1962
+ if (message.header.mode instanceof AcknowledgeDelivery ||
1963
+ message.header.mode instanceof AcknowledgeAnyWhere) {
1964
+ const upstreamHash = messageFrom?.publicKey.hashcode();
1965
+ const routeUpdate = computeSeekAckRouteUpdate({
1966
+ current: this.publicKeyHash,
1967
+ upstream: upstreamHash,
1968
+ downstream: messageThrough.publicKey.hashcode(),
1969
+ target: messageTargetHash,
1970
+ // Route "distance" is based on recipient-seen order (0 = fastest). This is relied upon by
1971
+ // `Routes.getFanout(...)` which uses `distance < redundancy` to select redundant next-hops.
1972
+ distance: seenCounter,
1973
+ });
1974
+ this.addRouteConnection(routeUpdate.from, routeUpdate.neighbour, messageTarget, routeUpdate.distance, session, Number(ack.header.session));
1730
1975
  }
1731
1976
  if (messageToSet.has(messageTargetHash)) {
1732
- // Only keep track of relevant acks
1733
- if (filterMessageForSeenCounter == null ||
1734
- seenCounter < filterMessageForSeenCounter) {
1735
- // TODO set limit correctly
1736
- if (seenCounter < MAX_ROUTE_DISTANCE) {
1737
- let arr = fastestNodesReached.get(messageTargetHash);
1738
- if (!arr) {
1739
- arr = [];
1740
- fastestNodesReached.set(messageTargetHash, arr);
1741
- }
1742
- arr.push(seenCounter);
1743
- uniqueAcks.add(messageTargetHash + seenCounter);
1977
+ // Any ACK from the target should satisfy delivery semantics.
1978
+ // Relying on only seenCounter=0 can fail under churn if the first ACK
1979
+ // is lost but a later ACK (seenCounter>0) arrives.
1980
+ if (seenCounter < MAX_ROUTE_DISTANCE) {
1981
+ let arr = fastestNodesReached.get(messageTargetHash);
1982
+ if (!arr) {
1983
+ arr = [];
1984
+ fastestNodesReached.set(messageTargetHash, arr);
1744
1985
  }
1986
+ arr.push(seenCounter);
1987
+ uniqueAcks.add(messageTargetHash);
1745
1988
  }
1746
1989
  }
1747
1990
  checkDone();
@@ -1757,7 +2000,9 @@ export class DirectStream extends TypedEventEmitter {
1757
2000
  if (this.stopping || !this.started) {
1758
2001
  throw new NotStartedError();
1759
2002
  }
2003
+ const isRelayed = relayed ?? from.hashcode() !== this.publicKeyHash;
1760
2004
  let delivereyPromise = undefined;
2005
+ let ackCallbackId;
1761
2006
  if ((!message.header.signatures ||
1762
2007
  message.header.signatures.publicKeys.length === 0) &&
1763
2008
  message instanceof DataMessage &&
@@ -1767,85 +2012,145 @@ export class DirectStream extends TypedEventEmitter {
1767
2012
  /**
1768
2013
  * Logic for handling acknowledge messages when we receive them (later)
1769
2014
  */
1770
- if (message instanceof DataMessage &&
1771
- message.header.mode instanceof SeekDelivery &&
1772
- !relayed) {
1773
- to = this.peers; // seek delivery will not work unless we try all possible paths
1774
- }
1775
- if (message.header.mode instanceof AcknowledgeDelivery) {
1776
- to = undefined;
1777
- }
1778
2015
  if ((message instanceof DataMessage || message instanceof Goodbye) &&
1779
- (message.header.mode instanceof SeekDelivery ||
1780
- message.header.mode instanceof AcknowledgeDelivery)) {
1781
- const deliveryDeferredPromise = await this.createDeliveryPromise(from, message, relayed, signal);
2016
+ (message.header.mode instanceof AcknowledgeDelivery ||
2017
+ message.header.mode instanceof AcknowledgeAnyWhere)) {
2018
+ const deliveryDeferredPromise = await this.createDeliveryPromise(from, message, isRelayed, signal);
1782
2019
  delivereyPromise = deliveryDeferredPromise.promise;
2020
+ ackCallbackId = toBase64(message.id);
1783
2021
  }
1784
- const bytes = message.bytes();
1785
- if (!relayed) {
1786
- const bytesArray = bytes instanceof Uint8Array ? bytes : bytes.subarray();
1787
- await this.modifySeenCache(bytesArray);
1788
- }
1789
- /**
1790
- * For non SEEKing message delivery modes, use routing
1791
- */
1792
- if (message instanceof DataMessage) {
1793
- if ((message.header.mode instanceof AcknowledgeDelivery ||
1794
- message.header.mode instanceof SilentDelivery) &&
1795
- !to) {
1796
- if (message.header.mode.to.length === 0) {
1797
- return delivereyPromise; // we defintely know that we should not forward the message anywhere
1798
- }
1799
- const fanout = this.routes.getFanout(from.hashcode(), message.header.mode.to, message.header.mode.redundancy);
1800
- if (fanout) {
1801
- if (fanout.size > 0) {
2022
+ try {
2023
+ const bytes = message.bytes();
2024
+ if (!isRelayed) {
2025
+ const bytesArray = bytes instanceof Uint8Array ? bytes : bytes.subarray();
2026
+ await this.modifySeenCache(bytesArray);
2027
+ }
2028
+ /**
2029
+ * For non SEEKing message delivery modes, use routing
2030
+ */
2031
+ if (message instanceof DataMessage) {
2032
+ if ((message.header.mode instanceof AcknowledgeDelivery ||
2033
+ message.header.mode instanceof SilentDelivery) &&
2034
+ !to) {
2035
+ if (message.header.mode.to.length === 0) {
2036
+ // we definitely know that we should not forward the message anywhere
2037
+ return delivereyPromise;
2038
+ }
2039
+ const fanout = this.routes.getFanout(from.hashcode(), message.header.mode.to, message.header.mode.redundancy);
2040
+ // If we have explicit routing information, send only along the chosen next-hops.
2041
+ // If `fanout` is empty (no route info yet), fall through to the flooding logic below
2042
+ // so acknowledged deliveries can discover/repair routes instead of timing out.
2043
+ if (fanout && fanout.size > 0) {
1802
2044
  const promises = [];
2045
+ const usedNeighbours = new Set();
2046
+ const originalTo = message.header.mode.to;
1803
2047
  for (const [neighbour, _distantPeers] of fanout) {
1804
2048
  const stream = this.peers.get(neighbour);
1805
- stream &&
2049
+ if (!stream)
2050
+ continue;
2051
+ if (message.header.mode instanceof SilentDelivery) {
2052
+ message.header.mode.to = [..._distantPeers.keys()];
2053
+ promises.push(stream.waitForWrite(message.bytes(), message.header.priority));
2054
+ }
2055
+ else {
1806
2056
  promises.push(stream.waitForWrite(bytes, message.header.priority));
2057
+ }
2058
+ usedNeighbours.add(neighbour);
2059
+ }
2060
+ if (message.header.mode instanceof SilentDelivery) {
2061
+ message.header.mode.to = originalTo;
2062
+ }
2063
+ // If the sender requested redundancy but we don't yet have enough distinct
2064
+ // next-hops for the target(s), opportunistically probe additional neighbours.
2065
+ // This replaces the previous "greedy fanout" probing behavior without needing
2066
+ // a separate delivery mode.
2067
+ if (!isRelayed &&
2068
+ message.header.mode instanceof AcknowledgeDelivery &&
2069
+ usedNeighbours.size < message.header.mode.redundancy) {
2070
+ for (const [neighbour, stream] of this.peers) {
2071
+ if (usedNeighbours.size >= message.header.mode.redundancy) {
2072
+ break;
2073
+ }
2074
+ if (usedNeighbours.has(neighbour))
2075
+ continue;
2076
+ usedNeighbours.add(neighbour);
2077
+ promises.push(stream.waitForWrite(bytes, message.header.priority));
2078
+ }
1807
2079
  }
1808
2080
  await Promise.all(promises);
1809
- return delivereyPromise; // we are done sending the message in all direction with updates 'to' lists
2081
+ return delivereyPromise;
2082
+ }
2083
+ // If we don't have routing information:
2084
+ // - For acknowledged deliveries, fall through to flooding (route discovery / repair).
2085
+ // - For silent deliveries, relays should not flood (prevents unnecessary fanout); origin may still flood.
2086
+ // We still allow direct neighbour delivery to explicit recipients (if connected).
2087
+ if (isRelayed && message.header.mode instanceof SilentDelivery) {
2088
+ const promises = [];
2089
+ const originalTo = message.header.mode.to;
2090
+ for (const recipient of originalTo) {
2091
+ if (recipient === this.publicKeyHash)
2092
+ continue;
2093
+ if (recipient === from.hashcode())
2094
+ continue; // never send back to previous hop
2095
+ const stream = this.peers.get(recipient);
2096
+ if (!stream)
2097
+ continue;
2098
+ if (message.header.signatures?.publicKeys.find((x) => x.hashcode() === recipient)) {
2099
+ continue; // recipient already signed/seen this message
2100
+ }
2101
+ message.header.mode.to = [recipient];
2102
+ promises.push(stream.waitForWrite(message.bytes(), message.header.priority));
2103
+ }
2104
+ message.header.mode.to = originalTo;
2105
+ if (promises.length > 0) {
2106
+ await Promise.all(promises);
2107
+ }
2108
+ return delivereyPromise;
1810
2109
  }
1811
- return; // we defintely know that we should not forward the message anywhere
1812
- }
1813
- // we end up here because we don't have enough information yet in how to send data to the peer (TODO test this codepath)
1814
- if (relayed) {
1815
- return;
1816
2110
  }
1817
- } // else send to all (fallthrough to code below)
1818
- }
1819
- // We fils to send the message directly, instead fallback to floodsub
1820
- const peers = to || this.peers;
1821
- if (peers == null ||
1822
- (Array.isArray(peers) && peers.length === 0) ||
1823
- (peers instanceof Map && peers.size === 0)) {
1824
- logger.trace("No peers to send to");
1825
- return delivereyPromise;
1826
- }
1827
- let sentOnce = false;
1828
- const promises = [];
1829
- for (const stream of peers.values()) {
1830
- const id = stream;
1831
- // Dont sent back to the sender
1832
- if (id.publicKey.equals(from)) {
1833
- continue;
1834
2111
  }
1835
- // Dont send message back to any of the signers (they have already seen the message)
1836
- if (message.header.signatures?.publicKeys.find((x) => x.equals(id.publicKey))) {
1837
- continue;
2112
+ // We fail to send the message directly, instead fallback to floodsub
2113
+ const peers = to || this.peers;
2114
+ if (peers == null ||
2115
+ (Array.isArray(peers) && peers.length === 0) ||
2116
+ (peers instanceof Map && peers.size === 0)) {
2117
+ logger.trace("No peers to send to");
2118
+ return delivereyPromise;
2119
+ }
2120
+ let sentOnce = false;
2121
+ const promises = [];
2122
+ for (const stream of peers.values()) {
2123
+ const id = stream;
2124
+ // Dont sent back to the sender
2125
+ if (id.publicKey.equals(from)) {
2126
+ continue;
2127
+ }
2128
+ // Dont send message back to any of the signers (they have already seen the message)
2129
+ if (message.header.signatures?.publicKeys.find((x) => x.equals(id.publicKey))) {
2130
+ continue;
2131
+ }
2132
+ sentOnce = true;
2133
+ promises.push(id.waitForWrite(bytes, message.header.priority));
2134
+ }
2135
+ await Promise.all(promises);
2136
+ if (!sentOnce) {
2137
+ // If the caller provided an explicit peer list, treat "no valid receivers" as an error
2138
+ // even when forwarding. This catches programming mistakes early and matches test expectations.
2139
+ if (!isRelayed || to != null) {
2140
+ throw new DeliveryError("Message did not have any valid receivers");
2141
+ }
1838
2142
  }
1839
- sentOnce = true;
1840
- promises.push(id.waitForWrite(bytes, message.header.priority));
2143
+ return delivereyPromise;
1841
2144
  }
1842
- await Promise.all(promises);
1843
- if (!sentOnce) {
1844
- if (!relayed) {
1845
- throw new DeliveryError("Message did not have any valid receivers");
2145
+ catch (error) {
2146
+ // If message fanout/write fails before publishMessage returns its delivery
2147
+ // promise, clear any ACK callback to avoid late timer rejections leaking as
2148
+ // unhandled rejections in fire-and-forget call paths.
2149
+ if (ackCallbackId) {
2150
+ this._ackCallbacks.get(ackCallbackId)?.clear();
1846
2151
  }
2152
+ throw error;
1847
2153
  }
1848
- return delivereyPromise;
1849
2154
  }
1850
2155
  async maybeConnectDirectly(toHash, origin) {
1851
2156
  if (this.peers.has(toHash) || this.prunedConnectionsCache?.has(toHash)) {
@@ -1926,10 +2231,12 @@ export class DirectStream extends TypedEventEmitter {
1926
2231
  for (const h of admitted)
1927
2232
  if (reached(h, target))
1928
2233
  wins.add(h);
2234
+ // Preserve input order in the returned list (important for deterministic callers/tests).
2235
+ const orderedWins = () => admitted.filter((h) => wins.has(h));
1929
2236
  if (settle === "any" && wins.size > 0)
1930
- return [...wins];
2237
+ return orderedWins();
1931
2238
  if (settle === "all" && wins.size === admitted.length)
1932
- return [...wins];
2239
+ return orderedWins();
1933
2240
  // Abort/timeout
1934
2241
  const abortSignals = [this.closeController.signal];
1935
2242
  if (signal) {
@@ -1949,7 +2256,7 @@ export class DirectStream extends TypedEventEmitter {
1949
2256
  signals: abortSignals,
1950
2257
  timeout,
1951
2258
  });
1952
- return [...wins];
2259
+ return orderedWins();
1953
2260
  }
1954
2261
  catch (e) {
1955
2262
  const abortSignal = abortSignals.find((s) => s.aborted);
@@ -1959,112 +2266,23 @@ export class DirectStream extends TypedEventEmitter {
1959
2266
  }
1960
2267
  throw new AbortError("Aborted waiting for peers: " + abortSignal.reason);
1961
2268
  }
1962
- if (e instanceof Error) {
1963
- throw e;
2269
+ if (e instanceof TimeoutError) {
2270
+ if (settle === "any") {
2271
+ if (wins.size > 0)
2272
+ return orderedWins();
2273
+ throw new TimeoutError(`Timeout waiting for peers (target=${target}, seek=${seek}, missing=${admitted.length}/${admitted.length})`);
2274
+ }
2275
+ const missing = admitted.filter((h) => !wins.has(h));
2276
+ const preview = missing.slice(0, 5).join(", ");
2277
+ throw new TimeoutError(`Timeout waiting for peers (target=${target}, seek=${seek}, missing=${missing.length}/${admitted.length}${preview ? `, e.g. ${preview}` : ""})`);
1964
2278
  }
2279
+ if (e instanceof Error)
2280
+ throw e;
1965
2281
  if (settle === "all")
1966
2282
  throw new TimeoutError("Timeout waiting for peers");
1967
- return [...wins]; // settle:any: return whatever successes we got
1968
- }
1969
- }
1970
- /* async waitFor(
1971
- peer: PeerId | PublicSignKey | string,
1972
- options?: {
1973
- timeout?: number;
1974
- signal?: AbortSignal;
1975
- neighbour?: boolean;
1976
- inflight?: boolean;
1977
- },
1978
- ) {
1979
- const hash =
1980
- typeof peer === "string"
1981
- ? peer
1982
- : (peer instanceof PublicSignKey
1983
- ? peer
1984
- : getPublicKeyFromPeerId(peer)
1985
- ).hashcode();
1986
- if (hash === this.publicKeyHash) {
1987
- return; // TODO throw error instead?
1988
- }
1989
-
1990
- if (options?.inflight) {
1991
- // if peer is not in active connections or dialQueue, return silenty
1992
- if (
1993
- !this.peers.has(hash) &&
1994
- !this.components.connectionManager
1995
- .getDialQueue()
1996
- .some((x) => getPublicKeyFromPeerId(x.peerId).hashcode() === hash) &&
1997
- !this.components.connectionManager
1998
- .getConnections()
1999
- .some((x) => getPublicKeyFromPeerId(x.remotePeer).hashcode() === hash)
2000
- ) {
2001
- return;
2002
- }
2003
- }
2004
-
2005
- const checkIsReachable = (deferred: DeferredPromise<void>) => {
2006
- if (options?.neighbour && !this.peers.has(hash)) {
2007
- return;
2008
- }
2009
-
2010
- if (!this.routes.isReachable(this.publicKeyHash, hash, 0)) {
2011
- return;
2012
- }
2013
-
2014
- deferred.resolve();
2015
- };
2016
- const abortSignals = [this.closeController.signal];
2017
- if (options?.signal) {
2018
- abortSignals.push(options.signal);
2283
+ return orderedWins(); // settle:any: return whatever successes we got
2019
2284
  }
2020
-
2021
- try {
2022
- await waitForEvent(this, ["peer:reachable"], checkIsReachable, {
2023
- signals: abortSignals,
2024
- timeout: options?.timeout,
2025
- });
2026
- } catch (error) {
2027
- throw new Error(
2028
- "Stream to " +
2029
- hash +
2030
- " from " +
2031
- this.publicKeyHash +
2032
- " does not exist. Connection exist: " +
2033
- this.peers.has(hash) +
2034
- ". Route exist: " +
2035
- this.routes.isReachable(this.publicKeyHash, hash, 0),
2036
- );
2037
- }
2038
-
2039
- if (options?.neighbour) {
2040
- const stream = this.peers.get(hash)!;
2041
- try {
2042
- let checkIsWritable = (pDefer: DeferredPromise<void>) => {
2043
- if (stream.isReadable && stream.isWritable) {
2044
- pDefer.resolve();
2045
- }
2046
- };
2047
- await waitForEvent(
2048
- stream,
2049
- ["stream:outbound", "stream:inbound"],
2050
- checkIsWritable,
2051
- {
2052
- signals: abortSignals,
2053
- timeout: options?.timeout,
2054
- },
2055
- );
2056
- } catch (error) {
2057
- throw new Error(
2058
- "Stream to " +
2059
- stream.publicKey.hashcode() +
2060
- " not ready. Readable: " +
2061
- stream.isReadable +
2062
- ". Writable " +
2063
- stream.isWritable,
2064
- );
2065
- }
2066
- }
2067
- } */
2285
+ }
2068
2286
  getPublicKey(hash) {
2069
2287
  return this.peerKeyHashToPublicKey.get(hash);
2070
2288
  }
@@ -2073,6 +2291,10 @@ export class DirectStream extends TypedEventEmitter {
2073
2291
  }
2074
2292
  // make this into a job? run every few ms
2075
2293
  maybePruneConnections() {
2294
+ // Hard cap on peer streams: treat as a primary pruning signal.
2295
+ if (this.peers.size > this.connectionManagerOptions.maxConnections) {
2296
+ return this.pruneConnectionsToLimits();
2297
+ }
2076
2298
  if (this.connectionManagerOptions.pruner) {
2077
2299
  if (this.connectionManagerOptions.pruner.bandwidth != null) {
2078
2300
  let usedBandwidth = 0;
@@ -2108,7 +2330,7 @@ export class DirectStream extends TypedEventEmitter {
2108
2330
  return;
2109
2331
  }
2110
2332
  const stream = this.peers.get(prunables[0]);
2111
- this.prunedConnectionsCache.add(stream.publicKey.hashcode());
2333
+ this.prunedConnectionsCache?.add(stream.publicKey.hashcode());
2112
2334
  await this.onPeerDisconnected(stream.peerId);
2113
2335
  return this.components.connectionManager.closeConnections(stream.peerId);
2114
2336
  }