Blame | Last modification | View Log | RSS feed
/* https://github.com/lpinca/stopcock/blob/master/index.js */
'use strict';
/**
* Class representing a token bucket.
*/
class TokenBucket {
/**
* Create a new `TokenBucket`.
*
* @param {Object} options Options object
* @param {Number} options.limit The tokens to add per `interval`
* @param {Number} options.interval The interval at which tokens are added
* @param {Number} options.bucketSize The capacity of the bucket
*/
constructor(options) {
this.tokens = this.capacity = options.bucketSize;
this.interval = options.interval;
this.limit = options.limit;
this.last = Date.now();
}
/**
* Refill the bucket with the proper amount of tokens.
*/
refill() {
const now = Date.now();
const tokens = Math.floor((now - this.last) * this.limit / this.interval);
this.tokens += tokens;
//
// `tokens` is rounded downward, so we only add the actual time required by
// those tokens.
//
this.last += Math.ceil(tokens * this.interval / this.limit);
if (this.tokens > this.capacity) {
this.tokens = this.capacity;
this.last = now;
}
}
/**
* Remove a token from the bucket.
*
* @return {Number} The amount of time to wait for a token to be available
*/
consume() {
this.refill();
if (this.tokens) {
this.tokens--;
return 0;
}
return Math.ceil(this.interval / this.limit - (Date.now() - this.last));
}
}
/**
* Limit the execution rate of a function using a leaky bucket algorithm.
*
* @param {Function} fn The function to rate limit calls to
* @param {Object} options Options object
* @param {Number} options.limit The number of allowed calls per `interval`
* @param {Number} options.interval The timespan where `limit` is calculated
* @param {Number} options.queueSize The maximum size of the queue
* @param {Number} options.bucketSize The capacity of the bucket
* @return {Function}
* @public
*/
function stopcock(fn, options) {
options = Object.assign({
queueSize: Math.pow(2, 32) - 1,
bucketSize: 40,
interval: 1000,
limit: 2
}, options);
const bucket = new TokenBucket(options);
const queue = [];
let timer = null;
function shift() {
clearTimeout(timer);
while (queue.length) {
const delay = bucket.consume();
if (delay > 0) {
timer = setTimeout(shift, delay);
break;
}
const data = queue.shift();
data[2](fn.apply(data[0], data[1]));
}
}
return function limiter() {
const args = arguments;
return new Promise((resolve, reject) => {
if (queue.length === options.queueSize) {
return reject(new Error('Queue is full'));
}
queue.push([this, args, resolve]);
shift();
});
};
}