throttled-reader
Version:
Node.js Readable stream capping average data rate by limiting reads from the source
104 lines (91 loc) • 2.98 kB
JavaScript
var stream = require('stream');
var util = require('util');
function ThrottledReader(readableStream, throttleOptions, streamOptions) {
var self = this;
stream.Readable.call(self, streamOptions);
throttleOptions = throttleOptions || {};
self._rate = Number(throttleOptions.rate) || 0;
self._cooldownInterval = Number(throttleOptions.cooldownInterval) || 100;
self._sourceReadable = false;
self._readPending = false;
self._sourceEnded = false;
self._byteBalance = 0;
self._lastChunkSize = self._rate;
self._bytesRead = 0;
self._lastCooldownExecTime = null;
self._cooldownTimer = null;
self._source = readableStream;
self._source.on('readable', function() {
self._sourceReadable = true;
self._tryRead();
});
self._source.on('end', self._endStream.bind(self));
// Forward other Readable events
['error', 'close'].forEach(function(event) {
self._source.on(event, self.emit.bind(self, event));
});
}
util.inherits(ThrottledReader, stream.Readable);
ThrottledReader.prototype.setRate = function(rate) { this._rate = rate };
ThrottledReader.prototype.getRate = function() { return this._rate };
ThrottledReader.prototype._activateCooldown = function() {
if (this._cooldownTimer) {
return;
}
this._lastCooldownExecTime = new Date();
this._cooldownTimer = setInterval(this._executeCooldown.bind(this),
this._cooldownInterval);
};
ThrottledReader.prototype._executeCooldown = function() {
var now = new Date();
this._byteBalance -= this._rate * (now - this._lastCooldownExecTime) / 1000;
this._lastCooldownExecTime = now;
this._tryRead();
if (this._isReadAllowed()) {
clearInterval(this._cooldownTimer);
this._cooldownTimer = null;
}
};
ThrottledReader.prototype._isReadAllowed = function() {
if (this._rate <= 0) {
return true;
}
if (!this._bytesRead) {
return true;
}
if (this._byteBalance <= Math.max(-this._lastChunkSize, -this._rate)) {
return true;
}
this._activateCooldown();
return false;
};
ThrottledReader.prototype._endStream = function() {
if (!this._sourceEnded) {
this.push(null);
this._sourceEnded = true;
}
};
ThrottledReader.prototype._tryRead = function() {
if (!this._readPending || !this._sourceReadable || !this._isReadAllowed()) {
return;
}
this._sourceReadable = false;
this._readPending = false;
var chunk = this._source.read();
if (chunk !== null) {
this._bytesRead += chunk.length;
this._lastChunkSize = chunk.length;
if (this._rate > 0)
this._byteBalance += chunk.length;
this.push(chunk);
this._activateCooldown();
} else {
this._endStream();
}
};
ThrottledReader.prototype._read = function() {
this._readPending = true;
if (!this._tryRead()) {
}
};
module.exports = ThrottledReader;