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