libp2p
Version:
JavaScript implementation of libp2p, a modular peer to peer network stack
269 lines • 11.8 kB
JavaScript
import { connectionSymbol, LimitedConnectionError, ConnectionClosedError, TooManyOutboundProtocolStreamsError, TooManyInboundProtocolStreamsError, StreamCloseEvent } from '@libp2p/interface';
import * as mss from '@libp2p/multistream-select';
import { CODE_P2P } from '@multiformats/multiaddr';
import { setMaxListeners, TypedEventEmitter } from 'main-event';
import { CONNECTION_CLOSE_TIMEOUT, PROTOCOL_NEGOTIATION_TIMEOUT } from "./connection-manager/constants.defaults.js";
import { isDirect } from "./connection-manager/utils.js";
import { MuxerUnavailableError } from "./errors.js";
import { DEFAULT_MAX_INBOUND_STREAMS, DEFAULT_MAX_OUTBOUND_STREAMS } from "./registrar.js";
/**
* An implementation of the js-libp2p connection.
* Any libp2p transport should use an upgrader to return this connection.
*/
export class Connection extends TypedEventEmitter {
id;
remoteAddr;
remotePeer;
direction;
timeline;
direct;
multiplexer;
encryption;
limits;
log;
maConn;
muxer;
components;
outboundStreamProtocolNegotiationTimeout;
inboundStreamProtocolNegotiationTimeout;
closeTimeout;
constructor(components, init) {
super();
this.components = components;
this.id = init.id;
this.remoteAddr = init.maConn.remoteAddr;
this.remotePeer = init.remotePeer;
this.direction = init.direction ?? 'outbound';
this.timeline = init.maConn.timeline;
this.encryption = init.cryptoProtocol;
this.limits = init.limits;
this.maConn = init.maConn;
this.log = init.maConn.log;
this.outboundStreamProtocolNegotiationTimeout = init.outboundStreamProtocolNegotiationTimeout ?? PROTOCOL_NEGOTIATION_TIMEOUT;
this.inboundStreamProtocolNegotiationTimeout = init.inboundStreamProtocolNegotiationTimeout ?? PROTOCOL_NEGOTIATION_TIMEOUT;
this.closeTimeout = init.closeTimeout ?? CONNECTION_CLOSE_TIMEOUT;
this.direct = isDirect(init.maConn.remoteAddr);
this.onIncomingStream = this.onIncomingStream.bind(this);
if (this.remoteAddr.getComponents().find(component => component.code === CODE_P2P) == null) {
this.remoteAddr = this.remoteAddr.encapsulate(`/p2p/${this.remotePeer}`);
}
if (init.muxer != null) {
this.multiplexer = init.muxer.protocol;
this.muxer = init.muxer;
this.muxer.addEventListener('stream', this.onIncomingStream);
}
this.maConn.addEventListener('close', (evt) => {
this.dispatchEvent(new StreamCloseEvent(evt.local, evt.error));
});
}
[Symbol.toStringTag] = 'Connection';
[connectionSymbol] = true;
get streams() {
return this.muxer?.streams ?? [];
}
get status() {
return this.maConn.status;
}
/**
* Create a new stream over this connection
*/
newStream = async (protocols, options = {}) => {
if (this.muxer == null) {
throw new MuxerUnavailableError('Connection is not multiplexed');
}
if (this.muxer.status !== 'open') {
throw new ConnectionClosedError(`The connection muxer is "${this.muxer.status}" and not "open"`);
}
if (this.maConn.status !== 'open') {
throw new ConnectionClosedError(`The connection is "${this.status}" and not "open"`);
}
if (this.limits != null && options?.runOnLimitedConnection !== true) {
throw new LimitedConnectionError('Cannot open protocol stream on limited connection');
}
if (!Array.isArray(protocols)) {
protocols = [protocols];
}
this.log.trace('starting new stream for protocols %s', protocols);
const muxedStream = await this.muxer.createStream({
...options,
// most underlying transports only support negotiating a single protocol
// so only pass the early protocol if a single protocol has been requested
// otherwise fall back to mss
protocol: protocols.length === 1 ? protocols[0] : undefined
});
this.log.trace('started new stream %s for protocols %s', muxedStream.id, protocols);
try {
if (options.signal == null) {
muxedStream.log('no abort signal was passed while trying to negotiate protocols %s falling back to default timeout', protocols);
const signal = AbortSignal.timeout(this.outboundStreamProtocolNegotiationTimeout);
setMaxListeners(Infinity, signal);
options = {
...options,
signal
};
}
if (muxedStream.protocol === '') {
muxedStream.log.trace('selecting protocol from protocols %s', protocols);
muxedStream.protocol = await mss.select(muxedStream, protocols, options);
muxedStream.log('negotiated protocol %s', muxedStream.protocol);
}
else {
muxedStream.log('pre-negotiated protocol %s', muxedStream.protocol);
}
const outgoingLimit = findOutgoingStreamLimit(muxedStream.protocol, this.components.registrar, options);
const streamCount = countStreams(muxedStream.protocol, 'outbound', this);
if (streamCount > outgoingLimit) {
const err = new TooManyOutboundProtocolStreamsError(`Too many outbound protocol streams for protocol "${muxedStream.protocol}" - ${streamCount}/${outgoingLimit}`);
muxedStream.abort(err);
throw err;
}
// If a protocol stream has been successfully negotiated and is to be passed to the application,
// the peer store should ensure that the peer is registered with that protocol
await this.components.peerStore.merge(this.remotePeer, {
protocols: [muxedStream.protocol]
});
this.components.metrics?.trackProtocolStream(muxedStream);
const middleware = this.components.registrar.getMiddleware(muxedStream.protocol);
return await this.runMiddlewareChain(muxedStream, this, middleware);
}
catch (err) {
if (muxedStream.status === 'open') {
muxedStream.abort(err);
}
else {
this.log.error('could not create new outbound stream on connection %s %a for protocols %s - %e', this.direction === 'inbound' ? 'from' : 'to', this.remoteAddr, protocols, err);
}
throw err;
}
};
async onIncomingStream(evt) {
const muxedStream = evt.detail;
const signal = AbortSignal.timeout(this.inboundStreamProtocolNegotiationTimeout);
setMaxListeners(Infinity, signal);
muxedStream.log('start protocol negotiation, timing out after %dms', this.inboundStreamProtocolNegotiationTimeout);
try {
if (muxedStream.protocol === '') {
const protocols = this.components.registrar.getProtocols();
muxedStream.log.trace('selecting protocol from protocols %s', protocols);
muxedStream.protocol = await mss.handle(muxedStream, protocols, {
signal
});
muxedStream.log('negotiated protocol %s', muxedStream.protocol);
}
else {
muxedStream.log('pre-negotiated protocol %s', muxedStream.protocol);
}
const incomingLimit = findIncomingStreamLimit(muxedStream.protocol, this.components.registrar);
const streamCount = countStreams(muxedStream.protocol, 'inbound', this);
if (streamCount > incomingLimit) {
throw new TooManyInboundProtocolStreamsError(`Too many inbound protocol streams for protocol "${muxedStream.protocol}" - limit ${incomingLimit}`);
}
// If a protocol stream has been successfully negotiated and is to be passed to the application,
// the peer store should ensure that the peer is registered with that protocol
await this.components.peerStore.merge(this.remotePeer, {
protocols: [muxedStream.protocol]
}, {
signal
});
this.components.metrics?.trackProtocolStream(muxedStream);
const { handler, options } = this.components.registrar.getHandler(muxedStream.protocol);
if (this.limits != null && options.runOnLimitedConnection !== true) {
throw new LimitedConnectionError('Cannot open protocol stream on limited connection');
}
const middleware = this.components.registrar.getMiddleware(muxedStream.protocol);
middleware.push(async (stream, connection, next) => {
await handler(stream, connection);
next(stream, connection);
});
await this.runMiddlewareChain(muxedStream, this, middleware);
}
catch (err) {
muxedStream.abort(err);
}
}
async runMiddlewareChain(stream, connection, middleware) {
for (let i = 0; i < middleware.length; i++) {
const mw = middleware[i];
stream.log.trace('running middleware', i, mw);
// eslint-disable-next-line no-loop-func
await new Promise((resolve, reject) => {
try {
const result = mw(stream, connection, (s, c) => {
stream = s;
connection = c;
resolve();
});
if (result instanceof Promise) {
result.catch(reject);
}
}
catch (err) {
reject(err);
}
});
stream.log.trace('ran middleware', i, mw);
}
return stream;
}
/**
* Close the connection
*/
async close(options = {}) {
this.log('closing connection to %a', this.remoteAddr);
if (options.signal == null) {
const signal = AbortSignal.timeout(this.closeTimeout);
setMaxListeners(Infinity, signal);
options = {
...options,
signal
};
}
await this.muxer?.close(options);
await this.maConn.close(options);
}
abort(err) {
this.muxer?.abort(err);
this.maConn.abort(err);
}
}
export function createConnection(components, init) {
return new Connection(components, init);
}
function findIncomingStreamLimit(protocol, registrar) {
try {
const { options } = registrar.getHandler(protocol);
if (options.maxInboundStreams != null) {
return options.maxInboundStreams;
}
}
catch (err) {
if (err.name !== 'UnhandledProtocolError') {
throw err;
}
}
return DEFAULT_MAX_INBOUND_STREAMS;
}
function findOutgoingStreamLimit(protocol, registrar, options = {}) {
try {
const { options } = registrar.getHandler(protocol);
if (options.maxOutboundStreams != null) {
return options.maxOutboundStreams;
}
}
catch (err) {
if (err.name !== 'UnhandledProtocolError') {
throw err;
}
}
return options.maxOutboundStreams ?? DEFAULT_MAX_OUTBOUND_STREAMS;
}
function countStreams(protocol, direction, connection) {
let streamCount = 0;
connection.streams.forEach(stream => {
if (stream.direction === direction && stream.protocol === protocol) {
streamCount++;
}
});
return streamCount;
}
//# sourceMappingURL=connection.js.map