630 lines
21 KiB
JavaScript
630 lines
21 KiB
JavaScript
var assert = require('assert');
|
|
|
|
// The Connection class
|
|
// ====================
|
|
|
|
// The Connection class manages HTTP/2 connections. Each instance corresponds to one transport
|
|
// stream (TCP stream). It operates by sending and receiving frames and is implemented as a
|
|
// [Flow](flow.html) subclass.
|
|
|
|
var Flow = require('./flow').Flow;
|
|
|
|
exports.Connection = Connection;
|
|
|
|
// Public API
|
|
// ----------
|
|
|
|
// * **new Connection(log, firstStreamId, settings)**: create a new Connection
|
|
//
|
|
// * **Event: 'error' (type)**: signals a connection level error made by the other end
|
|
//
|
|
// * **Event: 'peerError' (type)**: signals the receipt of a GOAWAY frame that contains an error
|
|
// code other than NO_ERROR
|
|
//
|
|
// * **Event: 'stream' (stream)**: signals that there's an incoming stream
|
|
//
|
|
// * **createStream(): stream**: initiate a new stream
|
|
//
|
|
// * **set(settings, callback)**: change the value of one or more settings according to the
|
|
// key-value pairs of `settings`. The callback is called after the peer acknowledged the changes.
|
|
//
|
|
// * **ping([callback])**: send a ping and call callback when the answer arrives
|
|
//
|
|
// * **close([error])**: close the stream with an error code
|
|
|
|
// Constructor
|
|
// -----------
|
|
|
|
// The main aspects of managing the connection are:
|
|
function Connection(log, firstStreamId, settings) {
|
|
// * initializing the base class
|
|
Flow.call(this, 0);
|
|
|
|
// * logging: every method uses the common logger object
|
|
this._log = log.child({ component: 'connection' });
|
|
|
|
// * stream management
|
|
this._initializeStreamManagement(firstStreamId);
|
|
|
|
// * lifecycle management
|
|
this._initializeLifecycleManagement();
|
|
|
|
// * flow control
|
|
this._initializeFlowControl();
|
|
|
|
// * settings management
|
|
this._initializeSettingsManagement(settings);
|
|
|
|
// * multiplexing
|
|
this._initializeMultiplexing();
|
|
}
|
|
Connection.prototype = Object.create(Flow.prototype, { constructor: { value: Connection } });
|
|
|
|
// Overview
|
|
// --------
|
|
|
|
// | ^ | ^
|
|
// v | v |
|
|
// +--------------+ +--------------+
|
|
// +---| stream1 |---| stream2 |---- .... ---+
|
|
// | | +----------+ | | +----------+ | |
|
|
// | | | stream1. | | | | stream2. | | |
|
|
// | +-| upstream |-+ +-| upstream |-+ |
|
|
// | +----------+ +----------+ |
|
|
// | | ^ | ^ |
|
|
// | v | v | |
|
|
// | +-----+-------------+-----+-------- .... |
|
|
// | ^ | | | |
|
|
// | | v | | |
|
|
// | +--------------+ | | |
|
|
// | | stream0 | | | |
|
|
// | | connection | | | |
|
|
// | | management | multiplexing |
|
|
// | +--------------+ flow control |
|
|
// | | ^ |
|
|
// | _read() | | _write() |
|
|
// | v | |
|
|
// | +------------+ +-----------+ |
|
|
// | |output queue| |input queue| |
|
|
// +----------------+------------+-+-----------+-----------------+
|
|
// | ^
|
|
// read() | | write()
|
|
// v |
|
|
|
|
// Stream management
|
|
// -----------------
|
|
|
|
var Stream = require('./stream').Stream;
|
|
|
|
// Initialization:
|
|
Connection.prototype._initializeStreamManagement = function _initializeStreamManagement(firstStreamId) {
|
|
// * streams are stored in two data structures:
|
|
// * `_streamIds` is an id -> stream map of the streams that are allowed to receive frames.
|
|
// * `_streamPriorities` is a priority -> [stream] map of stream that allowed to send frames.
|
|
this._streamIds = [];
|
|
this._streamPriorities = [];
|
|
|
|
// * The next outbound stream ID and the last inbound stream id
|
|
this._nextStreamId = firstStreamId;
|
|
this._lastIncomingStream = 0;
|
|
|
|
// * Calling `_writeControlFrame` when there's an incoming stream with 0 as stream ID
|
|
this._streamIds[0] = { upstream: { write: this._writeControlFrame.bind(this) } };
|
|
|
|
// * By default, the number of concurrent outbound streams is not limited. The `_streamLimit` can
|
|
// be set by the SETTINGS_MAX_CONCURRENT_STREAMS setting.
|
|
this._streamSlotsFree = Infinity;
|
|
this._streamLimit = Infinity;
|
|
this.on('RECEIVING_SETTINGS_MAX_CONCURRENT_STREAMS', this._updateStreamLimit);
|
|
};
|
|
|
|
// `_writeControlFrame` is called when there's an incoming frame in the `_control` stream. It
|
|
// broadcasts the message by creating an event on it.
|
|
Connection.prototype._writeControlFrame = function _writeControlFrame(frame) {
|
|
if ((frame.type === 'SETTINGS') || (frame.type === 'PING') ||
|
|
(frame.type === 'GOAWAY') || (frame.type === 'WINDOW_UPDATE') ||
|
|
(frame.type === 'ALTSVC') || (frame.type == 'ORIGIN')) {
|
|
this._log.debug({ frame: frame }, 'Receiving connection level frame');
|
|
this.emit(frame.type, frame);
|
|
} else {
|
|
this._log.error({ frame: frame }, 'Invalid connection level frame');
|
|
this.emit('error', 'PROTOCOL_ERROR');
|
|
}
|
|
};
|
|
|
|
// Methods to manage the stream slot pool:
|
|
Connection.prototype._updateStreamLimit = function _updateStreamLimit(newStreamLimit) {
|
|
var wakeup = (this._streamSlotsFree === 0) && (newStreamLimit > this._streamLimit);
|
|
this._streamSlotsFree += newStreamLimit - this._streamLimit;
|
|
this._streamLimit = newStreamLimit;
|
|
if (wakeup) {
|
|
this.emit('wakeup');
|
|
}
|
|
};
|
|
|
|
Connection.prototype._changeStreamCount = function _changeStreamCount(change) {
|
|
if (change) {
|
|
this._log.trace({ free: this._streamSlotsFree, change: change },
|
|
'Changing active stream count.');
|
|
var wakeup = (this._streamSlotsFree === 0) && (change < 0);
|
|
this._streamSlotsFree -= change;
|
|
if (wakeup) {
|
|
this.emit('wakeup');
|
|
}
|
|
}
|
|
};
|
|
|
|
// Creating a new *inbound or outbound* stream with the given `id` (which is undefined in case of
|
|
// an outbound stream) consists of three steps:
|
|
//
|
|
// 1. var stream = new Stream(this._log, this);
|
|
// 2. this._allocateId(stream, id);
|
|
// 2. this._allocatePriority(stream);
|
|
|
|
// Allocating an ID to a stream
|
|
Connection.prototype._allocateId = function _allocateId(stream, id) {
|
|
// * initiated stream without definite ID
|
|
if (id === undefined) {
|
|
id = this._nextStreamId;
|
|
this._nextStreamId += 2;
|
|
}
|
|
|
|
// * incoming stream with a legitim ID (larger than any previous and different parity than ours)
|
|
else if ((id > this._lastIncomingStream) && ((id - this._nextStreamId) % 2 !== 0)) {
|
|
this._lastIncomingStream = id;
|
|
}
|
|
|
|
// * incoming stream with invalid ID
|
|
else {
|
|
this._log.error({ stream_id: id, lastIncomingStream: this._lastIncomingStream },
|
|
'Invalid incoming stream ID.');
|
|
this.emit('error', 'PROTOCOL_ERROR');
|
|
return undefined;
|
|
}
|
|
|
|
assert(!(id in this._streamIds));
|
|
|
|
// * adding to `this._streamIds`
|
|
this._log.trace({ s: stream, stream_id: id }, 'Allocating ID for stream.');
|
|
this._streamIds[id] = stream;
|
|
stream.id = id;
|
|
this.emit('new_stream', stream, id);
|
|
|
|
// * forwarding connection errors from streams
|
|
stream.on('connectionError', this.emit.bind(this, 'error'));
|
|
|
|
return id;
|
|
};
|
|
|
|
// Allocating a priority to a stream, and managing priority changes
|
|
Connection.prototype._allocatePriority = function _allocatePriority(stream) {
|
|
this._log.trace({ s: stream }, 'Allocating priority for stream.');
|
|
this._insert(stream, stream._priority);
|
|
stream.on('priority', this._reprioritize.bind(this, stream));
|
|
stream.upstream.on('readable', this.emit.bind(this, 'wakeup'));
|
|
this.emit('wakeup');
|
|
};
|
|
|
|
Connection.prototype._insert = function _insert(stream, priority) {
|
|
if (priority in this._streamPriorities) {
|
|
this._streamPriorities[priority].push(stream);
|
|
} else {
|
|
this._streamPriorities[priority] = [stream];
|
|
}
|
|
};
|
|
|
|
Connection.prototype._reprioritize = function _reprioritize(stream, priority) {
|
|
var bucket = this._streamPriorities[stream._priority];
|
|
var index = bucket.indexOf(stream);
|
|
assert(index !== -1);
|
|
bucket.splice(index, 1);
|
|
if (bucket.length === 0) {
|
|
delete this._streamPriorities[stream._priority];
|
|
}
|
|
|
|
this._insert(stream, priority);
|
|
};
|
|
|
|
// Creating an *inbound* stream with the given ID. It is called when there's an incoming frame to
|
|
// a previously nonexistent stream.
|
|
Connection.prototype._createIncomingStream = function _createIncomingStream(id) {
|
|
this._log.debug({ stream_id: id }, 'New incoming stream.');
|
|
|
|
var stream = new Stream(this._log, this);
|
|
this._allocateId(stream, id);
|
|
this._allocatePriority(stream);
|
|
this.emit('stream', stream, id);
|
|
|
|
return stream;
|
|
};
|
|
|
|
// Creating an *outbound* stream
|
|
Connection.prototype.createStream = function createStream() {
|
|
this._log.trace('Creating new outbound stream.');
|
|
|
|
// * Receiving is enabled immediately, and an ID gets assigned to the stream
|
|
var stream = new Stream(this._log, this);
|
|
this._allocatePriority(stream);
|
|
|
|
return stream;
|
|
};
|
|
|
|
// Multiplexing
|
|
// ------------
|
|
|
|
Connection.prototype._initializeMultiplexing = function _initializeMultiplexing() {
|
|
this.on('window_update', this.emit.bind(this, 'wakeup'));
|
|
this._sendScheduled = false;
|
|
this._firstFrameReceived = false;
|
|
};
|
|
|
|
// The `_send` method is a virtual method of the [Flow class](flow.html) that has to be implemented
|
|
// by child classes. It reads frames from streams and pushes them to the output buffer.
|
|
Connection.prototype._send = function _send(immediate) {
|
|
// * Do not do anything if the connection is already closed
|
|
if (this._closed) {
|
|
return;
|
|
}
|
|
|
|
// * Collapsing multiple calls in a turn into a single deferred call
|
|
if (immediate) {
|
|
this._sendScheduled = false;
|
|
} else {
|
|
if (!this._sendScheduled) {
|
|
this._sendScheduled = true;
|
|
setImmediate(this._send.bind(this, true));
|
|
}
|
|
return;
|
|
}
|
|
|
|
this._log.trace('Starting forwarding frames from streams.');
|
|
|
|
// * Looping through priority `bucket`s in priority order.
|
|
priority_loop:
|
|
for (var priority in this._streamPriorities) {
|
|
var bucket = this._streamPriorities[priority];
|
|
var nextBucket = [];
|
|
|
|
// * Forwarding frames from buckets with round-robin scheduling.
|
|
// 1. pulling out frame
|
|
// 2. if there's no frame, skip this stream
|
|
// 3. if forwarding this frame would make `streamCount` greater than `streamLimit`, skip
|
|
// this stream
|
|
// 4. adding stream to the bucket of the next round
|
|
// 5. assigning an ID to the frame (allocating an ID to the stream if there isn't already)
|
|
// 6. if forwarding a PUSH_PROMISE, allocate ID to the promised stream
|
|
// 7. forwarding the frame, changing `streamCount` as appropriate
|
|
// 8. stepping to the next stream if there's still more frame needed in the output buffer
|
|
// 9. switching to the bucket of the next round
|
|
while (bucket.length > 0) {
|
|
for (var index = 0; index < bucket.length; index++) {
|
|
var stream = bucket[index];
|
|
var frame = stream.upstream.read((this._window > 0) ? this._window : -1);
|
|
|
|
if (!frame) {
|
|
continue;
|
|
} else if (frame.count_change > this._streamSlotsFree) {
|
|
stream.upstream.unshift(frame);
|
|
continue;
|
|
}
|
|
|
|
nextBucket.push(stream);
|
|
|
|
if (frame.stream === undefined) {
|
|
frame.stream = stream.id || this._allocateId(stream);
|
|
}
|
|
|
|
if (frame.type === 'PUSH_PROMISE') {
|
|
this._allocatePriority(frame.promised_stream);
|
|
frame.promised_stream = this._allocateId(frame.promised_stream);
|
|
}
|
|
|
|
this._log.trace({ s: stream, frame: frame }, 'Forwarding outgoing frame');
|
|
var moreNeeded = this.push(frame);
|
|
this._changeStreamCount(frame.count_change);
|
|
|
|
assert(moreNeeded !== null); // The frame shouldn't be unforwarded
|
|
if (moreNeeded === false) {
|
|
break priority_loop;
|
|
}
|
|
}
|
|
|
|
bucket = nextBucket;
|
|
nextBucket = [];
|
|
}
|
|
}
|
|
|
|
// * if we couldn't forward any frame, then sleep until window update, or some other wakeup event
|
|
if (moreNeeded === undefined) {
|
|
this.once('wakeup', this._send.bind(this));
|
|
}
|
|
|
|
this._log.trace({ moreNeeded: moreNeeded }, 'Stopping forwarding frames from streams.');
|
|
};
|
|
|
|
// The `_receive` method is another virtual method of the [Flow class](flow.html) that has to be
|
|
// implemented by child classes. It forwards the given frame to the appropriate stream:
|
|
Connection.prototype._receive = function _receive(frame, done) {
|
|
this._log.trace({ frame: frame }, 'Forwarding incoming frame');
|
|
|
|
// * first frame needs to be checked by the `_onFirstFrameReceived` method
|
|
if (!this._firstFrameReceived) {
|
|
this._firstFrameReceived = true;
|
|
this._onFirstFrameReceived(frame);
|
|
}
|
|
|
|
// Do some sanity checking here before we create a stream
|
|
if ((frame.type == 'SETTINGS' ||
|
|
frame.type == 'PING' ||
|
|
frame.type == 'GOAWAY') &&
|
|
frame.stream != 0) {
|
|
// Got connection-level frame on a stream - EEP!
|
|
this.close('PROTOCOL_ERROR');
|
|
return;
|
|
} else if ((frame.type == 'DATA' ||
|
|
frame.type == 'HEADERS' ||
|
|
frame.type == 'PRIORITY' ||
|
|
frame.type == 'RST_STREAM' ||
|
|
frame.type == 'PUSH_PROMISE' ||
|
|
frame.type == 'CONTINUATION') &&
|
|
frame.stream == 0) {
|
|
// Got stream-level frame on connection - EEP!
|
|
this.close('PROTOCOL_ERROR');
|
|
return;
|
|
}
|
|
// WINDOW_UPDATE can be on either stream or connection
|
|
|
|
// * gets the appropriate stream from the stream registry
|
|
var stream = this._streamIds[frame.stream];
|
|
|
|
// * or creates one if it's not in `this.streams`
|
|
if (!stream) {
|
|
stream = this._createIncomingStream(frame.stream);
|
|
}
|
|
|
|
// * in case of PUSH_PROMISE, replaces the promised stream id with a new incoming stream
|
|
if (frame.type === 'PUSH_PROMISE') {
|
|
frame.promised_stream = this._createIncomingStream(frame.promised_stream);
|
|
}
|
|
|
|
frame.count_change = this._changeStreamCount.bind(this);
|
|
|
|
// * and writes it to the `stream`'s `upstream`
|
|
stream.upstream.write(frame);
|
|
|
|
done();
|
|
};
|
|
|
|
// Settings management
|
|
// -------------------
|
|
|
|
var defaultSettings = {
|
|
};
|
|
|
|
// Settings management initialization:
|
|
Connection.prototype._initializeSettingsManagement = function _initializeSettingsManagement(settings) {
|
|
// * Setting up the callback queue for setting acknowledgements
|
|
this._settingsAckCallbacks = [];
|
|
|
|
// * Sending the initial settings.
|
|
this._log.debug({ settings: settings },
|
|
'Sending the first SETTINGS frame as part of the connection header.');
|
|
this.set(settings || defaultSettings);
|
|
|
|
// * Forwarding SETTINGS frames to the `_receiveSettings` method
|
|
this.on('SETTINGS', this._receiveSettings);
|
|
this.on('RECEIVING_SETTINGS_MAX_FRAME_SIZE', this._sanityCheckMaxFrameSize);
|
|
};
|
|
|
|
// * Checking that the first frame the other endpoint sends is SETTINGS
|
|
Connection.prototype._onFirstFrameReceived = function _onFirstFrameReceived(frame) {
|
|
if ((frame.stream === 0) && (frame.type === 'SETTINGS')) {
|
|
this._log.debug('Receiving the first SETTINGS frame as part of the connection header.');
|
|
} else {
|
|
this._log.fatal({ frame: frame }, 'Invalid connection header: first frame is not SETTINGS.');
|
|
this.emit('error', 'PROTOCOL_ERROR');
|
|
}
|
|
};
|
|
|
|
// Handling of incoming SETTINGS frames.
|
|
Connection.prototype._receiveSettings = function _receiveSettings(frame) {
|
|
// * If it's an ACK, call the appropriate callback
|
|
if (frame.flags.ACK) {
|
|
var callback = this._settingsAckCallbacks.shift();
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
// * If it's a setting change request, then send an ACK and change the appropriate settings
|
|
else {
|
|
if (!this._closed) {
|
|
this.push({
|
|
type: 'SETTINGS',
|
|
flags: { ACK: true },
|
|
stream: 0,
|
|
settings: {}
|
|
});
|
|
}
|
|
for (var name in frame.settings) {
|
|
this.emit('RECEIVING_' + name, frame.settings[name]);
|
|
}
|
|
}
|
|
};
|
|
|
|
Connection.prototype._sanityCheckMaxFrameSize = function _sanityCheckMaxFrameSize(value) {
|
|
if ((value < 0x4000) || (value >= 0x01000000)) {
|
|
this._log.fatal('Received invalid value for max frame size: ' + value);
|
|
this.emit('error');
|
|
}
|
|
};
|
|
|
|
// Changing one or more settings value and sending out a SETTINGS frame
|
|
Connection.prototype.set = function set(settings, callback) {
|
|
// * Calling the callback and emitting event when the change is acknowledges
|
|
var self = this;
|
|
this._settingsAckCallbacks.push(function() {
|
|
for (var name in settings) {
|
|
self.emit('ACKNOWLEDGED_' + name, settings[name]);
|
|
}
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
});
|
|
|
|
// * Sending out the SETTINGS frame
|
|
this.push({
|
|
type: 'SETTINGS',
|
|
flags: { ACK: false },
|
|
stream: 0,
|
|
settings: settings
|
|
});
|
|
for (var name in settings) {
|
|
this.emit('SENDING_' + name, settings[name]);
|
|
}
|
|
};
|
|
|
|
// Lifecycle management
|
|
// --------------------
|
|
|
|
// The main responsibilities of lifecycle management code:
|
|
//
|
|
// * keeping the connection alive by
|
|
// * sending PINGs when the connection is idle
|
|
// * answering PINGs
|
|
// * ending the connection
|
|
|
|
Connection.prototype._initializeLifecycleManagement = function _initializeLifecycleManagement() {
|
|
this._pings = {};
|
|
this.on('PING', this._receivePing);
|
|
this.on('GOAWAY', this._receiveGoaway);
|
|
this._closed = false;
|
|
};
|
|
|
|
// Generating a string of length 16 with random hexadecimal digits
|
|
Connection.prototype._generatePingId = function _generatePingId() {
|
|
do {
|
|
var id = '';
|
|
for (var i = 0; i < 16; i++) {
|
|
id += Math.floor(Math.random()*16).toString(16);
|
|
}
|
|
} while(id in this._pings);
|
|
return id;
|
|
};
|
|
|
|
// Sending a ping and calling `callback` when the answer arrives
|
|
Connection.prototype.ping = function ping(callback) {
|
|
var id = this._generatePingId();
|
|
var data = Buffer.from(id, 'hex');
|
|
this._pings[id] = callback;
|
|
|
|
this._log.debug({ data: data }, 'Sending PING.');
|
|
this.push({
|
|
type: 'PING',
|
|
flags: {
|
|
ACK: false
|
|
},
|
|
stream: 0,
|
|
data: data
|
|
});
|
|
};
|
|
|
|
// Answering pings
|
|
Connection.prototype._receivePing = function _receivePing(frame) {
|
|
if (frame.flags.ACK) {
|
|
var id = frame.data.toString('hex');
|
|
if (id in this._pings) {
|
|
this._log.debug({ data: frame.data }, 'Receiving answer for a PING.');
|
|
var callback = this._pings[id];
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
delete this._pings[id];
|
|
} else {
|
|
this._log.warn({ data: frame.data }, 'Unsolicited PING answer.');
|
|
}
|
|
|
|
} else {
|
|
this._log.debug({ data: frame.data }, 'Answering PING.');
|
|
this.push({
|
|
type: 'PING',
|
|
flags: {
|
|
ACK: true
|
|
},
|
|
stream: 0,
|
|
data: frame.data
|
|
});
|
|
}
|
|
};
|
|
|
|
Connection.prototype.originFrame = function originFrame(originList) {
|
|
this._log.debug(originList, 'emitting origin frame');
|
|
|
|
this.push({
|
|
type: 'ORIGIN',
|
|
flags: {},
|
|
stream: 0,
|
|
originList : originList,
|
|
});
|
|
};
|
|
|
|
// Terminating the connection
|
|
Connection.prototype.close = function close(error) {
|
|
if (this._closed) {
|
|
this._log.warn('Trying to close an already closed connection');
|
|
return;
|
|
}
|
|
|
|
this._log.debug({ error: error }, 'Closing the connection');
|
|
this.push({
|
|
type: 'GOAWAY',
|
|
flags: {},
|
|
stream: 0,
|
|
last_stream: this._lastIncomingStream,
|
|
error: error || 'NO_ERROR'
|
|
});
|
|
this.push(null);
|
|
this._closed = true;
|
|
};
|
|
|
|
Connection.prototype._receiveGoaway = function _receiveGoaway(frame) {
|
|
this._log.debug({ error: frame.error }, 'Other end closed the connection');
|
|
this.push(null);
|
|
this._closed = true;
|
|
if (frame.error !== 'NO_ERROR') {
|
|
this.emit('peerError', frame.error);
|
|
}
|
|
};
|
|
|
|
// Flow control
|
|
// ------------
|
|
|
|
Connection.prototype._initializeFlowControl = function _initializeFlowControl() {
|
|
// Handling of initial window size of individual streams.
|
|
this._initialStreamWindowSize = INITIAL_STREAM_WINDOW_SIZE;
|
|
this.on('new_stream', function(stream) {
|
|
stream.upstream.setInitialWindow(this._initialStreamWindowSize);
|
|
});
|
|
this.on('RECEIVING_SETTINGS_INITIAL_WINDOW_SIZE', this._setInitialStreamWindowSize);
|
|
this._streamIds[0].upstream.setInitialWindow = function noop() {};
|
|
};
|
|
|
|
// The initial connection flow control window is 65535 bytes.
|
|
var INITIAL_STREAM_WINDOW_SIZE = 65535;
|
|
|
|
// A SETTINGS frame can alter the initial flow control window size for all current streams. When the
|
|
// value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the window size of all
|
|
// stream by calling the `setInitialStreamWindowSize` method. The window size has to be modified by
|
|
// the difference between the new value and the old value.
|
|
Connection.prototype._setInitialStreamWindowSize = function _setInitialStreamWindowSize(size) {
|
|
if ((this._initialStreamWindowSize === Infinity) && (size !== Infinity)) {
|
|
this._log.error('Trying to manipulate initial flow control window size after flow control was turned off.');
|
|
this.emit('error', 'FLOW_CONTROL_ERROR');
|
|
} else {
|
|
this._log.debug({ size: size }, 'Changing stream initial window size.');
|
|
this._initialStreamWindowSize = size;
|
|
this._streamIds.forEach(function(stream) {
|
|
stream.upstream.setInitialWindow(size);
|
|
});
|
|
}
|
|
};
|