@peerbit/stream 5.2.1 → 5.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +16 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +209 -60
- package/dist/src/index.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +227 -85
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MuxerClosedError, StreamResetError, TypedEventEmitter, UnsupportedProtocolError, } from "@libp2p/interface";
|
|
1
|
+
import { ConnectionClosedError, MuxerClosedError, StreamResetError, TypedEventEmitter, UnsupportedProtocolError, } from "@libp2p/interface";
|
|
2
2
|
import { multiaddr } from "@multiformats/multiaddr";
|
|
3
3
|
import { Circuit } from "@multiformats/multiaddr-matcher";
|
|
4
4
|
import { Cache } from "@peerbit/cache";
|
|
@@ -111,6 +111,7 @@ const DEFAULT_OUTBOUND_QUEUE_RESERVED_PRIORITY_BYTES = 1024 * 1024;
|
|
|
111
111
|
const DEFAULT_PRUNE_CONNECTIONS_INTERVAL = 2e4;
|
|
112
112
|
const DEFAULT_MIN_CONNECTIONS = 2;
|
|
113
113
|
const DEFAULT_MAX_CONNECTIONS = 300;
|
|
114
|
+
const MAX_RETIRED_PEER_STREAMS = 256;
|
|
114
115
|
const DEFAULT_PRUNED_CONNNECTIONS_TIMEOUT = 30 * 1000;
|
|
115
116
|
const DEFAULT_CREATE_OUTBOUND_STREAM_TIMEOUT = 30_000;
|
|
116
117
|
const PRIORITY_LANES = 4;
|
|
@@ -184,6 +185,11 @@ export class PeerStreams extends TypedEventEmitter {
|
|
|
184
185
|
static MAX_INBOUND_STREAMS = 8; // sensible default to prevent flood
|
|
185
186
|
outboundAbortController;
|
|
186
187
|
closed;
|
|
188
|
+
closePromise;
|
|
189
|
+
/** Closing is irreversible, even while asynchronous cleanup is pending. */
|
|
190
|
+
get isClosed() {
|
|
191
|
+
return this.closed;
|
|
192
|
+
}
|
|
187
193
|
connId;
|
|
188
194
|
seekedOnce;
|
|
189
195
|
usedBandWidthTracker;
|
|
@@ -535,6 +541,7 @@ export class PeerStreams extends TypedEventEmitter {
|
|
|
535
541
|
* Attach a raw inbound stream and setup a read stream
|
|
536
542
|
*/
|
|
537
543
|
attachInboundStream(stream) {
|
|
544
|
+
this.assertOpenForAttachment(stream);
|
|
538
545
|
// Support multiple concurrent inbound streams with inactivity pruning.
|
|
539
546
|
// Enforce max inbound streams (drop least recently active)
|
|
540
547
|
if (this.inboundStreams.length >= PeerStreams.MAX_INBOUND_STREAMS) {
|
|
@@ -583,10 +590,12 @@ export class PeerStreams extends TypedEventEmitter {
|
|
|
583
590
|
return record;
|
|
584
591
|
}
|
|
585
592
|
_scheduleInboundPrune() {
|
|
586
|
-
if (this._inboundPruneTimer)
|
|
587
|
-
return;
|
|
593
|
+
if (this.closed || this._inboundPruneTimer)
|
|
594
|
+
return;
|
|
588
595
|
this._inboundPruneTimer = setTimeout(() => {
|
|
589
596
|
this._inboundPruneTimer = undefined;
|
|
597
|
+
if (this.closed)
|
|
598
|
+
return;
|
|
590
599
|
this._pruneInboundInactive();
|
|
591
600
|
if (this.inboundStreams.length > 1) {
|
|
592
601
|
// schedule again if still multiple
|
|
@@ -595,7 +604,7 @@ export class PeerStreams extends TypedEventEmitter {
|
|
|
595
604
|
}, PeerStreams.INBOUND_IDLE_MS);
|
|
596
605
|
}
|
|
597
606
|
_pruneInboundInactive() {
|
|
598
|
-
if (this.inboundStreams.length <= 1)
|
|
607
|
+
if (this.closed || this.inboundStreams.length <= 1)
|
|
599
608
|
return;
|
|
600
609
|
const now = Date.now();
|
|
601
610
|
// Keep at least one (the most recently active)
|
|
@@ -660,6 +669,7 @@ export class PeerStreams extends TypedEventEmitter {
|
|
|
660
669
|
* Attach a raw outbound stream and setup a write stream
|
|
661
670
|
*/
|
|
662
671
|
async attachOutboundStream(stream) {
|
|
672
|
+
this.assertOpenForAttachment(stream);
|
|
663
673
|
if (this.outboundStreams.some((candidate) => candidate.raw === stream)) {
|
|
664
674
|
return; // duplicate
|
|
665
675
|
}
|
|
@@ -670,6 +680,17 @@ export class PeerStreams extends TypedEventEmitter {
|
|
|
670
680
|
}
|
|
671
681
|
this._scheduleOutboundPrune(true);
|
|
672
682
|
}
|
|
683
|
+
assertOpenForAttachment(stream) {
|
|
684
|
+
if (!this.closed)
|
|
685
|
+
return;
|
|
686
|
+
const error = new AbortError("Closed");
|
|
687
|
+
try {
|
|
688
|
+
stream.abort?.(error);
|
|
689
|
+
}
|
|
690
|
+
catch { }
|
|
691
|
+
closeRawStreamBestEffort(stream);
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
673
694
|
pruneOutboundCandidates() {
|
|
674
695
|
try {
|
|
675
696
|
const candidates = this.outboundStreams;
|
|
@@ -756,15 +777,26 @@ export class PeerStreams extends TypedEventEmitter {
|
|
|
756
777
|
/**
|
|
757
778
|
* Closes the open connection to peer
|
|
758
779
|
*/
|
|
759
|
-
|
|
760
|
-
if (this.
|
|
761
|
-
return;
|
|
762
|
-
|
|
780
|
+
close() {
|
|
781
|
+
if (this.closePromise)
|
|
782
|
+
return this.closePromise;
|
|
783
|
+
const closing = pDefer();
|
|
784
|
+
// Publish the promise before callbacks can re-enter close().
|
|
785
|
+
this.closePromise = closing.promise;
|
|
763
786
|
this.closed = true;
|
|
787
|
+
// Cancel inbound maintenance before awaiting outbound shutdown.
|
|
788
|
+
if (this._inboundPruneTimer) {
|
|
789
|
+
clearTimeout(this._inboundPruneTimer);
|
|
790
|
+
this._inboundPruneTimer = undefined;
|
|
791
|
+
}
|
|
764
792
|
if (this._outboundPruneTimer) {
|
|
765
793
|
clearTimeout(this._outboundPruneTimer);
|
|
766
794
|
this._outboundPruneTimer = undefined;
|
|
767
795
|
}
|
|
796
|
+
void this.closeImpl().then(closing.resolve, closing.reject);
|
|
797
|
+
return this.closePromise;
|
|
798
|
+
}
|
|
799
|
+
async closeImpl() {
|
|
768
800
|
// End the outbound stream
|
|
769
801
|
if (this.outboundStreams.length) {
|
|
770
802
|
for (const c of this.outboundStreams) {
|
|
@@ -831,6 +863,8 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
831
863
|
* Map of peer streams
|
|
832
864
|
*/
|
|
833
865
|
peers;
|
|
866
|
+
// Replacements must not orphan teardown that beforeStop still needs to drain.
|
|
867
|
+
retiredPeerStreams = new Set();
|
|
834
868
|
peerKeyHashToPublicKey;
|
|
835
869
|
routes;
|
|
836
870
|
/**
|
|
@@ -856,6 +890,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
856
890
|
pruneToLimitsInFlight;
|
|
857
891
|
_startInFlight;
|
|
858
892
|
_stopInFlight;
|
|
893
|
+
_networkStopPromise;
|
|
859
894
|
routeMaxRetentionPeriod;
|
|
860
895
|
routeCacheMaxFromEntries;
|
|
861
896
|
routeCacheMaxTargetsPerFrom;
|
|
@@ -1053,13 +1088,14 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1053
1088
|
}
|
|
1054
1089
|
async start() {
|
|
1055
1090
|
// Do not queue a restart behind teardown; callers can retry after stop resolves.
|
|
1056
|
-
if (this._stopInFlight)
|
|
1091
|
+
if (this._stopInFlight || this.stopping)
|
|
1057
1092
|
return;
|
|
1058
1093
|
if (this.started)
|
|
1059
1094
|
return;
|
|
1060
1095
|
if (this._startInFlight)
|
|
1061
1096
|
return this._startInFlight;
|
|
1062
1097
|
this.stopping = false;
|
|
1098
|
+
this._networkStopPromise = undefined;
|
|
1063
1099
|
this._startInFlight = this._startImpl().finally(() => {
|
|
1064
1100
|
this._startInFlight = undefined;
|
|
1065
1101
|
});
|
|
@@ -1227,12 +1263,12 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1227
1263
|
}
|
|
1228
1264
|
if (this.connectionManagerOptions.pruner) {
|
|
1229
1265
|
const pruneConnectionsLoop = () => {
|
|
1230
|
-
if (!this.connectionManagerOptions.pruner) {
|
|
1266
|
+
if (this.stopping || !this.connectionManagerOptions.pruner) {
|
|
1231
1267
|
return;
|
|
1232
1268
|
}
|
|
1233
1269
|
this.pruneConnectionsTimeout = setTimeout(() => {
|
|
1234
1270
|
this.maybePruneConnections().finally(() => {
|
|
1235
|
-
if (!this.started) {
|
|
1271
|
+
if (this.stopping || !this.started) {
|
|
1236
1272
|
return;
|
|
1237
1273
|
}
|
|
1238
1274
|
pruneConnectionsLoop();
|
|
@@ -1242,14 +1278,26 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1242
1278
|
pruneConnectionsLoop();
|
|
1243
1279
|
}
|
|
1244
1280
|
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Finish network teardown while libp2p still owns open transport connections.
|
|
1283
|
+
* Subclass resource cleanup remains in the normal stop phase.
|
|
1284
|
+
*/
|
|
1285
|
+
beforeStop() {
|
|
1286
|
+
if (this._networkStopPromise)
|
|
1287
|
+
return this._networkStopPromise;
|
|
1288
|
+
if (!this.started && !this._startInFlight)
|
|
1289
|
+
return Promise.resolve();
|
|
1290
|
+
return this.stopNetwork();
|
|
1291
|
+
}
|
|
1245
1292
|
/**
|
|
1246
1293
|
* Unregister the pubsub protocol and the streams with other peers will be closed.
|
|
1247
1294
|
*/
|
|
1248
1295
|
stop() {
|
|
1249
1296
|
if (this._stopInFlight)
|
|
1250
1297
|
return this._stopInFlight;
|
|
1251
|
-
if (!this.started && !this._startInFlight)
|
|
1298
|
+
if (!this.started && !this._startInFlight && !this._networkStopPromise) {
|
|
1252
1299
|
return Promise.resolve();
|
|
1300
|
+
}
|
|
1253
1301
|
this.stopping = true;
|
|
1254
1302
|
const starting = this._startInFlight;
|
|
1255
1303
|
this._stopInFlight = this._stopAfterStart(starting).finally(() => {
|
|
@@ -1270,9 +1318,12 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1270
1318
|
}
|
|
1271
1319
|
let stopFailed = false;
|
|
1272
1320
|
let stopFailure;
|
|
1273
|
-
if (this.started) {
|
|
1321
|
+
if (this.started || this._networkStopPromise) {
|
|
1274
1322
|
try {
|
|
1275
|
-
|
|
1323
|
+
if (this.started)
|
|
1324
|
+
await this._stopImpl();
|
|
1325
|
+
else
|
|
1326
|
+
await this._networkStopPromise;
|
|
1276
1327
|
}
|
|
1277
1328
|
catch (error) {
|
|
1278
1329
|
stopFailed = true;
|
|
@@ -1287,9 +1338,22 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1287
1338
|
if (startFailed)
|
|
1288
1339
|
throw startFailure;
|
|
1289
1340
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1341
|
+
stopNetwork() {
|
|
1342
|
+
if (this._networkStopPromise)
|
|
1343
|
+
return this._networkStopPromise;
|
|
1344
|
+
this.stopping = true;
|
|
1345
|
+
this._networkStopPromise = this.stopNetworkAfterStart(this._startInFlight).finally(() => {
|
|
1346
|
+
// Startup can fail before marking the service started. In that case no
|
|
1347
|
+
// normal stop is required to make a subsequent start possible.
|
|
1348
|
+
if (!this.started && !this._stopInFlight)
|
|
1349
|
+
this.stopping = false;
|
|
1350
|
+
});
|
|
1351
|
+
return this._networkStopPromise;
|
|
1352
|
+
}
|
|
1353
|
+
async stopNetworkAfterStart(starting) {
|
|
1354
|
+
// A failed startup may still have installed handlers or opened streams.
|
|
1355
|
+
// Its caller observes the startup error; teardown must still drain them.
|
|
1356
|
+
await starting?.catch(() => { });
|
|
1293
1357
|
clearTimeout(this.pruneConnectionsTimeout);
|
|
1294
1358
|
try {
|
|
1295
1359
|
if (this._peerConnectListener) {
|
|
@@ -1309,18 +1373,27 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1309
1373
|
if (this._registrarTopologyIds != null) {
|
|
1310
1374
|
await Promise.all(this._registrarTopologyIds.map((id) => this.components.registrar.unregister(id)));
|
|
1311
1375
|
}
|
|
1312
|
-
|
|
1376
|
+
this.outboundInflightQueue?.end();
|
|
1377
|
+
this.closeController?.abort();
|
|
1378
|
+
for (const timer of this.healthChecks.values())
|
|
1379
|
+
clearTimeout(timer);
|
|
1380
|
+
this.healthChecks.clear();
|
|
1381
|
+
// A stream open may settle after abort and still need to dispose its raw
|
|
1382
|
+
// stream. Keep that work ahead of connection-manager shutdown as well.
|
|
1383
|
+
await this._outboundPump;
|
|
1384
|
+
this._outboundPump = undefined;
|
|
1385
|
+
const closes = await Promise.allSettled([...this.peers.values(), ...this.retiredPeerStreams].map((peer) => peer.close()));
|
|
1386
|
+
const failures = closes.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
1387
|
+
if (failures.length) {
|
|
1388
|
+
throw new AggregateError(failures, "Peer stream teardown failed");
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
async _stopImpl() {
|
|
1392
|
+
const sharedState = this.sharedRoutingState;
|
|
1393
|
+
const sharedKey = this.sharedRoutingKey;
|
|
1313
1394
|
this.started = false;
|
|
1314
|
-
this.outboundInflightQueue.end();
|
|
1315
|
-
this.closeController.abort();
|
|
1316
1395
|
logger.trace("stopping");
|
|
1317
|
-
|
|
1318
|
-
await peerStreams.close();
|
|
1319
|
-
}
|
|
1320
|
-
for (const [_k, v] of this.healthChecks) {
|
|
1321
|
-
clearTimeout(v);
|
|
1322
|
-
}
|
|
1323
|
-
this.healthChecks.clear();
|
|
1396
|
+
await this.stopNetwork();
|
|
1324
1397
|
this.prunedConnectionsCache?.clear();
|
|
1325
1398
|
this.queue.clear();
|
|
1326
1399
|
this.peers.clear();
|
|
@@ -1369,7 +1442,8 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1369
1442
|
* On an inbound stream opened
|
|
1370
1443
|
*/
|
|
1371
1444
|
async _onIncomingStream(stream, connection) {
|
|
1372
|
-
if (!this.isStarted()) {
|
|
1445
|
+
if (this.stopping || !this.isStarted()) {
|
|
1446
|
+
closeRawStreamBestEffort(stream);
|
|
1373
1447
|
return;
|
|
1374
1448
|
}
|
|
1375
1449
|
const peerId = connection.remotePeer;
|
|
@@ -1379,14 +1453,25 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1379
1453
|
}
|
|
1380
1454
|
const publicKey = getPublicKeyFromPeerId(peerId);
|
|
1381
1455
|
if (this.prunedConnectionsCache?.has(publicKey.hashcode())) {
|
|
1382
|
-
|
|
1456
|
+
// Reject immediately: graceful transport shutdown can race other
|
|
1457
|
+
// incoming protocol negotiations that still need to reset their streams.
|
|
1458
|
+
connection.abort(new AbortError("Connection was pruned"));
|
|
1383
1459
|
await this.components.peerStore.delete(peerId);
|
|
1384
1460
|
return;
|
|
1385
1461
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1462
|
+
try {
|
|
1463
|
+
const peer = this.addPeer(peerId, publicKey, stream.protocol, connection.id);
|
|
1464
|
+
const inboundRecord = peer.attachInboundStream(stream);
|
|
1465
|
+
this.processMessages(peer.publicKey, inboundRecord, peer).catch(logError);
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
try {
|
|
1469
|
+
stream.abort(error);
|
|
1470
|
+
}
|
|
1471
|
+
catch { }
|
|
1472
|
+
closeRawStreamBestEffort(stream);
|
|
1473
|
+
throw error;
|
|
1474
|
+
}
|
|
1390
1475
|
// try to create outbound stream
|
|
1391
1476
|
await this.outboundInflightQueue.push({ peerId, connection });
|
|
1392
1477
|
}
|
|
@@ -1404,7 +1489,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1404
1489
|
const peerKey = getPublicKeyFromPeerId(peerId);
|
|
1405
1490
|
while (tries <= 3) {
|
|
1406
1491
|
tries++;
|
|
1407
|
-
if (!this.started) {
|
|
1492
|
+
if (this.stopping || !this.started) {
|
|
1408
1493
|
return;
|
|
1409
1494
|
}
|
|
1410
1495
|
try {
|
|
@@ -1428,7 +1513,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1428
1513
|
stream.abort(new Error("Stream was not multiplexed"));
|
|
1429
1514
|
return;
|
|
1430
1515
|
}
|
|
1431
|
-
if (!this.started) {
|
|
1516
|
+
if (this.stopping || !this.started) {
|
|
1432
1517
|
// we closed before we could create the stream
|
|
1433
1518
|
stream.abort(new Error("Closed"));
|
|
1434
1519
|
return;
|
|
@@ -1437,6 +1522,13 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1437
1522
|
await peer.attachOutboundStream(stream);
|
|
1438
1523
|
}
|
|
1439
1524
|
catch (error) {
|
|
1525
|
+
if (stream) {
|
|
1526
|
+
try {
|
|
1527
|
+
stream.abort(error);
|
|
1528
|
+
}
|
|
1529
|
+
catch { }
|
|
1530
|
+
closeRawStreamBestEffort(stream);
|
|
1531
|
+
}
|
|
1440
1532
|
if (error.code === "ERR_UNSUPPORTED_PROTOCOL") {
|
|
1441
1533
|
await delay(100);
|
|
1442
1534
|
continue; // Retry
|
|
@@ -1445,12 +1537,22 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1445
1537
|
await delay(100);
|
|
1446
1538
|
continue; // Retry
|
|
1447
1539
|
}
|
|
1448
|
-
if (connection.status !== "open"
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
error instanceof
|
|
1540
|
+
if (connection.status !== "open")
|
|
1541
|
+
return;
|
|
1542
|
+
if (error?.message === "Muxer already closed" ||
|
|
1543
|
+
error instanceof ConnectionClosedError ||
|
|
1452
1544
|
error instanceof MuxerClosedError) {
|
|
1453
|
-
|
|
1545
|
+
// A closed muxer cannot open any protocol, even if the transport
|
|
1546
|
+
// still reports open. Dispose it so a later dial can reconnect.
|
|
1547
|
+
connection.abort(error);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (error.code === "ERR_STREAM_RESET" ||
|
|
1551
|
+
error instanceof StreamResetError) {
|
|
1552
|
+
// A single stream reset need not invalidate a healthy connection.
|
|
1553
|
+
// Retry within the existing attempt/signal budget; a muxer-wide
|
|
1554
|
+
// failure then surfaces on the next open instead of staying cached.
|
|
1555
|
+
continue;
|
|
1454
1556
|
}
|
|
1455
1557
|
throw error;
|
|
1456
1558
|
}
|
|
@@ -1464,14 +1566,15 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1464
1566
|
* Registrar notifies an established connection with protocol
|
|
1465
1567
|
*/
|
|
1466
1568
|
async onPeerConnected(peerId, connection) {
|
|
1467
|
-
if (
|
|
1569
|
+
if (this.stopping ||
|
|
1570
|
+
!this.isStarted() ||
|
|
1468
1571
|
connection.limits ||
|
|
1469
1572
|
connection.status !== "open") {
|
|
1470
1573
|
return;
|
|
1471
1574
|
}
|
|
1472
1575
|
const peerKey = getPublicKeyFromPeerId(peerId);
|
|
1473
1576
|
if (this.prunedConnectionsCache?.has(peerKey.hashcode())) {
|
|
1474
|
-
|
|
1577
|
+
connection.abort(new AbortError("Connection was pruned"));
|
|
1475
1578
|
await this.components.peerStore.delete(peerId);
|
|
1476
1579
|
return; // we recently pruned this connect, dont allow it to connect for a while
|
|
1477
1580
|
}
|
|
@@ -1487,6 +1590,9 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1487
1590
|
// PeerId could be me, if so, it means that I am disconnecting
|
|
1488
1591
|
const peerKey = getPublicKeyFromPeerId(peerId);
|
|
1489
1592
|
const peerKeyHash = peerKey.hashcode();
|
|
1593
|
+
const currentPeer = this.peers.get(peerKeyHash);
|
|
1594
|
+
if (!currentPeer || (conn && conn.id !== currentPeer.connId))
|
|
1595
|
+
return;
|
|
1490
1596
|
const allConnections = this.components.connectionManager.getConnections?.() ?? [];
|
|
1491
1597
|
const connections = allConnections.filter((connection) => connection.remotePeer.toString() === peerId.toString());
|
|
1492
1598
|
if (connections.length > 0) {
|
|
@@ -1505,8 +1611,11 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1505
1611
|
return;
|
|
1506
1612
|
}
|
|
1507
1613
|
if (!this.publicKey.equals(peerKey)) {
|
|
1508
|
-
await this._removePeer(peerKey);
|
|
1509
|
-
if (
|
|
1614
|
+
const removed = await this._removePeer(peerKey);
|
|
1615
|
+
if (removed !== currentPeer ||
|
|
1616
|
+
this.peers.has(peerKeyHash) ||
|
|
1617
|
+
this.stopping ||
|
|
1618
|
+
!this.started) {
|
|
1510
1619
|
return;
|
|
1511
1620
|
}
|
|
1512
1621
|
// tell dependent peers that there is a node that might have left
|
|
@@ -1521,7 +1630,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1521
1630
|
mode: new SilentDelivery({ to: dependent, redundancy: 2 }),
|
|
1522
1631
|
}),
|
|
1523
1632
|
}).sign(this.sign);
|
|
1524
|
-
if (this.stopping || !this.started) {
|
|
1633
|
+
if (this.stopping || !this.started || this.peers.has(peerKeyHash)) {
|
|
1525
1634
|
return;
|
|
1526
1635
|
}
|
|
1527
1636
|
await this.publishMessageMaybe(this.publicKey, goodbye);
|
|
@@ -1598,14 +1707,25 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1598
1707
|
* Notifies the router that a peer has been connected
|
|
1599
1708
|
*/
|
|
1600
1709
|
addPeer(peerId, publicKey, protocol, connId) {
|
|
1710
|
+
if (this.stopping)
|
|
1711
|
+
throw new AbortError("Closed");
|
|
1601
1712
|
const publicKeyHash = publicKey.hashcode();
|
|
1602
1713
|
this.clearHealthcheckTimer(publicKeyHash);
|
|
1603
1714
|
const existing = this.peers.get(publicKeyHash);
|
|
1604
|
-
//
|
|
1605
|
-
if (existing != null) {
|
|
1715
|
+
// Reuse only a live object; close has already made attachments impossible.
|
|
1716
|
+
if (existing != null && !existing.isClosed) {
|
|
1606
1717
|
existing.connId = connId;
|
|
1607
1718
|
return existing;
|
|
1608
1719
|
}
|
|
1720
|
+
if (existing) {
|
|
1721
|
+
if (this.retiredPeerStreams.size >= MAX_RETIRED_PEER_STREAMS) {
|
|
1722
|
+
throw new AbortError("Too many pending peer stream closes");
|
|
1723
|
+
}
|
|
1724
|
+
this.retiredPeerStreams.add(existing);
|
|
1725
|
+
const forget = () => this.retiredPeerStreams.delete(existing);
|
|
1726
|
+
// A failed close remains owned so beforeStop cannot report a clean drain.
|
|
1727
|
+
void existing.close().then(forget, () => { });
|
|
1728
|
+
}
|
|
1609
1729
|
// else create a new peer streams
|
|
1610
1730
|
const peerIdStr = peerId.toString();
|
|
1611
1731
|
logger.trace("new peer" + peerIdStr);
|
|
@@ -1620,24 +1740,37 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1620
1740
|
: undefined,
|
|
1621
1741
|
});
|
|
1622
1742
|
this.peers.set(publicKeyHash, peerStreams);
|
|
1623
|
-
|
|
1743
|
+
// Object replacement is not evidence of a new authenticated peer session.
|
|
1744
|
+
if (!existing)
|
|
1745
|
+
this.updateSession(publicKey, -1);
|
|
1624
1746
|
// Propagate per-peer stream readiness events to the parent emitter
|
|
1625
|
-
const
|
|
1626
|
-
const
|
|
1627
|
-
|
|
1747
|
+
const isCurrentPeer = () => this.peers.get(publicKeyHash) === peerStreams;
|
|
1748
|
+
const forwardOutbound = () => {
|
|
1749
|
+
if (isCurrentPeer())
|
|
1750
|
+
this.dispatchEvent(new CustomEvent("stream:outbound"));
|
|
1751
|
+
};
|
|
1752
|
+
const forwardInbound = () => {
|
|
1753
|
+
if (isCurrentPeer())
|
|
1754
|
+
this.dispatchEvent(new CustomEvent("stream:inbound"));
|
|
1755
|
+
};
|
|
1756
|
+
const forwardQueue = () => {
|
|
1757
|
+
if (isCurrentPeer())
|
|
1758
|
+
this.notifyTotalOutboundQueueWaiters();
|
|
1759
|
+
};
|
|
1628
1760
|
peerStreams.addEventListener("stream:outbound", forwardOutbound);
|
|
1629
1761
|
peerStreams.addEventListener("stream:inbound", forwardInbound);
|
|
1630
1762
|
peerStreams.addEventListener("queue:outbound", forwardQueue);
|
|
1631
|
-
peerStreams.addEventListener("close", () =>
|
|
1632
|
-
|
|
1633
|
-
|
|
1763
|
+
peerStreams.addEventListener("close", () => {
|
|
1764
|
+
if (isCurrentPeer())
|
|
1765
|
+
void this._removePeer(publicKey).catch(logError);
|
|
1766
|
+
}, { once: true });
|
|
1634
1767
|
peerStreams.addEventListener("close", () => {
|
|
1635
1768
|
peerStreams.removeEventListener("stream:outbound", forwardOutbound);
|
|
1636
1769
|
peerStreams.removeEventListener("stream:inbound", forwardInbound);
|
|
1637
1770
|
peerStreams.removeEventListener("queue:outbound", forwardQueue);
|
|
1638
1771
|
this.notifyTotalOutboundQueueWaiters();
|
|
1639
1772
|
}, { once: true });
|
|
1640
|
-
this.addRouteConnection(this.publicKeyHash, publicKey.hashcode(), publicKey, -1, +new Date(), -1);
|
|
1773
|
+
this.addRouteConnection(this.publicKeyHash, publicKey.hashcode(), publicKey, -1, +new Date(), existing ? this.routes.getSession(publicKeyHash) ?? -1 : -1);
|
|
1641
1774
|
// Enforce connection manager limits eagerly when new peers are added. Without this,
|
|
1642
1775
|
// join storms can create large temporary peer sets and OOM in single-process sims.
|
|
1643
1776
|
if (this.peers.size > this.connectionManagerOptions.maxConnections) {
|
|
@@ -1657,6 +1790,8 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1657
1790
|
}
|
|
1658
1791
|
// close peer streams
|
|
1659
1792
|
await peerStreams.close();
|
|
1793
|
+
if (this.peers.get(hash) !== peerStreams)
|
|
1794
|
+
return;
|
|
1660
1795
|
// delete peer streams
|
|
1661
1796
|
logger.trace("delete peer" + publicKey.toString());
|
|
1662
1797
|
this.peers.delete(hash);
|
|
@@ -1670,6 +1805,8 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1670
1805
|
let failed = false;
|
|
1671
1806
|
try {
|
|
1672
1807
|
for await (const data of record.iterable) {
|
|
1808
|
+
if (this.peers.get(peerId.hashcode()) !== peerStreams)
|
|
1809
|
+
break;
|
|
1673
1810
|
const now = Date.now();
|
|
1674
1811
|
record.lastActivity = now;
|
|
1675
1812
|
record.bytesReceived += data.length || data.byteLength || 0;
|
|
@@ -1691,11 +1828,16 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1691
1828
|
". " +
|
|
1692
1829
|
err?.message);
|
|
1693
1830
|
}
|
|
1694
|
-
this.
|
|
1831
|
+
if (this.peers.get(peerId.hashcode()) === peerStreams) {
|
|
1832
|
+
void this.onPeerDisconnected(peerStreams.peerId).catch(logError);
|
|
1833
|
+
}
|
|
1695
1834
|
}
|
|
1696
1835
|
finally {
|
|
1697
1836
|
const removed = peerStreams.detachInboundStream(record, new AbortError("Inbound stream reader ended"), { closeRaw: failed });
|
|
1698
|
-
if (removed &&
|
|
1837
|
+
if (removed &&
|
|
1838
|
+
!failed &&
|
|
1839
|
+
!peerStreams.isReadable &&
|
|
1840
|
+
this.peers.get(peerId.hashcode()) === peerStreams) {
|
|
1699
1841
|
void this.onPeerDisconnected(peerStreams.peerId).catch(logError);
|
|
1700
1842
|
}
|
|
1701
1843
|
}
|
|
@@ -1869,7 +2011,7 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1869
2011
|
* Handles a message from a peer
|
|
1870
2012
|
*/
|
|
1871
2013
|
async processMessage(from, peerStream, msg, decodedMessage) {
|
|
1872
|
-
if (!this.started) {
|
|
2014
|
+
if (this.stopping || !this.started) {
|
|
1873
2015
|
return;
|
|
1874
2016
|
}
|
|
1875
2017
|
// Ensure the message is valid before processing it
|
|
@@ -1980,12 +2122,15 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
1980
2122
|
this.relayMessage(from, message);
|
|
1981
2123
|
}
|
|
1982
2124
|
}
|
|
1983
|
-
async verifyAndProcess(message) {
|
|
2125
|
+
async verifyAndProcess(message, canProcess) {
|
|
2126
|
+
if (canProcess?.() === false)
|
|
2127
|
+
return false;
|
|
1984
2128
|
if (message._verified == null) {
|
|
1985
2129
|
this.wireCounters.tsSignatureVerifies++;
|
|
1986
2130
|
}
|
|
1987
2131
|
const verified = await message.verify(true);
|
|
1988
|
-
|
|
2132
|
+
// Async verification must not let an obsolete ingress owner update sessions.
|
|
2133
|
+
if (!verified || canProcess?.() === false) {
|
|
1989
2134
|
return false;
|
|
1990
2135
|
}
|
|
1991
2136
|
const from = message.header.signatures.publicKeys[0];
|
|
@@ -2892,6 +3037,8 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
2892
3037
|
return Promise.resolve();
|
|
2893
3038
|
}
|
|
2894
3039
|
async pruneConnections() {
|
|
3040
|
+
if (this.stopping || !this.started)
|
|
3041
|
+
return;
|
|
2895
3042
|
// TODO sort by bandwidth
|
|
2896
3043
|
if (this.peers.size <= this.connectionManagerOptions.minConnections) {
|
|
2897
3044
|
return;
|
|
@@ -2906,6 +3053,8 @@ export class DirectStream extends TypedEventEmitter {
|
|
|
2906
3053
|
const stream = this.peers.get(prunables[0]);
|
|
2907
3054
|
this.prunedConnectionsCache?.add(stream.publicKey.hashcode());
|
|
2908
3055
|
await this.onPeerDisconnected(stream.peerId);
|
|
3056
|
+
if (this.stopping || !this.started)
|
|
3057
|
+
return;
|
|
2909
3058
|
return this.components.connectionManager.closeConnections(stream.peerId);
|
|
2910
3059
|
}
|
|
2911
3060
|
getTotalQueueAdmissionLimitBytes(priority) {
|