@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/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ConnectionClosedError,
2
3
  MuxerClosedError,
3
4
  StreamResetError,
4
5
  TypedEventEmitter,
@@ -268,6 +269,7 @@ const DEFAULT_OUTBOUND_QUEUE_RESERVED_PRIORITY_BYTES = 1024 * 1024;
268
269
  const DEFAULT_PRUNE_CONNECTIONS_INTERVAL = 2e4;
269
270
  const DEFAULT_MIN_CONNECTIONS = 2;
270
271
  const DEFAULT_MAX_CONNECTIONS = 300;
272
+ const MAX_RETIRED_PEER_STREAMS = 256;
271
273
 
272
274
  const DEFAULT_PRUNED_CONNNECTIONS_TIMEOUT = 30 * 1000;
273
275
 
@@ -387,6 +389,11 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
387
389
  private outboundAbortController: AbortController;
388
390
 
389
391
  private closed: boolean;
392
+ private closePromise?: Promise<void>;
393
+ /** Closing is irreversible, even while asynchronous cleanup is pending. */
394
+ public get isClosed(): boolean {
395
+ return this.closed;
396
+ }
390
397
 
391
398
  public connId: string;
392
399
 
@@ -800,6 +807,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
800
807
  * Attach a raw inbound stream and setup a read stream
801
808
  */
802
809
  attachInboundStream(stream: Stream): InboundStreamRecord {
810
+ this.assertOpenForAttachment(stream);
803
811
  // Support multiple concurrent inbound streams with inactivity pruning.
804
812
  // Enforce max inbound streams (drop least recently active)
805
813
  if (this.inboundStreams.length >= PeerStreams.MAX_INBOUND_STREAMS) {
@@ -852,9 +860,10 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
852
860
  }
853
861
 
854
862
  private _scheduleInboundPrune() {
855
- if (this._inboundPruneTimer) return; // already scheduled
863
+ if (this.closed || this._inboundPruneTimer) return;
856
864
  this._inboundPruneTimer = setTimeout(() => {
857
865
  this._inboundPruneTimer = undefined;
866
+ if (this.closed) return;
858
867
  this._pruneInboundInactive();
859
868
  if (this.inboundStreams.length > 1) {
860
869
  // schedule again if still multiple
@@ -864,7 +873,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
864
873
  }
865
874
 
866
875
  private _pruneInboundInactive() {
867
- if (this.inboundStreams.length <= 1) return;
876
+ if (this.closed || this.inboundStreams.length <= 1) return;
868
877
  const now = Date.now();
869
878
  // Keep at least one (the most recently active)
870
879
  this.inboundStreams.sort((a, b) => b.lastActivity - a.lastActivity);
@@ -932,6 +941,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
932
941
  */
933
942
 
934
943
  async attachOutboundStream(stream: Stream) {
944
+ this.assertOpenForAttachment(stream);
935
945
  if (this.outboundStreams.some((candidate) => candidate.raw === stream)) {
936
946
  return; // duplicate
937
947
  }
@@ -943,6 +953,16 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
943
953
  this._scheduleOutboundPrune(true);
944
954
  }
945
955
 
956
+ private assertOpenForAttachment(stream: Stream) {
957
+ if (!this.closed) return;
958
+ const error = new AbortError("Closed");
959
+ try {
960
+ stream.abort?.(error);
961
+ } catch {}
962
+ closeRawStreamBestEffort(stream);
963
+ throw error;
964
+ }
965
+
946
966
  private pruneOutboundCandidates() {
947
967
  try {
948
968
  const candidates = this.outboundStreams;
@@ -1036,17 +1056,27 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
1036
1056
  /**
1037
1057
  * Closes the open connection to peer
1038
1058
  */
1039
- async close() {
1040
- if (this.closed) {
1041
- return;
1042
- }
1059
+ close(): Promise<void> {
1060
+ if (this.closePromise) return this.closePromise;
1061
+ const closing = pDefer<void>();
1062
+ // Publish the promise before callbacks can re-enter close().
1063
+ this.closePromise = closing.promise;
1043
1064
 
1044
1065
  this.closed = true;
1066
+ // Cancel inbound maintenance before awaiting outbound shutdown.
1067
+ if (this._inboundPruneTimer) {
1068
+ clearTimeout(this._inboundPruneTimer);
1069
+ this._inboundPruneTimer = undefined;
1070
+ }
1045
1071
  if (this._outboundPruneTimer) {
1046
1072
  clearTimeout(this._outboundPruneTimer);
1047
1073
  this._outboundPruneTimer = undefined;
1048
1074
  }
1075
+ void this.closeImpl().then(closing.resolve, closing.reject);
1076
+ return this.closePromise;
1077
+ }
1049
1078
 
1079
+ private async closeImpl(): Promise<void> {
1050
1080
  // End the outbound stream
1051
1081
  if (this.outboundStreams.length) {
1052
1082
  for (const c of this.outboundStreams) {
@@ -1060,29 +1090,29 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
1060
1090
  this.outboundAbortController.abort();
1061
1091
  }
1062
1092
 
1063
- // End inbound streams
1064
- if (this.inboundStreams.length) {
1065
- for (const inbound of this.inboundStreams) {
1093
+ // End inbound streams
1094
+ if (this.inboundStreams.length) {
1095
+ for (const inbound of this.inboundStreams) {
1096
+ try {
1097
+ inbound.abortController.abort();
1098
+ } catch {
1099
+ logger.error("Failed to abort inbound stream");
1100
+ }
1101
+ try {
1102
+ // Best-effort shutdown: on some transports (notably websockets),
1103
+ // awaiting a graceful close can hang indefinitely if the remote is
1104
+ // concurrently stopping. Abort immediately and do not await close.
1066
1105
  try {
1067
- inbound.abortController.abort();
1106
+ inbound.raw.abort?.(new AbortError("Closed"));
1068
1107
  } catch {
1069
- logger.error("Failed to abort inbound stream");
1070
- }
1071
- try {
1072
- // Best-effort shutdown: on some transports (notably websockets),
1073
- // awaiting a graceful close can hang indefinitely if the remote is
1074
- // concurrently stopping. Abort immediately and do not await close.
1075
- try {
1076
- inbound.raw.abort?.(new AbortError("Closed"));
1077
- } catch {
1078
- // ignore
1079
- }
1080
- closeRawStreamBestEffort(inbound.raw);
1081
- } catch {
1082
- logger.error("Failed to close inbound stream");
1108
+ // ignore
1083
1109
  }
1110
+ closeRawStreamBestEffort(inbound.raw);
1111
+ } catch {
1112
+ logger.error("Failed to close inbound stream");
1084
1113
  }
1085
1114
  }
1115
+ }
1086
1116
 
1087
1117
  this.usedBandWidthTracker.stop();
1088
1118
 
@@ -1251,6 +1281,8 @@ export abstract class DirectStream<
1251
1281
  * Map of peer streams
1252
1282
  */
1253
1283
  public peers: Map<string, PeerStreams>;
1284
+ // Replacements must not orphan teardown that beforeStop still needs to drain.
1285
+ private readonly retiredPeerStreams = new Set<PeerStreams>();
1254
1286
  public peerKeyHashToPublicKey: Map<string, PublicSignKey>;
1255
1287
  public routes: RoutesLike;
1256
1288
  /**
@@ -1277,6 +1309,7 @@ export abstract class DirectStream<
1277
1309
  private pruneToLimitsInFlight?: Promise<void>;
1278
1310
  private _startInFlight?: Promise<void>;
1279
1311
  private _stopInFlight?: Promise<void>;
1312
+ private _networkStopPromise?: Promise<void>;
1280
1313
  private routeMaxRetentionPeriod: number;
1281
1314
  private routeCacheMaxFromEntries?: number;
1282
1315
  private routeCacheMaxTargetsPerFrom?: number;
@@ -1557,10 +1590,11 @@ export abstract class DirectStream<
1557
1590
 
1558
1591
  async start() {
1559
1592
  // Do not queue a restart behind teardown; callers can retry after stop resolves.
1560
- if (this._stopInFlight) return;
1593
+ if (this._stopInFlight || this.stopping) return;
1561
1594
  if (this.started) return;
1562
1595
  if (this._startInFlight) return this._startInFlight;
1563
1596
  this.stopping = false;
1597
+ this._networkStopPromise = undefined;
1564
1598
  this._startInFlight = this._startImpl().finally(() => {
1565
1599
  this._startInFlight = undefined;
1566
1600
  });
@@ -1774,12 +1808,12 @@ export abstract class DirectStream<
1774
1808
  }
1775
1809
  if (this.connectionManagerOptions.pruner) {
1776
1810
  const pruneConnectionsLoop = () => {
1777
- if (!this.connectionManagerOptions.pruner) {
1811
+ if (this.stopping || !this.connectionManagerOptions.pruner) {
1778
1812
  return;
1779
1813
  }
1780
1814
  this.pruneConnectionsTimeout = setTimeout(() => {
1781
1815
  this.maybePruneConnections().finally(() => {
1782
- if (!this.started) {
1816
+ if (this.stopping || !this.started) {
1783
1817
  return;
1784
1818
  }
1785
1819
  pruneConnectionsLoop();
@@ -1790,12 +1824,24 @@ export abstract class DirectStream<
1790
1824
  }
1791
1825
  }
1792
1826
 
1827
+ /**
1828
+ * Finish network teardown while libp2p still owns open transport connections.
1829
+ * Subclass resource cleanup remains in the normal stop phase.
1830
+ */
1831
+ beforeStop(): Promise<void> {
1832
+ if (this._networkStopPromise) return this._networkStopPromise;
1833
+ if (!this.started && !this._startInFlight) return Promise.resolve();
1834
+ return this.stopNetwork();
1835
+ }
1836
+
1793
1837
  /**
1794
1838
  * Unregister the pubsub protocol and the streams with other peers will be closed.
1795
1839
  */
1796
1840
  stop(): Promise<void> {
1797
1841
  if (this._stopInFlight) return this._stopInFlight;
1798
- if (!this.started && !this._startInFlight) return Promise.resolve();
1842
+ if (!this.started && !this._startInFlight && !this._networkStopPromise) {
1843
+ return Promise.resolve();
1844
+ }
1799
1845
  this.stopping = true;
1800
1846
  const starting = this._startInFlight;
1801
1847
  this._stopInFlight = this._stopAfterStart(starting).finally(() => {
@@ -1817,9 +1863,10 @@ export abstract class DirectStream<
1817
1863
 
1818
1864
  let stopFailed = false;
1819
1865
  let stopFailure: unknown;
1820
- if (this.started) {
1866
+ if (this.started || this._networkStopPromise) {
1821
1867
  try {
1822
- await this._stopImpl();
1868
+ if (this.started) await this._stopImpl();
1869
+ else await this._networkStopPromise;
1823
1870
  } catch (error) {
1824
1871
  stopFailed = true;
1825
1872
  stopFailure = error;
@@ -1836,9 +1883,23 @@ export abstract class DirectStream<
1836
1883
  if (startFailed) throw startFailure;
1837
1884
  }
1838
1885
 
1839
- private async _stopImpl(): Promise<void> {
1840
- const sharedState = this.sharedRoutingState;
1841
- const sharedKey = this.sharedRoutingKey;
1886
+ private stopNetwork(): Promise<void> {
1887
+ if (this._networkStopPromise) return this._networkStopPromise;
1888
+ this.stopping = true;
1889
+ this._networkStopPromise = this.stopNetworkAfterStart(
1890
+ this._startInFlight,
1891
+ ).finally(() => {
1892
+ // Startup can fail before marking the service started. In that case no
1893
+ // normal stop is required to make a subsequent start possible.
1894
+ if (!this.started && !this._stopInFlight) this.stopping = false;
1895
+ });
1896
+ return this._networkStopPromise;
1897
+ }
1898
+
1899
+ private async stopNetworkAfterStart(starting?: Promise<void>): Promise<void> {
1900
+ // A failed startup may still have installed handlers or opened streams.
1901
+ // Its caller observes the startup error; teardown must still drain them.
1902
+ await starting?.catch(() => {});
1842
1903
 
1843
1904
  clearTimeout(this.pruneConnectionsTimeout);
1844
1905
  try {
@@ -1873,20 +1934,34 @@ export abstract class DirectStream<
1873
1934
  );
1874
1935
  }
1875
1936
 
1876
- // reset and clear up
1877
- this.started = false;
1878
- this.outboundInflightQueue.end();
1879
- this.closeController.abort();
1880
-
1881
- logger.trace("stopping");
1882
- for (const peerStreams of this.peers.values()) {
1883
- await peerStreams.close();
1937
+ this.outboundInflightQueue?.end();
1938
+ this.closeController?.abort();
1939
+ for (const timer of this.healthChecks.values()) clearTimeout(timer);
1940
+ this.healthChecks.clear();
1941
+ // A stream open may settle after abort and still need to dispose its raw
1942
+ // stream. Keep that work ahead of connection-manager shutdown as well.
1943
+ await this._outboundPump;
1944
+ this._outboundPump = undefined;
1945
+
1946
+ const closes = await Promise.allSettled(
1947
+ [...this.peers.values(), ...this.retiredPeerStreams].map((peer) =>
1948
+ peer.close(),
1949
+ ),
1950
+ );
1951
+ const failures = closes.flatMap((result) =>
1952
+ result.status === "rejected" ? [result.reason] : [],
1953
+ );
1954
+ if (failures.length) {
1955
+ throw new AggregateError(failures, "Peer stream teardown failed");
1884
1956
  }
1957
+ }
1885
1958
 
1886
- for (const [_k, v] of this.healthChecks) {
1887
- clearTimeout(v);
1888
- }
1889
- this.healthChecks.clear();
1959
+ private async _stopImpl(): Promise<void> {
1960
+ const sharedState = this.sharedRoutingState;
1961
+ const sharedKey = this.sharedRoutingKey;
1962
+ this.started = false;
1963
+ logger.trace("stopping");
1964
+ await this.stopNetwork();
1890
1965
  this.prunedConnectionsCache?.clear();
1891
1966
 
1892
1967
  this.queue.clear();
@@ -1939,7 +2014,8 @@ export abstract class DirectStream<
1939
2014
  */
1940
2015
 
1941
2016
  protected async _onIncomingStream(stream: Stream, connection: Connection) {
1942
- if (!this.isStarted()) {
2017
+ if (this.stopping || !this.isStarted()) {
2018
+ closeRawStreamBestEffort(stream);
1943
2019
  return;
1944
2020
  }
1945
2021
  const peerId = connection.remotePeer;
@@ -1951,21 +2027,24 @@ export abstract class DirectStream<
1951
2027
  const publicKey = getPublicKeyFromPeerId(peerId);
1952
2028
 
1953
2029
  if (this.prunedConnectionsCache?.has(publicKey.hashcode())) {
1954
- await connection.close();
2030
+ // Reject immediately: graceful transport shutdown can race other
2031
+ // incoming protocol negotiations that still need to reset their streams.
2032
+ connection.abort(new AbortError("Connection was pruned"));
1955
2033
  await this.components.peerStore.delete(peerId);
1956
2034
  return;
1957
2035
  }
1958
2036
 
1959
- const peer = this.addPeer(
1960
- peerId,
1961
- publicKey,
1962
- stream.protocol,
1963
- connection.id,
1964
- );
1965
-
1966
- // handle inbound
1967
- const inboundRecord = peer.attachInboundStream(stream);
1968
- this.processMessages(peer.publicKey, inboundRecord, peer).catch(logError);
2037
+ try {
2038
+ const peer = this.addPeer(peerId, publicKey, stream.protocol, connection.id);
2039
+ const inboundRecord = peer.attachInboundStream(stream);
2040
+ this.processMessages(peer.publicKey, inboundRecord, peer).catch(logError);
2041
+ } catch (error) {
2042
+ try {
2043
+ stream.abort(error as Error);
2044
+ } catch {}
2045
+ closeRawStreamBestEffort(stream);
2046
+ throw error;
2047
+ }
1969
2048
 
1970
2049
  // try to create outbound stream
1971
2050
  await this.outboundInflightQueue.push({ peerId, connection });
@@ -1992,7 +2071,7 @@ export abstract class DirectStream<
1992
2071
  const peerKey = getPublicKeyFromPeerId(peerId);
1993
2072
  while (tries <= 3) {
1994
2073
  tries++;
1995
- if (!this.started) {
2074
+ if (this.stopping || !this.started) {
1996
2075
  return;
1997
2076
  }
1998
2077
 
@@ -2018,7 +2097,7 @@ export abstract class DirectStream<
2018
2097
  return;
2019
2098
  }
2020
2099
 
2021
- if (!this.started) {
2100
+ if (this.stopping || !this.started) {
2022
2101
  // we closed before we could create the stream
2023
2102
  stream.abort(new Error("Closed"));
2024
2103
  return;
@@ -2026,6 +2105,12 @@ export abstract class DirectStream<
2026
2105
  peer = this.addPeer(peerId, peerKey, stream.protocol!, connection.id); // TODO types
2027
2106
  await peer.attachOutboundStream(stream);
2028
2107
  } catch (error: any) {
2108
+ if (stream) {
2109
+ try {
2110
+ stream.abort(error);
2111
+ } catch {}
2112
+ closeRawStreamBestEffort(stream);
2113
+ }
2029
2114
  if (error.code === "ERR_UNSUPPORTED_PROTOCOL") {
2030
2115
  await delay(100);
2031
2116
  continue; // Retry
@@ -2036,14 +2121,25 @@ export abstract class DirectStream<
2036
2121
  continue; // Retry
2037
2122
  }
2038
2123
 
2124
+ if (connection.status !== "open") return;
2039
2125
  if (
2040
- connection.status !== "open" ||
2041
2126
  error?.message === "Muxer already closed" ||
2042
- error.code === "ERR_STREAM_RESET" ||
2043
- error instanceof StreamResetError ||
2127
+ error instanceof ConnectionClosedError ||
2044
2128
  error instanceof MuxerClosedError
2045
2129
  ) {
2046
- return; // fail silenty
2130
+ // A closed muxer cannot open any protocol, even if the transport
2131
+ // still reports open. Dispose it so a later dial can reconnect.
2132
+ connection.abort(error);
2133
+ return;
2134
+ }
2135
+ if (
2136
+ error.code === "ERR_STREAM_RESET" ||
2137
+ error instanceof StreamResetError
2138
+ ) {
2139
+ // A single stream reset need not invalidate a healthy connection.
2140
+ // Retry within the existing attempt/signal budget; a muxer-wide
2141
+ // failure then surfaces on the next open instead of staying cached.
2142
+ continue;
2047
2143
  }
2048
2144
 
2049
2145
  throw error;
@@ -2060,6 +2156,7 @@ export abstract class DirectStream<
2060
2156
  */
2061
2157
  public async onPeerConnected(peerId: PeerId, connection: Connection) {
2062
2158
  if (
2159
+ this.stopping ||
2063
2160
  !this.isStarted() ||
2064
2161
  connection.limits ||
2065
2162
  connection.status !== "open"
@@ -2069,7 +2166,7 @@ export abstract class DirectStream<
2069
2166
  const peerKey = getPublicKeyFromPeerId(peerId);
2070
2167
 
2071
2168
  if (this.prunedConnectionsCache?.has(peerKey.hashcode())) {
2072
- await connection.close();
2169
+ connection.abort(new AbortError("Connection was pruned"));
2073
2170
  await this.components.peerStore.delete(peerId);
2074
2171
  return; // we recently pruned this connect, dont allow it to connect for a while
2075
2172
  }
@@ -2088,6 +2185,8 @@ export abstract class DirectStream<
2088
2185
  // PeerId could be me, if so, it means that I am disconnecting
2089
2186
  const peerKey = getPublicKeyFromPeerId(peerId);
2090
2187
  const peerKeyHash = peerKey.hashcode();
2188
+ const currentPeer = this.peers.get(peerKeyHash);
2189
+ if (!currentPeer || (conn && conn.id !== currentPeer.connId)) return;
2091
2190
  const allConnections =
2092
2191
  this.components.connectionManager.getConnections?.() ?? [];
2093
2192
  const connections = allConnections.filter(
@@ -2110,8 +2209,13 @@ export abstract class DirectStream<
2110
2209
  return;
2111
2210
  }
2112
2211
  if (!this.publicKey.equals(peerKey)) {
2113
- await this._removePeer(peerKey);
2114
- if (this.stopping || !this.started) {
2212
+ const removed = await this._removePeer(peerKey);
2213
+ if (
2214
+ removed !== currentPeer ||
2215
+ this.peers.has(peerKeyHash) ||
2216
+ this.stopping ||
2217
+ !this.started
2218
+ ) {
2115
2219
  return;
2116
2220
  }
2117
2221
 
@@ -2129,7 +2233,7 @@ export abstract class DirectStream<
2129
2233
  mode: new SilentDelivery({ to: dependent, redundancy: 2 }),
2130
2234
  }),
2131
2235
  }).sign(this.sign);
2132
- if (this.stopping || !this.started) {
2236
+ if (this.stopping || !this.started || this.peers.has(peerKeyHash)) {
2133
2237
  return;
2134
2238
  }
2135
2239
  await this.publishMessageMaybe(
@@ -2252,17 +2356,27 @@ export abstract class DirectStream<
2252
2356
  protocol: string,
2253
2357
  connId: string,
2254
2358
  ): PeerStreams {
2359
+ if (this.stopping) throw new AbortError("Closed");
2255
2360
  const publicKeyHash = publicKey.hashcode();
2256
2361
 
2257
2362
  this.clearHealthcheckTimer(publicKeyHash);
2258
2363
 
2259
2364
  const existing = this.peers.get(publicKeyHash);
2260
2365
 
2261
- // If peer streams already exists, do nothing
2262
- if (existing != null) {
2366
+ // Reuse only a live object; close has already made attachments impossible.
2367
+ if (existing != null && !existing.isClosed) {
2263
2368
  existing.connId = connId;
2264
2369
  return existing;
2265
2370
  }
2371
+ if (existing) {
2372
+ if (this.retiredPeerStreams.size >= MAX_RETIRED_PEER_STREAMS) {
2373
+ throw new AbortError("Too many pending peer stream closes");
2374
+ }
2375
+ this.retiredPeerStreams.add(existing);
2376
+ const forget = () => this.retiredPeerStreams.delete(existing);
2377
+ // A failed close remains owned so beforeStop cannot report a clean drain.
2378
+ void existing.close().then(forget, () => {});
2379
+ }
2266
2380
 
2267
2381
  // else create a new peer streams
2268
2382
  const peerIdStr = peerId.toString();
@@ -2280,21 +2394,31 @@ export abstract class DirectStream<
2280
2394
  });
2281
2395
 
2282
2396
  this.peers.set(publicKeyHash, peerStreams);
2283
- this.updateSession(publicKey, -1);
2397
+ // Object replacement is not evidence of a new authenticated peer session.
2398
+ if (!existing) this.updateSession(publicKey, -1);
2284
2399
 
2285
2400
  // Propagate per-peer stream readiness events to the parent emitter
2286
- const forwardOutbound = () =>
2287
- this.dispatchEvent(new CustomEvent("stream:outbound"));
2288
- const forwardInbound = () =>
2289
- this.dispatchEvent(new CustomEvent("stream:inbound"));
2290
- const forwardQueue = () => this.notifyTotalOutboundQueueWaiters();
2401
+ const isCurrentPeer = () => this.peers.get(publicKeyHash) === peerStreams;
2402
+ const forwardOutbound = () => {
2403
+ if (isCurrentPeer()) this.dispatchEvent(new CustomEvent("stream:outbound"));
2404
+ };
2405
+ const forwardInbound = () => {
2406
+ if (isCurrentPeer()) this.dispatchEvent(new CustomEvent("stream:inbound"));
2407
+ };
2408
+ const forwardQueue = () => {
2409
+ if (isCurrentPeer()) this.notifyTotalOutboundQueueWaiters();
2410
+ };
2291
2411
  peerStreams.addEventListener("stream:outbound", forwardOutbound);
2292
2412
  peerStreams.addEventListener("stream:inbound", forwardInbound);
2293
2413
  peerStreams.addEventListener("queue:outbound", forwardQueue);
2294
2414
 
2295
- peerStreams.addEventListener("close", () => this._removePeer(publicKey), {
2296
- once: true,
2297
- });
2415
+ peerStreams.addEventListener(
2416
+ "close",
2417
+ () => {
2418
+ if (isCurrentPeer()) void this._removePeer(publicKey).catch(logError);
2419
+ },
2420
+ { once: true },
2421
+ );
2298
2422
  peerStreams.addEventListener(
2299
2423
  "close",
2300
2424
  () => {
@@ -2312,7 +2436,7 @@ export abstract class DirectStream<
2312
2436
  publicKey,
2313
2437
  -1,
2314
2438
  +new Date(),
2315
- -1,
2439
+ existing ? this.routes.getSession(publicKeyHash) ?? -1 : -1,
2316
2440
  );
2317
2441
 
2318
2442
  // Enforce connection manager limits eagerly when new peers are added. Without this,
@@ -2327,7 +2451,9 @@ export abstract class DirectStream<
2327
2451
  /**
2328
2452
  * Notifies the router that a peer has been disconnected
2329
2453
  */
2330
- protected async _removePeer(publicKey: PublicSignKey) {
2454
+ protected async _removePeer(
2455
+ publicKey: PublicSignKey,
2456
+ ): Promise<PeerStreams | undefined> {
2331
2457
  const hash = publicKey.hashcode();
2332
2458
  const peerStreams = this.peers.get(hash);
2333
2459
  this.clearHealthcheckTimer(hash);
@@ -2338,6 +2464,7 @@ export abstract class DirectStream<
2338
2464
 
2339
2465
  // close peer streams
2340
2466
  await peerStreams.close();
2467
+ if (this.peers.get(hash) !== peerStreams) return;
2341
2468
 
2342
2469
  // delete peer streams
2343
2470
  logger.trace("delete peer" + publicKey.toString());
@@ -2358,6 +2485,7 @@ export abstract class DirectStream<
2358
2485
  let failed = false;
2359
2486
  try {
2360
2487
  for await (const data of record.iterable) {
2488
+ if (this.peers.get(peerId.hashcode()) !== peerStreams) break;
2361
2489
  const now = Date.now();
2362
2490
  record.lastActivity = now;
2363
2491
  record.bytesReceived += data.length || data.byteLength || 0;
@@ -2382,14 +2510,21 @@ export abstract class DirectStream<
2382
2510
  err?.message,
2383
2511
  );
2384
2512
  }
2385
- this.onPeerDisconnected(peerStreams.peerId);
2513
+ if (this.peers.get(peerId.hashcode()) === peerStreams) {
2514
+ void this.onPeerDisconnected(peerStreams.peerId).catch(logError);
2515
+ }
2386
2516
  } finally {
2387
2517
  const removed = peerStreams.detachInboundStream(
2388
2518
  record,
2389
2519
  new AbortError("Inbound stream reader ended"),
2390
2520
  { closeRaw: failed },
2391
2521
  );
2392
- if (removed && !failed && !peerStreams.isReadable) {
2522
+ if (
2523
+ removed &&
2524
+ !failed &&
2525
+ !peerStreams.isReadable &&
2526
+ this.peers.get(peerId.hashcode()) === peerStreams
2527
+ ) {
2393
2528
  void this.onPeerDisconnected(peerStreams.peerId).catch(logError);
2394
2529
  }
2395
2530
  }
@@ -2612,7 +2747,7 @@ export abstract class DirectStream<
2612
2747
  msg: Uint8ArrayList,
2613
2748
  decodedMessage?: Message,
2614
2749
  ) {
2615
- if (!this.started) {
2750
+ if (this.stopping || !this.started) {
2616
2751
  return;
2617
2752
  }
2618
2753
 
@@ -4034,6 +4169,7 @@ export abstract class DirectStream<
4034
4169
  }
4035
4170
 
4036
4171
  async pruneConnections(): Promise<void> {
4172
+ if (this.stopping || !this.started) return;
4037
4173
  // TODO sort by bandwidth
4038
4174
  if (this.peers.size <= this.connectionManagerOptions.minConnections) {
4039
4175
  return;
@@ -4051,6 +4187,7 @@ export abstract class DirectStream<
4051
4187
  this.prunedConnectionsCache?.add(stream.publicKey.hashcode());
4052
4188
 
4053
4189
  await this.onPeerDisconnected(stream.peerId);
4190
+ if (this.stopping || !this.started) return;
4054
4191
  return this.components.connectionManager.closeConnections(stream.peerId);
4055
4192
  }
4056
4193