UNPKG

libp2p

Version:

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

288 lines • 10.8 kB
import { FaultTolerance, InvalidParametersError, NotStartedError } from '@libp2p/interface'; import { trackedMap } from '@libp2p/utils/tracked-map'; import { IP4, IP6 } from '@multiformats/multiaddr-matcher'; import { CustomProgressEvent } from 'progress-events'; import { TransportUnavailableError, UnsupportedListenAddressError, UnsupportedListenAddressesError } from './errors.js'; export class DefaultTransportManager { log; components; transports; listeners; faultTolerance; started; constructor(components, init = {}) { this.log = components.logger.forComponent('libp2p:transports'); this.components = components; this.started = false; this.transports = trackedMap({ name: 'libp2p_transport_manager_transports', metrics: this.components.metrics }); this.listeners = trackedMap({ name: 'libp2p_transport_manager_listeners', metrics: this.components.metrics }); this.faultTolerance = init.faultTolerance ?? FaultTolerance.FATAL_ALL; } [Symbol.toStringTag] = '@libp2p/transport-manager'; /** * Adds a `Transport` to the manager */ add(transport) { const tag = transport[Symbol.toStringTag]; if (tag == null) { throw new InvalidParametersError('Transport must have a valid tag'); } if (this.transports.has(tag)) { throw new InvalidParametersError(`There is already a transport with the tag ${tag}`); } this.log('adding transport %s', tag); this.transports.set(tag, transport); if (!this.listeners.has(tag)) { this.listeners.set(tag, []); } } isStarted() { return this.started; } start() { this.started = true; } async afterStart() { // Listen on the provided transports for the provided addresses const addrs = this.components.addressManager.getListenAddrs(); await this.listen(addrs); } /** * Stops all listeners */ async stop() { const tasks = []; for (const [key, listeners] of this.listeners) { this.log('closing listeners for %s', key); while (listeners.length > 0) { const listener = listeners.pop(); if (listener == null) { continue; } tasks.push(listener.close()); } } await Promise.all(tasks); this.log('all listeners closed'); for (const key of this.listeners.keys()) { this.listeners.set(key, []); } this.started = false; } /** * Dials the given Multiaddr over it's supported transport */ async dial(ma, options) { const transport = this.dialTransportForMultiaddr(ma); if (transport == null) { throw new TransportUnavailableError(`No transport available for address ${String(ma)}`); } options?.onProgress?.(new CustomProgressEvent('transport-manager:selected-transport', transport[Symbol.toStringTag])); // @ts-expect-error the transport has a typed onProgress option but we // can't predict what transport implementation we selected so all we can // do is pass the onProgress handler in and hope for the best return transport.dial(ma, { ...options, upgrader: this.components.upgrader }); } /** * Returns all Multiaddr's the listeners are using */ getAddrs() { let addrs = []; for (const listeners of this.listeners.values()) { for (const listener of listeners) { addrs = [...addrs, ...listener.getAddrs()]; } } return addrs; } /** * Returns all the transports instances */ getTransports() { return Array.of(...this.transports.values()); } /** * Returns all the listener instances */ getListeners() { return Array.of(...this.listeners.values()).flat(); } /** * Finds a transport that matches the given Multiaddr */ dialTransportForMultiaddr(ma) { for (const transport of this.transports.values()) { const addrs = transport.dialFilter([ma]); if (addrs.length > 0) { return transport; } } } /** * Finds a transport that matches the given Multiaddr */ listenTransportForMultiaddr(ma) { for (const transport of this.transports.values()) { const addrs = transport.listenFilter([ma]); if (addrs.length > 0) { return transport; } } } /** * Starts listeners for each listen Multiaddr */ async listen(addrs) { if (!this.isStarted()) { throw new NotStartedError('Not started'); } if (addrs == null || addrs.length === 0) { this.log('no addresses were provided for listening, this node is dial only'); return; } // track IPv4/IPv6 results - if we succeed on IPv4 but all IPv6 attempts // fail then we are probably on a network without IPv6 support const listenStats = { errors: new Map(), ipv4: { success: 0, attempts: 0 }, ipv6: { success: 0, attempts: 0 } }; addrs.forEach(ma => { listenStats.errors.set(ma.toString(), new UnsupportedListenAddressError()); }); const tasks = []; for (const [key, transport] of this.transports.entries()) { const supportedAddrs = transport.listenFilter(addrs); // For each supported multiaddr, create a listener for (const addr of supportedAddrs) { this.log('creating listener for %s on %a', key, addr); const listener = transport.createListener({ upgrader: this.components.upgrader }); let listeners = this.listeners.get(key) ?? []; if (listeners == null) { listeners = []; this.listeners.set(key, listeners); } listeners.push(listener); // Track listen/close events listener.addEventListener('listening', () => { this.components.events.safeDispatchEvent('transport:listening', { detail: listener }); }); listener.addEventListener('close', () => { const index = listeners.findIndex(l => l === listener); // remove the listener listeners.splice(index, 1); this.components.events.safeDispatchEvent('transport:close', { detail: listener }); }); // track IPv4/IPv6 support if (IP4.matches(addr)) { listenStats.ipv4.attempts++; } else if (IP6.matches(addr)) { listenStats.ipv6.attempts++; } // We need to attempt to listen on everything tasks.push(listener.listen(addr) .then(() => { listenStats.errors.delete(addr.toString()); if (IP4.matches(addr)) { listenStats.ipv4.success++; } if (IP6.matches(addr)) { listenStats.ipv6.success++; } }, (err) => { this.log.error('transport %s could not listen on address %a - %e', key, addr, err); listenStats.errors.set(addr.toString(), err); throw err; })); } } const results = await Promise.allSettled(tasks); // listening on all addresses, all good if (results.length > 0 && results.every(res => res.status === 'fulfilled')) { return; } // detect lack of IPv6 support on the current network - if we tried to // listen on IPv4 and IPv6 addresses, and all IPv4 addresses succeeded but // all IPv6 addresses fail, then we can assume there's no IPv6 here if (this.ipv6Unsupported(listenStats)) { this.log('all IPv4 addresses succeed but all IPv6 failed'); return; } if (this.faultTolerance === FaultTolerance.NO_FATAL) { // ok to be dial-only this.log('failed to listen on any address but fault tolerance allows this'); return; } // if a configured address was not able to be listened on, throw an error throw new UnsupportedListenAddressesError(`Some configured addresses failed to be listened on, you may need to remove one or more listen addresses from your configuration or set \`transportManager.faultTolerance\` to NO_FATAL:\n${[...listenStats.errors.entries()].map(([addr, err]) => { return ` ${addr}: ${`${err.stack ?? err}`.split('\n').join('\n ')} `; }).join('')}`); } ipv6Unsupported(listenStats) { if (listenStats.ipv4.attempts === 0 || listenStats.ipv6.attempts === 0) { return false; } const allIpv4Succeeded = listenStats.ipv4.attempts === listenStats.ipv4.success; const allIpv6Failed = listenStats.ipv6.success === 0; return allIpv4Succeeded && allIpv6Failed; } /** * Removes the given transport from the manager. * If a transport has any running listeners, they will be closed. */ async remove(key) { const listeners = this.listeners.get(key) ?? []; this.log.trace('removing transport %s', key); // Close any running listeners const tasks = []; this.log.trace('closing listeners for %s', key); while (listeners.length > 0) { const listener = listeners.pop(); if (listener == null) { continue; } tasks.push(listener.close()); } await Promise.all(tasks); this.transports.delete(key); this.listeners.delete(key); } /** * Removes all transports from the manager. * If any listeners are running, they will be closed. * * @async */ async removeAll() { const tasks = []; for (const key of this.transports.keys()) { tasks.push(this.remove(key)); } await Promise.all(tasks); } } //# sourceMappingURL=transport-manager.js.map