Home Reference Source Test Repository

src/RateLimiter.js

'use strict';

const TokenBucket = require('./TokenBucket');
const _           = require('lodash');

/**
 * RateLimiter Class
 *
 * Handles creation and destruction of TokenBuckets by key.
 */
class RateLimiter {

  /**
   * Creates a RateLimiter object.
   *
   * @param {object} config configuration values
   * @param {integer} config.limit The max amount of tokens per incrementInterval.
   * @param {integer} config.incrementInterval The time in ms before tokens will increment.
   * @param {integer} config.increment The amount of tokens that will increment each incrementInterval.
   */
  constructor(config) {
    let limit             = null;
    let incrementInterval = null;
    let increment         = null;

    if (config != null) {
      limit             = config.limit;
      incrementInterval = config.incrementInterval;
      increment         = config.increment;
    }

    this.config = { limit, incrementInterval, increment };
    this.tokenBuckets = {};

    this._interval = setInterval(() => {
      this._cleanup();
    }, incrementInterval);
  }

  /**
   * Cleans up unused TokenBuckets
   */
  _cleanup() {
    _.each(this.tokenBuckets, (value, key) => {
      const tokenBucket = this.tokenBuckets[key];
      if (tokenBucket.tokens == tokenBucket.limit) {
        tokenBucket.destroy();
        delete this.tokenBuckets[key];
      }
    });
  }

  /**
   * Cleans up all TokenBuckets and nulls the array.
   */
  destroy() {
    _.each(this.tokenBuckets, (value, key) => {
      this.tokenBuckets[key].destroy();
      delete this.tokenBuckets[key];
    });
  }

  /**
   * Helper to throw an error if the key is invalid.
   *
   * @param {key} key The identifier for the request
   * @throws {Error} throw error when key is invalid
   * @private
   */
  _errorCheckForKey(key) {
    if (key == null) {
      throw new Error('key is required');
    }
    else if(typeof(key) != 'string') {
      throw new Error('key is not a string');
    }
  }

  /**
   * Helper to create a new TokenBucket if needed.
   *
   * @param {string} key The identifier for the request
   * @private
   */
  _getTokenBucket(key) {
    this._errorCheckForKey(key);

    if (this.tokenBuckets[key] == null) {
      const limit = this.config.limit;
      const increment = this.config.increment;
      const incrementInterval = this.config.incrementInterval;

      this.tokenBuckets[key] = new TokenBucket({ limit, increment, incrementInterval });
    }

    return this.tokenBuckets[key];
  }

  /**
   * Appropriately decrements the remaining token availability
   * when called and returns the remainder.
   *
   * @param {string} key The identifier for the request
   * @param {integer} [amount] The number to decrement by
   * @return {integer} Remaining request count
   */
  decrementTokens(key, amount) {
    const tokenBucket = this._getTokenBucket(key);

    return tokenBucket.decrementTokens(amount);
  }

  /**
   * Returns the remaining request count for the given key.
   * If the key doesn't already exist, it will create it.
   *
   * @param {string} key The identifier for the request
   * @return {integer} Remaining request count
   */
  getTokensRemaining(key) {
    const tokenBucket = this._getTokenBucket(key);

    return tokenBucket.getTokensRemaining();
  }

}

module.exports = RateLimiter;