@peerbit/stream 4.5.3 → 4.6.0-000e3f1

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,13 +83,17 @@ 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
+ const PRIORITY_LANES = 4;
87
88
  const getLaneFromPriority = (priority) => {
88
- if (priority > 0) {
89
- return 0;
90
- }
91
- return 1;
89
+ // Higher priority numbers should be drained first.
90
+ // Lane 0 is the fastest/highest-priority lane.
91
+ const maxLane = PRIORITY_LANES - 1;
92
+ if (!Number.isFinite(priority)) {
93
+ return maxLane;
94
+ }
95
+ const clampedPriority = Math.max(0, Math.min(maxLane, Math.floor(priority)));
96
+ return maxLane - clampedPriority;
92
97
  };
93
98
  // Hook for tests to override queued length measurement (peerStreams, default impl)
94
99
  export let measureOutboundQueuedBytes = (ps) => {
@@ -99,8 +104,7 @@ export let measureOutboundQueuedBytes = (ps) => {
99
104
  // @ts-ignore - optional test helper
100
105
  if (typeof active.getReadableLength === "function") {
101
106
  try {
102
- // lane 0 only
103
- return active.getReadableLength(0) || 0;
107
+ return active.getReadableLength() || 0;
104
108
  }
105
109
  catch {
106
110
  // ignore
@@ -144,6 +148,19 @@ export class PeerStreams extends TypedEventEmitter {
144
148
  _getActiveOutboundPushable() {
145
149
  return this.outboundStreams[0]?.pushable;
146
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
+ }
147
164
  _getOutboundCount() {
148
165
  return this.outboundStreams.length;
149
166
  }
@@ -165,7 +182,7 @@ export class PeerStreams extends TypedEventEmitter {
165
182
  if (existing)
166
183
  return existing;
167
184
  const pushableInst = pushableLanes({
168
- lanes: 2,
185
+ lanes: PRIORITY_LANES,
169
186
  onPush: (val) => {
170
187
  candidate.bytesDelivered += val.length || val.byteLength || 0;
171
188
  },
@@ -290,9 +307,7 @@ export class PeerStreams extends TypedEventEmitter {
290
307
  continue;
291
308
  }
292
309
  try {
293
- c.pushable.push(payload, c.pushable.getReadableLength(0) === 0
294
- ? 0
295
- : getLaneFromPriority(priority));
310
+ c.pushable.push(payload, getLaneFromPriority(priority));
296
311
  successes++;
297
312
  }
298
313
  catch (e) {
@@ -347,7 +362,10 @@ export class PeerStreams extends TypedEventEmitter {
347
362
  this.write(bytes, priority);
348
363
  return;
349
364
  }
350
- 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;
351
369
  await new Promise((resolve, reject) => {
352
370
  const onOutbound = () => {
353
371
  cleanup();
@@ -624,7 +642,16 @@ export class PeerStreams extends TypedEventEmitter {
624
642
  logger.error("Failed to abort inbound stream");
625
643
  }
626
644
  try {
627
- 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(() => { });
628
655
  }
629
656
  catch {
630
657
  logger.error("Failed to close inbound stream");
@@ -639,6 +666,7 @@ export class PeerStreams extends TypedEventEmitter {
639
666
  this.inboundStreams = [];
640
667
  }
641
668
  }
669
+ const sharedRoutingByPrivateKey = new WeakMap();
642
670
  export class DirectStream extends TypedEventEmitter {
643
671
  components;
644
672
  peerId;
@@ -664,6 +692,8 @@ export class DirectStream extends TypedEventEmitter {
664
692
  multicodecs;
665
693
  seenCache;
666
694
  _registrarTopologyIds;
695
+ _peerConnectListener;
696
+ _peerDisconnectListener;
667
697
  maxInboundStreams;
668
698
  maxOutboundStreams;
669
699
  connectionManagerOptions;
@@ -671,10 +701,17 @@ export class DirectStream extends TypedEventEmitter {
671
701
  healthChecks;
672
702
  pruneConnectionsTimeout;
673
703
  prunedConnectionsCache;
704
+ pruneToLimitsInFlight;
705
+ _startInFlight;
674
706
  routeMaxRetentionPeriod;
707
+ routeCacheMaxFromEntries;
708
+ routeCacheMaxTargetsPerFrom;
709
+ routeCacheMaxRelaysPerTarget;
710
+ sharedRouting;
711
+ sharedRoutingKey;
712
+ sharedRoutingState;
675
713
  // for sequential creation of outbound streams
676
714
  outboundInflightQueue;
677
- routeSeekInterval;
678
715
  seekTimeout;
679
716
  closeController;
680
717
  session;
@@ -683,7 +720,7 @@ export class DirectStream extends TypedEventEmitter {
683
720
  constructor(components, multicodecs, options) {
684
721
  super();
685
722
  this.components = components;
686
- 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 || {};
687
724
  const signKey = getKeypairFromPrivateKey(components.privateKey);
688
725
  this.seekTimeout = seekTimeout;
689
726
  this.sign = signKey.sign.bind(signKey);
@@ -699,12 +736,18 @@ export class DirectStream extends TypedEventEmitter {
699
736
  this.peers = new Map();
700
737
  this.canRelayMessage = canRelayMessage;
701
738
  this.healthChecks = new Map();
702
- this.routeSeekInterval = routeSeekInterval;
703
739
  this.queue = new Queue({ concurrency: messageProcessingConcurrency });
704
740
  this.maxInboundStreams = maxInboundStreams;
705
741
  this.maxOutboundStreams = maxOutboundStreams;
706
- 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
+ });
707
746
  this.routeMaxRetentionPeriod = routeMaxRetentionPeriod;
747
+ this.routeCacheMaxFromEntries = routeCacheMaxFromEntries;
748
+ this.routeCacheMaxTargetsPerFrom = routeCacheMaxTargetsPerFrom;
749
+ this.routeCacheMaxRelaysPerTarget = routeCacheMaxRelaysPerTarget;
750
+ this.sharedRouting = sharedRouting !== false;
708
751
  this.peerKeyHashToPublicKey = new Map();
709
752
  this._onIncomingStream = this._onIncomingStream.bind(this);
710
753
  this.onPeerConnected = this.onPeerConnected.bind(this);
@@ -751,11 +794,46 @@ export class DirectStream extends TypedEventEmitter {
751
794
  })
752
795
  : undefined;
753
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
+ }
754
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() {
755
834
  if (this.started) {
756
835
  return;
757
836
  }
758
- this.session = +new Date();
759
837
  await ready;
760
838
  this.closeController = new AbortController();
761
839
  this.outboundInflightQueue = pushable({ objectMode: true });
@@ -808,10 +886,45 @@ export class DirectStream extends TypedEventEmitter {
808
886
  this.closeController.signal.addEventListener("abort", () => {
809
887
  this.outboundInflightQueue.return();
810
888
  });
811
- this.routes = new Routes(this.publicKeyHash, {
812
- routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
813
- signal: this.closeController.signal,
814
- });
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
+ }
815
928
  this.started = true;
816
929
  this.stopping = false;
817
930
  logger.trace("starting");
@@ -829,6 +942,45 @@ export class DirectStream extends TypedEventEmitter {
829
942
  onDisconnect: this.onPeerDisconnected.bind(this),
830
943
  notifyOnLimitedConnection: false,
831
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
+ }
832
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)
833
985
  const peerToConnections = new Map();
834
986
  const connections = this.components.connectionManager.getConnections();
@@ -875,7 +1027,22 @@ export class DirectStream extends TypedEventEmitter {
875
1027
  if (!this.started) {
876
1028
  return;
877
1029
  }
1030
+ const sharedState = this.sharedRoutingState;
1031
+ const sharedKey = this.sharedRoutingKey;
878
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;
879
1046
  await Promise.all(this.multicodecs.map((x) => this.components.registrar.unhandle(x)));
880
1047
  // unregister protocol and handlers
881
1048
  if (this._registrarTopologyIds != null) {
@@ -897,12 +1064,36 @@ export class DirectStream extends TypedEventEmitter {
897
1064
  this.queue.clear();
898
1065
  this.peers.clear();
899
1066
  this.seenCache.clear();
900
- 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
+ }
901
1072
  this.peerKeyHashToPublicKey.clear();
902
1073
  for (const [_k, v] of this._ackCallbacks) {
903
1074
  v.clear();
904
1075
  }
905
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
+ }
906
1097
  logger.trace("stopped");
907
1098
  this.stopping = false;
908
1099
  }
@@ -1054,7 +1245,8 @@ export class DirectStream extends TypedEventEmitter {
1054
1245
  }),
1055
1246
  }).sign(this.sign)).catch(dontThrowIfDeliveryError);
1056
1247
  }
1057
- this.checkIsAlive([peerKeyHash]);
1248
+ // Best-effort liveness probe; never let background probe failures crash callers.
1249
+ void this.checkIsAlive([peerKeyHash]).catch(() => false);
1058
1250
  }
1059
1251
  logger.trace("connection ended:" + peerKey.toString());
1060
1252
  }
@@ -1070,12 +1262,16 @@ export class DirectStream extends TypedEventEmitter {
1070
1262
  }
1071
1263
  addRouteConnection(from, neighbour, target, distance, session, remoteSession) {
1072
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
+ }
1073
1270
  const update = this.routes.add(from, neighbour, targetHash, distance, session, remoteSession);
1074
1271
  // second condition is that we don't want to emit 'reachable' events for routes where we act only as a relay
1075
1272
  // in this case, from is != this.publicKeyhash
1076
1273
  if (from === this.publicKeyHash) {
1077
1274
  if (update === "new") {
1078
- this.peerKeyHashToPublicKey.set(target.hashcode(), target);
1079
1275
  this.onPeerReachable(target);
1080
1276
  }
1081
1277
  }
@@ -1086,11 +1282,15 @@ export class DirectStream extends TypedEventEmitter {
1086
1282
  }
1087
1283
  onPeerUnreachable(hash) {
1088
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
+ }
1089
1291
  this.dispatchEvent(
1090
1292
  // TODO types
1091
- new CustomEvent("peer:unreachable", {
1092
- detail: this.peerKeyHashToPublicKey.get(hash),
1093
- }));
1293
+ new CustomEvent("peer:unreachable", { detail: key }));
1094
1294
  }
1095
1295
  updateSession(key, session) {
1096
1296
  if (this.routes.updateSession(key.hashcode(), session)) {
@@ -1143,6 +1343,11 @@ export class DirectStream extends TypedEventEmitter {
1143
1343
  peerStreams.removeEventListener("stream:inbound", forwardInbound);
1144
1344
  }, { once: true });
1145
1345
  this.addRouteConnection(this.publicKeyHash, publicKey.hashcode(), publicKey, -1, +new Date(), -1);
1346
+ // Enforce connection manager limits eagerly when new peers are added. Without this,
1347
+ // join storms can create large temporary peer sets and OOM in single-process sims.
1348
+ if (this.peers.size > this.connectionManagerOptions.maxConnections) {
1349
+ void this.pruneConnectionsToLimits().catch(() => { });
1350
+ }
1146
1351
  return peerStreams;
1147
1352
  }
1148
1353
  /**
@@ -1258,32 +1463,33 @@ export class DirectStream extends TypedEventEmitter {
1258
1463
  }
1259
1464
  }
1260
1465
  shouldIgnore(message, seenBefore) {
1261
- const fromMe = message.header.signatures?.publicKeys.find((x) => x.equals(this.publicKey));
1262
- if (fromMe) {
1466
+ const signedBySelf = message.header.signatures?.publicKeys.some((x) => x.equals(this.publicKey)) ?? false;
1467
+ if (signedBySelf) {
1263
1468
  return true;
1264
1469
  }
1265
- if ((seenBefore > 0 &&
1266
- message.header.mode instanceof SeekDelivery === false) ||
1267
- (message.header.mode instanceof SeekDelivery &&
1268
- seenBefore >= message.header.mode.redundancy)) {
1269
- return true;
1470
+ // For acknowledged modes, allow limited duplicate forwarding so that we can
1471
+ // discover and maintain multiple candidate routes (distance=seenCounter).
1472
+ if (message.header.mode instanceof AcknowledgeDelivery ||
1473
+ message.header.mode instanceof AcknowledgeAnyWhere) {
1474
+ return seenBefore >= message.header.mode.redundancy;
1270
1475
  }
1271
- return false;
1476
+ return seenBefore > 0;
1272
1477
  }
1273
1478
  async onDataMessage(from, peerStream, message, seenBefore) {
1274
1479
  if (this.shouldIgnore(message, seenBefore)) {
1275
1480
  return false;
1276
1481
  }
1277
1482
  let isForMe = false;
1278
- if (message.header.mode instanceof AnyWhere) {
1483
+ if (message.header.mode instanceof AnyWhere ||
1484
+ message.header.mode instanceof AcknowledgeAnyWhere) {
1279
1485
  isForMe = true;
1280
1486
  }
1281
1487
  else {
1282
1488
  const isFromSelf = this.publicKey.equals(from);
1283
- if (!isFromSelf) {
1284
- isForMe =
1285
- message.header.mode.to == null ||
1286
- message.header.mode.to.find((x) => x === this.publicKeyHash) != null;
1489
+ if (!isFromSelf &&
1490
+ (message.header.mode instanceof SilentDelivery ||
1491
+ message.header.mode instanceof AcknowledgeDelivery)) {
1492
+ isForMe = message.header.mode.to.includes(this.publicKeyHash);
1287
1493
  }
1288
1494
  }
1289
1495
  if (isForMe) {
@@ -1309,19 +1515,13 @@ export class DirectStream extends TypedEventEmitter {
1309
1515
  }
1310
1516
  }
1311
1517
  // Forward
1312
- if (message.header.mode instanceof SeekDelivery || seenBefore === 0) {
1518
+ const shouldForward = seenBefore === 0 ||
1519
+ ((message.header.mode instanceof AcknowledgeDelivery ||
1520
+ message.header.mode instanceof AcknowledgeAnyWhere) &&
1521
+ seenBefore < message.header.mode.redundancy);
1522
+ if (shouldForward) {
1313
1523
  // DONT await this since it might introduce a dead-lock
1314
- if (message.header.mode instanceof SeekDelivery) {
1315
- if (seenBefore < message.header.mode.redundancy) {
1316
- const to = [...this.peers.values()].filter((x) => !message.header.signatures?.publicKeys.find((y) => y.equals(x.publicKey)) && x !== peerStream);
1317
- if (to.length > 0) {
1318
- this.relayMessage(from, message, to);
1319
- }
1320
- }
1321
- }
1322
- else {
1323
- this.relayMessage(from, message);
1324
- }
1524
+ this.relayMessage(from, message);
1325
1525
  }
1326
1526
  }
1327
1527
  async verifyAndProcess(message) {
@@ -1339,12 +1539,16 @@ export class DirectStream extends TypedEventEmitter {
1339
1539
  return true;
1340
1540
  }
1341
1541
  async maybeAcknowledgeMessage(peerStream, message, seenBefore) {
1342
- if ((message.header.mode instanceof SeekDelivery ||
1343
- message.header.mode instanceof AcknowledgeDelivery) &&
1344
- seenBefore < message.header.mode.redundancy) {
1345
- const shouldAcknowldege = message.header.mode.to == null ||
1346
- message.header.mode.to.includes(this.publicKeyHash);
1347
- if (!shouldAcknowldege) {
1542
+ if (message.header.mode instanceof AcknowledgeDelivery ||
1543
+ message.header.mode instanceof AcknowledgeAnyWhere) {
1544
+ const isRecipient = message.header.mode instanceof AcknowledgeAnyWhere
1545
+ ? true
1546
+ : message.header.mode.to.includes(this.publicKeyHash);
1547
+ if (!shouldAcknowledgeDataMessage({
1548
+ isRecipient,
1549
+ seenBefore,
1550
+ redundancy: message.header.mode.redundancy,
1551
+ })) {
1348
1552
  return;
1349
1553
  }
1350
1554
  const signers = message.header.signatures.publicKeys.map((x) => x.hashcode());
@@ -1355,8 +1559,12 @@ export class DirectStream extends TypedEventEmitter {
1355
1559
  header: new MessageHeader({
1356
1560
  mode: new TracedDelivery(signers),
1357
1561
  session: this.session,
1358
- // include our origin if message is SeekDelivery and we have not recently pruned a connection to this peer
1359
- origin: message.header.mode instanceof SeekDelivery &&
1562
+ // include our origin for route-learning/dialer hints (best-effort privacy/anti-spam control):
1563
+ // only include once (seenBefore=0) and only if we have not recently pruned
1564
+ // a connection to any signer in the path
1565
+ origin: (message.header.mode instanceof AcknowledgeAnyWhere ||
1566
+ message.header.mode instanceof AcknowledgeDelivery) &&
1567
+ seenBefore === 0 &&
1360
1568
  !message.header.signatures.publicKeys.find((x) => this.prunedConnectionsCache?.has(x.hashcode()))
1361
1569
  ? new MultiAddrinfo(this.components.addressManager
1362
1570
  .getAddresses()
@@ -1441,37 +1649,40 @@ export class DirectStream extends TypedEventEmitter {
1441
1649
  for (const remote of remotes) {
1442
1650
  this.invalidateSession(remote);
1443
1651
  }
1444
- this.checkIsAlive(remotes);
1652
+ // Best-effort liveness probe; never let background probe failures crash callers.
1653
+ void this.checkIsAlive(remotes).catch(() => false);
1445
1654
  }
1446
1655
  async checkIsAlive(remotes) {
1447
1656
  if (this.peers.size === 0) {
1448
1657
  return false;
1449
1658
  }
1450
1659
  if (remotes.length > 0) {
1451
- return this.publish(undefined, {
1452
- mode: new SeekDelivery({
1453
- to: remotes,
1454
- redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
1455
- }),
1456
- })
1457
- .then(() => true)
1458
- .catch((e) => {
1459
- if (e instanceof DeliveryError) {
1460
- return false;
1461
- }
1462
- else if (e instanceof NotStartedError) {
1463
- return false;
1464
- }
1465
- else if (e instanceof TimeoutError) {
1466
- return false;
1467
- }
1468
- else if (e instanceof AbortError) {
1660
+ try {
1661
+ // Keepalive probes are best-effort, but must still require ACKs so we can
1662
+ // conclusively prune stale routes when peers actually went away.
1663
+ await this.publish(undefined, {
1664
+ mode: new AcknowledgeDelivery({
1665
+ to: remotes,
1666
+ redundancy: DEFAULT_SILENT_MESSAGE_REDUDANCY,
1667
+ }),
1668
+ });
1669
+ return true;
1670
+ }
1671
+ catch (e) {
1672
+ const errorName = e?.name ?? e?.constructor?.name ?? "UnknownError";
1673
+ if (e instanceof DeliveryError ||
1674
+ e instanceof NotStartedError ||
1675
+ e instanceof TimeoutError ||
1676
+ e instanceof AbortError ||
1677
+ errorName === "DeliveryError" ||
1678
+ errorName === "NotStartedError" ||
1679
+ errorName === "TimeoutError" ||
1680
+ errorName === "AbortError") {
1469
1681
  return false;
1470
1682
  }
1471
- else {
1472
- throw e;
1473
- }
1474
- }); // this will remove the target if it is still not reable
1683
+ warn(`checkIsAlive unexpected error: ${errorName}`);
1684
+ return false;
1685
+ }
1475
1686
  }
1476
1687
  return false;
1477
1688
  }
@@ -1484,8 +1695,7 @@ export class DirectStream extends TypedEventEmitter {
1484
1695
  redundancy: DEFAULT_SILENT_MESSAGE_REDUDANCY,
1485
1696
  });
1486
1697
  if (mode instanceof AcknowledgeDelivery ||
1487
- mode instanceof SilentDelivery ||
1488
- mode instanceof SeekDelivery) {
1698
+ mode instanceof SilentDelivery) {
1489
1699
  if (mode.to) {
1490
1700
  let preLength = mode.to.length;
1491
1701
  mode.to = mode.to.filter((x) => x !== this.publicKeyHash);
@@ -1493,7 +1703,7 @@ export class DirectStream extends TypedEventEmitter {
1493
1703
  if (preLength > 0 && mode.to?.length === 0) {
1494
1704
  throw new InvalidMessageError("Unexpected to create a message with self as the only receiver");
1495
1705
  }
1496
- if (mode.to.length === 0 && mode instanceof SeekDelivery === false) {
1706
+ if (mode.to.length === 0) {
1497
1707
  throw new InvalidMessageError("Unexpected to deliver message with mode: " +
1498
1708
  mode.constructor.name +
1499
1709
  " without recipents");
@@ -1501,24 +1711,6 @@ export class DirectStream extends TypedEventEmitter {
1501
1711
  }
1502
1712
  }
1503
1713
  }
1504
- if (mode instanceof AcknowledgeDelivery || mode instanceof SilentDelivery) {
1505
- const now = +new Date();
1506
- for (const hash of mode.to) {
1507
- const neighbourRoutes = this.routes.routes
1508
- .get(this.publicKeyHash)
1509
- ?.get(hash);
1510
- if (!neighbourRoutes ||
1511
- now - neighbourRoutes.session >
1512
- neighbourRoutes.list.length * this.routeSeekInterval ||
1513
- !this.routes.isUpToDate(hash, neighbourRoutes)) {
1514
- mode = new SeekDelivery({
1515
- to: mode.to,
1516
- redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
1517
- });
1518
- break;
1519
- }
1520
- }
1521
- }
1522
1714
  const message = new DataMessage({
1523
1715
  data: data instanceof Uint8ArrayList ? data.subarray() : data,
1524
1716
  header: new MessageHeader({
@@ -1541,7 +1733,9 @@ export class DirectStream extends TypedEventEmitter {
1541
1733
  * Publishes messages to all peers
1542
1734
  */
1543
1735
  async publish(data, options = {
1544
- mode: new SeekDelivery({ redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY }),
1736
+ mode: new AcknowledgeAnyWhere({
1737
+ redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
1738
+ }),
1545
1739
  }) {
1546
1740
  if (!this.started) {
1547
1741
  throw new NotStartedError();
@@ -1577,7 +1771,7 @@ export class DirectStream extends TypedEventEmitter {
1577
1771
  if (this.canRelayMessage) {
1578
1772
  if (message instanceof DataMessage) {
1579
1773
  if (message.header.mode instanceof AcknowledgeDelivery ||
1580
- message.header.mode instanceof SeekDelivery) {
1774
+ message.header.mode instanceof AcknowledgeAnyWhere) {
1581
1775
  await message.sign(this.sign);
1582
1776
  }
1583
1777
  }
@@ -1598,6 +1792,31 @@ export class DirectStream extends TypedEventEmitter {
1598
1792
  clearTimeout(timer);
1599
1793
  this.healthChecks.delete(to);
1600
1794
  }
1795
+ formatDeliveryDebugState(targets, fastestNodesReached) {
1796
+ const sampleSize = 16;
1797
+ const sampledTargets = [...targets].slice(0, sampleSize);
1798
+ const sampled = sampledTargets.map((target) => {
1799
+ const route = this.routes.findNeighbor(this.publicKeyHash, target);
1800
+ const routeSample = route?.list
1801
+ .slice(0, 3)
1802
+ .map((r) => `${r.hash}:${r.distance}${r.expireAt ? ":old" : ""}`)
1803
+ .join(",") ?? "-";
1804
+ const reachable = this.routes.isReachable(this.publicKeyHash, target);
1805
+ const stream = this.peers.get(target);
1806
+ let openConnections = 0;
1807
+ if (stream) {
1808
+ try {
1809
+ openConnections = this.components.connectionManager.getConnections(stream.peerId).length;
1810
+ }
1811
+ catch {
1812
+ openConnections = stream.isReadable || stream.isWritable ? 1 : 0;
1813
+ }
1814
+ }
1815
+ return `${target}{reachable=${reachable ? 1 : 0},ack=${fastestNodesReached.has(target) ? 1 : 0},stream=${stream ? 1 : 0},conns=${openConnections},routes=${routeSample}}`;
1816
+ });
1817
+ const more = targets.size - sampledTargets.length;
1818
+ return `deliveryState peers=${this.peers.size} routesLocal=${this.routes.count()} routesAll=${this.routes.countAll()} targets=[${sampled.join(";")}]${more > 0 ? ` (+${more} more)` : ""}`;
1819
+ }
1601
1820
  async createDeliveryPromise(from, message, relayed, signal) {
1602
1821
  if (message.header.mode instanceof AnyWhere) {
1603
1822
  return {
@@ -1613,11 +1832,10 @@ export class DirectStream extends TypedEventEmitter {
1613
1832
  }
1614
1833
  const fastestNodesReached = new Map();
1615
1834
  const messageToSet = new Set();
1616
- if (message.header.mode.to) {
1835
+ if (deliveryModeHasReceiver(message.header.mode)) {
1617
1836
  for (const to of message.header.mode.to) {
1618
- if (to === from.hashcode()) {
1837
+ if (to === from.hashcode())
1619
1838
  continue;
1620
- }
1621
1839
  messageToSet.add(to);
1622
1840
  if (!relayed && !this.healthChecks.has(to)) {
1623
1841
  this.healthChecks.set(to, setTimeout(() => {
@@ -1638,20 +1856,16 @@ export class DirectStream extends TypedEventEmitter {
1638
1856
  }
1639
1857
  const willGetAllAcknowledgements = !relayed; // Only the origin will get all acks
1640
1858
  // Expected to receive at least 'filterMessageForSeenCounter' acknowledgements from each peer
1641
- const filterMessageForSeenCounter = relayed
1642
- ? undefined
1643
- : message.header.mode instanceof SeekDelivery
1644
- ? Math.min(this.peers.size, message.header.mode.redundancy)
1645
- : 1; /* message.deliveryMode instanceof SeekDelivery ? Math.min(this.peers.size - (relayed ? 1 : 0), message.deliveryMode.redundancy) : 1 */
1859
+ const filterMessageForSeenCounter = relayed ? undefined : 1;
1646
1860
  const uniqueAcks = new Set();
1647
1861
  const session = +new Date();
1648
1862
  const onUnreachable = !relayed &&
1649
1863
  ((ev) => {
1650
- const deletedReceiver = messageToSet.delete(ev.detail.hashcode());
1651
- if (deletedReceiver) {
1652
- // Only reject if we are the sender
1653
- clear();
1654
- deliveryDeferredPromise.reject(new DeliveryError(`At least one recipent became unreachable while delivering messsage of type ${message.constructor.name}} to ${ev.detail.hashcode()}`));
1864
+ const target = ev.detail.hashcode();
1865
+ if (messageToSet.has(target)) {
1866
+ // A peer can transiently appear unreachable while routes are being updated.
1867
+ // Keep waiting for ACK/timeout instead of failing delivery immediately.
1868
+ logger.trace("peer unreachable during delivery (msg=%s, target=%s); waiting for ACK/timeout", idString, target);
1655
1869
  }
1656
1870
  });
1657
1871
  onUnreachable && this.addEventListener("peer:unreachable", onUnreachable);
@@ -1678,9 +1892,13 @@ export class DirectStream extends TypedEventEmitter {
1678
1892
  }
1679
1893
  }
1680
1894
  if (!hasAll && willGetAllAcknowledgements) {
1895
+ const debugState = this.formatDeliveryDebugState(messageToSet, fastestNodesReached);
1896
+ const msgMeta = `msgType=${message.constructor.name} dataBytes=${message instanceof DataMessage
1897
+ ? (message.data?.byteLength ?? 0)
1898
+ : 0} relayed=${relayed ? 1 : 0}`;
1681
1899
  deliveryDeferredPromise.reject(new DeliveryError(`Failed to get message ${idString} ${filterMessageForSeenCounter} ${[
1682
1900
  ...messageToSet,
1683
- ]} delivery acknowledges from all nodes (${fastestNodesReached.size}/${messageToSet.size}). Mode: ${message.header.mode.constructor.name}. Redundancy: ${message.header.mode["redundancy"]}`));
1901
+ ]} delivery acknowledges from all nodes (${fastestNodesReached.size}/${messageToSet.size}). Mode: ${message.header.mode.constructor.name}. Redundancy: ${message.header.mode["redundancy"]}. ${msgMeta}. ${debugState}`));
1684
1902
  }
1685
1903
  else {
1686
1904
  deliveryDeferredPromise.resolve();
@@ -1703,11 +1921,22 @@ export class DirectStream extends TypedEventEmitter {
1703
1921
  // know how many ACKs we will get
1704
1922
  if (filterMessageForSeenCounter != null &&
1705
1923
  uniqueAcks.size >= messageToSet.size * filterMessageForSeenCounter) {
1706
- if (haveReceivers) {
1707
- // 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
1708
- // only remove callback function if we actually expected a expected amount of responses
1924
+ const shouldKeepCallbackForRouteLearning = !relayed &&
1925
+ message.header.mode instanceof AcknowledgeDelivery &&
1926
+ message.header.mode.redundancy > 1;
1927
+ if (haveReceivers && !shouldKeepCallbackForRouteLearning) {
1928
+ // If we have an explicit recipient list we can clear the ACK callback once we
1929
+ // got the expected acknowledgements.
1709
1930
  clear();
1710
1931
  }
1932
+ else if (haveReceivers && shouldKeepCallbackForRouteLearning) {
1933
+ // Resolve delivery early, but keep ACK callbacks alive until timeout so we can
1934
+ // learn additional redundant routes (seenCounter=1..redundancy-1).
1935
+ onUnreachable &&
1936
+ this.removeEventListener("peer:unreachable", onUnreachable);
1937
+ onAbort && signal?.removeEventListener("abort", onAbort);
1938
+ onAbort = undefined;
1939
+ }
1711
1940
  deliveryDeferredPromise.resolve();
1712
1941
  return true;
1713
1942
  }
@@ -1721,25 +1950,35 @@ export class DirectStream extends TypedEventEmitter {
1721
1950
  const seenCounter = ack.seenCounter;
1722
1951
  // remove the automatic removal of route timeout since we have observed lifesigns of a peer
1723
1952
  this.clearHealthcheckTimer(messageTargetHash);
1724
- // if the target is not inside the original message to, we still ad the target to our routes
1725
- // this because a relay might modify the 'to' list and we might receive more answers than initially set
1726
- if (message.header.mode instanceof SeekDelivery) {
1727
- 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
1953
+ // If the target is not inside the original message `to`, we can still add the target to our routes.
1954
+ // This matters because relays may modify `to`, and because "ack-anywhere" style probes intentionally
1955
+ // do not provide an explicit recipient list.
1956
+ if (message.header.mode instanceof AcknowledgeDelivery ||
1957
+ message.header.mode instanceof AcknowledgeAnyWhere) {
1958
+ const upstreamHash = messageFrom?.publicKey.hashcode();
1959
+ const routeUpdate = computeSeekAckRouteUpdate({
1960
+ current: this.publicKeyHash,
1961
+ upstream: upstreamHash,
1962
+ downstream: messageThrough.publicKey.hashcode(),
1963
+ target: messageTargetHash,
1964
+ // Route "distance" is based on recipient-seen order (0 = fastest). This is relied upon by
1965
+ // `Routes.getFanout(...)` which uses `distance < redundancy` to select redundant next-hops.
1966
+ distance: seenCounter,
1967
+ });
1968
+ this.addRouteConnection(routeUpdate.from, routeUpdate.neighbour, messageTarget, routeUpdate.distance, session, Number(ack.header.session));
1728
1969
  }
1729
1970
  if (messageToSet.has(messageTargetHash)) {
1730
- // Only keep track of relevant acks
1731
- if (filterMessageForSeenCounter == null ||
1732
- seenCounter < filterMessageForSeenCounter) {
1733
- // TODO set limit correctly
1734
- if (seenCounter < MAX_ROUTE_DISTANCE) {
1735
- let arr = fastestNodesReached.get(messageTargetHash);
1736
- if (!arr) {
1737
- arr = [];
1738
- fastestNodesReached.set(messageTargetHash, arr);
1739
- }
1740
- arr.push(seenCounter);
1741
- uniqueAcks.add(messageTargetHash + seenCounter);
1971
+ // Any ACK from the target should satisfy delivery semantics.
1972
+ // Relying on only seenCounter=0 can fail under churn if the first ACK
1973
+ // is lost but a later ACK (seenCounter>0) arrives.
1974
+ if (seenCounter < MAX_ROUTE_DISTANCE) {
1975
+ let arr = fastestNodesReached.get(messageTargetHash);
1976
+ if (!arr) {
1977
+ arr = [];
1978
+ fastestNodesReached.set(messageTargetHash, arr);
1742
1979
  }
1980
+ arr.push(seenCounter);
1981
+ uniqueAcks.add(messageTargetHash);
1743
1982
  }
1744
1983
  }
1745
1984
  checkDone();
@@ -1755,7 +1994,9 @@ export class DirectStream extends TypedEventEmitter {
1755
1994
  if (this.stopping || !this.started) {
1756
1995
  throw new NotStartedError();
1757
1996
  }
1997
+ const isRelayed = relayed ?? from.hashcode() !== this.publicKeyHash;
1758
1998
  let delivereyPromise = undefined;
1999
+ let ackCallbackId;
1759
2000
  if ((!message.header.signatures ||
1760
2001
  message.header.signatures.publicKeys.length === 0) &&
1761
2002
  message instanceof DataMessage &&
@@ -1765,85 +2006,145 @@ export class DirectStream extends TypedEventEmitter {
1765
2006
  /**
1766
2007
  * Logic for handling acknowledge messages when we receive them (later)
1767
2008
  */
1768
- if (message instanceof DataMessage &&
1769
- message.header.mode instanceof SeekDelivery &&
1770
- !relayed) {
1771
- to = this.peers; // seek delivery will not work unless we try all possible paths
1772
- }
1773
- if (message.header.mode instanceof AcknowledgeDelivery) {
1774
- to = undefined;
1775
- }
1776
2009
  if ((message instanceof DataMessage || message instanceof Goodbye) &&
1777
- (message.header.mode instanceof SeekDelivery ||
1778
- message.header.mode instanceof AcknowledgeDelivery)) {
1779
- const deliveryDeferredPromise = await this.createDeliveryPromise(from, message, relayed, signal);
2010
+ (message.header.mode instanceof AcknowledgeDelivery ||
2011
+ message.header.mode instanceof AcknowledgeAnyWhere)) {
2012
+ const deliveryDeferredPromise = await this.createDeliveryPromise(from, message, isRelayed, signal);
1780
2013
  delivereyPromise = deliveryDeferredPromise.promise;
2014
+ ackCallbackId = toBase64(message.id);
1781
2015
  }
1782
- const bytes = message.bytes();
1783
- if (!relayed) {
1784
- const bytesArray = bytes instanceof Uint8Array ? bytes : bytes.subarray();
1785
- await this.modifySeenCache(bytesArray);
1786
- }
1787
- /**
1788
- * For non SEEKing message delivery modes, use routing
1789
- */
1790
- if (message instanceof DataMessage) {
1791
- if ((message.header.mode instanceof AcknowledgeDelivery ||
1792
- message.header.mode instanceof SilentDelivery) &&
1793
- !to) {
1794
- if (message.header.mode.to.length === 0) {
1795
- return delivereyPromise; // we defintely know that we should not forward the message anywhere
1796
- }
1797
- const fanout = this.routes.getFanout(from.hashcode(), message.header.mode.to, message.header.mode.redundancy);
1798
- if (fanout) {
1799
- if (fanout.size > 0) {
2016
+ try {
2017
+ const bytes = message.bytes();
2018
+ if (!isRelayed) {
2019
+ const bytesArray = bytes instanceof Uint8Array ? bytes : bytes.subarray();
2020
+ await this.modifySeenCache(bytesArray);
2021
+ }
2022
+ /**
2023
+ * For non SEEKing message delivery modes, use routing
2024
+ */
2025
+ if (message instanceof DataMessage) {
2026
+ if ((message.header.mode instanceof AcknowledgeDelivery ||
2027
+ message.header.mode instanceof SilentDelivery) &&
2028
+ !to) {
2029
+ if (message.header.mode.to.length === 0) {
2030
+ // we definitely know that we should not forward the message anywhere
2031
+ return delivereyPromise;
2032
+ }
2033
+ const fanout = this.routes.getFanout(from.hashcode(), message.header.mode.to, message.header.mode.redundancy);
2034
+ // If we have explicit routing information, send only along the chosen next-hops.
2035
+ // If `fanout` is empty (no route info yet), fall through to the flooding logic below
2036
+ // so acknowledged deliveries can discover/repair routes instead of timing out.
2037
+ if (fanout && fanout.size > 0) {
1800
2038
  const promises = [];
2039
+ const usedNeighbours = new Set();
2040
+ const originalTo = message.header.mode.to;
1801
2041
  for (const [neighbour, _distantPeers] of fanout) {
1802
2042
  const stream = this.peers.get(neighbour);
1803
- stream &&
2043
+ if (!stream)
2044
+ continue;
2045
+ if (message.header.mode instanceof SilentDelivery) {
2046
+ message.header.mode.to = [..._distantPeers.keys()];
2047
+ promises.push(stream.waitForWrite(message.bytes(), message.header.priority));
2048
+ }
2049
+ else {
1804
2050
  promises.push(stream.waitForWrite(bytes, message.header.priority));
2051
+ }
2052
+ usedNeighbours.add(neighbour);
2053
+ }
2054
+ if (message.header.mode instanceof SilentDelivery) {
2055
+ message.header.mode.to = originalTo;
2056
+ }
2057
+ // If the sender requested redundancy but we don't yet have enough distinct
2058
+ // next-hops for the target(s), opportunistically probe additional neighbours.
2059
+ // This replaces the previous "greedy fanout" probing behavior without needing
2060
+ // a separate delivery mode.
2061
+ if (!isRelayed &&
2062
+ message.header.mode instanceof AcknowledgeDelivery &&
2063
+ usedNeighbours.size < message.header.mode.redundancy) {
2064
+ for (const [neighbour, stream] of this.peers) {
2065
+ if (usedNeighbours.size >= message.header.mode.redundancy) {
2066
+ break;
2067
+ }
2068
+ if (usedNeighbours.has(neighbour))
2069
+ continue;
2070
+ usedNeighbours.add(neighbour);
2071
+ promises.push(stream.waitForWrite(bytes, message.header.priority));
2072
+ }
1805
2073
  }
1806
2074
  await Promise.all(promises);
1807
- return delivereyPromise; // we are done sending the message in all direction with updates 'to' lists
2075
+ return delivereyPromise;
2076
+ }
2077
+ // If we don't have routing information:
2078
+ // - For acknowledged deliveries, fall through to flooding (route discovery / repair).
2079
+ // - For silent deliveries, relays should not flood (prevents unnecessary fanout); origin may still flood.
2080
+ // We still allow direct neighbour delivery to explicit recipients (if connected).
2081
+ if (isRelayed && message.header.mode instanceof SilentDelivery) {
2082
+ const promises = [];
2083
+ const originalTo = message.header.mode.to;
2084
+ for (const recipient of originalTo) {
2085
+ if (recipient === this.publicKeyHash)
2086
+ continue;
2087
+ if (recipient === from.hashcode())
2088
+ continue; // never send back to previous hop
2089
+ const stream = this.peers.get(recipient);
2090
+ if (!stream)
2091
+ continue;
2092
+ if (message.header.signatures?.publicKeys.find((x) => x.hashcode() === recipient)) {
2093
+ continue; // recipient already signed/seen this message
2094
+ }
2095
+ message.header.mode.to = [recipient];
2096
+ promises.push(stream.waitForWrite(message.bytes(), message.header.priority));
2097
+ }
2098
+ message.header.mode.to = originalTo;
2099
+ if (promises.length > 0) {
2100
+ await Promise.all(promises);
2101
+ }
2102
+ return delivereyPromise;
1808
2103
  }
1809
- return; // we defintely know that we should not forward the message anywhere
1810
- }
1811
- // we end up here because we don't have enough information yet in how to send data to the peer (TODO test this codepath)
1812
- if (relayed) {
1813
- return;
1814
2104
  }
1815
- } // else send to all (fallthrough to code below)
1816
- }
1817
- // We fils to send the message directly, instead fallback to floodsub
1818
- const peers = to || this.peers;
1819
- if (peers == null ||
1820
- (Array.isArray(peers) && peers.length === 0) ||
1821
- (peers instanceof Map && peers.size === 0)) {
1822
- logger.trace("No peers to send to");
1823
- return delivereyPromise;
1824
- }
1825
- let sentOnce = false;
1826
- const promises = [];
1827
- for (const stream of peers.values()) {
1828
- const id = stream;
1829
- // Dont sent back to the sender
1830
- if (id.publicKey.equals(from)) {
1831
- continue;
1832
2105
  }
1833
- // Dont send message back to any of the signers (they have already seen the message)
1834
- if (message.header.signatures?.publicKeys.find((x) => x.equals(id.publicKey))) {
1835
- continue;
2106
+ // We fail to send the message directly, instead fallback to floodsub
2107
+ const peers = to || this.peers;
2108
+ if (peers == null ||
2109
+ (Array.isArray(peers) && peers.length === 0) ||
2110
+ (peers instanceof Map && peers.size === 0)) {
2111
+ logger.trace("No peers to send to");
2112
+ return delivereyPromise;
2113
+ }
2114
+ let sentOnce = false;
2115
+ const promises = [];
2116
+ for (const stream of peers.values()) {
2117
+ const id = stream;
2118
+ // Dont sent back to the sender
2119
+ if (id.publicKey.equals(from)) {
2120
+ continue;
2121
+ }
2122
+ // Dont send message back to any of the signers (they have already seen the message)
2123
+ if (message.header.signatures?.publicKeys.find((x) => x.equals(id.publicKey))) {
2124
+ continue;
2125
+ }
2126
+ sentOnce = true;
2127
+ promises.push(id.waitForWrite(bytes, message.header.priority));
2128
+ }
2129
+ await Promise.all(promises);
2130
+ if (!sentOnce) {
2131
+ // If the caller provided an explicit peer list, treat "no valid receivers" as an error
2132
+ // even when forwarding. This catches programming mistakes early and matches test expectations.
2133
+ if (!isRelayed || to != null) {
2134
+ throw new DeliveryError("Message did not have any valid receivers");
2135
+ }
1836
2136
  }
1837
- sentOnce = true;
1838
- promises.push(id.waitForWrite(bytes, message.header.priority));
2137
+ return delivereyPromise;
1839
2138
  }
1840
- await Promise.all(promises);
1841
- if (!sentOnce) {
1842
- if (!relayed) {
1843
- throw new DeliveryError("Message did not have any valid receivers");
2139
+ catch (error) {
2140
+ // If message fanout/write fails before publishMessage returns its delivery
2141
+ // promise, clear any ACK callback to avoid late timer rejections leaking as
2142
+ // unhandled rejections in fire-and-forget call paths.
2143
+ if (ackCallbackId) {
2144
+ this._ackCallbacks.get(ackCallbackId)?.clear();
1844
2145
  }
2146
+ throw error;
1845
2147
  }
1846
- return delivereyPromise;
1847
2148
  }
1848
2149
  async maybeConnectDirectly(toHash, origin) {
1849
2150
  if (this.peers.has(toHash) || this.prunedConnectionsCache?.has(toHash)) {
@@ -1924,10 +2225,12 @@ export class DirectStream extends TypedEventEmitter {
1924
2225
  for (const h of admitted)
1925
2226
  if (reached(h, target))
1926
2227
  wins.add(h);
2228
+ // Preserve input order in the returned list (important for deterministic callers/tests).
2229
+ const orderedWins = () => admitted.filter((h) => wins.has(h));
1927
2230
  if (settle === "any" && wins.size > 0)
1928
- return [...wins];
2231
+ return orderedWins();
1929
2232
  if (settle === "all" && wins.size === admitted.length)
1930
- return [...wins];
2233
+ return orderedWins();
1931
2234
  // Abort/timeout
1932
2235
  const abortSignals = [this.closeController.signal];
1933
2236
  if (signal) {
@@ -1947,7 +2250,7 @@ export class DirectStream extends TypedEventEmitter {
1947
2250
  signals: abortSignals,
1948
2251
  timeout,
1949
2252
  });
1950
- return [...wins];
2253
+ return orderedWins();
1951
2254
  }
1952
2255
  catch (e) {
1953
2256
  const abortSignal = abortSignals.find((s) => s.aborted);
@@ -1957,112 +2260,23 @@ export class DirectStream extends TypedEventEmitter {
1957
2260
  }
1958
2261
  throw new AbortError("Aborted waiting for peers: " + abortSignal.reason);
1959
2262
  }
1960
- if (e instanceof Error) {
1961
- throw e;
2263
+ if (e instanceof TimeoutError) {
2264
+ if (settle === "any") {
2265
+ if (wins.size > 0)
2266
+ return orderedWins();
2267
+ throw new TimeoutError(`Timeout waiting for peers (target=${target}, seek=${seek}, missing=${admitted.length}/${admitted.length})`);
2268
+ }
2269
+ const missing = admitted.filter((h) => !wins.has(h));
2270
+ const preview = missing.slice(0, 5).join(", ");
2271
+ throw new TimeoutError(`Timeout waiting for peers (target=${target}, seek=${seek}, missing=${missing.length}/${admitted.length}${preview ? `, e.g. ${preview}` : ""})`);
1962
2272
  }
2273
+ if (e instanceof Error)
2274
+ throw e;
1963
2275
  if (settle === "all")
1964
2276
  throw new TimeoutError("Timeout waiting for peers");
1965
- return [...wins]; // settle:any: return whatever successes we got
1966
- }
1967
- }
1968
- /* async waitFor(
1969
- peer: PeerId | PublicSignKey | string,
1970
- options?: {
1971
- timeout?: number;
1972
- signal?: AbortSignal;
1973
- neighbour?: boolean;
1974
- inflight?: boolean;
1975
- },
1976
- ) {
1977
- const hash =
1978
- typeof peer === "string"
1979
- ? peer
1980
- : (peer instanceof PublicSignKey
1981
- ? peer
1982
- : getPublicKeyFromPeerId(peer)
1983
- ).hashcode();
1984
- if (hash === this.publicKeyHash) {
1985
- return; // TODO throw error instead?
1986
- }
1987
-
1988
- if (options?.inflight) {
1989
- // if peer is not in active connections or dialQueue, return silenty
1990
- if (
1991
- !this.peers.has(hash) &&
1992
- !this.components.connectionManager
1993
- .getDialQueue()
1994
- .some((x) => getPublicKeyFromPeerId(x.peerId).hashcode() === hash) &&
1995
- !this.components.connectionManager
1996
- .getConnections()
1997
- .some((x) => getPublicKeyFromPeerId(x.remotePeer).hashcode() === hash)
1998
- ) {
1999
- return;
2000
- }
2001
- }
2002
-
2003
- const checkIsReachable = (deferred: DeferredPromise<void>) => {
2004
- if (options?.neighbour && !this.peers.has(hash)) {
2005
- return;
2006
- }
2007
-
2008
- if (!this.routes.isReachable(this.publicKeyHash, hash, 0)) {
2009
- return;
2010
- }
2011
-
2012
- deferred.resolve();
2013
- };
2014
- const abortSignals = [this.closeController.signal];
2015
- if (options?.signal) {
2016
- abortSignals.push(options.signal);
2277
+ return orderedWins(); // settle:any: return whatever successes we got
2017
2278
  }
2018
-
2019
- try {
2020
- await waitForEvent(this, ["peer:reachable"], checkIsReachable, {
2021
- signals: abortSignals,
2022
- timeout: options?.timeout,
2023
- });
2024
- } catch (error) {
2025
- throw new Error(
2026
- "Stream to " +
2027
- hash +
2028
- " from " +
2029
- this.publicKeyHash +
2030
- " does not exist. Connection exist: " +
2031
- this.peers.has(hash) +
2032
- ". Route exist: " +
2033
- this.routes.isReachable(this.publicKeyHash, hash, 0),
2034
- );
2035
- }
2036
-
2037
- if (options?.neighbour) {
2038
- const stream = this.peers.get(hash)!;
2039
- try {
2040
- let checkIsWritable = (pDefer: DeferredPromise<void>) => {
2041
- if (stream.isReadable && stream.isWritable) {
2042
- pDefer.resolve();
2043
- }
2044
- };
2045
- await waitForEvent(
2046
- stream,
2047
- ["stream:outbound", "stream:inbound"],
2048
- checkIsWritable,
2049
- {
2050
- signals: abortSignals,
2051
- timeout: options?.timeout,
2052
- },
2053
- );
2054
- } catch (error) {
2055
- throw new Error(
2056
- "Stream to " +
2057
- stream.publicKey.hashcode() +
2058
- " not ready. Readable: " +
2059
- stream.isReadable +
2060
- ". Writable " +
2061
- stream.isWritable,
2062
- );
2063
- }
2064
- }
2065
- } */
2279
+ }
2066
2280
  getPublicKey(hash) {
2067
2281
  return this.peerKeyHashToPublicKey.get(hash);
2068
2282
  }
@@ -2071,6 +2285,10 @@ export class DirectStream extends TypedEventEmitter {
2071
2285
  }
2072
2286
  // make this into a job? run every few ms
2073
2287
  maybePruneConnections() {
2288
+ // Hard cap on peer streams: treat as a primary pruning signal.
2289
+ if (this.peers.size > this.connectionManagerOptions.maxConnections) {
2290
+ return this.pruneConnectionsToLimits();
2291
+ }
2074
2292
  if (this.connectionManagerOptions.pruner) {
2075
2293
  if (this.connectionManagerOptions.pruner.bandwidth != null) {
2076
2294
  let usedBandwidth = 0;
@@ -2106,7 +2324,7 @@ export class DirectStream extends TypedEventEmitter {
2106
2324
  return;
2107
2325
  }
2108
2326
  const stream = this.peers.get(prunables[0]);
2109
- this.prunedConnectionsCache.add(stream.publicKey.hashcode());
2327
+ this.prunedConnectionsCache?.add(stream.publicKey.hashcode());
2110
2328
  await this.onPeerDisconnected(stream.peerId);
2111
2329
  return this.components.connectionManager.closeConnections(stream.peerId);
2112
2330
  }