2019-10-21 16:53:58 +02:00
|
|
|
'use strict'
|
|
|
|
|
|
|
|
const multiaddr = require('multiaddr')
|
|
|
|
const errCode = require('err-code')
|
2019-12-10 13:48:34 +01:00
|
|
|
const TimeoutController = require('timeout-abort-controller')
|
2019-12-04 15:59:01 +01:00
|
|
|
const anySignal = require('any-signal')
|
2019-12-15 17:33:16 +01:00
|
|
|
const PeerId = require('peer-id')
|
2019-10-21 16:53:58 +02:00
|
|
|
const debug = require('debug')
|
|
|
|
const log = debug('libp2p:dialer')
|
|
|
|
log.error = debug('libp2p:dialer:error')
|
2019-12-04 23:04:43 +01:00
|
|
|
const { DialRequest } = require('./dial-request')
|
2019-10-21 16:53:58 +02:00
|
|
|
|
2019-12-04 23:04:43 +01:00
|
|
|
const { codes } = require('../errors')
|
2019-10-21 16:53:58 +02:00
|
|
|
const {
|
2019-12-03 10:28:52 +01:00
|
|
|
DIAL_TIMEOUT,
|
2019-10-21 16:53:58 +02:00
|
|
|
MAX_PARALLEL_DIALS,
|
2019-12-04 13:58:23 +01:00
|
|
|
MAX_PER_PEER_DIALS
|
2019-12-04 23:04:43 +01:00
|
|
|
} = require('../constants')
|
2019-10-21 16:53:58 +02:00
|
|
|
|
|
|
|
class Dialer {
|
|
|
|
/**
|
|
|
|
* @constructor
|
|
|
|
* @param {object} options
|
|
|
|
* @param {TransportManager} options.transportManager
|
2019-11-26 16:40:04 +01:00
|
|
|
* @param {Peerstore} peerStore
|
2019-10-21 16:53:58 +02:00
|
|
|
* @param {number} options.concurrency Number of max concurrent dials. Defaults to `MAX_PARALLEL_DIALS`
|
|
|
|
* @param {number} options.timeout How long a dial attempt is allowed to take. Defaults to `DIAL_TIMEOUT`
|
|
|
|
*/
|
|
|
|
constructor ({
|
|
|
|
transportManager,
|
2019-11-26 16:40:04 +01:00
|
|
|
peerStore,
|
2019-10-21 16:53:58 +02:00
|
|
|
concurrency = MAX_PARALLEL_DIALS,
|
2019-12-03 10:28:52 +01:00
|
|
|
timeout = DIAL_TIMEOUT,
|
2019-12-04 13:58:23 +01:00
|
|
|
perPeerLimit = MAX_PER_PEER_DIALS
|
2019-10-21 16:53:58 +02:00
|
|
|
}) {
|
|
|
|
this.transportManager = transportManager
|
2019-11-26 16:40:04 +01:00
|
|
|
this.peerStore = peerStore
|
2019-10-21 16:53:58 +02:00
|
|
|
this.concurrency = concurrency
|
|
|
|
this.timeout = timeout
|
2019-12-03 10:28:52 +01:00
|
|
|
this.perPeerLimit = perPeerLimit
|
|
|
|
this.tokens = [...new Array(concurrency)].map((_, index) => index)
|
2019-12-15 17:33:16 +01:00
|
|
|
this._pendingDials = new Map()
|
2019-12-10 13:48:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clears any pending dials
|
|
|
|
*/
|
|
|
|
destroy () {
|
|
|
|
for (const dial of this._pendingDials.values()) {
|
|
|
|
try {
|
|
|
|
dial.controller.abort()
|
|
|
|
} catch (err) {
|
|
|
|
log.error(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this._pendingDials.clear()
|
2019-10-21 16:53:58 +02:00
|
|
|
}
|
|
|
|
|
2019-12-03 10:28:52 +01:00
|
|
|
/**
|
2019-12-15 17:33:16 +01:00
|
|
|
* Connects to a given `PeerId` or `Multiaddr` by dialing all of its known addresses.
|
|
|
|
* The dial to the first address that is successfully able to upgrade a connection
|
|
|
|
* will be used.
|
2019-12-03 10:28:52 +01:00
|
|
|
*
|
2020-04-09 16:07:18 +02:00
|
|
|
* @param {PeerId|Multiaddr} peerId The peer to dial
|
2019-12-03 10:28:52 +01:00
|
|
|
* @param {object} [options]
|
|
|
|
* @param {AbortSignal} [options.signal] An AbortController signal
|
|
|
|
* @returns {Promise<Connection>}
|
|
|
|
*/
|
2020-04-09 16:07:18 +02:00
|
|
|
async connectToPeer (peerId, options = {}) {
|
|
|
|
const dialTarget = this._createDialTarget(peerId)
|
2019-12-15 17:33:16 +01:00
|
|
|
if (dialTarget.addrs.length === 0) {
|
2020-02-11 16:32:40 +01:00
|
|
|
throw errCode(new Error('The dial request has no addresses'), codes.ERR_NO_VALID_ADDRESSES)
|
2019-12-04 16:27:33 +01:00
|
|
|
}
|
2019-12-15 17:33:16 +01:00
|
|
|
const pendingDial = this._pendingDials.get(dialTarget.id) || this._createPendingDial(dialTarget, options)
|
2019-12-06 17:45:29 +01:00
|
|
|
|
2019-10-21 16:53:58 +02:00
|
|
|
try {
|
2019-12-15 17:33:16 +01:00
|
|
|
const connection = await pendingDial.promise
|
|
|
|
log('dial succeeded to %s', dialTarget.id)
|
|
|
|
return connection
|
2019-10-21 16:53:58 +02:00
|
|
|
} catch (err) {
|
2019-12-04 16:59:38 +01:00
|
|
|
// Error is a timeout
|
2019-12-15 17:33:16 +01:00
|
|
|
if (pendingDial.controller.signal.aborted) {
|
2019-12-04 17:32:11 +01:00
|
|
|
err.code = codes.ERR_TIMEOUT
|
2019-12-04 16:59:38 +01:00
|
|
|
}
|
2019-12-03 10:28:52 +01:00
|
|
|
log.error(err)
|
2019-10-21 16:53:58 +02:00
|
|
|
throw err
|
2019-12-06 17:45:29 +01:00
|
|
|
} finally {
|
2019-12-15 17:33:16 +01:00
|
|
|
pendingDial.destroy()
|
2019-10-21 16:53:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-12-15 17:33:16 +01:00
|
|
|
* @typedef DialTarget
|
|
|
|
* @property {string} id
|
|
|
|
* @property {Multiaddr[]} addrs
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a DialTarget. The DialTarget is used to create and track
|
|
|
|
* the DialRequest to a given peer.
|
|
|
|
* @private
|
2020-04-09 16:07:18 +02:00
|
|
|
* @param {PeerId|Multiaddr} peer A PeerId or Multiaddr
|
2019-12-15 17:33:16 +01:00
|
|
|
* @returns {DialTarget}
|
|
|
|
*/
|
|
|
|
_createDialTarget (peer) {
|
|
|
|
const dialable = Dialer.getDialable(peer)
|
|
|
|
if (multiaddr.isMultiaddr(dialable)) {
|
|
|
|
return {
|
|
|
|
id: dialable.toString(),
|
|
|
|
addrs: [dialable]
|
|
|
|
}
|
|
|
|
}
|
2020-04-09 16:07:18 +02:00
|
|
|
|
|
|
|
dialable.multiaddrs && this.peerStore.addressBook.add(dialable.id, Array.from(dialable.multiaddrs))
|
|
|
|
const addrs = this.peerStore.addressBook.getMultiaddrsForPeer(dialable.id)
|
|
|
|
|
2019-12-15 17:33:16 +01:00
|
|
|
return {
|
2020-01-22 11:47:30 +01:00
|
|
|
id: dialable.id.toB58String(),
|
2019-12-15 17:33:16 +01:00
|
|
|
addrs
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef PendingDial
|
|
|
|
* @property {DialRequest} dialRequest
|
|
|
|
* @property {TimeoutController} controller
|
|
|
|
* @property {Promise} promise
|
|
|
|
* @property {function():void} destroy
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a PendingDial that wraps the underlying DialRequest
|
|
|
|
* @private
|
|
|
|
* @param {DialTarget} dialTarget
|
2019-10-21 16:53:58 +02:00
|
|
|
* @param {object} [options]
|
|
|
|
* @param {AbortSignal} [options.signal] An AbortController signal
|
2019-12-15 17:33:16 +01:00
|
|
|
* @returns {PendingDial}
|
2019-10-21 16:53:58 +02:00
|
|
|
*/
|
2019-12-15 17:33:16 +01:00
|
|
|
_createPendingDial (dialTarget, options) {
|
|
|
|
const dialAction = (addr, options) => {
|
2020-02-11 16:32:40 +01:00
|
|
|
if (options.signal.aborted) throw errCode(new Error('already aborted'), codes.ERR_ALREADY_ABORTED)
|
2019-12-15 17:33:16 +01:00
|
|
|
return this.transportManager.dial(addr, options)
|
|
|
|
}
|
2019-11-26 16:40:04 +01:00
|
|
|
|
2019-12-15 17:33:16 +01:00
|
|
|
const dialRequest = new DialRequest({
|
|
|
|
addrs: dialTarget.addrs,
|
|
|
|
dialAction,
|
|
|
|
dialer: this
|
|
|
|
})
|
|
|
|
|
|
|
|
// Combine the timeout signal and options.signal, if provided
|
|
|
|
const timeoutController = new TimeoutController(this.timeout)
|
|
|
|
const signals = [timeoutController.signal]
|
|
|
|
options.signal && signals.push(options.signal)
|
|
|
|
const signal = anySignal(signals)
|
2019-12-03 10:28:52 +01:00
|
|
|
|
2019-12-15 17:33:16 +01:00
|
|
|
const pendingDial = {
|
|
|
|
dialRequest,
|
|
|
|
controller: timeoutController,
|
|
|
|
promise: dialRequest.run({ ...options, signal }),
|
|
|
|
destroy: () => {
|
|
|
|
timeoutController.clear()
|
|
|
|
this._pendingDials.delete(dialTarget.id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this._pendingDials.set(dialTarget.id, pendingDial)
|
|
|
|
return pendingDial
|
2019-12-03 10:28:52 +01:00
|
|
|
}
|
2019-10-21 16:53:58 +02:00
|
|
|
|
2019-12-03 10:28:52 +01:00
|
|
|
getTokens (num) {
|
|
|
|
const total = Math.min(num, this.perPeerLimit, this.tokens.length)
|
|
|
|
const tokens = this.tokens.splice(0, total)
|
|
|
|
log('%d tokens request, returning %d, %d remaining', num, total, this.tokens.length)
|
|
|
|
return tokens
|
|
|
|
}
|
|
|
|
|
|
|
|
releaseToken (token) {
|
2019-12-04 22:58:52 +01:00
|
|
|
// Guard against duplicate releases
|
|
|
|
if (this.tokens.indexOf(token) > -1) return
|
2019-12-03 10:28:52 +01:00
|
|
|
log('token %d released', token)
|
|
|
|
this.tokens.push(token)
|
2019-10-21 16:53:58 +02:00
|
|
|
}
|
2019-12-15 17:33:16 +01:00
|
|
|
|
2020-04-09 16:07:18 +02:00
|
|
|
/**
|
|
|
|
* PeerInfo object
|
|
|
|
* @typedef {Object} peerInfo
|
|
|
|
* @property {Multiaddr} multiaddr peer multiaddr.
|
|
|
|
* @property {PeerId} id peer id.
|
|
|
|
*/
|
|
|
|
|
2019-12-15 17:33:16 +01:00
|
|
|
/**
|
|
|
|
* Converts the given `peer` into a `PeerInfo` or `Multiaddr`.
|
|
|
|
* @static
|
2020-04-09 16:07:18 +02:00
|
|
|
* @param {PeerId|Multiaddr|string} peer
|
|
|
|
* @returns {peerInfo|Multiaddr}
|
2019-12-15 17:33:16 +01:00
|
|
|
*/
|
|
|
|
static getDialable (peer) {
|
|
|
|
if (typeof peer === 'string') {
|
|
|
|
peer = multiaddr(peer)
|
|
|
|
}
|
|
|
|
|
2020-04-09 16:07:18 +02:00
|
|
|
let addrs
|
2019-12-15 17:33:16 +01:00
|
|
|
if (multiaddr.isMultiaddr(peer)) {
|
2020-04-09 16:07:18 +02:00
|
|
|
addrs = new Set([peer]) // TODO: after peer-info removal, a Set should not be needed
|
2019-12-15 17:33:16 +01:00
|
|
|
try {
|
|
|
|
peer = PeerId.createFromCID(peer.getPeerId())
|
|
|
|
} catch (err) {
|
2020-02-11 16:32:40 +01:00
|
|
|
throw errCode(new Error('The multiaddr did not contain a valid peer id'), codes.ERR_INVALID_PEER)
|
2019-12-15 17:33:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (PeerId.isPeerId(peer)) {
|
2020-04-09 16:07:18 +02:00
|
|
|
peer = {
|
|
|
|
id: peer,
|
|
|
|
multiaddrs: addrs
|
|
|
|
}
|
2019-12-15 17:33:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return peer
|
|
|
|
}
|
2019-10-21 16:53:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Dialer
|