js-libp2p/src/connection-manager/auto-dialler.ts
Alex Potsides d4dd664071
feat!: update libp2p interfaces (#1252)
BREAKING CHANGE: uses new single-issue libp2p interface modules
2022-06-15 18:30:39 +01:00

155 lines
4.1 KiB
TypeScript

import { logger } from '@libp2p/logger'
import mergeOptions from 'merge-options'
// @ts-expect-error retimer does not have types
import retimer from 'retimer'
import all from 'it-all'
import { pipe } from 'it-pipe'
import filter from 'it-filter'
import sort from 'it-sort'
import type { Startable } from '@libp2p/interfaces/startable'
import type { Components } from '@libp2p/components'
const log = logger('libp2p:connection-manager:auto-dialler')
export interface AutoDiallerInit {
/**
* Should preemptively guarantee connections are above the low watermark
*/
enabled?: boolean
/**
* The minimum number of connections to avoid pruning
*/
minConnections?: number
/**
* How often, in milliseconds, it should preemptively guarantee connections are above the low watermark
*/
autoDialInterval?: number
}
const defaultOptions: Partial<AutoDiallerInit> = {
enabled: true,
minConnections: 0,
autoDialInterval: 10000
}
export class AutoDialler implements Startable {
private readonly components: Components
private readonly options: Required<AutoDiallerInit>
private running: boolean
private autoDialTimeout?: ReturnType<retimer>
/**
* Proactively tries to connect to known peers stored in the PeerStore.
* It will keep the number of connections below the upper limit and sort
* the peers to connect based on wether we know their keys and protocols.
*/
constructor (components: Components, init: AutoDiallerInit) {
this.components = components
this.options = mergeOptions.call({ ignoreUndefined: true }, defaultOptions, init)
this.running = false
this._autoDial = this._autoDial.bind(this)
log('options: %j', this.options)
}
isStarted () {
return this.running
}
/**
* Starts the auto dialer
*/
async start () {
if (!this.options.enabled) {
log('not enabled')
return
}
this.running = true
void this._autoDial().catch(err => {
log.error('could start autodial', err)
})
log('started')
}
/**
* Stops the auto dialler
*/
async stop () {
if (!this.options.enabled) {
log('not enabled')
return
}
this.running = false
if (this.autoDialTimeout != null) {
this.autoDialTimeout.clear()
}
log('stopped')
}
async _autoDial () {
if (this.autoDialTimeout != null) {
this.autoDialTimeout.clear()
}
const minConnections = this.options.minConnections
// Already has enough connections
if (this.components.getConnectionManager().getConnections().length >= minConnections) {
this.autoDialTimeout = retimer(this._autoDial, this.options.autoDialInterval)
return
}
// Sort peers on whether we know protocols or public keys for them
const allPeers = await this.components.getPeerStore().all()
const peers = await pipe(
// shuffle the peers
allPeers.sort(() => Math.random() > 0.5 ? 1 : -1),
(source) => filter(source, (peer) => !peer.id.equals(this.components.getPeerId())),
(source) => sort(source, (a, b) => {
if (b.protocols.length > a.protocols.length) {
return 1
} else if (b.id.publicKey != null && a.id.publicKey == null) {
return 1
}
return -1
}),
async (source) => await all(source)
)
for (let i = 0; this.running && i < peers.length && this.components.getConnectionManager().getConnections().length < minConnections; i++) {
// Connection Manager was stopped during async dial
if (!this.running) {
return
}
const peer = peers[i]
if (this.components.getConnectionManager().getConnections(peer.id).length === 0) {
log('connecting to a peerStore stored peer %p', peer.id)
try {
await this.components.getConnectionManager().openConnection(peer.id)
} catch (err: any) {
log.error('could not connect to peerStore stored peer', err)
}
}
}
// Connection Manager was stopped
if (!this.running) {
return
}
this.autoDialTimeout = retimer(this._autoDial, this.options.autoDialInterval)
}
}