UNPKG

nerdbank-streams

Version:
227 lines 9.82 kB
"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