Subversion Repositories munaweb

Rev

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();
    });
  };
}