@peerbit/stream 4.6.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +39 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +522 -300
- package/dist/src/index.js.map +1 -1
- package/dist/src/routes.d.ts +15 -0
- package/dist/src/routes.d.ts.map +1 -1
- package/dist/src/routes.js +135 -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 +7 -7
- package/src/core/seek-routing.ts +75 -0
- package/src/index.ts +805 -509
- package/src/routes.ts +158 -18
- 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)) {
|
|
@@ -1102,6 +1300,12 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1102
1300
|
invalidateSession(key) {
|
|
1103
1301
|
this.routes.updateSession(key, undefined);
|
|
1104
1302
|
}
|
|
1303
|
+
getRouteHints(target, from = this.publicKeyHash) {
|
|
1304
|
+
return this.routes.getRouteHints(from, target);
|
|
1305
|
+
}
|
|
1306
|
+
getBestRouteHint(target, from = this.publicKeyHash) {
|
|
1307
|
+
return this.routes.getBestRouteHint(from, target);
|
|
1308
|
+
}
|
|
1105
1309
|
onPeerSession(key, session) {
|
|
1106
1310
|
this.dispatchEvent(
|
|
1107
1311
|
// TODO types
|
|
@@ -1145,6 +1349,11 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1145
1349
|
peerStreams.removeEventListener("stream:inbound", forwardInbound);
|
|
1146
1350
|
}, { once: true });
|
|
1147
1351
|
this.addRouteConnection(this.publicKeyHash, publicKey.hashcode(), publicKey, -1, +new Date(), -1);
|
|
1352
|
+
// Enforce connection manager limits eagerly when new peers are added. Without this,
|
|
1353
|
+
// join storms can create large temporary peer sets and OOM in single-process sims.
|
|
1354
|
+
if (this.peers.size > this.connectionManagerOptions.maxConnections) {
|
|
1355
|
+
void this.pruneConnectionsToLimits().catch(() => { });
|
|
1356
|
+
}
|
|
1148
1357
|
return peerStreams;
|
|
1149
1358
|
}
|
|
1150
1359
|
/**
|
|
@@ -1260,32 +1469,33 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1260
1469
|
}
|
|
1261
1470
|
}
|
|
1262
1471
|
shouldIgnore(message, seenBefore) {
|
|
1263
|
-
const
|
|
1264
|
-
if (
|
|
1472
|
+
const signedBySelf = message.header.signatures?.publicKeys.some((x) => x.equals(this.publicKey)) ?? false;
|
|
1473
|
+
if (signedBySelf) {
|
|
1265
1474
|
return true;
|
|
1266
1475
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
return
|
|
1476
|
+
// For acknowledged modes, allow limited duplicate forwarding so that we can
|
|
1477
|
+
// discover and maintain multiple candidate routes (distance=seenCounter).
|
|
1478
|
+
if (message.header.mode instanceof AcknowledgeDelivery ||
|
|
1479
|
+
message.header.mode instanceof AcknowledgeAnyWhere) {
|
|
1480
|
+
return seenBefore >= message.header.mode.redundancy;
|
|
1272
1481
|
}
|
|
1273
|
-
return
|
|
1482
|
+
return seenBefore > 0;
|
|
1274
1483
|
}
|
|
1275
1484
|
async onDataMessage(from, peerStream, message, seenBefore) {
|
|
1276
1485
|
if (this.shouldIgnore(message, seenBefore)) {
|
|
1277
1486
|
return false;
|
|
1278
1487
|
}
|
|
1279
1488
|
let isForMe = false;
|
|
1280
|
-
if (message.header.mode instanceof AnyWhere
|
|
1489
|
+
if (message.header.mode instanceof AnyWhere ||
|
|
1490
|
+
message.header.mode instanceof AcknowledgeAnyWhere) {
|
|
1281
1491
|
isForMe = true;
|
|
1282
1492
|
}
|
|
1283
1493
|
else {
|
|
1284
1494
|
const isFromSelf = this.publicKey.equals(from);
|
|
1285
|
-
if (!isFromSelf
|
|
1286
|
-
|
|
1287
|
-
message.header.mode
|
|
1288
|
-
|
|
1495
|
+
if (!isFromSelf &&
|
|
1496
|
+
(message.header.mode instanceof SilentDelivery ||
|
|
1497
|
+
message.header.mode instanceof AcknowledgeDelivery)) {
|
|
1498
|
+
isForMe = message.header.mode.to.includes(this.publicKeyHash);
|
|
1289
1499
|
}
|
|
1290
1500
|
}
|
|
1291
1501
|
if (isForMe) {
|
|
@@ -1311,19 +1521,13 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1311
1521
|
}
|
|
1312
1522
|
}
|
|
1313
1523
|
// Forward
|
|
1314
|
-
|
|
1524
|
+
const shouldForward = seenBefore === 0 ||
|
|
1525
|
+
((message.header.mode instanceof AcknowledgeDelivery ||
|
|
1526
|
+
message.header.mode instanceof AcknowledgeAnyWhere) &&
|
|
1527
|
+
seenBefore < message.header.mode.redundancy);
|
|
1528
|
+
if (shouldForward) {
|
|
1315
1529
|
// DONT await this since it might introduce a dead-lock
|
|
1316
|
-
|
|
1317
|
-
if (seenBefore < message.header.mode.redundancy) {
|
|
1318
|
-
const to = [...this.peers.values()].filter((x) => !message.header.signatures?.publicKeys.find((y) => y.equals(x.publicKey)) && x !== peerStream);
|
|
1319
|
-
if (to.length > 0) {
|
|
1320
|
-
this.relayMessage(from, message, to);
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
else {
|
|
1325
|
-
this.relayMessage(from, message);
|
|
1326
|
-
}
|
|
1530
|
+
this.relayMessage(from, message);
|
|
1327
1531
|
}
|
|
1328
1532
|
}
|
|
1329
1533
|
async verifyAndProcess(message) {
|
|
@@ -1341,12 +1545,16 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1341
1545
|
return true;
|
|
1342
1546
|
}
|
|
1343
1547
|
async maybeAcknowledgeMessage(peerStream, message, seenBefore) {
|
|
1344
|
-
if (
|
|
1345
|
-
message.header.mode instanceof
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
message.header.mode.to.includes(this.publicKeyHash);
|
|
1349
|
-
if (!
|
|
1548
|
+
if (message.header.mode instanceof AcknowledgeDelivery ||
|
|
1549
|
+
message.header.mode instanceof AcknowledgeAnyWhere) {
|
|
1550
|
+
const isRecipient = message.header.mode instanceof AcknowledgeAnyWhere
|
|
1551
|
+
? true
|
|
1552
|
+
: message.header.mode.to.includes(this.publicKeyHash);
|
|
1553
|
+
if (!shouldAcknowledgeDataMessage({
|
|
1554
|
+
isRecipient,
|
|
1555
|
+
seenBefore,
|
|
1556
|
+
redundancy: message.header.mode.redundancy,
|
|
1557
|
+
})) {
|
|
1350
1558
|
return;
|
|
1351
1559
|
}
|
|
1352
1560
|
const signers = message.header.signatures.publicKeys.map((x) => x.hashcode());
|
|
@@ -1357,8 +1565,12 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1357
1565
|
header: new MessageHeader({
|
|
1358
1566
|
mode: new TracedDelivery(signers),
|
|
1359
1567
|
session: this.session,
|
|
1360
|
-
// include our origin
|
|
1361
|
-
|
|
1568
|
+
// include our origin for route-learning/dialer hints (best-effort privacy/anti-spam control):
|
|
1569
|
+
// only include once (seenBefore=0) and only if we have not recently pruned
|
|
1570
|
+
// a connection to any signer in the path
|
|
1571
|
+
origin: (message.header.mode instanceof AcknowledgeAnyWhere ||
|
|
1572
|
+
message.header.mode instanceof AcknowledgeDelivery) &&
|
|
1573
|
+
seenBefore === 0 &&
|
|
1362
1574
|
!message.header.signatures.publicKeys.find((x) => this.prunedConnectionsCache?.has(x.hashcode()))
|
|
1363
1575
|
? new MultiAddrinfo(this.components.addressManager
|
|
1364
1576
|
.getAddresses()
|
|
@@ -1443,37 +1655,40 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1443
1655
|
for (const remote of remotes) {
|
|
1444
1656
|
this.invalidateSession(remote);
|
|
1445
1657
|
}
|
|
1446
|
-
|
|
1658
|
+
// Best-effort liveness probe; never let background probe failures crash callers.
|
|
1659
|
+
void this.checkIsAlive(remotes).catch(() => false);
|
|
1447
1660
|
}
|
|
1448
1661
|
async checkIsAlive(remotes) {
|
|
1449
1662
|
if (this.peers.size === 0) {
|
|
1450
1663
|
return false;
|
|
1451
1664
|
}
|
|
1452
1665
|
if (remotes.length > 0) {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1666
|
+
try {
|
|
1667
|
+
// Keepalive probes are best-effort, but must still require ACKs so we can
|
|
1668
|
+
// conclusively prune stale routes when peers actually went away.
|
|
1669
|
+
await this.publish(undefined, {
|
|
1670
|
+
mode: new AcknowledgeDelivery({
|
|
1671
|
+
to: remotes,
|
|
1672
|
+
redundancy: DEFAULT_SILENT_MESSAGE_REDUDANCY,
|
|
1673
|
+
}),
|
|
1674
|
+
});
|
|
1675
|
+
return true;
|
|
1676
|
+
}
|
|
1677
|
+
catch (e) {
|
|
1678
|
+
const errorName = e?.name ?? e?.constructor?.name ?? "UnknownError";
|
|
1679
|
+
if (e instanceof DeliveryError ||
|
|
1680
|
+
e instanceof NotStartedError ||
|
|
1681
|
+
e instanceof TimeoutError ||
|
|
1682
|
+
e instanceof AbortError ||
|
|
1683
|
+
errorName === "DeliveryError" ||
|
|
1684
|
+
errorName === "NotStartedError" ||
|
|
1685
|
+
errorName === "TimeoutError" ||
|
|
1686
|
+
errorName === "AbortError") {
|
|
1471
1687
|
return false;
|
|
1472
1688
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
}); // this will remove the target if it is still not reable
|
|
1689
|
+
warn(`checkIsAlive unexpected error: ${errorName}`);
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1477
1692
|
}
|
|
1478
1693
|
return false;
|
|
1479
1694
|
}
|
|
@@ -1486,8 +1701,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1486
1701
|
redundancy: DEFAULT_SILENT_MESSAGE_REDUDANCY,
|
|
1487
1702
|
});
|
|
1488
1703
|
if (mode instanceof AcknowledgeDelivery ||
|
|
1489
|
-
mode instanceof SilentDelivery
|
|
1490
|
-
mode instanceof SeekDelivery) {
|
|
1704
|
+
mode instanceof SilentDelivery) {
|
|
1491
1705
|
if (mode.to) {
|
|
1492
1706
|
let preLength = mode.to.length;
|
|
1493
1707
|
mode.to = mode.to.filter((x) => x !== this.publicKeyHash);
|
|
@@ -1495,7 +1709,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1495
1709
|
if (preLength > 0 && mode.to?.length === 0) {
|
|
1496
1710
|
throw new InvalidMessageError("Unexpected to create a message with self as the only receiver");
|
|
1497
1711
|
}
|
|
1498
|
-
if (mode.to.length === 0
|
|
1712
|
+
if (mode.to.length === 0) {
|
|
1499
1713
|
throw new InvalidMessageError("Unexpected to deliver message with mode: " +
|
|
1500
1714
|
mode.constructor.name +
|
|
1501
1715
|
" without recipents");
|
|
@@ -1503,24 +1717,6 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1503
1717
|
}
|
|
1504
1718
|
}
|
|
1505
1719
|
}
|
|
1506
|
-
if (mode instanceof AcknowledgeDelivery || mode instanceof SilentDelivery) {
|
|
1507
|
-
const now = +new Date();
|
|
1508
|
-
for (const hash of mode.to) {
|
|
1509
|
-
const neighbourRoutes = this.routes.routes
|
|
1510
|
-
.get(this.publicKeyHash)
|
|
1511
|
-
?.get(hash);
|
|
1512
|
-
if (!neighbourRoutes ||
|
|
1513
|
-
now - neighbourRoutes.session >
|
|
1514
|
-
neighbourRoutes.list.length * this.routeSeekInterval ||
|
|
1515
|
-
!this.routes.isUpToDate(hash, neighbourRoutes)) {
|
|
1516
|
-
mode = new SeekDelivery({
|
|
1517
|
-
to: mode.to,
|
|
1518
|
-
redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
|
|
1519
|
-
});
|
|
1520
|
-
break;
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
1720
|
const message = new DataMessage({
|
|
1525
1721
|
data: data instanceof Uint8ArrayList ? data.subarray() : data,
|
|
1526
1722
|
header: new MessageHeader({
|
|
@@ -1543,7 +1739,9 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1543
1739
|
* Publishes messages to all peers
|
|
1544
1740
|
*/
|
|
1545
1741
|
async publish(data, options = {
|
|
1546
|
-
mode: new
|
|
1742
|
+
mode: new AcknowledgeAnyWhere({
|
|
1743
|
+
redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY,
|
|
1744
|
+
}),
|
|
1547
1745
|
}) {
|
|
1548
1746
|
if (!this.started) {
|
|
1549
1747
|
throw new NotStartedError();
|
|
@@ -1579,7 +1777,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1579
1777
|
if (this.canRelayMessage) {
|
|
1580
1778
|
if (message instanceof DataMessage) {
|
|
1581
1779
|
if (message.header.mode instanceof AcknowledgeDelivery ||
|
|
1582
|
-
message.header.mode instanceof
|
|
1780
|
+
message.header.mode instanceof AcknowledgeAnyWhere) {
|
|
1583
1781
|
await message.sign(this.sign);
|
|
1584
1782
|
}
|
|
1585
1783
|
}
|
|
@@ -1600,6 +1798,31 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1600
1798
|
clearTimeout(timer);
|
|
1601
1799
|
this.healthChecks.delete(to);
|
|
1602
1800
|
}
|
|
1801
|
+
formatDeliveryDebugState(targets, fastestNodesReached) {
|
|
1802
|
+
const sampleSize = 16;
|
|
1803
|
+
const sampledTargets = [...targets].slice(0, sampleSize);
|
|
1804
|
+
const sampled = sampledTargets.map((target) => {
|
|
1805
|
+
const route = this.routes.findNeighbor(this.publicKeyHash, target);
|
|
1806
|
+
const routeSample = route?.list
|
|
1807
|
+
.slice(0, 3)
|
|
1808
|
+
.map((r) => `${r.hash}:${r.distance}${r.expireAt ? ":old" : ""}`)
|
|
1809
|
+
.join(",") ?? "-";
|
|
1810
|
+
const reachable = this.routes.isReachable(this.publicKeyHash, target);
|
|
1811
|
+
const stream = this.peers.get(target);
|
|
1812
|
+
let openConnections = 0;
|
|
1813
|
+
if (stream) {
|
|
1814
|
+
try {
|
|
1815
|
+
openConnections = this.components.connectionManager.getConnections(stream.peerId).length;
|
|
1816
|
+
}
|
|
1817
|
+
catch {
|
|
1818
|
+
openConnections = stream.isReadable || stream.isWritable ? 1 : 0;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return `${target}{reachable=${reachable ? 1 : 0},ack=${fastestNodesReached.has(target) ? 1 : 0},stream=${stream ? 1 : 0},conns=${openConnections},routes=${routeSample}}`;
|
|
1822
|
+
});
|
|
1823
|
+
const more = targets.size - sampledTargets.length;
|
|
1824
|
+
return `deliveryState peers=${this.peers.size} routesLocal=${this.routes.count()} routesAll=${this.routes.countAll()} targets=[${sampled.join(";")}]${more > 0 ? ` (+${more} more)` : ""}`;
|
|
1825
|
+
}
|
|
1603
1826
|
async createDeliveryPromise(from, message, relayed, signal) {
|
|
1604
1827
|
if (message.header.mode instanceof AnyWhere) {
|
|
1605
1828
|
return {
|
|
@@ -1615,11 +1838,10 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1615
1838
|
}
|
|
1616
1839
|
const fastestNodesReached = new Map();
|
|
1617
1840
|
const messageToSet = new Set();
|
|
1618
|
-
if (message.header.mode
|
|
1841
|
+
if (deliveryModeHasReceiver(message.header.mode)) {
|
|
1619
1842
|
for (const to of message.header.mode.to) {
|
|
1620
|
-
if (to === from.hashcode())
|
|
1843
|
+
if (to === from.hashcode())
|
|
1621
1844
|
continue;
|
|
1622
|
-
}
|
|
1623
1845
|
messageToSet.add(to);
|
|
1624
1846
|
if (!relayed && !this.healthChecks.has(to)) {
|
|
1625
1847
|
this.healthChecks.set(to, setTimeout(() => {
|
|
@@ -1640,20 +1862,16 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1640
1862
|
}
|
|
1641
1863
|
const willGetAllAcknowledgements = !relayed; // Only the origin will get all acks
|
|
1642
1864
|
// Expected to receive at least 'filterMessageForSeenCounter' acknowledgements from each peer
|
|
1643
|
-
const filterMessageForSeenCounter = relayed
|
|
1644
|
-
? undefined
|
|
1645
|
-
: message.header.mode instanceof SeekDelivery
|
|
1646
|
-
? Math.min(this.peers.size, message.header.mode.redundancy)
|
|
1647
|
-
: 1; /* message.deliveryMode instanceof SeekDelivery ? Math.min(this.peers.size - (relayed ? 1 : 0), message.deliveryMode.redundancy) : 1 */
|
|
1865
|
+
const filterMessageForSeenCounter = relayed ? undefined : 1;
|
|
1648
1866
|
const uniqueAcks = new Set();
|
|
1649
1867
|
const session = +new Date();
|
|
1650
1868
|
const onUnreachable = !relayed &&
|
|
1651
1869
|
((ev) => {
|
|
1652
|
-
const
|
|
1653
|
-
if (
|
|
1654
|
-
//
|
|
1655
|
-
|
|
1656
|
-
|
|
1870
|
+
const target = ev.detail.hashcode();
|
|
1871
|
+
if (messageToSet.has(target)) {
|
|
1872
|
+
// A peer can transiently appear unreachable while routes are being updated.
|
|
1873
|
+
// Keep waiting for ACK/timeout instead of failing delivery immediately.
|
|
1874
|
+
logger.trace("peer unreachable during delivery (msg=%s, target=%s); waiting for ACK/timeout", idString, target);
|
|
1657
1875
|
}
|
|
1658
1876
|
});
|
|
1659
1877
|
onUnreachable && this.addEventListener("peer:unreachable", onUnreachable);
|
|
@@ -1680,9 +1898,13 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1680
1898
|
}
|
|
1681
1899
|
}
|
|
1682
1900
|
if (!hasAll && willGetAllAcknowledgements) {
|
|
1901
|
+
const debugState = this.formatDeliveryDebugState(messageToSet, fastestNodesReached);
|
|
1902
|
+
const msgMeta = `msgType=${message.constructor.name} dataBytes=${message instanceof DataMessage
|
|
1903
|
+
? (message.data?.byteLength ?? 0)
|
|
1904
|
+
: 0} relayed=${relayed ? 1 : 0}`;
|
|
1683
1905
|
deliveryDeferredPromise.reject(new DeliveryError(`Failed to get message ${idString} ${filterMessageForSeenCounter} ${[
|
|
1684
1906
|
...messageToSet,
|
|
1685
|
-
]} delivery acknowledges from all nodes (${fastestNodesReached.size}/${messageToSet.size}). Mode: ${message.header.mode.constructor.name}. Redundancy: ${message.header.mode["redundancy"]}`));
|
|
1907
|
+
]} delivery acknowledges from all nodes (${fastestNodesReached.size}/${messageToSet.size}). Mode: ${message.header.mode.constructor.name}. Redundancy: ${message.header.mode["redundancy"]}. ${msgMeta}. ${debugState}`));
|
|
1686
1908
|
}
|
|
1687
1909
|
else {
|
|
1688
1910
|
deliveryDeferredPromise.resolve();
|
|
@@ -1705,11 +1927,22 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1705
1927
|
// know how many ACKs we will get
|
|
1706
1928
|
if (filterMessageForSeenCounter != null &&
|
|
1707
1929
|
uniqueAcks.size >= messageToSet.size * filterMessageForSeenCounter) {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1930
|
+
const shouldKeepCallbackForRouteLearning = !relayed &&
|
|
1931
|
+
message.header.mode instanceof AcknowledgeDelivery &&
|
|
1932
|
+
message.header.mode.redundancy > 1;
|
|
1933
|
+
if (haveReceivers && !shouldKeepCallbackForRouteLearning) {
|
|
1934
|
+
// If we have an explicit recipient list we can clear the ACK callback once we
|
|
1935
|
+
// got the expected acknowledgements.
|
|
1711
1936
|
clear();
|
|
1712
1937
|
}
|
|
1938
|
+
else if (haveReceivers && shouldKeepCallbackForRouteLearning) {
|
|
1939
|
+
// Resolve delivery early, but keep ACK callbacks alive until timeout so we can
|
|
1940
|
+
// learn additional redundant routes (seenCounter=1..redundancy-1).
|
|
1941
|
+
onUnreachable &&
|
|
1942
|
+
this.removeEventListener("peer:unreachable", onUnreachable);
|
|
1943
|
+
onAbort && signal?.removeEventListener("abort", onAbort);
|
|
1944
|
+
onAbort = undefined;
|
|
1945
|
+
}
|
|
1713
1946
|
deliveryDeferredPromise.resolve();
|
|
1714
1947
|
return true;
|
|
1715
1948
|
}
|
|
@@ -1723,25 +1956,35 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1723
1956
|
const seenCounter = ack.seenCounter;
|
|
1724
1957
|
// remove the automatic removal of route timeout since we have observed lifesigns of a peer
|
|
1725
1958
|
this.clearHealthcheckTimer(messageTargetHash);
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
|
|
1729
|
-
|
|
1959
|
+
// If the target is not inside the original message `to`, we can still add the target to our routes.
|
|
1960
|
+
// This matters because relays may modify `to`, and because "ack-anywhere" style probes intentionally
|
|
1961
|
+
// do not provide an explicit recipient list.
|
|
1962
|
+
if (message.header.mode instanceof AcknowledgeDelivery ||
|
|
1963
|
+
message.header.mode instanceof AcknowledgeAnyWhere) {
|
|
1964
|
+
const upstreamHash = messageFrom?.publicKey.hashcode();
|
|
1965
|
+
const routeUpdate = computeSeekAckRouteUpdate({
|
|
1966
|
+
current: this.publicKeyHash,
|
|
1967
|
+
upstream: upstreamHash,
|
|
1968
|
+
downstream: messageThrough.publicKey.hashcode(),
|
|
1969
|
+
target: messageTargetHash,
|
|
1970
|
+
// Route "distance" is based on recipient-seen order (0 = fastest). This is relied upon by
|
|
1971
|
+
// `Routes.getFanout(...)` which uses `distance < redundancy` to select redundant next-hops.
|
|
1972
|
+
distance: seenCounter,
|
|
1973
|
+
});
|
|
1974
|
+
this.addRouteConnection(routeUpdate.from, routeUpdate.neighbour, messageTarget, routeUpdate.distance, session, Number(ack.header.session));
|
|
1730
1975
|
}
|
|
1731
1976
|
if (messageToSet.has(messageTargetHash)) {
|
|
1732
|
-
//
|
|
1733
|
-
if
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
fastestNodesReached.set(messageTargetHash, arr);
|
|
1741
|
-
}
|
|
1742
|
-
arr.push(seenCounter);
|
|
1743
|
-
uniqueAcks.add(messageTargetHash + seenCounter);
|
|
1977
|
+
// Any ACK from the target should satisfy delivery semantics.
|
|
1978
|
+
// Relying on only seenCounter=0 can fail under churn if the first ACK
|
|
1979
|
+
// is lost but a later ACK (seenCounter>0) arrives.
|
|
1980
|
+
if (seenCounter < MAX_ROUTE_DISTANCE) {
|
|
1981
|
+
let arr = fastestNodesReached.get(messageTargetHash);
|
|
1982
|
+
if (!arr) {
|
|
1983
|
+
arr = [];
|
|
1984
|
+
fastestNodesReached.set(messageTargetHash, arr);
|
|
1744
1985
|
}
|
|
1986
|
+
arr.push(seenCounter);
|
|
1987
|
+
uniqueAcks.add(messageTargetHash);
|
|
1745
1988
|
}
|
|
1746
1989
|
}
|
|
1747
1990
|
checkDone();
|
|
@@ -1757,7 +2000,9 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1757
2000
|
if (this.stopping || !this.started) {
|
|
1758
2001
|
throw new NotStartedError();
|
|
1759
2002
|
}
|
|
2003
|
+
const isRelayed = relayed ?? from.hashcode() !== this.publicKeyHash;
|
|
1760
2004
|
let delivereyPromise = undefined;
|
|
2005
|
+
let ackCallbackId;
|
|
1761
2006
|
if ((!message.header.signatures ||
|
|
1762
2007
|
message.header.signatures.publicKeys.length === 0) &&
|
|
1763
2008
|
message instanceof DataMessage &&
|
|
@@ -1767,85 +2012,145 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1767
2012
|
/**
|
|
1768
2013
|
* Logic for handling acknowledge messages when we receive them (later)
|
|
1769
2014
|
*/
|
|
1770
|
-
if (message instanceof DataMessage &&
|
|
1771
|
-
message.header.mode instanceof SeekDelivery &&
|
|
1772
|
-
!relayed) {
|
|
1773
|
-
to = this.peers; // seek delivery will not work unless we try all possible paths
|
|
1774
|
-
}
|
|
1775
|
-
if (message.header.mode instanceof AcknowledgeDelivery) {
|
|
1776
|
-
to = undefined;
|
|
1777
|
-
}
|
|
1778
2015
|
if ((message instanceof DataMessage || message instanceof Goodbye) &&
|
|
1779
|
-
(message.header.mode instanceof
|
|
1780
|
-
message.header.mode instanceof
|
|
1781
|
-
const deliveryDeferredPromise = await this.createDeliveryPromise(from, message,
|
|
2016
|
+
(message.header.mode instanceof AcknowledgeDelivery ||
|
|
2017
|
+
message.header.mode instanceof AcknowledgeAnyWhere)) {
|
|
2018
|
+
const deliveryDeferredPromise = await this.createDeliveryPromise(from, message, isRelayed, signal);
|
|
1782
2019
|
delivereyPromise = deliveryDeferredPromise.promise;
|
|
2020
|
+
ackCallbackId = toBase64(message.id);
|
|
1783
2021
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
if (
|
|
1794
|
-
message.header.mode instanceof
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
2022
|
+
try {
|
|
2023
|
+
const bytes = message.bytes();
|
|
2024
|
+
if (!isRelayed) {
|
|
2025
|
+
const bytesArray = bytes instanceof Uint8Array ? bytes : bytes.subarray();
|
|
2026
|
+
await this.modifySeenCache(bytesArray);
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* For non SEEKing message delivery modes, use routing
|
|
2030
|
+
*/
|
|
2031
|
+
if (message instanceof DataMessage) {
|
|
2032
|
+
if ((message.header.mode instanceof AcknowledgeDelivery ||
|
|
2033
|
+
message.header.mode instanceof SilentDelivery) &&
|
|
2034
|
+
!to) {
|
|
2035
|
+
if (message.header.mode.to.length === 0) {
|
|
2036
|
+
// we definitely know that we should not forward the message anywhere
|
|
2037
|
+
return delivereyPromise;
|
|
2038
|
+
}
|
|
2039
|
+
const fanout = this.routes.getFanout(from.hashcode(), message.header.mode.to, message.header.mode.redundancy);
|
|
2040
|
+
// If we have explicit routing information, send only along the chosen next-hops.
|
|
2041
|
+
// If `fanout` is empty (no route info yet), fall through to the flooding logic below
|
|
2042
|
+
// so acknowledged deliveries can discover/repair routes instead of timing out.
|
|
2043
|
+
if (fanout && fanout.size > 0) {
|
|
1802
2044
|
const promises = [];
|
|
2045
|
+
const usedNeighbours = new Set();
|
|
2046
|
+
const originalTo = message.header.mode.to;
|
|
1803
2047
|
for (const [neighbour, _distantPeers] of fanout) {
|
|
1804
2048
|
const stream = this.peers.get(neighbour);
|
|
1805
|
-
stream
|
|
2049
|
+
if (!stream)
|
|
2050
|
+
continue;
|
|
2051
|
+
if (message.header.mode instanceof SilentDelivery) {
|
|
2052
|
+
message.header.mode.to = [..._distantPeers.keys()];
|
|
2053
|
+
promises.push(stream.waitForWrite(message.bytes(), message.header.priority));
|
|
2054
|
+
}
|
|
2055
|
+
else {
|
|
1806
2056
|
promises.push(stream.waitForWrite(bytes, message.header.priority));
|
|
2057
|
+
}
|
|
2058
|
+
usedNeighbours.add(neighbour);
|
|
2059
|
+
}
|
|
2060
|
+
if (message.header.mode instanceof SilentDelivery) {
|
|
2061
|
+
message.header.mode.to = originalTo;
|
|
2062
|
+
}
|
|
2063
|
+
// If the sender requested redundancy but we don't yet have enough distinct
|
|
2064
|
+
// next-hops for the target(s), opportunistically probe additional neighbours.
|
|
2065
|
+
// This replaces the previous "greedy fanout" probing behavior without needing
|
|
2066
|
+
// a separate delivery mode.
|
|
2067
|
+
if (!isRelayed &&
|
|
2068
|
+
message.header.mode instanceof AcknowledgeDelivery &&
|
|
2069
|
+
usedNeighbours.size < message.header.mode.redundancy) {
|
|
2070
|
+
for (const [neighbour, stream] of this.peers) {
|
|
2071
|
+
if (usedNeighbours.size >= message.header.mode.redundancy) {
|
|
2072
|
+
break;
|
|
2073
|
+
}
|
|
2074
|
+
if (usedNeighbours.has(neighbour))
|
|
2075
|
+
continue;
|
|
2076
|
+
usedNeighbours.add(neighbour);
|
|
2077
|
+
promises.push(stream.waitForWrite(bytes, message.header.priority));
|
|
2078
|
+
}
|
|
1807
2079
|
}
|
|
1808
2080
|
await Promise.all(promises);
|
|
1809
|
-
return delivereyPromise;
|
|
2081
|
+
return delivereyPromise;
|
|
2082
|
+
}
|
|
2083
|
+
// If we don't have routing information:
|
|
2084
|
+
// - For acknowledged deliveries, fall through to flooding (route discovery / repair).
|
|
2085
|
+
// - For silent deliveries, relays should not flood (prevents unnecessary fanout); origin may still flood.
|
|
2086
|
+
// We still allow direct neighbour delivery to explicit recipients (if connected).
|
|
2087
|
+
if (isRelayed && message.header.mode instanceof SilentDelivery) {
|
|
2088
|
+
const promises = [];
|
|
2089
|
+
const originalTo = message.header.mode.to;
|
|
2090
|
+
for (const recipient of originalTo) {
|
|
2091
|
+
if (recipient === this.publicKeyHash)
|
|
2092
|
+
continue;
|
|
2093
|
+
if (recipient === from.hashcode())
|
|
2094
|
+
continue; // never send back to previous hop
|
|
2095
|
+
const stream = this.peers.get(recipient);
|
|
2096
|
+
if (!stream)
|
|
2097
|
+
continue;
|
|
2098
|
+
if (message.header.signatures?.publicKeys.find((x) => x.hashcode() === recipient)) {
|
|
2099
|
+
continue; // recipient already signed/seen this message
|
|
2100
|
+
}
|
|
2101
|
+
message.header.mode.to = [recipient];
|
|
2102
|
+
promises.push(stream.waitForWrite(message.bytes(), message.header.priority));
|
|
2103
|
+
}
|
|
2104
|
+
message.header.mode.to = originalTo;
|
|
2105
|
+
if (promises.length > 0) {
|
|
2106
|
+
await Promise.all(promises);
|
|
2107
|
+
}
|
|
2108
|
+
return delivereyPromise;
|
|
1810
2109
|
}
|
|
1811
|
-
return; // we defintely know that we should not forward the message anywhere
|
|
1812
|
-
}
|
|
1813
|
-
// we end up here because we don't have enough information yet in how to send data to the peer (TODO test this codepath)
|
|
1814
|
-
if (relayed) {
|
|
1815
|
-
return;
|
|
1816
2110
|
}
|
|
1817
|
-
} // else send to all (fallthrough to code below)
|
|
1818
|
-
}
|
|
1819
|
-
// We fils to send the message directly, instead fallback to floodsub
|
|
1820
|
-
const peers = to || this.peers;
|
|
1821
|
-
if (peers == null ||
|
|
1822
|
-
(Array.isArray(peers) && peers.length === 0) ||
|
|
1823
|
-
(peers instanceof Map && peers.size === 0)) {
|
|
1824
|
-
logger.trace("No peers to send to");
|
|
1825
|
-
return delivereyPromise;
|
|
1826
|
-
}
|
|
1827
|
-
let sentOnce = false;
|
|
1828
|
-
const promises = [];
|
|
1829
|
-
for (const stream of peers.values()) {
|
|
1830
|
-
const id = stream;
|
|
1831
|
-
// Dont sent back to the sender
|
|
1832
|
-
if (id.publicKey.equals(from)) {
|
|
1833
|
-
continue;
|
|
1834
2111
|
}
|
|
1835
|
-
//
|
|
1836
|
-
|
|
1837
|
-
|
|
2112
|
+
// We fail to send the message directly, instead fallback to floodsub
|
|
2113
|
+
const peers = to || this.peers;
|
|
2114
|
+
if (peers == null ||
|
|
2115
|
+
(Array.isArray(peers) && peers.length === 0) ||
|
|
2116
|
+
(peers instanceof Map && peers.size === 0)) {
|
|
2117
|
+
logger.trace("No peers to send to");
|
|
2118
|
+
return delivereyPromise;
|
|
2119
|
+
}
|
|
2120
|
+
let sentOnce = false;
|
|
2121
|
+
const promises = [];
|
|
2122
|
+
for (const stream of peers.values()) {
|
|
2123
|
+
const id = stream;
|
|
2124
|
+
// Dont sent back to the sender
|
|
2125
|
+
if (id.publicKey.equals(from)) {
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
// Dont send message back to any of the signers (they have already seen the message)
|
|
2129
|
+
if (message.header.signatures?.publicKeys.find((x) => x.equals(id.publicKey))) {
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
sentOnce = true;
|
|
2133
|
+
promises.push(id.waitForWrite(bytes, message.header.priority));
|
|
2134
|
+
}
|
|
2135
|
+
await Promise.all(promises);
|
|
2136
|
+
if (!sentOnce) {
|
|
2137
|
+
// If the caller provided an explicit peer list, treat "no valid receivers" as an error
|
|
2138
|
+
// even when forwarding. This catches programming mistakes early and matches test expectations.
|
|
2139
|
+
if (!isRelayed || to != null) {
|
|
2140
|
+
throw new DeliveryError("Message did not have any valid receivers");
|
|
2141
|
+
}
|
|
1838
2142
|
}
|
|
1839
|
-
|
|
1840
|
-
promises.push(id.waitForWrite(bytes, message.header.priority));
|
|
2143
|
+
return delivereyPromise;
|
|
1841
2144
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2145
|
+
catch (error) {
|
|
2146
|
+
// If message fanout/write fails before publishMessage returns its delivery
|
|
2147
|
+
// promise, clear any ACK callback to avoid late timer rejections leaking as
|
|
2148
|
+
// unhandled rejections in fire-and-forget call paths.
|
|
2149
|
+
if (ackCallbackId) {
|
|
2150
|
+
this._ackCallbacks.get(ackCallbackId)?.clear();
|
|
1846
2151
|
}
|
|
2152
|
+
throw error;
|
|
1847
2153
|
}
|
|
1848
|
-
return delivereyPromise;
|
|
1849
2154
|
}
|
|
1850
2155
|
async maybeConnectDirectly(toHash, origin) {
|
|
1851
2156
|
if (this.peers.has(toHash) || this.prunedConnectionsCache?.has(toHash)) {
|
|
@@ -1926,10 +2231,12 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1926
2231
|
for (const h of admitted)
|
|
1927
2232
|
if (reached(h, target))
|
|
1928
2233
|
wins.add(h);
|
|
2234
|
+
// Preserve input order in the returned list (important for deterministic callers/tests).
|
|
2235
|
+
const orderedWins = () => admitted.filter((h) => wins.has(h));
|
|
1929
2236
|
if (settle === "any" && wins.size > 0)
|
|
1930
|
-
return
|
|
2237
|
+
return orderedWins();
|
|
1931
2238
|
if (settle === "all" && wins.size === admitted.length)
|
|
1932
|
-
return
|
|
2239
|
+
return orderedWins();
|
|
1933
2240
|
// Abort/timeout
|
|
1934
2241
|
const abortSignals = [this.closeController.signal];
|
|
1935
2242
|
if (signal) {
|
|
@@ -1949,7 +2256,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1949
2256
|
signals: abortSignals,
|
|
1950
2257
|
timeout,
|
|
1951
2258
|
});
|
|
1952
|
-
return
|
|
2259
|
+
return orderedWins();
|
|
1953
2260
|
}
|
|
1954
2261
|
catch (e) {
|
|
1955
2262
|
const abortSignal = abortSignals.find((s) => s.aborted);
|
|
@@ -1959,112 +2266,23 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1959
2266
|
}
|
|
1960
2267
|
throw new AbortError("Aborted waiting for peers: " + abortSignal.reason);
|
|
1961
2268
|
}
|
|
1962
|
-
if (e instanceof
|
|
1963
|
-
|
|
2269
|
+
if (e instanceof TimeoutError) {
|
|
2270
|
+
if (settle === "any") {
|
|
2271
|
+
if (wins.size > 0)
|
|
2272
|
+
return orderedWins();
|
|
2273
|
+
throw new TimeoutError(`Timeout waiting for peers (target=${target}, seek=${seek}, missing=${admitted.length}/${admitted.length})`);
|
|
2274
|
+
}
|
|
2275
|
+
const missing = admitted.filter((h) => !wins.has(h));
|
|
2276
|
+
const preview = missing.slice(0, 5).join(", ");
|
|
2277
|
+
throw new TimeoutError(`Timeout waiting for peers (target=${target}, seek=${seek}, missing=${missing.length}/${admitted.length}${preview ? `, e.g. ${preview}` : ""})`);
|
|
1964
2278
|
}
|
|
2279
|
+
if (e instanceof Error)
|
|
2280
|
+
throw e;
|
|
1965
2281
|
if (settle === "all")
|
|
1966
2282
|
throw new TimeoutError("Timeout waiting for peers");
|
|
1967
|
-
return
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
/* async waitFor(
|
|
1971
|
-
peer: PeerId | PublicSignKey | string,
|
|
1972
|
-
options?: {
|
|
1973
|
-
timeout?: number;
|
|
1974
|
-
signal?: AbortSignal;
|
|
1975
|
-
neighbour?: boolean;
|
|
1976
|
-
inflight?: boolean;
|
|
1977
|
-
},
|
|
1978
|
-
) {
|
|
1979
|
-
const hash =
|
|
1980
|
-
typeof peer === "string"
|
|
1981
|
-
? peer
|
|
1982
|
-
: (peer instanceof PublicSignKey
|
|
1983
|
-
? peer
|
|
1984
|
-
: getPublicKeyFromPeerId(peer)
|
|
1985
|
-
).hashcode();
|
|
1986
|
-
if (hash === this.publicKeyHash) {
|
|
1987
|
-
return; // TODO throw error instead?
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
if (options?.inflight) {
|
|
1991
|
-
// if peer is not in active connections or dialQueue, return silenty
|
|
1992
|
-
if (
|
|
1993
|
-
!this.peers.has(hash) &&
|
|
1994
|
-
!this.components.connectionManager
|
|
1995
|
-
.getDialQueue()
|
|
1996
|
-
.some((x) => getPublicKeyFromPeerId(x.peerId).hashcode() === hash) &&
|
|
1997
|
-
!this.components.connectionManager
|
|
1998
|
-
.getConnections()
|
|
1999
|
-
.some((x) => getPublicKeyFromPeerId(x.remotePeer).hashcode() === hash)
|
|
2000
|
-
) {
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
const checkIsReachable = (deferred: DeferredPromise<void>) => {
|
|
2006
|
-
if (options?.neighbour && !this.peers.has(hash)) {
|
|
2007
|
-
return;
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
if (!this.routes.isReachable(this.publicKeyHash, hash, 0)) {
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
deferred.resolve();
|
|
2015
|
-
};
|
|
2016
|
-
const abortSignals = [this.closeController.signal];
|
|
2017
|
-
if (options?.signal) {
|
|
2018
|
-
abortSignals.push(options.signal);
|
|
2283
|
+
return orderedWins(); // settle:any: return whatever successes we got
|
|
2019
2284
|
}
|
|
2020
|
-
|
|
2021
|
-
try {
|
|
2022
|
-
await waitForEvent(this, ["peer:reachable"], checkIsReachable, {
|
|
2023
|
-
signals: abortSignals,
|
|
2024
|
-
timeout: options?.timeout,
|
|
2025
|
-
});
|
|
2026
|
-
} catch (error) {
|
|
2027
|
-
throw new Error(
|
|
2028
|
-
"Stream to " +
|
|
2029
|
-
hash +
|
|
2030
|
-
" from " +
|
|
2031
|
-
this.publicKeyHash +
|
|
2032
|
-
" does not exist. Connection exist: " +
|
|
2033
|
-
this.peers.has(hash) +
|
|
2034
|
-
". Route exist: " +
|
|
2035
|
-
this.routes.isReachable(this.publicKeyHash, hash, 0),
|
|
2036
|
-
);
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
if (options?.neighbour) {
|
|
2040
|
-
const stream = this.peers.get(hash)!;
|
|
2041
|
-
try {
|
|
2042
|
-
let checkIsWritable = (pDefer: DeferredPromise<void>) => {
|
|
2043
|
-
if (stream.isReadable && stream.isWritable) {
|
|
2044
|
-
pDefer.resolve();
|
|
2045
|
-
}
|
|
2046
|
-
};
|
|
2047
|
-
await waitForEvent(
|
|
2048
|
-
stream,
|
|
2049
|
-
["stream:outbound", "stream:inbound"],
|
|
2050
|
-
checkIsWritable,
|
|
2051
|
-
{
|
|
2052
|
-
signals: abortSignals,
|
|
2053
|
-
timeout: options?.timeout,
|
|
2054
|
-
},
|
|
2055
|
-
);
|
|
2056
|
-
} catch (error) {
|
|
2057
|
-
throw new Error(
|
|
2058
|
-
"Stream to " +
|
|
2059
|
-
stream.publicKey.hashcode() +
|
|
2060
|
-
" not ready. Readable: " +
|
|
2061
|
-
stream.isReadable +
|
|
2062
|
-
". Writable " +
|
|
2063
|
-
stream.isWritable,
|
|
2064
|
-
);
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
} */
|
|
2285
|
+
}
|
|
2068
2286
|
getPublicKey(hash) {
|
|
2069
2287
|
return this.peerKeyHashToPublicKey.get(hash);
|
|
2070
2288
|
}
|
|
@@ -2073,6 +2291,10 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
2073
2291
|
}
|
|
2074
2292
|
// make this into a job? run every few ms
|
|
2075
2293
|
maybePruneConnections() {
|
|
2294
|
+
// Hard cap on peer streams: treat as a primary pruning signal.
|
|
2295
|
+
if (this.peers.size > this.connectionManagerOptions.maxConnections) {
|
|
2296
|
+
return this.pruneConnectionsToLimits();
|
|
2297
|
+
}
|
|
2076
2298
|
if (this.connectionManagerOptions.pruner) {
|
|
2077
2299
|
if (this.connectionManagerOptions.pruner.bandwidth != null) {
|
|
2078
2300
|
let usedBandwidth = 0;
|
|
@@ -2108,7 +2330,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
2108
2330
|
return;
|
|
2109
2331
|
}
|
|
2110
2332
|
const stream = this.peers.get(prunables[0]);
|
|
2111
|
-
this.prunedConnectionsCache
|
|
2333
|
+
this.prunedConnectionsCache?.add(stream.publicKey.hashcode());
|
|
2112
2334
|
await this.onPeerDisconnected(stream.peerId);
|
|
2113
2335
|
return this.components.connectionManager.closeConnections(stream.peerId);
|
|
2114
2336
|
}
|