'use strict' const errcode = require('err-code') const debug = require('debug') const log = debug('libp2p:peer-store') log.error = debug('libp2p:peer-store:error') const { EventEmitter } = require('events') const PeerId = require('peer-id') const PeerInfo = require('peer-info') const { ERR_INVALID_PARAMETERS } = require('../errors') /** * Responsible for managing known peers, as well as their addresses and metadata * @fires PeerStore#peer Emitted when a peer is connected to this node * @fires PeerStore#change:protocols * @fires PeerStore#change:multiaddrs */ class PeerStore extends EventEmitter { constructor () { super() /** * Map of peers * * @type {Map} */ this.peers = new Map() /** * Map known peers to their known multiaddrs. * @type {Map} */ this.addressBook = new Map() /** * Map known peers to their known supported protocols. * @type {Map} */ this.protoBook = new Map() } /** * Stores the peerInfo of a new peer. * If already exist, its info is updated. If `silent` is set to * true, no 'peer' event will be emitted. This can be useful if you * are already in the process of dialing the peer. The peer is technically * known, but may not have been added to the PeerStore yet. * @param {PeerInfo} peerInfo * @param {object} [options] * @param {boolean} [options.silent] (Default=false) * @return {PeerInfo} */ put (peerInfo, options = { silent: false }) { if (!PeerInfo.isPeerInfo(peerInfo)) { throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS) } let peer // Already know the peer? if (this.has(peerInfo.id)) { peer = this.update(peerInfo) } else { peer = this.add(peerInfo) // Emit the peer if silent = false !options.silent && this.emit('peer', peerInfo) } return peer } /** * Add a new peer to the store. * @param {PeerInfo} peerInfo * @return {PeerInfo} */ add (peerInfo) { if (!PeerInfo.isPeerInfo(peerInfo)) { throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS) } // Create new instance and add values to it const newPeerInfo = new PeerInfo(peerInfo.id) peerInfo.multiaddrs.forEach((ma) => newPeerInfo.multiaddrs.add(ma)) peerInfo.protocols.forEach((p) => newPeerInfo.protocols.add(p)) // const connectedMa = peerInfo.isConnected() // connectedMa && newPeerInfo.connect(connectedMa) const peerProxy = new Proxy(newPeerInfo, { set: (obj, prop, value) => { if (prop === 'multiaddrs') { this.emit('change:multiaddrs', { peerInfo: obj, multiaddrs: value.toArray() }) } else if (prop === 'protocols') { this.emit('change:protocols', { peerInfo: obj, protocols: Array.from(value) }) } return Reflect.set(...arguments) } }) this.peers.set(peerInfo.id.toB58String(), peerProxy) return peerProxy } /** * Updates an already known peer. * @param {PeerInfo} peerInfo * @return {PeerInfo} */ update (peerInfo) { if (!PeerInfo.isPeerInfo(peerInfo)) { throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS) } const id = peerInfo.id.toB58String() const recorded = this.peers.get(id) // pass active connection state // const ma = peerInfo.isConnected() // if (ma) { // recorded.connect(ma) // } // Verify new multiaddrs // TODO: better track added and removed multiaddrs const multiaddrsIntersection = [ ...recorded.multiaddrs.toArray() ].filter((m) => peerInfo.multiaddrs.has(m)) if (multiaddrsIntersection.length !== peerInfo.multiaddrs.size || multiaddrsIntersection.length !== recorded.multiaddrs.size) { for (const ma of peerInfo.multiaddrs.toArray()) { recorded.multiaddrs.add(ma) } this.emit('change:multiaddrs', { peerInfo: recorded, multiaddrs: recorded.multiaddrs.toArray() }) } // Update protocols // TODO: better track added and removed protocols const protocolsIntersection = new Set( [...recorded.protocols].filter((p) => peerInfo.protocols.has(p)) ) if (protocolsIntersection.size !== peerInfo.protocols.size || protocolsIntersection.size !== recorded.protocols.size) { for (const protocol of peerInfo.protocols) { recorded.protocols.add(protocol) } this.emit('change:protocols', { peerInfo: recorded, protocols: Array.from(recorded.protocols) }) } // Add the public key if missing if (!recorded.id.pubKey && peerInfo.id.pubKey) { recorded.id.pubKey = peerInfo.id.pubKey } return recorded } /** * Get the info to the given id. * @param {PeerId|string} peerId b58str id * @returns {PeerInfo} */ get (peerId) { // TODO: deprecate this and just accept `PeerId` instances if (PeerId.isPeerId(peerId)) { peerId = peerId.toB58String() } return this.peers.get(peerId) } /** * Has the info to the given id. * @param {PeerId|string} peerId b58str id * @returns {boolean} */ has (peerId) { // TODO: deprecate this and just accept `PeerId` instances if (PeerId.isPeerId(peerId)) { peerId = peerId.toB58String() } return this.peers.has(peerId) } /** * Removes the Peer with the matching `peerId` from the PeerStore * @param {PeerId|string} peerId b58str id * @returns {boolean} true if found and removed */ remove (peerId) { // TODO: deprecate this and just accept `PeerId` instances if (PeerId.isPeerId(peerId)) { peerId = peerId.toB58String() } return this.peers.delete(peerId) } /** * Completely replaces the existing peers metadata with the given `peerInfo` * @param {PeerInfo} peerInfo * @returns {void} */ replace (peerInfo) { if (!PeerInfo.isPeerInfo(peerInfo)) { throw errcode(new Error('peerInfo must be an instance of peer-info'), ERR_INVALID_PARAMETERS) } this.remove(peerInfo.id.toB58String()) this.add(peerInfo) // This should be cleaned up in PeerStore v2 this.emit('change:multiaddrs', { peerInfo, multiaddrs: peerInfo.multiaddrs.toArray() }) this.emit('change:protocols', { peerInfo, protocols: Array.from(peerInfo.protocols) }) } /** * Returns the known multiaddrs for a given `PeerInfo`. All returned multiaddrs * will include the encapsulated `PeerId` of the peer. * @param {PeerInfo} peer * @returns {Array} */ multiaddrsForPeer (peer) { return this.put(peer, true).multiaddrs.toArray().map(addr => { const idString = addr.getPeerId() if (idString && idString === peer.id.toB58String()) return addr return addr.encapsulate(`/p2p/${peer.id.toB58String()}`) }) } } module.exports = PeerStore