all files / src/ RateLimiter.js

100% Statements 37/37
100% Branches 10/10
100% Functions 7/7
100% Lines 37/37
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129    32× 32×                                   32× 32× 32×   32× 26× 26× 26×     32× 32×   32×                               29× 15× 15×                       34×   32×                     34×   26× 19× 19× 19×   19×     26×                       12×                       21×   17×         32×  
'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;