@waku/sdk
Version:
A unified SDK for easy creation and management of js-waku nodes.
168 lines • 7.2 kB
JavaScript
import { TypedEventEmitter } from "@libp2p/interface";
import { FilterCodecs, LightPushCodec, StoreCodec } from "@waku/core";
import { CONNECTION_LOCKED_TAG, Protocols } from "@waku/interfaces";
import { Logger } from "@waku/utils";
const log = new Logger("peer-manager");
const DEFAULT_NUM_PEERS_TO_USE = 2;
export var PeerManagerEventNames;
(function (PeerManagerEventNames) {
PeerManagerEventNames["Connect"] = "filter:connect";
PeerManagerEventNames["Disconnect"] = "filter:disconnect";
})(PeerManagerEventNames || (PeerManagerEventNames = {}));
/**
* @description
* PeerManager is responsible for:
* - finding available peers based on shard / protocols;
* - notifying when peers for a specific protocol are connected;
* - notifying when peers for a specific protocol are disconnected;
*/
export class PeerManager {
events = new TypedEventEmitter();
numPeersToUse;
libp2p;
connectionManager;
lockedPeers = new Set();
unlockedPeers = new Map();
constructor(params) {
this.onConnected = this.onConnected.bind(this);
this.onDisconnected = this.onDisconnected.bind(this);
this.numPeersToUse =
params?.config?.numPeersToUse || DEFAULT_NUM_PEERS_TO_USE;
this.libp2p = params.libp2p;
this.connectionManager = params.connectionManager;
}
start() {
this.libp2p.addEventListener("peer:identify", this.onConnected);
this.libp2p.addEventListener("peer:disconnect", this.onDisconnected);
}
stop() {
this.libp2p.removeEventListener("peer:identify", this.onConnected);
this.libp2p.removeEventListener("peer:disconnect", this.onDisconnected);
}
async getPeers(params) {
log.info(`Getting peers for protocol: ${params.protocol}, pubsubTopic: ${params.pubsubTopic}`);
const connectedPeers = await this.connectionManager.getConnectedPeers();
log.info(`Found ${connectedPeers.length} connected peers`);
let results = [];
for (const peer of connectedPeers) {
const hasProtocol = this.hasPeerProtocol(peer, params.protocol);
const hasSamePubsub = await this.connectionManager.isPeerOnTopic(peer.id, params.pubsubTopic);
const isPeerAvailableForUse = this.isPeerAvailableForUse(peer.id);
if (hasProtocol && hasSamePubsub && isPeerAvailableForUse) {
results.push(peer);
log.info(`Peer ${peer.id} qualifies for protocol ${params.protocol}`);
}
}
const lockedPeers = results.filter((p) => this.isPeerLocked(p.id));
log.info(`Found ${lockedPeers.length} locked peers out of ${results.length} qualifying peers`);
if (lockedPeers.length >= this.numPeersToUse) {
const selectedPeers = lockedPeers
.slice(0, this.numPeersToUse)
.map((p) => p.id);
log.info(`Using ${selectedPeers.length} locked peers: ${selectedPeers.map((p) => p.toString())}`);
return selectedPeers;
}
const notLockedPeers = results.filter((p) => !this.isPeerLocked(p.id));
log.info(`Found ${notLockedPeers.length} unlocked peers, need ${this.numPeersToUse - lockedPeers.length} more`);
results = [...lockedPeers, ...notLockedPeers]
.slice(0, this.numPeersToUse)
.map((p) => {
this.lockPeer(p.id);
return p;
});
const finalPeers = results.map((p) => p.id);
log.info(`Selected ${finalPeers.length} peers: ${finalPeers.map((p) => p.toString())}`);
return finalPeers;
}
async renewPeer(id, params) {
log.info(`Renewing peer ${id} for protocol: ${params.protocol}, pubsubTopic: ${params.pubsubTopic}`);
const connectedPeers = await this.connectionManager.getConnectedPeers();
const renewedPeer = connectedPeers.find((p) => p.id.equals(id));
if (!renewedPeer) {
log.warn(`Cannot renew peer:${id}, no connection to the peer.`);
return;
}
log.info(`Found peer ${id} in connected peers, unlocking and getting new peers`);
this.unlockPeer(renewedPeer.id);
await this.getPeers(params);
}
async isPeerOnPubsub(id, pubsubTopic) {
const hasShardInfo = await this.connectionManager.hasShardInfo(id);
// allow to use peers that we don't know information about yet
if (!hasShardInfo) {
return true;
}
return this.connectionManager.isPeerOnTopic(id, pubsubTopic);
}
async onConnected(event) {
const result = event.detail;
const isFilterPeer = result.protocols.includes(this.matchProtocolToCodec(Protocols.Filter));
if (isFilterPeer) {
this.dispatchFilterPeerConnect(result.peerId);
}
}
async onDisconnected(event) {
const peerId = event.detail;
try {
// we need to read from peerStore as peer is already disconnected
const peer = await this.libp2p.peerStore.get(peerId);
const isFilterPeer = this.hasPeerProtocol(peer, Protocols.Filter);
if (isFilterPeer) {
this.dispatchFilterPeerDisconnect(peer.id);
}
}
catch (error) {
log.error(`Failed to dispatch Filter disconnect event:${error}`);
}
}
hasPeerProtocol(peer, protocol) {
return peer.protocols.includes(this.matchProtocolToCodec(protocol));
}
lockPeer(id) {
log.info(`Locking peer ${id}`);
this.lockedPeers.add(id.toString());
this.libp2p
.getConnections()
.filter((c) => c.remotePeer.equals(id))
.forEach((c) => c.tags.push(CONNECTION_LOCKED_TAG));
this.unlockedPeers.delete(id.toString());
}
isPeerLocked(id) {
return this.lockedPeers.has(id.toString());
}
unlockPeer(id) {
log.info(`Unlocking peer ${id}`);
this.lockedPeers.delete(id.toString());
this.libp2p
.getConnections()
.filter((c) => c.remotePeer.equals(id))
.forEach((c) => {
c.tags = c.tags.filter((t) => t !== CONNECTION_LOCKED_TAG);
});
this.unlockedPeers.set(id.toString(), Date.now());
}
isPeerAvailableForUse(id) {
const value = this.unlockedPeers.get(id.toString());
if (!value) {
return true;
}
const wasUnlocked = new Date(value).getTime();
return Date.now() - wasUnlocked >= 10_000 ? true : false;
}
dispatchFilterPeerConnect(id) {
this.events.dispatchEvent(new CustomEvent(PeerManagerEventNames.Connect, { detail: id }));
}
dispatchFilterPeerDisconnect(id) {
this.events.dispatchEvent(new CustomEvent(PeerManagerEventNames.Disconnect, { detail: id }));
}
matchProtocolToCodec(protocol) {
const protocolToCodec = {
[Protocols.Filter]: FilterCodecs.SUBSCRIBE,
[Protocols.LightPush]: LightPushCodec,
[Protocols.Store]: StoreCodec,
[Protocols.Relay]: ""
};
return protocolToCodec[protocol];
}
}
//# sourceMappingURL=peer_manager.js.map