@waku/core
Version:
TypeScript implementation of the Waku v2 protocol
177 lines • 8.1 kB
JavaScript
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