libp2p
Version:
JavaScript implementation of libp2p, a modular peer to peer network stack
290 lines • 12.3 kB
JavaScript
import { connectionSymbol, LimitedConnectionError, ConnectionClosedError, ConnectionClosingError, TooManyOutboundProtocolStreamsError, TooManyInboundProtocolStreamsError } from '@libp2p/interface';
import * as mss from '@libp2p/multistream-select';
import { setMaxListeners } from 'main-event';
import { PROTOCOL_NEGOTIATION_TIMEOUT } from "./connection-manager/constants.defaults.js";
import { MuxerUnavailableError } from "./errors.js";
import { DEFAULT_MAX_INBOUND_STREAMS, DEFAULT_MAX_OUTBOUND_STREAMS } from "./registrar.js";
const CLOSE_TIMEOUT = 500;
/**
* An implementation of the js-libp2p connection.
* Any libp2p transport should use an upgrader to return this connection.
*/
export class Connection {
id;
remoteAddr;
remotePeer;
direction;
timeline;
multiplexer;
encryption;
status;
limits;
log;
tags;
maConn;
muxer;
components;
outboundStreamProtocolNegotiationTimeout;
inboundStreamProtocolNegotiationTimeout;
constructor(components, init) {
this.components = components;
this.id = init.id;
this.remoteAddr = init.maConn.remoteAddr;
this.remotePeer = init.remotePeer;
this.direction = init.direction ?? 'outbound';
this.status = 'open';
this.timeline = init.maConn.timeline;
this.encryption = init.encryption;
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;
if (this.remoteAddr.getPeerId() == null) {
this.remoteAddr = this.remoteAddr.encapsulate(`/p2p/${this.remotePeer}`);
}
this.tags = [];
if (init.muxerFactory != null) {
this.multiplexer = init.muxerFactory.protocol;
this.muxer = init.muxerFactory.createStreamMuxer({
direction: this.direction,
log: this.log,
// Run anytime a remote stream is created
onIncomingStream: (stream) => {
this.onIncomingStream(stream);
}
});
// Pipe all data through the muxer
void Promise.all([
this.muxer.sink(this.maConn.source),
this.maConn.sink(this.muxer.source)
]).catch(err => {
this.log.error('error piping data through muxer - %e', err);
});
}
}
[Symbol.toStringTag] = 'Connection';
[connectionSymbol] = true;
get streams() {
return this.muxer?.streams ?? [];
}
/**
* Create a new stream over this connection
*/
newStream = async (protocols, options = {}) => {
if (this.status === 'closing') {
throw new ConnectionClosingError('the connection is being closed');
}
if (this.status === 'closed') {
throw new ConnectionClosedError('the connection is closed');
}
if (!Array.isArray(protocols)) {
protocols = [protocols];
}
if (this.limits != null && options?.runOnLimitedConnection !== true) {
throw new LimitedConnectionError('Cannot open protocol stream on limited connection');
}
if (this.muxer == null) {
throw new MuxerUnavailableError('Connection is not multiplexed');
}
this.log.trace('starting new stream for protocols %s', protocols);
const muxedStream = await this.muxer.newStream();
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
};
}
muxedStream.log.trace('selecting protocol from protocols %s', protocols);
const { stream, protocol } = await mss.select(muxedStream, protocols, {
...options,
log: muxedStream.log,
yieldBytes: true
});
muxedStream.log('selected protocol %s', protocol);
const outgoingLimit = findOutgoingStreamLimit(protocol, this.components.registrar, options);
const streamCount = countStreams(protocol, 'outbound', this);
if (streamCount >= outgoingLimit) {
const err = new TooManyOutboundProtocolStreamsError(`Too many outbound protocol streams for protocol "${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: [protocol]
});
// after the handshake the returned stream can have early data so override
// the source/sink
muxedStream.source = stream.source;
muxedStream.sink = stream.sink;
muxedStream.protocol = protocol;
// allow closing the write end of a not-yet-negotiated stream
if (stream.closeWrite != null) {
muxedStream.closeWrite = stream.closeWrite;
}
// allow closing the read end of a not-yet-negotiated stream
if (stream.closeRead != null) {
muxedStream.closeRead = stream.closeRead;
}
// make sure we don't try to negotiate a stream we are closing
if (stream.close != null) {
muxedStream.close = stream.close;
}
this.components.metrics?.trackProtocolStream(muxedStream, this);
muxedStream.direction = 'outbound';
return muxedStream;
}
catch (err) {
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);
if (muxedStream.timeline.close == null) {
muxedStream.abort(err);
}
throw err;
}
};
onIncomingStream(muxedStream) {
const signal = AbortSignal.timeout(this.inboundStreamProtocolNegotiationTimeout);
setMaxListeners(Infinity, signal);
void Promise.resolve()
.then(async () => {
const protocols = this.components.registrar.getProtocols();
const { stream, protocol } = await mss.handle(muxedStream, protocols, {
signal,
log: muxedStream.log,
yieldBytes: false
});
this.log('incoming %s stream opened', protocol);
const incomingLimit = findIncomingStreamLimit(protocol, this.components.registrar);
const streamCount = countStreams(protocol, 'inbound', this);
if (streamCount === incomingLimit) {
const err = new TooManyInboundProtocolStreamsError(`Too many inbound protocol streams for protocol "${protocol}" - limit ${incomingLimit}`);
muxedStream.abort(err);
throw err;
}
// after the handshake the returned stream can have early data so override
// the source/sink
muxedStream.source = stream.source;
muxedStream.sink = stream.sink;
muxedStream.protocol = protocol;
// allow closing the write end of a not-yet-negotiated stream
if (stream.closeWrite != null) {
muxedStream.closeWrite = stream.closeWrite;
}
// allow closing the read end of a not-yet-negotiated stream
if (stream.closeRead != null) {
muxedStream.closeRead = stream.closeRead;
}
// make sure we don't try to negotiate a stream we are closing
if (stream.close != null) {
muxedStream.close = stream.close;
}
// 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: [protocol]
}, {
signal
});
this.components.metrics?.trackProtocolStream(muxedStream, this);
const { handler, options } = this.components.registrar.getHandler(protocol);
if (this.limits != null && options.runOnLimitedConnection !== true) {
throw new LimitedConnectionError('Cannot open protocol stream on limited connection');
}
await handler({ connection: this, stream: muxedStream });
})
.catch(async (err) => {
this.log.error('error handling incoming stream id %s - %e', muxedStream.id, err);
muxedStream.abort(err);
});
}
/**
* Close the connection
*/
async close(options = {}) {
if (this.status === 'closed' || this.status === 'closing') {
return;
}
this.log('closing connection to %a', this.remoteAddr);
this.status = 'closing';
if (options.signal == null) {
const signal = AbortSignal.timeout(CLOSE_TIMEOUT);
setMaxListeners(Infinity, signal);
options = {
...options,
signal
};
}
try {
this.log.trace('closing underlying transport');
// ensure remaining streams are closed gracefully
await this.muxer?.close(options);
// close the underlying transport
await this.maConn.close(options);
this.log.trace('updating timeline with close time');
this.status = 'closed';
this.timeline.close = Date.now();
}
catch (err) {
this.log.error('error encountered during graceful close of connection to %a', this.remoteAddr, err);
this.abort(err);
}
}
abort(err) {
if (this.status === 'closed') {
return;
}
this.log.error('aborting connection to %a due to error', this.remoteAddr, err);
this.status = 'closing';
// ensure remaining streams are aborted
this.muxer?.abort(err);
// abort the underlying transport
this.maConn.abort(err);
this.status = 'closed';
this.timeline.close = Date.now();
}
}
export function createConnection(components, init) {
return new Connection(components, init);
}
function findIncomingStreamLimit(protocol, registrar) {
try {
const { options } = registrar.getHandler(protocol);
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