nerdbank-streams
Version:
Multiplexing of streams
227 lines • 9.82 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChannelClass = exports.Channel = void 0;
const cancellationtoken_1 = require("cancellationtoken");
const stream_1 = require("stream");
const ControlCode_1 = require("./ControlCode");
const Deferred_1 = require("./Deferred");
const FrameHeader_1 = require("./FrameHeader");
const MultiplexingStream_1 = require("./MultiplexingStream");
const QualifiedChannelId_1 = require("./QualifiedChannelId");
const caught = require("caught");
class Channel {
/**
* The id of the channel.
* @obsolete Use qualifiedId instead.
*/
get id() {
return this.qualifiedId.id;
}
constructor(id) {
this._isDisposed = false;
this.qualifiedId = id;
}
/**
* Gets a value indicating whether this channel has been disposed.
*/
get isDisposed() {
return this._isDisposed;
}
/**
* Closes this channel.
* @param error An optional error to send to the remote side, if this multiplexing stream is using protocol versions >= 2.
*/
dispose(error) {
// The interesting stuff is in the derived class.
this._isDisposed = true;
}
}
exports.Channel = Channel;
// tslint:disable-next-line:max-classes-per-file
class ChannelClass extends Channel {
constructor(multiplexingStream, id, offerParameters) {
super(id);
this._acceptance = new Deferred_1.Deferred();
this._completion = new Deferred_1.Deferred();
/**
* The number of bytes transmitted from here but not yet acknowledged as processed from there,
* and thus occupying some portion of the full AcceptanceParameters.RemoteWindowSize.
*/
this.remoteWindowFilled = 0;
const self = this;
this.name = offerParameters.name;
switch (id.source) {
case QualifiedChannelId_1.ChannelSource.Local:
this.localWindowSize = offerParameters.remoteWindowSize;
break;
case QualifiedChannelId_1.ChannelSource.Remote:
this.remoteWindowSize = offerParameters.remoteWindowSize;
break;
case QualifiedChannelId_1.ChannelSource.Seeded:
this.remoteWindowSize = offerParameters.remoteWindowSize;
this.localWindowSize = offerParameters.remoteWindowSize;
break;
default:
throw new Error('Channel source not recognized.');
}
this._multiplexingStream = multiplexingStream;
this.remoteWindowHasCapacity = new Deferred_1.Deferred();
if (!this._multiplexingStream.backpressureSupportEnabled || this.remoteWindowSize) {
this.remoteWindowHasCapacity.resolve();
}
this._duplex = new stream_1.Duplex({
async write(chunk, _, callback) {
let error;
try {
let payload = Buffer.from(chunk);
while (payload.length > 0) {
// Never transmit more than one frame's worth at a time.
let bytesTransmitted = Math.min(payload.length, MultiplexingStream_1.MultiplexingStream.framePayloadMaxLength);
// Don't send more than will fit in the remote's receiving window size.
if (self._multiplexingStream.backpressureSupportEnabled) {
await self.remoteWindowHasCapacity.promise;
if (!self.remoteWindowSize) {
throw new Error('Remote window size unknown.');
}
bytesTransmitted = Math.min(self.remoteWindowSize - self.remoteWindowFilled, bytesTransmitted);
}
self.onTransmittingBytes(bytesTransmitted);
const header = new FrameHeader_1.FrameHeader(ControlCode_1.ControlCode.content, id);
await multiplexingStream.sendFrameAsync(header, payload.slice(0, bytesTransmitted));
payload = payload.slice(bytesTransmitted);
}
}
catch (err) {
error = err;
}
if (callback) {
callback(error);
}
},
async final(cb) {
let error;
try {
await multiplexingStream.onChannelWritingCompleted(self);
}
catch (err) {
error = err;
}
if (cb) {
cb(error);
}
},
read() {
// Nothing to do here since data is pushed to us.
},
});
}
get stream() {
return this._duplex;
}
get acceptance() {
return this._acceptance.promise;
}
get isAccepted() {
return this._acceptance.isResolved;
}
get isRejectedOrCanceled() {
return this._acceptance.isRejected;
}
get completion() {
return this._completion.promise;
}
tryAcceptOffer(options) {
if (this._acceptance.resolve()) {
this.localWindowSize =
(options === null || options === void 0 ? void 0 : options.channelReceivingWindowSize) !== undefined
? Math.max(this._multiplexingStream.defaultChannelReceivingWindowSize, options === null || options === void 0 ? void 0 : options.channelReceivingWindowSize)
: this._multiplexingStream.defaultChannelReceivingWindowSize;
return true;
}
return false;
}
tryCancelOffer(reason) {
const cancellationReason = new cancellationtoken_1.default.CancellationError(reason);
this._acceptance.reject(cancellationReason);
this._completion.reject(cancellationReason);
// Also mark completion's promise rejections as 'caught' since we do not require
// or even expect it to be recognized by anyone else.
// The acceptance promise rejection is observed by the offer channel method.
caught(this._completion.promise);
// Inform the remote side that the offer is rescinded.
this.dispose();
}
onAccepted(acceptanceParameter) {
if (this._multiplexingStream.backpressureSupportEnabled) {
this.remoteWindowSize = acceptanceParameter.remoteWindowSize;
this.remoteWindowHasCapacity.resolve();
}
return this._acceptance.resolve();
}
onContent(buffer) {
const priorReadableFlowing = this._duplex.readableFlowing;
this._duplex.push(buffer);
// Large buffer pushes can switch a stream from flowing to non-flowing
// when it meets or exceeds the highWaterMark. We need to resume the stream
// in this case so that the user can continue to receive data.
if (priorReadableFlowing && this._duplex.readableFlowing === false) {
this._duplex.resume();
}
// We should find a way to detect when we *actually* share the received buffer with the Channel's user
// and only report consumption when they receive the buffer from us so that we effectively apply
// backpressure to the remote party based on our user's actual consumption rather than continually allocating memory.
if (this._multiplexingStream.backpressureSupportEnabled && buffer) {
this._multiplexingStream.localContentExamined(this, buffer.length);
}
}
onContentProcessed(bytesProcessed) {
if (bytesProcessed < 0) {
throw new Error('A non-negative number is required.');
}
if (bytesProcessed > this.remoteWindowFilled) {
throw new Error('More bytes processed than we thought were in the window.');
}
if (this.remoteWindowSize === undefined) {
throw new Error("Unexpected content processed message given we don't know the remote window size.");
}
this.remoteWindowFilled -= bytesProcessed;
if (this.remoteWindowFilled < this.remoteWindowSize) {
this.remoteWindowHasCapacity.resolve();
}
}
dispose(error) {
if (!this.isDisposed) {
super.dispose();
if (this._acceptance.reject(new cancellationtoken_1.default.CancellationError('disposed'))) {
// Don't crash node due to an unnoticed rejection when dispose was explicitly called.
caught(this.acceptance);
}
// For the pipes, we Complete *our* ends, and leave the user's ends alone.
// The completion will propagate when it's ready to.
this._duplex.end();
this._duplex.push(null);
if (error) {
this._completion.reject(error);
}
else {
this._completion.resolve();
}
// Send the notification, but we can't await the result of this.
caught(this._multiplexingStream.onChannelDisposed(this, error !== null && error !== void 0 ? error : null));
}
}
onTransmittingBytes(transmittedBytes) {
if (this._multiplexingStream.backpressureSupportEnabled) {
if (transmittedBytes < 0) {
throw new Error('Negative byte count transmitted.');
}
this.remoteWindowFilled += transmittedBytes;
if (this.remoteWindowFilled === this.remoteWindowSize) {
// Suspend writing.
this.remoteWindowHasCapacity = new Deferred_1.Deferred();
}
}
}
}
exports.ChannelClass = ChannelClass;
//# sourceMappingURL=Channel.js.map