mirror of
https://github.com/logos-messaging/logos-messaging-js.git
synced 2026-02-07 09:43:18 +00:00
Concepts are being mixed up between the global network config (static vs auto sharding), that needs to be the same of all nodes in the network, individual node configuration (eg relay node subscribing to a given shard), and the routing characteristic of a specific message (eg pubsub topic, shard). This stops proper configuration of nwaku post 0.36.0 because we know need to be deliberate on whether nwaku nodes are running with auto or static sharding. It also included various back and forth conversions between shards, pubsub topics, etc. With this change, we tidy up the network configuration, and make it explicit whether it is static or auto sharded. We also introduce the concept of routing info, which is specific to a message, and tied to the overall network configuration. Routing info abstract pubsub topic, shard, and autosharding needs. Which should lead to easier tidy up of the pubsub concept at a later stage.
302 lines
8.1 KiB
TypeScript
302 lines
8.1 KiB
TypeScript
import {
|
|
IdentifyResult,
|
|
Peer,
|
|
PeerId,
|
|
TypedEventEmitter
|
|
} from "@libp2p/interface";
|
|
import {
|
|
ConnectionManager,
|
|
FilterCodecs,
|
|
LightPushCodec,
|
|
StoreCodec
|
|
} from "@waku/core";
|
|
import {
|
|
CONNECTION_LOCKED_TAG,
|
|
Libp2p,
|
|
Libp2pEventHandler,
|
|
Protocols
|
|
} from "@waku/interfaces";
|
|
import { Logger, RoutingInfo } from "@waku/utils";
|
|
|
|
const log = new Logger("peer-manager");
|
|
|
|
const DEFAULT_NUM_PEERS_TO_USE = 2;
|
|
|
|
type PeerManagerConfig = {
|
|
numPeersToUse?: number;
|
|
};
|
|
|
|
type PeerManagerParams = {
|
|
libp2p: Libp2p;
|
|
config?: PeerManagerConfig;
|
|
connectionManager: ConnectionManager;
|
|
};
|
|
|
|
type GetPeersParams = {
|
|
protocol: Protocols;
|
|
routingInfo: RoutingInfo;
|
|
};
|
|
|
|
export enum PeerManagerEventNames {
|
|
Connect = "filter:connect",
|
|
Disconnect = "filter:disconnect"
|
|
}
|
|
|
|
interface IPeerManagerEvents {
|
|
/**
|
|
* Notifies about Filter peer being connected.
|
|
*/
|
|
[PeerManagerEventNames.Connect]: CustomEvent<PeerId>;
|
|
|
|
/**
|
|
* Notifies about Filter peer being disconnected.
|
|
*/
|
|
[PeerManagerEventNames.Disconnect]: CustomEvent<PeerId>;
|
|
}
|
|
|
|
/**
|
|
* @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 {
|
|
public readonly events = new TypedEventEmitter<IPeerManagerEvents>();
|
|
|
|
private readonly numPeersToUse: number;
|
|
|
|
private readonly libp2p: Libp2p;
|
|
private readonly connectionManager: ConnectionManager;
|
|
|
|
private readonly lockedPeers = new Set<string>();
|
|
private readonly unlockedPeers = new Map<string, number>();
|
|
|
|
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;
|
|
this.connectionManager = params.connectionManager;
|
|
}
|
|
|
|
public start(): void {
|
|
this.libp2p.addEventListener(
|
|
"peer:identify",
|
|
this.onConnected as Libp2pEventHandler<IdentifyResult>
|
|
);
|
|
this.libp2p.addEventListener(
|
|
"peer:disconnect",
|
|
this.onDisconnected as Libp2pEventHandler<PeerId>
|
|
);
|
|
}
|
|
|
|
public stop(): void {
|
|
this.libp2p.removeEventListener(
|
|
"peer:identify",
|
|
this.onConnected as Libp2pEventHandler<IdentifyResult>
|
|
);
|
|
this.libp2p.removeEventListener(
|
|
"peer:disconnect",
|
|
this.onDisconnected as Libp2pEventHandler<PeerId>
|
|
);
|
|
}
|
|
|
|
public async getPeers(params: GetPeersParams): Promise<PeerId[]> {
|
|
log.info(
|
|
`Getting peers for protocol: ${params.protocol}, ` +
|
|
`clusterId: ${params.routingInfo.networkConfig.clusterId},` +
|
|
` shard: ${params.routingInfo.shardId}`
|
|
);
|
|
|
|
const connectedPeers = await this.connectionManager.getConnectedPeers();
|
|
log.info(`Found ${connectedPeers.length} connected peers`);
|
|
|
|
let results: Peer[] = [];
|
|
|
|
for (const peer of connectedPeers) {
|
|
const hasProtocol = this.hasPeerProtocol(peer, params.protocol);
|
|
|
|
const isOnSameShard = await this.connectionManager.isPeerOnShard(
|
|
peer.id,
|
|
params.routingInfo.networkConfig.clusterId,
|
|
params.routingInfo.shardId
|
|
);
|
|
if (!isOnSameShard) {
|
|
continue;
|
|
}
|
|
|
|
const isPeerAvailableForUse = this.isPeerAvailableForUse(peer.id);
|
|
|
|
if (hasProtocol && 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;
|
|
}
|
|
|
|
public async renewPeer(id: PeerId, params: GetPeersParams): Promise<void> {
|
|
log.info(
|
|
`Renewing peer ${id} for protocol: ${params.protocol}, routingInfo: ${params.routingInfo}`
|
|
);
|
|
|
|
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);
|
|
}
|
|
|
|
public async isPeerOnPubsub(
|
|
id: PeerId,
|
|
pubsubTopic: string
|
|
): Promise<boolean> {
|
|
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);
|
|
}
|
|
|
|
private async onConnected(event: CustomEvent<IdentifyResult>): Promise<void> {
|
|
const result = event.detail;
|
|
const isFilterPeer = result.protocols.includes(
|
|
this.matchProtocolToCodec(Protocols.Filter)
|
|
);
|
|
|
|
if (isFilterPeer) {
|
|
this.dispatchFilterPeerConnect(result.peerId);
|
|
}
|
|
}
|
|
|
|
private async onDisconnected(event: CustomEvent<PeerId>): Promise<void> {
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
private hasPeerProtocol(peer: Peer, protocol: Protocols): boolean {
|
|
return peer.protocols.includes(this.matchProtocolToCodec(protocol));
|
|
}
|
|
|
|
private lockPeer(id: PeerId): void {
|
|
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());
|
|
}
|
|
|
|
private isPeerLocked(id: PeerId): boolean {
|
|
return this.lockedPeers.has(id.toString());
|
|
}
|
|
|
|
private unlockPeer(id: PeerId): void {
|
|
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());
|
|
}
|
|
|
|
private isPeerAvailableForUse(id: PeerId): boolean {
|
|
const value = this.unlockedPeers.get(id.toString());
|
|
|
|
if (!value) {
|
|
return true;
|
|
}
|
|
|
|
const wasUnlocked = new Date(value).getTime();
|
|
return Date.now() - wasUnlocked >= 10_000;
|
|
}
|
|
|
|
private dispatchFilterPeerConnect(id: PeerId): void {
|
|
this.events.dispatchEvent(
|
|
new CustomEvent(PeerManagerEventNames.Connect, { detail: id })
|
|
);
|
|
}
|
|
|
|
private dispatchFilterPeerDisconnect(id: PeerId): void {
|
|
this.events.dispatchEvent(
|
|
new CustomEvent(PeerManagerEventNames.Disconnect, { detail: id })
|
|
);
|
|
}
|
|
|
|
private matchProtocolToCodec(protocol: Protocols): string {
|
|
const protocolToCodec = {
|
|
[Protocols.Filter]: FilterCodecs.SUBSCRIBE,
|
|
[Protocols.LightPush]: LightPushCodec,
|
|
[Protocols.Store]: StoreCodec,
|
|
[Protocols.Relay]: ""
|
|
};
|
|
|
|
return protocolToCodec[protocol];
|
|
}
|
|
}
|