@peerbit/stream 4.6.0-bbf27fa → 4.6.0-cb91e7b
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/benchmark/directstream-sim.d.ts +12 -0
- package/dist/benchmark/directstream-sim.d.ts.map +1 -0
- package/dist/benchmark/directstream-sim.js +299 -0
- package/dist/benchmark/directstream-sim.js.map +1 -0
- package/dist/benchmark/index.d.ts +10 -0
- package/dist/benchmark/index.d.ts.map +1 -0
- package/dist/benchmark/index.js +48 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/topology-sim.d.ts +12 -0
- package/dist/benchmark/topology-sim.d.ts.map +1 -0
- package/dist/benchmark/topology-sim.js +410 -0
- package/dist/benchmark/topology-sim.js.map +1 -0
- package/dist/benchmark/transfer.js +2 -2
- package/dist/benchmark/transfer.js.map +1 -1
- package/dist/src/core/seek-routing.d.ts +39 -0
- package/dist/src/core/seek-routing.d.ts.map +1 -0
- package/dist/src/core/seek-routing.js +33 -0
- package/dist/src/core/seek-routing.js.map +1 -0
- package/dist/src/index.d.ts +36 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +516 -300
- package/dist/src/index.js.map +1 -1
- package/dist/src/routes.d.ts +11 -0
- package/dist/src/routes.d.ts.map +1 -1
- package/dist/src/routes.js +105 -16
- package/dist/src/routes.js.map +1 -1
- package/dist/src/wait-for-event.d.ts.map +1 -1
- package/dist/src/wait-for-event.js +14 -3
- package/dist/src/wait-for-event.js.map +1 -1
- package/package.json +8 -8
- package/src/core/seek-routing.ts +75 -0
- package/src/index.ts +789 -505
- package/src/routes.ts +121 -19
- package/src/wait-for-event.ts +23 -10
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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({
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)) {
|
|
@@ -1145,6 +1343,11 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1145
1343
|
peerStreams.removeEventListener("stream:inbound", forwardInbound);
|
|
1146
1344
|
}, { once: true });
|
|
1147
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
|
+
}
|
|
1148
1351
|
return peerStreams;
|
|
1149
1352
|
}
|
|
1150
1353
|
/**
|
|
@@ -1260,32 +1463,33 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1260
1463
|
}
|
|
1261
1464
|
}
|
|
1262
1465
|
shouldIgnore(message, seenBefore) {
|
|
1263
|
-
const
|
|
1264
|
-
if (
|
|
1466
|
+
const signedBySelf = message.header.signatures?.publicKeys.some((x) => x.equals(this.publicKey)) ?? false;
|
|
1467
|
+
if (signedBySelf) {
|
|
1265
1468
|
return true;
|
|
1266
1469
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
return
|
|
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;
|
|
1272
1475
|
}
|
|
1273
|
-
return
|
|
1476
|
+
return seenBefore > 0;
|
|
1274
1477
|
}
|
|
1275
1478
|
async onDataMessage(from, peerStream, message, seenBefore) {
|
|
1276
1479
|
if (this.shouldIgnore(message, seenBefore)) {
|
|
1277
1480
|
return false;
|
|
1278
1481
|
}
|
|
1279
1482
|
let isForMe = false;
|
|
1280
|
-
if (message.header.mode instanceof AnyWhere
|
|
1483
|
+
if (message.header.mode instanceof AnyWhere ||
|
|
1484
|
+
message.header.mode instanceof AcknowledgeAnyWhere) {
|
|
1281
1485
|
isForMe = true;
|
|
1282
1486
|
}
|
|
1283
1487
|
else {
|
|
1284
1488
|
const isFromSelf = this.publicKey.equals(from);
|
|
1285
|
-
if (!isFromSelf
|
|
1286
|
-
|
|
1287
|
-
message.header.mode
|
|
1288
|
-
|
|
1489
|
+
if (!isFromSelf &&
|
|
1490
|
+
(message.header.mode instanceof SilentDelivery ||
|
|
1491
|
+
message.header.mode instanceof AcknowledgeDelivery)) {
|
|
1492
|
+
isForMe = message.header.mode.to.includes(this.publicKeyHash);
|
|
1289
1493
|
}
|
|
1290
1494
|
}
|
|
1291
1495
|
if (isForMe) {
|
|
@@ -1311,19 +1515,13 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1311
1515
|
}
|
|
1312
1516
|
}
|
|
1313
1517
|
// Forward
|
|
1314
|
-
|
|
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) {
|
|
1315
1523
|
// DONT await this since it might introduce a dead-lock
|
|
1316
|
-
|
|
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
|
-
}
|
|
1524
|
+
this.relayMessage(from, message);
|
|
1327
1525
|
}
|
|
1328
1526
|
}
|
|
1329
1527
|
async verifyAndProcess(message) {
|
|
@@ -1341,12 +1539,16 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1341
1539
|
return true;
|
|
1342
1540
|
}
|
|
1343
1541
|
async maybeAcknowledgeMessage(peerStream, message, seenBefore) {
|
|
1344
|
-
if (
|
|
1345
|
-
message.header.mode instanceof
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
message.header.mode.to.includes(this.publicKeyHash);
|
|
1349
|
-
if (!
|
|
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
|
+
})) {
|
|
1350
1552
|
return;
|
|
1351
1553
|
}
|
|
1352
1554
|
const signers = message.header.signatures.publicKeys.map((x) => x.hashcode());
|
|
@@ -1357,8 +1559,12 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1357
1559
|
header: new MessageHeader({
|
|
1358
1560
|
mode: new TracedDelivery(signers),
|
|
1359
1561
|
session: this.session,
|
|
1360
|
-
// include our origin
|
|
1361
|
-
|
|
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 &&
|
|
1362
1568
|
!message.header.signatures.publicKeys.find((x) => this.prunedConnectionsCache?.has(x.hashcode()))
|
|
1363
1569
|
? new MultiAddrinfo(this.components.addressManager
|
|
1364
1570
|
.getAddresses()
|
|
@@ -1443,37 +1649,40 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1443
1649
|
for (const remote of remotes) {
|
|
1444
1650
|
this.invalidateSession(remote);
|
|
1445
1651
|
}
|
|
1446
|
-
|
|
1652
|
+
// Best-effort liveness probe; never let background probe failures crash callers.
|
|
1653
|
+
void this.checkIsAlive(remotes).catch(() => false);
|
|
1447
1654
|
}
|
|
1448
1655
|
async checkIsAlive(remotes) {
|
|
1449
1656
|
if (this.peers.size === 0) {
|
|
1450
1657
|
return false;
|
|
1451
1658
|
}
|
|
1452
1659
|
if (remotes.length > 0) {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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") {
|
|
1471
1681
|
return false;
|
|
1472
1682
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
}); // this will remove the target if it is still not reable
|
|
1683
|
+
warn(`checkIsAlive unexpected error: ${errorName}`);
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1477
1686
|
}
|
|
1478
1687
|
return false;
|
|
1479
1688
|
}
|
|
@@ -1486,8 +1695,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1486
1695
|
redundancy: DEFAULT_SILENT_MESSAGE_REDUDANCY,
|
|
1487
1696
|
});
|
|
1488
1697
|
if (mode instanceof AcknowledgeDelivery ||
|
|
1489
|
-
mode instanceof SilentDelivery
|
|
1490
|
-
mode instanceof SeekDelivery) {
|
|
1698
|
+
mode instanceof SilentDelivery) {
|
|
1491
1699
|
if (mode.to) {
|
|
1492
1700
|
let preLength = mode.to.length;
|
|
1493
1701
|
mode.to = mode.to.filter((x) => x !== this.publicKeyHash);
|
|
@@ -1495,7 +1703,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1495
1703
|
if (preLength > 0 && mode.to?.length === 0) {
|
|
1496
1704
|
throw new InvalidMessageError("Unexpected to create a message with self as the only receiver");
|
|
1497
1705
|
}
|
|
1498
|
-
if (mode.to.length === 0
|
|
1706
|
+
if (mode.to.length === 0) {
|
|
1499
1707
|
throw new InvalidMessageError("Unexpected to deliver message with mode: " +
|
|
1500
1708
|
mode.constructor.name +
|
|
1501
1709
|
" without recipents");
|
|
@@ -1503,24 +1711,6 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1503
1711
|
}
|
|
1504
1712
|
}
|
|
1505
1713
|
}
|
|
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
1714
|
const message = new DataMessage({
|
|
1525
1715
|
data: data instanceof Uint8ArrayList ? data.subarray() : data,
|
|
1526
1716
|
header: new MessageHeader({
|
|
@@ -1543,7 +1733,9 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1543
1733
|
* Publishes messages to all peers
|
|
1544
1734
|
*/
|
|
1545
1735
|
async publish(data, options = {
|
|
1546
|
-
mode: new
|
|
1736
|
+
mode: new AcknowledgeAnyWhere({
|
|
1737
|
+
redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
|
|
1738
|
+
}),
|
|
1547
1739
|
}) {
|
|
1548
1740
|
if (!this.started) {
|
|
1549
1741
|
throw new NotStartedError();
|
|
@@ -1579,7 +1771,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1579
1771
|
if (this.canRelayMessage) {
|
|
1580
1772
|
if (message instanceof DataMessage) {
|
|
1581
1773
|
if (message.header.mode instanceof AcknowledgeDelivery ||
|
|
1582
|
-
message.header.mode instanceof
|
|
1774
|
+
message.header.mode instanceof AcknowledgeAnyWhere) {
|
|
1583
1775
|
await message.sign(this.sign);
|
|
1584
1776
|
}
|
|
1585
1777
|
}
|
|
@@ -1600,6 +1792,31 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1600
1792
|
clearTimeout(timer);
|
|
1601
1793
|
this.healthChecks.delete(to);
|
|
1602
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
|
+
}
|
|
1603
1820
|
async createDeliveryPromise(from, message, relayed, signal) {
|
|
1604
1821
|
if (message.header.mode instanceof AnyWhere) {
|
|
1605
1822
|
return {
|
|
@@ -1615,11 +1832,10 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1615
1832
|
}
|
|
1616
1833
|
const fastestNodesReached = new Map();
|
|
1617
1834
|
const messageToSet = new Set();
|
|
1618
|
-
if (message.header.mode
|
|
1835
|
+
if (deliveryModeHasReceiver(message.header.mode)) {
|
|
1619
1836
|
for (const to of message.header.mode.to) {
|
|
1620
|
-
if (to === from.hashcode())
|
|
1837
|
+
if (to === from.hashcode())
|
|
1621
1838
|
continue;
|
|
1622
|
-
}
|
|
1623
1839
|
messageToSet.add(to);
|
|
1624
1840
|
if (!relayed && !this.healthChecks.has(to)) {
|
|
1625
1841
|
this.healthChecks.set(to, setTimeout(() => {
|
|
@@ -1640,20 +1856,16 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1640
1856
|
}
|
|
1641
1857
|
const willGetAllAcknowledgements = !relayed; // Only the origin will get all acks
|
|
1642
1858
|
// 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 */
|
|
1859
|
+
const filterMessageForSeenCounter = relayed ? undefined : 1;
|
|
1648
1860
|
const uniqueAcks = new Set();
|
|
1649
1861
|
const session = +new Date();
|
|
1650
1862
|
const onUnreachable = !relayed &&
|
|
1651
1863
|
((ev) => {
|
|
1652
|
-
const
|
|
1653
|
-
if (
|
|
1654
|
-
//
|
|
1655
|
-
|
|
1656
|
-
|
|
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);
|
|
1657
1869
|
}
|
|
1658
1870
|
});
|
|
1659
1871
|
onUnreachable && this.addEventListener("peer:unreachable", onUnreachable);
|
|
@@ -1680,9 +1892,13 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1680
1892
|
}
|
|
1681
1893
|
}
|
|
1682
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}`;
|
|
1683
1899
|
deliveryDeferredPromise.reject(new DeliveryError(`Failed to get message ${idString} ${filterMessageForSeenCounter} ${[
|
|
1684
1900
|
...messageToSet,
|
|
1685
|
-
]} 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}`));
|
|
1686
1902
|
}
|
|
1687
1903
|
else {
|
|
1688
1904
|
deliveryDeferredPromise.resolve();
|
|
@@ -1705,11 +1921,22 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1705
1921
|
// know how many ACKs we will get
|
|
1706
1922
|
if (filterMessageForSeenCounter != null &&
|
|
1707
1923
|
uniqueAcks.size >= messageToSet.size * filterMessageForSeenCounter) {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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.
|
|
1711
1930
|
clear();
|
|
1712
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
|
+
}
|
|
1713
1940
|
deliveryDeferredPromise.resolve();
|
|
1714
1941
|
return true;
|
|
1715
1942
|
}
|
|
@@ -1723,25 +1950,35 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1723
1950
|
const seenCounter = ack.seenCounter;
|
|
1724
1951
|
// remove the automatic removal of route timeout since we have observed lifesigns of a peer
|
|
1725
1952
|
this.clearHealthcheckTimer(messageTargetHash);
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
|
|
1729
|
-
|
|
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));
|
|
1730
1969
|
}
|
|
1731
1970
|
if (messageToSet.has(messageTargetHash)) {
|
|
1732
|
-
//
|
|
1733
|
-
if
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
fastestNodesReached.set(messageTargetHash, arr);
|
|
1741
|
-
}
|
|
1742
|
-
arr.push(seenCounter);
|
|
1743
|
-
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);
|
|
1744
1979
|
}
|
|
1980
|
+
arr.push(seenCounter);
|
|
1981
|
+
uniqueAcks.add(messageTargetHash);
|
|
1745
1982
|
}
|
|
1746
1983
|
}
|
|
1747
1984
|
checkDone();
|
|
@@ -1757,7 +1994,9 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1757
1994
|
if (this.stopping || !this.started) {
|
|
1758
1995
|
throw new NotStartedError();
|
|
1759
1996
|
}
|
|
1997
|
+
const isRelayed = relayed ?? from.hashcode() !== this.publicKeyHash;
|
|
1760
1998
|
let delivereyPromise = undefined;
|
|
1999
|
+
let ackCallbackId;
|
|
1761
2000
|
if ((!message.header.signatures ||
|
|
1762
2001
|
message.header.signatures.publicKeys.length === 0) &&
|
|
1763
2002
|
message instanceof DataMessage &&
|
|
@@ -1767,85 +2006,145 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1767
2006
|
/**
|
|
1768
2007
|
* Logic for handling acknowledge messages when we receive them (later)
|
|
1769
2008
|
*/
|
|
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
2009
|
if ((message instanceof DataMessage || message instanceof Goodbye) &&
|
|
1779
|
-
(message.header.mode instanceof
|
|
1780
|
-
message.header.mode instanceof
|
|
1781
|
-
const deliveryDeferredPromise = await this.createDeliveryPromise(from, message,
|
|
2010
|
+
(message.header.mode instanceof AcknowledgeDelivery ||
|
|
2011
|
+
message.header.mode instanceof AcknowledgeAnyWhere)) {
|
|
2012
|
+
const deliveryDeferredPromise = await this.createDeliveryPromise(from, message, isRelayed, signal);
|
|
1782
2013
|
delivereyPromise = deliveryDeferredPromise.promise;
|
|
2014
|
+
ackCallbackId = toBase64(message.id);
|
|
1783
2015
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
if (
|
|
1794
|
-
message.header.mode instanceof
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
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) {
|
|
1802
2038
|
const promises = [];
|
|
2039
|
+
const usedNeighbours = new Set();
|
|
2040
|
+
const originalTo = message.header.mode.to;
|
|
1803
2041
|
for (const [neighbour, _distantPeers] of fanout) {
|
|
1804
2042
|
const stream = this.peers.get(neighbour);
|
|
1805
|
-
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 {
|
|
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);
|
|
1806
2071
|
promises.push(stream.waitForWrite(bytes, message.header.priority));
|
|
2072
|
+
}
|
|
1807
2073
|
}
|
|
1808
2074
|
await Promise.all(promises);
|
|
1809
|
-
return delivereyPromise;
|
|
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;
|
|
1810
2103
|
}
|
|
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
2104
|
}
|
|
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
2105
|
}
|
|
1835
|
-
//
|
|
1836
|
-
|
|
1837
|
-
|
|
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
|
+
}
|
|
1838
2136
|
}
|
|
1839
|
-
|
|
1840
|
-
promises.push(id.waitForWrite(bytes, message.header.priority));
|
|
2137
|
+
return delivereyPromise;
|
|
1841
2138
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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();
|
|
1846
2145
|
}
|
|
2146
|
+
throw error;
|
|
1847
2147
|
}
|
|
1848
|
-
return delivereyPromise;
|
|
1849
2148
|
}
|
|
1850
2149
|
async maybeConnectDirectly(toHash, origin) {
|
|
1851
2150
|
if (this.peers.has(toHash) || this.prunedConnectionsCache?.has(toHash)) {
|
|
@@ -1926,10 +2225,12 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1926
2225
|
for (const h of admitted)
|
|
1927
2226
|
if (reached(h, target))
|
|
1928
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));
|
|
1929
2230
|
if (settle === "any" && wins.size > 0)
|
|
1930
|
-
return
|
|
2231
|
+
return orderedWins();
|
|
1931
2232
|
if (settle === "all" && wins.size === admitted.length)
|
|
1932
|
-
return
|
|
2233
|
+
return orderedWins();
|
|
1933
2234
|
// Abort/timeout
|
|
1934
2235
|
const abortSignals = [this.closeController.signal];
|
|
1935
2236
|
if (signal) {
|
|
@@ -1949,7 +2250,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1949
2250
|
signals: abortSignals,
|
|
1950
2251
|
timeout,
|
|
1951
2252
|
});
|
|
1952
|
-
return
|
|
2253
|
+
return orderedWins();
|
|
1953
2254
|
}
|
|
1954
2255
|
catch (e) {
|
|
1955
2256
|
const abortSignal = abortSignals.find((s) => s.aborted);
|
|
@@ -1959,112 +2260,23 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1959
2260
|
}
|
|
1960
2261
|
throw new AbortError("Aborted waiting for peers: " + abortSignal.reason);
|
|
1961
2262
|
}
|
|
1962
|
-
if (e instanceof
|
|
1963
|
-
|
|
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}` : ""})`);
|
|
1964
2272
|
}
|
|
2273
|
+
if (e instanceof Error)
|
|
2274
|
+
throw e;
|
|
1965
2275
|
if (settle === "all")
|
|
1966
2276
|
throw new TimeoutError("Timeout waiting for peers");
|
|
1967
|
-
return
|
|
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);
|
|
2277
|
+
return orderedWins(); // settle:any: return whatever successes we got
|
|
2019
2278
|
}
|
|
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
|
-
} */
|
|
2279
|
+
}
|
|
2068
2280
|
getPublicKey(hash) {
|
|
2069
2281
|
return this.peerKeyHashToPublicKey.get(hash);
|
|
2070
2282
|
}
|
|
@@ -2073,6 +2285,10 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
2073
2285
|
}
|
|
2074
2286
|
// make this into a job? run every few ms
|
|
2075
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
|
+
}
|
|
2076
2292
|
if (this.connectionManagerOptions.pruner) {
|
|
2077
2293
|
if (this.connectionManagerOptions.pruner.bandwidth != null) {
|
|
2078
2294
|
let usedBandwidth = 0;
|
|
@@ -2108,7 +2324,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
2108
2324
|
return;
|
|
2109
2325
|
}
|
|
2110
2326
|
const stream = this.peers.get(prunables[0]);
|
|
2111
|
-
this.prunedConnectionsCache
|
|
2327
|
+
this.prunedConnectionsCache?.add(stream.publicKey.hashcode());
|
|
2112
2328
|
await this.onPeerDisconnected(stream.peerId);
|
|
2113
2329
|
return this.components.connectionManager.closeConnections(stream.peerId);
|
|
2114
2330
|
}
|