UNPKG

@waku/core

Version:

TypeScript implementation of the Waku v2 protocol

177 lines 8.1 kB
import { CONNECTION_LOCKED_TAG, Tags } from "@waku/interfaces"; import { Logger } from "@waku/utils"; const log = new Logger("connection-limiter"); const DEFAULT_CONNECTION_MONITOR_INTERVAL = 5 * 1_000; /** * This class is responsible for limiting the number of connections to peers. * It also dials all known peers because libp2p might have emitted `peer:discovery` before initialization * and listen to `peer:connect` and `peer:disconnect` events to manage connections. */ export class ConnectionLimiter { libp2p; events; networkMonitor; dialer; connectionMonitorInterval = null; options; constructor(options) { this.libp2p = options.libp2p; this.events = options.events; this.networkMonitor = options.networkMonitor; this.dialer = options.dialer; this.options = options.options; this.onWakuConnectionEvent = this.onWakuConnectionEvent.bind(this); this.onDisconnectedEvent = this.onDisconnectedEvent.bind(this); } start() { // dial all known peers because libp2p might have emitted `peer:discovery` before initialization void this.dialPeersFromStore(); if (this.options.enableAutoRecovery && this.connectionMonitorInterval === null) { this.connectionMonitorInterval = setInterval(() => void this.maintainConnections(), DEFAULT_CONNECTION_MONITOR_INTERVAL); } this.events.addEventListener("waku:connection", this.onWakuConnectionEvent); /** * NOTE: Event is not being emitted on closing nor losing a connection. * @see https://github.com/libp2p/js-libp2p/issues/939 * @see https://github.com/status-im/js-waku/issues/252 * * >This event will be triggered anytime we are disconnected from another peer, * >regardless of the circumstances of that disconnection. * >If we happen to have multiple connections to a peer, * >this event will **only** be triggered when the last connection is closed. * @see https://github.com/libp2p/js-libp2p/blob/bad9e8c0ff58d60a78314077720c82ae331cc55b/doc/API.md?plain=1#L2100 */ this.libp2p.addEventListener("peer:disconnect", this.onDisconnectedEvent); } stop() { this.events.removeEventListener("waku:connection", this.onWakuConnectionEvent); this.libp2p.removeEventListener("peer:disconnect", this.onDisconnectedEvent); if (this.connectionMonitorInterval) { clearInterval(this.connectionMonitorInterval); this.connectionMonitorInterval = null; } } onWakuConnectionEvent() { if (!this.options.enableAutoRecovery) { log.info(`Auto recovery is disabled, skipping`); return; } if (this.networkMonitor.isBrowserConnected()) { void this.dialPeersFromStore(); } } async maintainConnections() { await this.maintainConnectionsCount(); await this.maintainBootstrapConnections(); } async onDisconnectedEvent() { if (this.libp2p.getConnections().length === 0) { log.info(`No connections, dialing peers from store`); await this.dialPeersFromStore(); } } async maintainConnectionsCount() { log.info(`Maintaining connections count`); const connections = this.libp2p.getConnections(); if (connections.length <= this.options.maxConnections) { log.info(`Node has less than max connections ${this.options.maxConnections}, trying to dial more peers`); const peers = await this.getPrioritizedPeers(); if (peers.length === 0) { log.info(`No peers to dial, node is utilizing all known peers`); return; } const promises = peers .slice(0, this.options.maxConnections - connections.length) .map((p) => this.dialer.dial(p.id)); await Promise.all(promises); return; } log.info(`Node has more than max connections ${this.options.maxConnections}, dropping connections`); try { const connectionsToDrop = connections .filter((c) => !c.tags.includes(CONNECTION_LOCKED_TAG)) .slice(this.options.maxConnections); if (connectionsToDrop.length === 0) { log.info(`No connections to drop, skipping`); return; } const promises = connectionsToDrop.map((c) => this.libp2p.hangUp(c.remotePeer)); await Promise.all(promises); log.info(`Dropped ${connectionsToDrop.length} connections`); } catch (error) { log.error(`Unexpected error while maintaining connections`, error); } } async maintainBootstrapConnections() { log.info(`Maintaining bootstrap connections`); const bootstrapPeers = await this.getBootstrapPeers(); if (bootstrapPeers.length <= this.options.maxBootstrapPeers) { return; } try { const peersToDrop = bootstrapPeers.slice(this.options.maxBootstrapPeers); log.info(`Dropping ${peersToDrop.length} bootstrap connections because node has more than max bootstrap connections ${this.options.maxBootstrapPeers}`); const promises = peersToDrop.map((p) => this.libp2p.hangUp(p.id)); await Promise.all(promises); log.info(`Dropped ${peersToDrop.length} bootstrap connections`); } catch (error) { log.error(`Unexpected error while maintaining bootstrap connections`, error); } } async dialPeersFromStore() { log.info(`Dialing peers from store`); try { const peers = await this.getPrioritizedPeers(); if (peers.length === 0) { log.info(`No peers to dial, skipping`); return; } const promises = peers.map((p) => this.dialer.dial(p.id)); log.info(`Dialing ${peers.length} peers from store`); await Promise.all(promises); log.info(`Dialed ${promises.length} peers from store`); } catch (error) { log.error(`Unexpected error while dialing peer store peers`, error); } } /** * Returns a list of peers ordered by priority: * - bootstrap peers * - peers from peer exchange * - peers from local store (last because we are not sure that locally stored information is up to date) */ async getPrioritizedPeers() { const allPeers = await this.libp2p.peerStore.all(); const allConnections = this.libp2p.getConnections(); log.info(`Found ${allPeers.length} peers in store, and found ${allConnections.length} connections`); const notConnectedPeers = allPeers.filter((p) => !allConnections.some((c) => c.remotePeer.equals(p.id)) && p.addresses.some((a) => a.multiaddr.toString().includes("wss") || a.multiaddr.toString().includes("ws"))); const bootstrapPeers = notConnectedPeers.filter((p) => p.tags.has(Tags.BOOTSTRAP)); const peerExchangePeers = notConnectedPeers.filter((p) => p.tags.has(Tags.PEER_EXCHANGE)); const localStorePeers = notConnectedPeers.filter((p) => p.tags.has(Tags.LOCAL)); return [...bootstrapPeers, ...peerExchangePeers, ...localStorePeers]; } async getBootstrapPeers() { const peers = await Promise.all(this.libp2p .getConnections() .map((conn) => conn.remotePeer) .map((id) => this.getPeer(id))); const bootstrapPeers = peers.filter((peer) => peer && peer.tags.has(Tags.BOOTSTRAP)); return bootstrapPeers; } async getPeer(peerId) { try { return await this.libp2p.peerStore.get(peerId); } catch (error) { log.error(`Failed to get peer ${peerId}, error: ${error}`); return null; } } } //# sourceMappingURL=connection_limiter.js.map