@peerbit/stream 5.2.1 → 5.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -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; // already scheduled
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
- async close() {
760
- if (this.closed) {
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
- await this._stopImpl();
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
- async _stopImpl() {
1291
- const sharedState = this.sharedRoutingState;
1292
- const sharedKey = this.sharedRoutingKey;
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
- // reset and clear up
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
- for (const peerStreams of this.peers.values()) {
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
- await connection.close();
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
- const peer = this.addPeer(peerId, publicKey, stream.protocol, connection.id);
1387
- // handle inbound
1388
- const inboundRecord = peer.attachInboundStream(stream);
1389
- this.processMessages(peer.publicKey, inboundRecord, peer).catch(logError);
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
- error?.message === "Muxer already closed" ||
1450
- error.code === "ERR_STREAM_RESET" ||
1451
- error instanceof StreamResetError ||
1540
+ if (connection.status !== "open")
1541
+ return;
1542
+ if (error?.message === "Muxer already closed" ||
1543
+ error instanceof ConnectionClosedError ||
1452
1544
  error instanceof MuxerClosedError) {
1453
- return; // fail silenty
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 (!this.isStarted() ||
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
- await connection.close();
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 (this.stopping || !this.started) {
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
- // If peer streams already exists, do nothing
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
- this.updateSession(publicKey, -1);
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 forwardOutbound = () => this.dispatchEvent(new CustomEvent("stream:outbound"));
1626
- const forwardInbound = () => this.dispatchEvent(new CustomEvent("stream:inbound"));
1627
- const forwardQueue = () => this.notifyTotalOutboundQueueWaiters();
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", () => this._removePeer(publicKey), {
1632
- once: true,
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.onPeerDisconnected(peerStreams.peerId);
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 && !failed && !peerStreams.isReadable) {
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
@@ -2892,6 +3034,8 @@ export class DirectStream extends TypedEventEmitter {
2892
3034
  return Promise.resolve();
2893
3035
  }
2894
3036
  async pruneConnections() {
3037
+ if (this.stopping || !this.started)
3038
+ return;
2895
3039
  // TODO sort by bandwidth
2896
3040
  if (this.peers.size <= this.connectionManagerOptions.minConnections) {
2897
3041
  return;
@@ -2906,6 +3050,8 @@ export class DirectStream extends TypedEventEmitter {
2906
3050
  const stream = this.peers.get(prunables[0]);
2907
3051
  this.prunedConnectionsCache?.add(stream.publicKey.hashcode());
2908
3052
  await this.onPeerDisconnected(stream.peerId);
3053
+ if (this.stopping || !this.started)
3054
+ return;
2909
3055
  return this.components.connectionManager.closeConnections(stream.peerId);
2910
3056
  }
2911
3057
  getTotalQueueAdmissionLimitBytes(priority) {