UNPKG

@waku/discovery

Version:

Contains various discovery mechanisms: DNS Discovery (EIP-1459, Peer Exchange, Local Peer Cache Discovery.

165 lines 6.3 kB
import { TypedEventEmitter } from "@libp2p/interface"; import { peerDiscoverySymbol as symbol } from "@libp2p/interface"; import { encodeRelayShard, Logger } from "@waku/utils"; import { DEFAULT_PEER_EXCHANGE_REQUEST_NODES, DEFAULT_PEER_EXCHANGE_TAG_NAME, DEFAULT_PEER_EXCHANGE_TAG_TTL, DEFAULT_PEER_EXCHANGE_TAG_VALUE, PeerExchangeCodec } from "./constants.js"; import { PeerExchange } from "./peer_exchange.js"; const log = new Logger("peer-exchange-discovery"); export class PeerExchangeDiscovery extends TypedEventEmitter { components; peerExchange; options; isStarted = false; queryingPeers = new Set(); peerExpirationRecords = new Map(); continuousDiscoveryInterval = null; constructor(components, options = {}) { super(); this.components = components; this.peerExchange = new PeerExchange(components); this.options = { ...options, TTL: options.TTL ?? DEFAULT_PEER_EXCHANGE_TAG_TTL }; this.handleDiscoveredPeer = this.handleDiscoveredPeer.bind(this); } /** * Start Peer Exchange. * Subscribe to "peer:identify" events and handle them. */ start() { if (this.isStarted) { return; } log.info("Starting peer exchange node discovery, discovering peers"); this.isStarted = true; this.components.events.addEventListener("peer:identify", this.handleDiscoveredPeer); this.continuousDiscoveryInterval = setInterval(() => { void this.handlePeriodicDiscovery(); }, this.options.TTL); } /** * Stop Peer Exchange. * Unsubscribe from "peer:identify" events. */ stop() { if (!this.isStarted) { return; } log.info("Stopping peer exchange node discovery"); this.isStarted = false; this.queryingPeers.clear(); this.peerExpirationRecords.clear(); if (this.continuousDiscoveryInterval) { clearInterval(this.continuousDiscoveryInterval); } this.components.events.removeEventListener("peer:identify", this.handleDiscoveredPeer); } get [symbol]() { return true; } get [Symbol.toStringTag]() { return "@waku/peer-exchange"; } async handleDiscoveredPeer(event) { void this.runQuery(event.detail.peerId, event.detail.protocols); } async handlePeriodicDiscovery() { const connections = this.components.connectionManager.getConnections(); await Promise.all(connections.map(async (connection) => { try { const peerIdStr = connection.remotePeer.toString(); const shouldQuery = this.peerExpirationRecords.has(peerIdStr) ? this.peerExpirationRecords.get(peerIdStr) <= Date.now() : true; if (!shouldQuery) { return null; } const peer = await this.components.peerStore.get(connection.remotePeer); return this.runQuery(connection.remotePeer, peer.protocols); } catch (error) { log.warn("Error getting peer info", error); return null; } })); } async runQuery(peerId, protocols) { if (!protocols.includes(PeerExchangeCodec) || this.queryingPeers.has(peerId.toString())) { log.info(`Skipping peer ${peerId} as it is already querying or does not support peer exchange`); return; } try { this.queryingPeers.add(peerId.toString()); await this.query(peerId); } catch (error) { log.error("Error querying peer", error); } this.peerExpirationRecords.set(peerId.toString(), Date.now() + this.options.TTL); this.queryingPeers.delete(peerId.toString()); } async query(peerId) { const peerIdStr = peerId.toString(); log.info(`Querying peer exchange for ${peerIdStr}`); const { error, peerInfos } = await this.peerExchange.query({ numPeers: DEFAULT_PEER_EXCHANGE_REQUEST_NODES, peerId }); if (error) { log.error(`Peer exchange query to ${peerIdStr} failed`, error); return; } for (const { ENR } of peerInfos) { if (!ENR) { log.warn(`No ENR in peerInfo object from ${peerIdStr}, skipping`); continue; } const { peerInfo, shardInfo } = ENR; if (!peerInfo) { log.warn(`No peerInfo in ENR from ${peerIdStr}, skipping`); continue; } const hasPrevShardInfo = await this.hasShardInfo(peerInfo.id); const metadata = !hasPrevShardInfo && shardInfo ? { metadata: { shardInfo: encodeRelayShard(shardInfo) } } : undefined; // merge is smart enough to overwrite only changed parts await this.components.peerStore.merge(peerInfo.id, { tags: { [DEFAULT_PEER_EXCHANGE_TAG_NAME]: { value: DEFAULT_PEER_EXCHANGE_TAG_VALUE } }, ...metadata, ...(peerInfo.multiaddrs && { multiaddrs: peerInfo.multiaddrs }) }); log.info(`Discovered peer: ${peerInfo.id.toString()}`); this.dispatchEvent(new CustomEvent("peer", { detail: { id: peerInfo.id, multiaddrs: peerInfo.multiaddrs } })); } } async hasShardInfo(peerId) { try { const peer = await this.components.peerStore.get(peerId); if (!peer) { return false; } return peer.metadata.has("shardInfo"); } catch (err) { log.warn(`Error getting shard info for ${peerId.toString()}`, err); } return false; } } export function wakuPeerExchangeDiscovery(options = {}) { return (components) => new PeerExchangeDiscovery(components, options); } //# sourceMappingURL=peer_exchange_discovery.js.map