UNPKG

@animetosho/parpar

Version:

High performance multi-threaded PAR2 creation library

195 lines (176 loc) 5.04 kB
"use strict"; var Timer = require('./timeoutwrap'); function ThrottleCancelToken(parent, id) { this.cancel = parent._cancelItem.bind(parent, id); } function emptyFn() {} // DelayThrottle: simple throttle mechanism by delaying every request function DelayThrottle(time) { this.queue = {}; this.waitTime = time; } DelayThrottle.prototype = { queue: null, qId: 0, waitTime: 0, pass: function(cost, cb) { var id = this.qId++; this.queue[id] = [cb, Timer('throttle', this._timeout.bind(this, id), this.waitTime)]; return new ThrottleCancelToken(this, id); }, _timeout: function(id) { this.queue[id][0](false, emptyFn); delete this.queue[id]; }, _cancelItem: function(id) { if(!(id in this.queue)) return false; this.queue[id][1].cancel(); this.queue[id][0](true, emptyFn); delete this.queue[id]; return true; }, cancel: function() { this._finish(true); }, flush: function() { this._finish(false); }, _finish: function(cancelled) { for(var i in this.queue) i[1].cancel(); for(var i in this.queue) i[0](cancelled, emptyFn); this.queue = {}; } }; // RateThrottle: throttle by allowing maxAmount items per timeWindow function RateThrottle(maxAmount, timeWindow) { this.queue = []; this.maxAmount = maxAmount | 0; this.timeWindow = timeWindow | 0; this.onTimeout = this._timeout.bind(this); } RateThrottle.prototype = { timer: null, debt: 0, debtTime: 0, qId: 0, pass: function(cost, cb) { var currentDebt = this._adjustDebt(); if(currentDebt >= this.maxAmount || this.queue.length) { // queue item up + set timer this.queue.push({cost: cost, cb: cb, id: ++this.qId}); this._setTimer(currentDebt); return new ThrottleCancelToken(this, this.qId); } else { // up cost + let through this.debt += cost; process.nextTick(cb.bind(null, false, emptyFn)); return null; } }, _adjustDebt: function() { if(this.timeWindow <= 0) return 0; // timeWindow == 0 -> no throttling var now = Date.now(); // we do a somewhat staggered update to get around potential precision issues with precise time calculations // the result is that this strategy should be more accurate than the naive method // how many time periods have passed since the last update? if(this.debtTime) { var periods = Math.floor((now - this.debtTime) / this.timeWindow); if(periods > 0) { this.debt -= (periods * this.maxAmount); this.debtTime += periods * this.timeWindow; if(this.debt <= 0) { // all debt cleared, reset this.debt = 0; this.debtTime = now; } } } else { // not initialized - start counting from here this.debtTime = now; } // return the current (precise) value of debt return this.debt - ((now - this.debtTime) * this.maxAmount / this.timeWindow); }, _timeout: function() { this.timer = null; var currentDebt = this._adjustDebt(); var toRun = []; do { // always allow the first item in queue to run, since this was fired from a timer var item = this.queue.shift(); this.debt += item.cost; currentDebt += item.cost; toRun.push(item.cb); } while(currentDebt < this.maxAmount && this.queue.length); process.nextTick(function() { toRun.forEach(function(fn) { fn(false, emptyFn); }); }); // setup timer for next if(this.queue.length) this._setTimer(currentDebt); }, _setTimer: function(currentDebt) { if(this.timer) return; var waitTime = Math.ceil((currentDebt - this.maxAmount +1) * this.timeWindow / this.maxAmount); if(waitTime <= 0) // in case this happens setImmediate(this.onTimeout); else this.timer = Timer('thottle', this.onTimeout, waitTime); }, // TODO: support dynamically adjusting limits _cancelItem: function(id) { // too lazy to use binary search, so find the item the noob way for(var idx in this.queue) { if(this.queue[idx].id == id) { this.queue[idx].cb(true, emptyFn); this.queue.splice(idx, 1); if(!this.queue.length && this.timer) { this.timer.cancel(); this.timer = null; } return true; } } return false; }, cancel: function() { this._finish(true); }, flush: function() { this._finish(false); }, _finish: function(cancelled) { if(this.timer) this.timer.cancel(); this.timer = null; this.queue.forEach(function(item) { item.cb(cancelled, emptyFn); }); this.queue = []; this.debt = this.debtTime = 0; } }; // ReqRateThrottle: like RateThrottle but cost is always 1 function ReqRateThrottle(maxAmount, timeWindow) { RateThrottle.call(this, maxAmount, timeWindow); } ReqRateThrottle.prototype = Object.create(RateThrottle.prototype); ReqRateThrottle.prototype.pass = function(cost, cb) { RateThrottle.prototype.pass.call(this, 1, cb); }; function NoThrottle() {} NoThrottle.prototype = { pass: function(cost, cb) { cb(false, emptyFn); return null; }, cancel: emptyFn, flush: emptyFn }; module.exports = { DelayThrottle: DelayThrottle, RateThrottle: RateThrottle, ReqRateThrottle: ReqRateThrottle, NoThrottle: NoThrottle };