UNPKG

libp2p

Version:

JavaScript implementation of libp2p, a modular peer to peer network stack

167 lines (145 loc) 4.57 kB
import { KEEP_ALIVE } from '@libp2p/interface' import { PeerQueue } from '@libp2p/utils/peer-queue' import pRetry from 'p-retry' import { MAX_PARALLEL_RECONNECTS } from './constants.js' import type { ComponentLogger, Libp2pEvents, Logger, Metrics, Peer, PeerId, PeerStore, Startable } from '@libp2p/interface' import type { ConnectionManager } from '@libp2p/interface-internal' import type { TypedEventTarget } from 'main-event' export interface ReconnectQueueComponents { connectionManager: ConnectionManager events: TypedEventTarget<Libp2pEvents> peerStore: PeerStore logger: ComponentLogger metrics?: Metrics } export interface ReconnectQueueInit { retries?: number retryInterval?: number backoffFactor?: number maxParallelReconnects?: number } /** * When peers tagged with `KEEP_ALIVE` disconnect, this component attempts to * redial them */ export class ReconnectQueue implements Startable { private readonly log: Logger private readonly queue: PeerQueue private started: boolean private readonly peerStore: PeerStore private readonly retries: number private readonly retryInterval?: number private readonly backoffFactor?: number private readonly connectionManager: ConnectionManager private readonly events: TypedEventTarget<Libp2pEvents> constructor (components: ReconnectQueueComponents, init: ReconnectQueueInit = {}) { this.log = components.logger.forComponent('libp2p:reconnect-queue') this.peerStore = components.peerStore this.connectionManager = components.connectionManager this.queue = new PeerQueue({ concurrency: init.maxParallelReconnects ?? MAX_PARALLEL_RECONNECTS, metricName: 'libp2p_reconnect_queue', metrics: components.metrics }) this.started = false this.retries = init.retries ?? 5 this.backoffFactor = init.backoffFactor this.retryInterval = init.retryInterval this.events = components.events components.events.addEventListener('peer:disconnect', (evt) => { this.maybeReconnect(evt.detail) .catch(err => { this.log.error('failed to maybe reconnect to %p - %e', evt.detail, err) }) }) } private async maybeReconnect (peerId: PeerId): Promise<void> { if (!this.started) { return } const peer = await this.peerStore.get(peerId) if (!hasKeepAliveTag(peer)) { return } if (this.queue.has(peerId)) { return } this.queue.add(async (options) => { await pRetry(async (attempt) => { if (!this.started) { return } try { await this.connectionManager.openConnection(peerId, { signal: options?.signal }) } catch (err) { this.log('reconnecting to %p attempt %d of %d failed - %e', peerId, attempt, this.retries, err) throw err } }, { signal: options?.signal, retries: this.retries, factor: this.backoffFactor, minTimeout: this.retryInterval }) }, { peerId }) .catch(async err => { this.log.error('failed to reconnect to %p - %e', peerId, err) const tags: Record<string, undefined> = {} ;[...peer.tags.keys()].forEach(key => { if (key.startsWith(KEEP_ALIVE)) { tags[key] = undefined } }) await this.peerStore.merge(peerId, { tags }) this.events.safeDispatchEvent('peer:reconnect-failure', { detail: peerId }) }) .catch(async err => { this.log.error('failed to remove keep-alive tag from %p - %e', peerId, err) }) } start (): void { this.started = true } async afterStart (): Promise<void> { // re-connect to any peers with the KEEP_ALIVE tag void Promise.resolve() .then(async () => { const keepAlivePeers: Peer[] = await this.peerStore.all({ filters: [ (peer) => hasKeepAliveTag(peer) ] }) await Promise.all( keepAlivePeers.map(async peer => { await this.connectionManager.openConnection(peer.id) .catch(err => { this.log.error(err) }) }) ) }) .catch(err => { this.log.error(err) }) } stop (): void { this.started = false this.queue.abort() } } function hasKeepAliveTag (peer: Peer): boolean { for (const tag of peer.tags.keys()) { if (tag.startsWith(KEEP_ALIVE)) { return true } } return false }