2025-07-02 23:03:47 +02:00
|
|
|
import {
|
|
|
|
|
IdentifyResult,
|
|
|
|
|
Peer,
|
|
|
|
|
PeerId,
|
|
|
|
|
TypedEventEmitter
|
|
|
|
|
} from "@libp2p/interface";
|
2025-07-21 14:47:07 +10:00
|
|
|
import { FilterCodecs, LightPushCodec, StoreCodec } from "@waku/core";
|
2025-07-17 01:15:36 +02:00
|
|
|
import {
|
|
|
|
|
CONNECTION_LOCKED_TAG,
|
2025-07-21 14:47:07 +10:00
|
|
|
type IConnectionManager,
|
2025-07-17 01:15:36 +02:00
|
|
|
Libp2p,
|
|
|
|
|
Libp2pEventHandler,
|
|
|
|
|
Protocols
|
|
|
|
|
} from "@waku/interfaces";
|
2025-07-19 14:30:44 +10:00
|
|
|
import { Logger } from "@waku/utils";
|
2025-01-31 00:16:00 +01:00
|
|
|
|
|
|
|
|
const log = new Logger("peer-manager");
|
|
|
|
|
|
|
|
|
|
const DEFAULT_NUM_PEERS_TO_USE = 2;
|
|
|
|
|
|
|
|
|
|
type PeerManagerConfig = {
|
|
|
|
|
numPeersToUse?: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type PeerManagerParams = {
|
|
|
|
|
libp2p: Libp2p;
|
|
|
|
|
config?: PeerManagerConfig;
|
2025-07-21 14:47:07 +10:00
|
|
|
connectionManager: IConnectionManager;
|
2025-01-31 00:16:00 +01:00
|
|
|
};
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
type GetPeersParams = {
|
|
|
|
|
protocol: Protocols;
|
2025-07-19 21:25:21 +10:00
|
|
|
pubsubTopic: string;
|
2025-07-02 23:03:47 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export enum PeerManagerEventNames {
|
2025-08-27 12:29:22 +10:00
|
|
|
FilterConnect = "filter:connect",
|
|
|
|
|
FilterDisconnect = "filter:disconnect",
|
|
|
|
|
StoreConnect = "store:connect"
|
2025-07-02 23:03:47 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-27 12:29:22 +10:00
|
|
|
export interface IPeerManagerEvents {
|
2025-07-02 23:03:47 +02:00
|
|
|
/**
|
|
|
|
|
* Notifies about Filter peer being connected.
|
|
|
|
|
*/
|
2025-08-27 12:29:22 +10:00
|
|
|
[PeerManagerEventNames.FilterConnect]: CustomEvent<PeerId>;
|
2025-07-02 23:03:47 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Notifies about Filter peer being disconnected.
|
|
|
|
|
*/
|
2025-08-27 12:29:22 +10:00
|
|
|
[PeerManagerEventNames.FilterDisconnect]: CustomEvent<PeerId>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Notifies about a Store peer being connected.
|
|
|
|
|
*/
|
|
|
|
|
[PeerManagerEventNames.StoreConnect]: CustomEvent<PeerId>;
|
2025-07-02 23:03:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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;
|
|
|
|
|
*/
|
2025-01-31 00:16:00 +01:00
|
|
|
export class PeerManager {
|
2025-07-02 23:03:47 +02:00
|
|
|
public readonly events = new TypedEventEmitter<IPeerManagerEvents>();
|
|
|
|
|
|
2025-01-31 00:16:00 +01:00
|
|
|
private readonly numPeersToUse: number;
|
|
|
|
|
|
|
|
|
|
private readonly libp2p: Libp2p;
|
2025-07-21 14:47:07 +10:00
|
|
|
private readonly connectionManager: IConnectionManager;
|
2025-07-02 23:03:47 +02:00
|
|
|
|
|
|
|
|
private readonly lockedPeers = new Set<string>();
|
|
|
|
|
private readonly unlockedPeers = new Map<string, number>();
|
2025-01-31 00:16:00 +01:00
|
|
|
|
|
|
|
|
public constructor(params: PeerManagerParams) {
|
|
|
|
|
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;
|
2025-07-02 23:03:47 +02:00
|
|
|
this.connectionManager = params.connectionManager;
|
2025-02-25 22:40:03 +01:00
|
|
|
}
|
2025-01-31 00:16:00 +01:00
|
|
|
|
2025-02-25 22:40:03 +01:00
|
|
|
public start(): void {
|
2025-07-02 23:03:47 +02:00
|
|
|
this.libp2p.addEventListener(
|
|
|
|
|
"peer:identify",
|
|
|
|
|
this.onConnected as Libp2pEventHandler<IdentifyResult>
|
|
|
|
|
);
|
|
|
|
|
this.libp2p.addEventListener(
|
|
|
|
|
"peer:disconnect",
|
|
|
|
|
this.onDisconnected as Libp2pEventHandler<PeerId>
|
|
|
|
|
);
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public stop(): void {
|
2025-07-02 23:03:47 +02:00
|
|
|
this.libp2p.removeEventListener(
|
|
|
|
|
"peer:identify",
|
|
|
|
|
this.onConnected as Libp2pEventHandler<IdentifyResult>
|
|
|
|
|
);
|
|
|
|
|
this.libp2p.removeEventListener(
|
|
|
|
|
"peer:disconnect",
|
|
|
|
|
this.onDisconnected as Libp2pEventHandler<PeerId>
|
|
|
|
|
);
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
public async getPeers(params: GetPeersParams): Promise<PeerId[]> {
|
|
|
|
|
log.info(
|
2025-07-19 21:25:21 +10:00
|
|
|
`Getting peers for protocol: ${params.protocol}, pubsubTopic: ${params.pubsubTopic}`
|
2025-07-02 23:03:47 +02:00
|
|
|
);
|
2025-01-31 00:16:00 +01:00
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
const connectedPeers = await this.connectionManager.getConnectedPeers();
|
|
|
|
|
log.info(`Found ${connectedPeers.length} connected peers`);
|
2025-01-31 00:16:00 +01:00
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
let results: Peer[] = [];
|
|
|
|
|
|
|
|
|
|
for (const peer of connectedPeers) {
|
|
|
|
|
const hasProtocol = this.hasPeerProtocol(peer, params.protocol);
|
2025-07-19 21:25:21 +10:00
|
|
|
const hasSamePubsub = await this.connectionManager.isPeerOnTopic(
|
2025-07-02 23:03:47 +02:00
|
|
|
peer.id,
|
2025-07-19 21:25:21 +10:00
|
|
|
params.pubsubTopic
|
2025-07-02 23:03:47 +02:00
|
|
|
);
|
|
|
|
|
const isPeerAvailableForUse = this.isPeerAvailableForUse(peer.id);
|
|
|
|
|
|
2025-07-19 21:25:21 +10:00
|
|
|
if (hasProtocol && hasSamePubsub && isPeerAvailableForUse) {
|
2025-07-02 23:03:47 +02:00
|
|
|
results.push(peer);
|
|
|
|
|
log.info(`Peer ${peer.id} qualifies for protocol ${params.protocol}`);
|
|
|
|
|
}
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
const lockedPeers = results.filter((p) => this.isPeerLocked(p.id));
|
|
|
|
|
log.info(
|
|
|
|
|
`Found ${lockedPeers.length} locked peers out of ${results.length} qualifying peers`
|
|
|
|
|
);
|
2025-01-31 00:16:00 +01:00
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
if (lockedPeers.length >= this.numPeersToUse) {
|
|
|
|
|
const selectedPeers = lockedPeers
|
|
|
|
|
.slice(0, this.numPeersToUse)
|
|
|
|
|
.map((p) => p.id);
|
2025-01-31 00:16:00 +01:00
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
log.info(
|
|
|
|
|
`Using ${selectedPeers.length} locked peers: ${selectedPeers.map((p) => p.toString())}`
|
2025-01-31 00:16:00 +01:00
|
|
|
);
|
2025-07-02 23:03:47 +02:00
|
|
|
|
|
|
|
|
return selectedPeers;
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
const notLockedPeers = results.filter((p) => !this.isPeerLocked(p.id));
|
2025-01-31 00:16:00 +01:00
|
|
|
log.info(
|
2025-07-02 23:03:47 +02:00
|
|
|
`Found ${notLockedPeers.length} unlocked peers, need ${this.numPeersToUse - lockedPeers.length} more`
|
2025-01-31 00:16:00 +01:00
|
|
|
);
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
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;
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
public async renewPeer(id: PeerId, params: GetPeersParams): Promise<void> {
|
|
|
|
|
log.info(
|
2025-07-19 21:25:21 +10:00
|
|
|
`Renewing peer ${id} for protocol: ${params.protocol}, pubsubTopic: ${params.pubsubTopic}`
|
2025-07-02 23:03:47 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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);
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
public async isPeerOnPubsub(
|
|
|
|
|
id: PeerId,
|
|
|
|
|
pubsubTopic: string
|
|
|
|
|
): Promise<boolean> {
|
feat!: re-architect connection manager (#2445)
* remove public pubsub field and redundant util
* add hangUp and improve dial operations, improve keepAliveManager and remove unused method, move utils and add tests
* improve public dial method to start keep alive checks
* move dial method
* implement discovery dialer
* implement discovery dialer with queue with tests
* add discovery dialer e2e tests, change local discovery log tag, update other tests
* remove comment
* add issue link, remove only
* implement shard reader component
* create evetns module, remove unused connection manager events and related tests
* implement network indicator
* implement connection limiter, change public API of connection manager, implement recovery strategy
* decouple keep alive maanger
* add connection manager js-doc
* refactor keep alive manager, cover with tests
* add tests for connection manager main facade
* add tests for connection limiter
* add e2e tests for connection manager modules
pass js-waku config during test node init
remove dns discovery for js-waku
* restructure dialing tests
* address last e2e tests
* address review
* add logging for main methods
* decouple pure dialer class, update network monitor with specific metrics
* remove console.log
* remove usage of protocols
* update sdk package tests
* add connect await promise
* add debug for e2e tests
* enable only packages tests
* use only one file
* revert debugging
* up interface for netwrok manager
* add logs
* add more logs
* add more logs
* add another logs
* remove .only
* remove log statements
* skip the test with follow up
2025-07-09 21:23:14 +02:00
|
|
|
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);
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
private async onConnected(event: CustomEvent<IdentifyResult>): Promise<void> {
|
|
|
|
|
const result = event.detail;
|
2025-08-27 12:29:22 +10:00
|
|
|
if (
|
|
|
|
|
result.protocols.includes(this.matchProtocolToCodec(Protocols.Filter))
|
|
|
|
|
) {
|
2025-07-02 23:03:47 +02:00
|
|
|
this.dispatchFilterPeerConnect(result.peerId);
|
|
|
|
|
}
|
2025-08-27 12:29:22 +10:00
|
|
|
if (result.protocols.includes(this.matchProtocolToCodec(Protocols.Store))) {
|
|
|
|
|
this.dispatchStorePeerConnect(result.peerId);
|
|
|
|
|
}
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
private async onDisconnected(event: CustomEvent<PeerId>): Promise<void> {
|
2025-01-31 00:16:00 +01:00
|
|
|
const peerId = event.detail;
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
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);
|
2025-01-31 00:16:00 +01:00
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
if (isFilterPeer) {
|
|
|
|
|
this.dispatchFilterPeerDisconnect(peer.id);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
log.error(`Failed to dispatch Filter disconnect event:${error}`);
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
2025-07-02 23:03:47 +02:00
|
|
|
}
|
2025-01-31 00:16:00 +01:00
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
private hasPeerProtocol(peer: Peer, protocol: Protocols): boolean {
|
|
|
|
|
return peer.protocols.includes(this.matchProtocolToCodec(protocol));
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
private lockPeer(id: PeerId): void {
|
|
|
|
|
log.info(`Locking peer ${id}`);
|
|
|
|
|
this.lockedPeers.add(id.toString());
|
2025-07-17 01:15:36 +02:00
|
|
|
this.libp2p
|
|
|
|
|
.getConnections()
|
|
|
|
|
.filter((c) => c.remotePeer.equals(id))
|
|
|
|
|
.forEach((c) => c.tags.push(CONNECTION_LOCKED_TAG));
|
2025-07-02 23:03:47 +02:00
|
|
|
this.unlockedPeers.delete(id.toString());
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
private isPeerLocked(id: PeerId): boolean {
|
|
|
|
|
return this.lockedPeers.has(id.toString());
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
private unlockPeer(id: PeerId): void {
|
|
|
|
|
log.info(`Unlocking peer ${id}`);
|
|
|
|
|
this.lockedPeers.delete(id.toString());
|
2025-07-17 01:15:36 +02:00
|
|
|
this.libp2p
|
|
|
|
|
.getConnections()
|
|
|
|
|
.filter((c) => c.remotePeer.equals(id))
|
|
|
|
|
.forEach((c) => {
|
|
|
|
|
c.tags = c.tags.filter((t) => t !== CONNECTION_LOCKED_TAG);
|
|
|
|
|
});
|
2025-07-02 23:03:47 +02:00
|
|
|
this.unlockedPeers.set(id.toString(), Date.now());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private isPeerAvailableForUse(id: PeerId): boolean {
|
|
|
|
|
const value = this.unlockedPeers.get(id.toString());
|
|
|
|
|
|
|
|
|
|
if (!value) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const wasUnlocked = new Date(value).getTime();
|
2025-08-27 12:29:22 +10:00
|
|
|
return Date.now() - wasUnlocked >= 10_000;
|
2025-07-02 23:03:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private dispatchFilterPeerConnect(id: PeerId): void {
|
|
|
|
|
this.events.dispatchEvent(
|
2025-08-27 12:29:22 +10:00
|
|
|
new CustomEvent(PeerManagerEventNames.FilterConnect, { detail: id })
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private dispatchStorePeerConnect(id: PeerId): void {
|
|
|
|
|
this.events.dispatchEvent(
|
|
|
|
|
new CustomEvent(PeerManagerEventNames.StoreConnect, { detail: id })
|
2025-01-31 00:16:00 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 23:03:47 +02:00
|
|
|
private dispatchFilterPeerDisconnect(id: PeerId): void {
|
|
|
|
|
this.events.dispatchEvent(
|
2025-08-27 12:29:22 +10:00
|
|
|
new CustomEvent(PeerManagerEventNames.FilterDisconnect, { detail: id })
|
2025-07-02 23:03:47 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private matchProtocolToCodec(protocol: Protocols): string {
|
|
|
|
|
const protocolToCodec = {
|
|
|
|
|
[Protocols.Filter]: FilterCodecs.SUBSCRIBE,
|
|
|
|
|
[Protocols.LightPush]: LightPushCodec,
|
|
|
|
|
[Protocols.Store]: StoreCodec,
|
|
|
|
|
[Protocols.Relay]: ""
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return protocolToCodec[protocol];
|
2025-01-31 00:16:00 +01:00
|
|
|
}
|
|
|
|
|
}
|