I wanted to write a function that when applied to another would rate limit it, but permit all calls to eventually make it through.
Comments and criticism welcome.
var MAX_RUNS_PER_WINDOW = 10;
var RUN_WINDOW = 1000;
function limit(fn) {
var callQueue = [],
invokeTimes = Object.create(circularQueue),
waitId = null;
function limited() {
callQueue.push(() => {
invokeTimes.unshift(performance.now())
fn.apply(this, arguments);
});
if (mayProceed()) {
return dequeue();
}
if (waitId === null) {
waitId = setTimeout(dequeue, timeToWait());
}
}
limited.cancel = function() {
clearTimeout(waitId);
};
return limited;
function dequeue() {
waitId = null ;
clearTimeout(waitId);
callQueue.shift()();
if (mayProceed()) {
return dequeue();
}
if (callQueue.length) {
waitId = setTimeout(dequeue, timeToWait());
}
}
function mayProceed() {
return callQueue.length && (timeForMaxRuns() >= RUN_WINDOW);
}
function timeToWait() {
var ttw = RUN_WINDOW - timeForMaxRuns();
return ttw < 0 ? 0 : ttw;
}
function timeForMaxRuns() {
return (performance.now() - (invokeTimes[MAX_RUNS_PER_WINDOW - 1] || 0));
}
}
var circularQueue = [];
var originalUnshift = circularQueue.unshift;
circularQueue.MAX_LENGTH = MAX_RUNS_PER_WINDOW;
circularQueue.unshift = function(element) {
if (this.length === this.MAX_LENGTH) {
this.pop();
}
return originalUnshift.call(this, element);
}
var printLetter = limit(function(letter) {
document.write(letter);
});
['A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'X', 'Y', 'Z'].forEach(printLetter);
printLetter
should be called 10 times or less. \$\endgroup\$callQueue.push(() => ...
), but if I run it with Traceur, it just printsundefined
indicating that the throttling isn't quite working... \$\endgroup\$arguments
(line 12) refers to the anonymous arrow function's arguments, and notlimited
's arguments. It'll be fixed in Firefox 43.0. The code works on Chrome. \$\endgroup\$