nerdbank-streams
Version:
Multiplexing of streams
625 lines • 30.4 kB
JavaScript
"use strict";
/* TODO:
* Tracing
* Auto-terminate channels when both ends have finished writing (AutoCloseOnPipesClosureAsync)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MultiplexingStreamClass = exports.MultiplexingStream = void 0;
const await_semaphore_1 = require("await-semaphore");
const cancellationtoken_1 = require("cancellationtoken");
const events_1 = require("events");
const Channel_1 = require("./Channel");
const ControlCode_1 = require("./ControlCode");
const Deferred_1 = require("./Deferred");
const FrameHeader_1 = require("./FrameHeader");
const MultiplexingStreamFormatters_1 = require("./MultiplexingStreamFormatters");
require("./MultiplexingStreamOptions");
const QualifiedChannelId_1 = require("./QualifiedChannelId");
const Utilities_1 = require("./Utilities");
const caught = require("caught");
class MultiplexingStream {
get disposalToken() {
return this.disposalTokenSource.token;
}
/**
* Gets a promise that is resolved or rejected based on how this stream is disposed or fails.
*/
get completion() {
return this._completionSource.promise;
}
/**
* Gets a value indicating whether this instance has been disposed.
*/
get isDisposed() {
return this.disposalTokenSource.token.isCancelled;
}
/**
* Initializes a new instance of the `MultiplexingStream` class.
* @param stream The duplex stream to read and write to.
* Use `FullDuplexStream.Splice` if you have distinct input/output streams.
* @param options Options to customize the behavior of the stream.
* @param cancellationToken A token whose cancellation aborts the handshake with the remote end.
* @returns The multiplexing stream, once the handshake is complete.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
static async CreateAsync(stream, options, cancellationToken = cancellationtoken_1.default.CONTINUE) {
var _a;
if (!stream) {
throw new Error('stream must be specified.');
}
options = options || {};
(_a = options.protocolMajorVersion) !== null && _a !== void 0 ? _a : (options.protocolMajorVersion = 1);
options.defaultChannelReceivingWindowSize = options.defaultChannelReceivingWindowSize || MultiplexingStream.recommendedDefaultChannelReceivingWindowSize;
if (options.protocolMajorVersion < 3 && options.seededChannels && options.seededChannels.length > 0) {
throw new Error('Seeded channels require v3 of the protocol at least.');
}
// Send the protocol magic number, and a random 16-byte number to establish even/odd assignments.
const formatter = options.protocolMajorVersion === 1
? new MultiplexingStreamFormatters_1.MultiplexingStreamV1Formatter(stream)
: options.protocolMajorVersion === 2
? new MultiplexingStreamFormatters_1.MultiplexingStreamV2Formatter(stream)
: options.protocolMajorVersion === 3
? new MultiplexingStreamFormatters_1.MultiplexingStreamV3Formatter(stream)
: undefined;
if (!formatter) {
throw new Error(`Protocol major version ${options.protocolMajorVersion} is not supported.`);
}
const writeHandshakeData = await formatter.writeHandshakeAsync();
const handshakeResult = await formatter.readHandshakeAsync(writeHandshakeData, cancellationToken);
formatter.isOdd = handshakeResult.isOdd;
return new MultiplexingStreamClass(formatter, handshakeResult.isOdd, options);
}
/**
* Initializes a new instance of the `MultiplexingStream` class
* with protocolMajorVersion set to 3.
* @param stream The duplex stream to read and write to.
* Use `FullDuplexStream.Splice` if you have distinct input/output streams.
* @param options Options to customize the behavior of the stream.
* @returns The multiplexing stream.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
static Create(stream, options) {
var _a;
options !== null && options !== void 0 ? options : (options = { protocolMajorVersion: 3 });
(_a = options.protocolMajorVersion) !== null && _a !== void 0 ? _a : (options.protocolMajorVersion = 3);
const formatter = options.protocolMajorVersion === 3 ? new MultiplexingStreamFormatters_1.MultiplexingStreamV3Formatter(stream) : undefined;
if (!formatter) {
throw new Error(`Protocol major version ${options.protocolMajorVersion} is not supported. Try CreateAsync instead.`);
}
return new MultiplexingStreamClass(formatter, undefined, options);
}
constructor(formatter, isOdd, options) {
var _a, _b, _c;
this.formatter = formatter;
this.isOdd = isOdd;
this._completionSource = new Deferred_1.Deferred();
/**
* A dictionary of channels that were seeded, keyed by their ID.
*/
this.seededOpenChannels = {};
/**
* A dictionary of channels that were offered locally, keyed by their ID.
*/
this.locallyOfferedOpenChannels = {};
/**
* A dictionary of channels that were offered remotely, keyed by their ID.
*/
this.remotelyOfferedOpenChannels = {};
/**
* A map of channel names to queues of channels waiting for local acceptance.
*/
this.channelsOfferedByThemByName = {};
/**
* A map of channel names to queues of Deferred<Channel> from waiting accepters.
*/
this.acceptingChannels = {};
this.eventEmitter = new events_1.EventEmitter();
this.disposalTokenSource = cancellationtoken_1.default.create();
this.defaultChannelReceivingWindowSize = (_a = options.defaultChannelReceivingWindowSize) !== null && _a !== void 0 ? _a : MultiplexingStream.recommendedDefaultChannelReceivingWindowSize;
this.protocolMajorVersion = (_b = options.protocolMajorVersion) !== null && _b !== void 0 ? _b : 1;
if (options.seededChannels) {
for (let i = 0; i < options.seededChannels.length; i++) {
const channelOptions = options.seededChannels[i];
const id = { id: i, source: QualifiedChannelId_1.ChannelSource.Seeded };
this.setOpenChannel(new Channel_1.ChannelClass(this, id, {
name: '',
remoteWindowSize: (_c = channelOptions.channelReceivingWindowSize) !== null && _c !== void 0 ? _c : this.defaultChannelReceivingWindowSize,
}));
}
}
}
/**
* Creates an anonymous channel that may be accepted by <see cref="AcceptChannel(int, ChannelOptions)"/>.
* Its existance must be communicated by other means (typically another, existing channel) to encourage acceptance.
* @param options A set of options that describe local treatment of this channel.
* @returns The anonymous channel.
* @description Note that while the channel is created immediately, any local write to that channel will be
* buffered locally until the remote party accepts the channel.
*/
createChannel(options) {
var _a;
const offerParameters = {
name: '',
remoteWindowSize: (_a = options === null || options === void 0 ? void 0 : options.channelReceivingWindowSize) !== null && _a !== void 0 ? _a : this.defaultChannelReceivingWindowSize,
};
const payload = this.formatter.serializeOfferParameters(offerParameters);
const channel = new Channel_1.ChannelClass(this, { id: this.getUnusedChannelId(), source: QualifiedChannelId_1.ChannelSource.Local }, offerParameters);
this.setOpenChannel(channel);
this.rejectOnFailure(this.sendFrameAsync(new FrameHeader_1.FrameHeader(ControlCode_1.ControlCode.offer, channel.qualifiedId), payload, this.disposalToken));
return channel;
}
/**
* Accepts a channel with a specific ID.
* @param id The id of the channel to accept.
* @param options A set of options that describe local treatment of this channel.
* @description This method can be used to accept anonymous channels created with <see cref="CreateChannel"/>.
* Unlike <see cref="AcceptChannelAsync(string, ChannelOptions, CancellationToken)"/> which will await
* for a channel offer if a matching one has not been made yet, this method only accepts an offer
* for a channel that has already been made.
*/
acceptChannel(id, options) {
const channel = this.remotelyOfferedOpenChannels[id] || this.seededOpenChannels[id];
if (!channel) {
throw new Error('No channel with ID ' + id);
}
this.removeChannelFromOfferedQueue(channel);
this.acceptChannelOrThrow(channel, options);
return channel;
}
/**
* Rejects an offer for the channel with a specified ID.
* @param id The ID of the channel whose offer should be rejected.
*/
rejectChannel(id) {
const channel = this.remotelyOfferedOpenChannels[id];
if (channel) {
(0, Utilities_1.removeFromQueue)(channel, this.channelsOfferedByThemByName[channel.name]);
}
else {
throw new Error('No channel with that ID found.');
}
// Rejecting a channel rejects a couple promises that we don't want the caller to have to observe
// separately since they are explicitly stating they want to take this action now.
caught(channel.acceptance);
caught(channel.completion);
channel.dispose();
}
/**
* Offers a new, named channel to the remote party so they may accept it with
* {@link acceptChannelAsync}.
* @param name A name for the channel, which must be accepted on the remote end to complete creation.
* It need not be unique, and may be empty but must not be null.
* Any characters are allowed, and max length is determined by the maximum frame payload (based on UTF-8 encoding).
* @param options A set of options that describe local treatment of this channel.
* @param cancellationToken A cancellation token. Do NOT let this be a long-lived token
* or a memory leak will result since we add continuations to its promise.
* @returns A task that completes with the `Channel` if the offer is accepted on the remote end
* or faults with `MultiplexingProtocolException` if the remote end rejects the channel.
*/
async offerChannelAsync(name, options, cancellationToken = cancellationtoken_1.default.CONTINUE) {
var _a;
if (name === null || name === undefined) {
throw new Error('Name must be specified (but may be empty).');
}
cancellationToken.throwIfCancelled();
(0, Utilities_1.throwIfDisposed)(this);
const offerParameters = {
name,
remoteWindowSize: (_a = options === null || options === void 0 ? void 0 : options.channelReceivingWindowSize) !== null && _a !== void 0 ? _a : this.defaultChannelReceivingWindowSize,
};
const payload = this.formatter.serializeOfferParameters(offerParameters);
const channel = new Channel_1.ChannelClass(this, { id: this.getUnusedChannelId(), source: QualifiedChannelId_1.ChannelSource.Local }, offerParameters);
this.setOpenChannel(channel);
const header = new FrameHeader_1.FrameHeader(ControlCode_1.ControlCode.offer, channel.qualifiedId);
const unsubscribeFromCT = cancellationToken.onCancelled(reason => this.offerChannelCanceled(channel, reason));
try {
// We *will* recognize rejection of this promise. But just in case sendFrameAsync completes synchronously,
// we want to signify that we *will* catch it first to avoid node.js emitting warnings or crashing.
caught(channel.acceptance);
await this.sendFrameAsync(header, payload, cancellationToken);
await channel.acceptance;
return channel;
}
finally {
unsubscribeFromCT();
}
}
/**
* Accepts a channel that the remote end has attempted or may attempt to create.
* @param name The name of the channel to accept.
* An empty string will match an offer made via {@link offerChannelAsync} with an empty channel name.
* It will also match an anonymous channel offer made with {@link createChannel}.
* @param options A set of options that describe local treatment of this channel.
* @param cancellationToken A token to indicate lost interest in accepting the channel.
* Do NOT let this be a long-lived token
* or a memory leak will result since we add continuations to its promise.
* @returns The `Channel`, after its offer has been received from the remote party and accepted.
* @description If multiple offers exist with the specified `name`, the first one received will be accepted.
*/
async acceptChannelAsync(name, options, cancellationToken = cancellationtoken_1.default.CONTINUE) {
if (name === null || name === undefined) {
throw new Error('Name must be specified (but may be empty).');
}
cancellationToken.throwIfCancelled();
(0, Utilities_1.throwIfDisposed)(this);
let channel;
let pendingAcceptChannel;
const channelsOfferedByThem = this.channelsOfferedByThemByName[name];
if (channelsOfferedByThem) {
while (channel === undefined && channelsOfferedByThem.length > 0) {
channel = channelsOfferedByThem.shift();
if (channel.isAccepted || channel.isRejectedOrCanceled) {
channel = undefined;
continue;
}
}
}
if (channel === undefined) {
let acceptingChannels = this.acceptingChannels[name];
if (!acceptingChannels) {
this.acceptingChannels[name] = acceptingChannels = [];
}
pendingAcceptChannel = new Deferred_1.Deferred(options);
acceptingChannels.push(pendingAcceptChannel);
}
if (channel !== undefined) {
this.acceptChannelOrThrow(channel, options);
return channel;
}
else {
const unsubscribeFromCT = cancellationToken.onCancelled(reason => this.acceptChannelCanceled(pendingAcceptChannel, name, reason));
try {
return await pendingAcceptChannel.promise;
}
finally {
unsubscribeFromCT();
}
}
}
/**
* Disposes the stream.
*/
dispose() {
this.disposeCore(false);
}
disposeCore(remoteDisconnect) {
this.disposalTokenSource.cancel();
this._completionSource.resolve();
this.formatter.end();
[this.locallyOfferedOpenChannels, this.remotelyOfferedOpenChannels].forEach(cb => {
for (const channelId in cb) {
if (cb.hasOwnProperty(channelId)) {
const channel = cb[channelId];
// Acceptance gets rejected when a channel is disposed.
// Avoid a node.js crash or test failure for unobserved channels (e.g. offers for channels from the other party that no one cared to receive on this side).
caught(channel.acceptance);
channel.dispose();
}
}
});
// Iterate over acceptingChannels to reject each promise
const errorMessage = remoteDisconnect ? 'The remote party has disconnected.' : 'The stream is being disposed locally.';
for (const name in this.acceptingChannels) {
if (this.acceptingChannels.hasOwnProperty(name)) {
const acceptingChannels = this.acceptingChannels[name];
for (const channelDeferral of acceptingChannels) {
channelDeferral.reject(new Error(errorMessage));
}
delete this.acceptingChannels[name];
}
}
}
on(event, listener) {
this.eventEmitter.on(event, listener);
}
off(event, listener) {
this.eventEmitter.off(event, listener);
}
once(event, listener) {
this.eventEmitter.once(event, listener);
}
raiseChannelOffered(id, name, isAccepted) {
const args = {
id,
isAccepted,
name,
};
try {
this.eventEmitter.emit('channelOffered', args);
}
catch (err) {
this._completionSource.reject(err);
}
}
acceptChannelOrThrow(channel, options) {
var _a, _b;
if (channel.tryAcceptOffer(options)) {
const acceptanceParameters = {
remoteWindowSize: (_b = (_a = options === null || options === void 0 ? void 0 : options.channelReceivingWindowSize) !== null && _a !== void 0 ? _a : channel.localWindowSize) !== null && _b !== void 0 ? _b : this.defaultChannelReceivingWindowSize,
};
if (acceptanceParameters.remoteWindowSize < this.defaultChannelReceivingWindowSize) {
acceptanceParameters.remoteWindowSize = this.defaultChannelReceivingWindowSize;
}
if (channel.qualifiedId.source !== QualifiedChannelId_1.ChannelSource.Seeded) {
const payload = this.formatter.serializeAcceptanceParameters(acceptanceParameters);
this.rejectOnFailure(this.sendFrameAsync(new FrameHeader_1.FrameHeader(ControlCode_1.ControlCode.offerAccepted, channel.qualifiedId), payload, this.disposalToken));
}
}
else if (channel.isAccepted) {
throw new Error('Channel is already accepted.');
}
else if (channel.isRejectedOrCanceled) {
throw new Error('Channel is no longer available for acceptance.');
}
else {
throw new Error('Channel could not be accepted.');
}
}
/**
* Disposes this instance if the specified promise is rejected.
* @param promise The promise to check for failures.
*/
async rejectOnFailure(promise) {
try {
await promise;
}
catch (err) {
this._completionSource.reject(err);
}
}
removeChannelFromOfferedQueue(channel) {
if (channel.name) {
(0, Utilities_1.removeFromQueue)(channel, this.channelsOfferedByThemByName[channel.name]);
}
}
getOpenChannel(qualifiedId) {
return this.getChannelCollection(qualifiedId.source)[qualifiedId.id];
}
setOpenChannel(channel) {
this.getChannelCollection(channel.qualifiedId.source)[channel.qualifiedId.id] = channel;
}
deleteOpenChannel(qualifiedId) {
delete this.getChannelCollection(qualifiedId.source)[qualifiedId.id];
}
getChannelCollection(source) {
switch (source) {
case QualifiedChannelId_1.ChannelSource.Local:
return this.locallyOfferedOpenChannels;
case QualifiedChannelId_1.ChannelSource.Remote:
return this.remotelyOfferedOpenChannels;
case QualifiedChannelId_1.ChannelSource.Seeded:
return this.seededOpenChannels;
}
}
/**
* Cancels a prior call to acceptChannelAsync
* @param channel The promise of a channel to be canceled.
* @param name The name of the channel the caller was accepting.
* @param reason The reason for cancellation.
*/
acceptChannelCanceled(channel, name, reason) {
if (channel.reject(new cancellationtoken_1.default.CancellationError(reason))) {
(0, Utilities_1.removeFromQueue)(channel, this.acceptingChannels[name]);
}
}
/**
* Responds to cancellation of a prior call to offerChannelAsync.
* @param channel The channel previously offered.
*/
offerChannelCanceled(channel, reason) {
channel.tryCancelOffer(reason);
}
/**
* Gets a unique number that can be used to represent a channel.
* @description The channel numbers increase by two in order to maintain odd or even numbers,
* since each party is allowed to create only one or the other.
*/
getUnusedChannelId() {
return (this.lastOfferedChannelId += this.isOdd !== undefined ? 2 : 1);
}
}
exports.MultiplexingStream = MultiplexingStream;
/**
* The maximum length of a frame's payload.
*/
MultiplexingStream.framePayloadMaxLength = 20 * 1024;
MultiplexingStream.recommendedDefaultChannelReceivingWindowSize = 5 * MultiplexingStream.framePayloadMaxLength;
/**
* The options to use for channels we create in response to incoming offers.
* @description Whatever these settings are, they can be replaced when the channel is accepted.
*/
MultiplexingStream.defaultChannelOptions = {};
/**
* The encoding used for characters in control frames.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
MultiplexingStream.ControlFrameEncoding = 'utf-8';
// tslint:disable-next-line:max-classes-per-file
class MultiplexingStreamClass extends MultiplexingStream {
constructor(formatter, isOdd, options) {
var _a, _b;
super(formatter, isOdd, options);
this.sendingSemaphore = new await_semaphore_1.Semaphore(1);
this.lastOfferedChannelId = isOdd ? -1 : 0; // the first channel created should be 1 or 2
this.lastOfferedChannelId += (_b = (_a = options.seededChannels) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
// Initiate reading from the transport stream. This will not end until the stream does, or we're disposed.
// If reading the stream fails, we'll dispose ourselves.
this.readFromStream(this.disposalToken).catch(err => this._completionSource.reject(err));
}
get backpressureSupportEnabled() {
return this.protocolMajorVersion > 1;
}
async sendFrameAsync(header, payload, cancellationToken = cancellationtoken_1.default.CONTINUE) {
if (!header) {
throw new Error('Header is required.');
}
await this.sendingSemaphore.use(async () => {
cancellationToken.throwIfCancelled();
(0, Utilities_1.throwIfDisposed)(this);
await this.formatter.writeFrameAsync(header, payload);
});
}
/**
* Transmits a frame over the stream.
* @param code The op code for the channel.
* @param channelId The ID of the channel to receive the frame.
* @description The promise returned from this function is always resolved (not rejected)
* since it is anticipated that callers may not be awaiting its result.
*/
async sendFrame(code, channelId) {
try {
if (this._completionSource.isCompleted) {
// Any frames that come in after we're done are most likely frames just informing that channels are
// being terminated, which we do not need to communicate since the connection going down implies that.
return;
}
const header = new FrameHeader_1.FrameHeader(code, channelId);
await this.sendFrameAsync(header);
}
catch (error) {
// We mustn't throw back to our caller. So report the failure by disposing with failure.
this._completionSource.reject(error);
}
}
onChannelWritingCompleted(channel) {
// Only inform the remote side if this channel has not already been terminated.
if (!channel.isDisposed && this.getOpenChannel(channel.qualifiedId)) {
this.sendFrame(ControlCode_1.ControlCode.contentWritingCompleted, channel.qualifiedId);
}
}
async onChannelDisposed(channel, error) {
if (!this._completionSource.isCompleted) {
try {
const payload = this.protocolMajorVersion > 1 && error ? this.formatter.serializeException(error) : Buffer.alloc(0);
const frameHeader = new FrameHeader_1.FrameHeader(ControlCode_1.ControlCode.channelTerminated, channel.qualifiedId);
await this.sendFrameAsync(frameHeader, payload);
}
catch (err) {
// Swallow exceptions thrown about channel disposal if the whole stream has been taken down.
if (this.isDisposed) {
return;
}
throw err;
}
}
}
localContentExamined(channel, bytesConsumed) {
const payload = this.formatter.serializeContentProcessed(bytesConsumed);
this.rejectOnFailure(this.sendFrameAsync(new FrameHeader_1.FrameHeader(ControlCode_1.ControlCode.contentProcessed, channel.qualifiedId), payload));
}
async readFromStream(cancellationToken) {
while (!this.isDisposed) {
const frame = await this.formatter.readFrameAsync(cancellationToken);
if (frame === null) {
break;
}
frame.header.flipChannelPerspective();
switch (frame.header.code) {
case ControlCode_1.ControlCode.offer:
this.onOffer(frame.header.requiredChannel, frame.payload);
break;
case ControlCode_1.ControlCode.offerAccepted:
this.onOfferAccepted(frame.header.requiredChannel, frame.payload);
break;
case ControlCode_1.ControlCode.content:
this.onContent(frame.header.requiredChannel, frame.payload);
break;
case ControlCode_1.ControlCode.contentProcessed:
this.onContentProcessed(frame.header.requiredChannel, frame.payload);
break;
case ControlCode_1.ControlCode.contentWritingCompleted:
this.onContentWritingCompleted(frame.header.requiredChannel);
break;
case ControlCode_1.ControlCode.channelTerminated:
this.onChannelTerminated(frame.header.requiredChannel, frame.payload);
break;
default:
break;
}
}
this.disposeCore(true);
}
onOffer(channelId, payload) {
const offerParameters = this.formatter.deserializeOfferParameters(payload);
const channel = new Channel_1.ChannelClass(this, channelId, offerParameters);
let acceptingChannelAlreadyPresent = false;
let options;
let acceptingChannels;
if ((acceptingChannels = this.acceptingChannels[offerParameters.name]) !== undefined) {
while (acceptingChannels.length > 0) {
const candidate = acceptingChannels.shift();
if (candidate.resolve(channel)) {
acceptingChannelAlreadyPresent = true;
options = candidate.state;
break;
}
}
}
if (!acceptingChannelAlreadyPresent) {
let offeredChannels;
if (!(offeredChannels = this.channelsOfferedByThemByName[offerParameters.name])) {
this.channelsOfferedByThemByName[offerParameters.name] = offeredChannels = [];
}
offeredChannels.push(channel);
}
this.setOpenChannel(channel);
if (acceptingChannelAlreadyPresent) {
this.acceptChannelOrThrow(channel, options);
}
this.raiseChannelOffered(channel.id, channel.name, acceptingChannelAlreadyPresent);
}
onOfferAccepted(channelId, payload) {
if (channelId.source !== QualifiedChannelId_1.ChannelSource.Local) {
throw new Error('Remote party tried to accept a channel that they offered.');
}
const acceptanceParameter = this.formatter.deserializeAcceptanceParameters(payload);
const channel = this.getOpenChannel(channelId);
if (!channel) {
throw new Error(`Unexpected channel created with ID ${QualifiedChannelId_1.QualifiedChannelId.toString(channelId)}`);
}
if (!channel.onAccepted(acceptanceParameter)) {
// This may be an acceptance of a channel that we canceled an offer for, and a race condition
// led to our cancellation notification crossing in transit with their acceptance notification.
// In this case, do nothing since we already sent a channel termination message, and the remote side
// should notice it soon.
}
}
onContent(channelId, payload) {
const channel = this.getOpenChannel(channelId);
if (!channel) {
throw new Error(`No channel with id ${channelId} found.`);
}
channel.onContent(payload);
}
onContentProcessed(channelId, payload) {
const channel = this.getOpenChannel(channelId);
if (!channel) {
throw new Error(`No channel with id ${channelId} found.`);
}
const bytesProcessed = this.formatter.deserializeContentProcessed(payload);
channel.onContentProcessed(bytesProcessed);
}
onContentWritingCompleted(channelId) {
const channel = this.getOpenChannel(channelId);
if (!channel) {
throw new Error(`No channel with id ${channelId} found.`);
}
channel.onContent(null); // signify that the remote is done writing.
}
/**
* Occurs when the remote party has terminated a channel (including canceling an offer).
* @param channelId The ID of the terminated channel.
*/
onChannelTerminated(channelId, payload) {
const channel = this.getOpenChannel(channelId);
if (channel) {
this.deleteOpenChannel(channelId);
this.removeChannelFromOfferedQueue(channel);
// Extract the exception that we received from the remote side.
const remoteException = this.protocolMajorVersion > 1 ? this.formatter.deserializeException(payload) : null;
channel.dispose(remoteException);
}
}
}
exports.MultiplexingStreamClass = MultiplexingStreamClass;
//# sourceMappingURL=MultiplexingStream.js.map