text: (function () {
  
  /* Imports */
  var Meteor = Package.meteor.Meteor;
  var global = Package.meteor.global;
  var meteorEnv = Package.meteor.meteorEnv;
  var _ = Package.underscore._;
  var Random = Package.random.Random;
  
  /* Package-scope variables */
  var RateLimiter;
  
  (function(){
  
  //////////////////////////////////////////////////////////////////////////////////
  //                                                                              //
  // packages/rate-limit/rate-limit.js                                            //
  //                                                                              //
  //////////////////////////////////////////////////////////////////////////////////
                                                                                  //
  // Default time interval (in milliseconds) to reset rate limit counters
  var DEFAULT_INTERVAL_TIME_IN_MILLISECONDS = 1000;
  // Default number of events allowed per time interval
  var DEFAULT_REQUESTS_PER_INTERVAL = 10;
  
  // A rule is defined by an options object that contains two fields,
  // `numRequestsAllowed` which is the number of events allowed per interval, and
  // an `intervalTime` which is the amount of time in milliseconds before the
  // rate limit restarts its internal counters, and by a matchers object. A
  // matchers object is a POJO that contains a set of keys with values that
  // define the entire set of inputs that match for each key. The values can
  // either be null (optional), a primitive or a function that returns a boolean
  // of whether the provided input's value matches for this key.
  //
  // Rules are uniquely assigned an `id` and they store a dictionary of counters,
  // which are records used to keep track of inputs that match the rule. If a
  // counter reaches the `numRequestsAllowed` within a given `intervalTime`, a
  // rate limit is reached and future inputs that map to that counter will
  // result in errors being returned to the client.
  var Rule = function (options, matchers) {
    var self = this;
  
    self.id = Random.id();
  
    self.options = options;
  
    self._matchers = matchers;
  
    self._lastResetTime = new Date().getTime();
  
    // Dictionary of input keys to counters
    self.counters = {};
  };
  
  _.extend(Rule.prototype, {
    // Determine if this rule applies to the given input by comparing all
    // rule.matchers. If the match fails, search short circuits instead of
    // iterating through all matchers.
    match: function (input) {
      var self = this;
      var ruleMatches = true;
      return _.every(self._matchers, function (matcher, key) {
        if (matcher !== null) {
          if (!(_.has(input,key))) {
            return false;
          } else {
            if (typeof matcher === 'function') {
              if (!(matcher(input[key]))) {
                return false;
              }
            } else {
              if (matcher !== input[key]) {
                return false;
              }
            }
          }
        }
        return true;
      });
    },
  
    // Generates unique key string for provided input by concatenating all the
    // keys in the matcher with the corresponding values in the input.
    // Only called if rule matches input.
    _generateKeyString: function (input) {
      var self = this;
      var returnString = "";
      _.each(self._matchers, function (matcher, key) {
        if (matcher !== null) {
          if (typeof matcher === 'function') {
            if (matcher(input[key])) {
              returnString += key + input[key];
            }
          } else {
            returnString += key + input[key];
          }
        }
      });
      return returnString;
    },
  
    // Applies the provided input and returns the key string, time since counters
    // were last reset and time to next reset.
    apply: function (input) {
      var self = this;
      var keyString = self._generateKeyString(input);
      var timeSinceLastReset = new Date().getTime() - self._lastResetTime;
      var timeToNextReset = self.options.intervalTime - timeSinceLastReset;
      return {
        key: keyString,
        timeSinceLastReset: timeSinceLastReset,
        timeToNextReset: timeToNextReset
      };
    },
    // Reset counter dictionary for this specific rule. Called once the
    // timeSinceLastReset has exceeded the intervalTime. _lastResetTime is
    // set to be the current time in milliseconds.
    resetCounter: function () {
      var self = this;
  
      // Delete the old counters dictionary to allow for garbage collection
      self.counters = {};
      self._lastResetTime = new Date().getTime();
    },
    _executeCallback: function (reply, ruleInput) {
      try {
        if (this.options.callback) {
          this.options.callback(reply, ruleInput);
        }
      } catch (e) {
        // Do not throw error here
        console.error(e);
      }
    },
  });
  
  // Initialize rules to be an empty dictionary.
  RateLimiter = function () {
    var self = this;
  
    // Dictionary of all rules associated with this RateLimiter, keyed by their
    // id. Each rule object stores the rule pattern, number of events allowed,
    // last reset time and the rule reset interval in milliseconds.
    self.rules = {};
  };
  
  /**
   * Checks if this input has exceeded any rate limits.
   * @param  {object} input dictionary containing key-value pairs of attributes
   * that match to rules
   * @return {object} Returns object of following structure
   * { 'allowed': boolean - is this input allowed
   *   'timeToReset': integer | Infinity - returns time until counters are reset
   *                   in milliseconds
   *   'numInvocationsLeft': integer | Infinity - returns number of calls left
   *   before limit is reached
   * }
   * If multiple rules match, the least number of invocations left is returned.
   * If the rate limit has been reached, the longest timeToReset is returned.
   */
  RateLimiter.prototype.check = function (input) {
    var self = this;
    var reply = {
      allowed: true,
      timeToReset: 0,
      numInvocationsLeft: Infinity,
    };
  
    var matchedRules = self._findAllMatchingRules(input);
    _.each(matchedRules, function (rule) {
      var ruleResult = rule.apply(input);
      var numInvocations = rule.counters[ruleResult.key];
  
      if (ruleResult.timeToNextReset < 0) {
        // Reset all the counters since the rule has reset
        rule.resetCounter();
        ruleResult.timeSinceLastReset = new Date().getTime() -
          rule._lastResetTime;
        ruleResult.timeToNextReset = rule.options.intervalTime;
        numInvocations = 0;
      }
  
      if (numInvocations > rule.options.numRequestsAllowed) {
        // Only update timeToReset if the new time would be longer than the
        // previously set time. This is to ensure that if this input triggers
        // multiple rules, we return the longest period of time until they can
        // successfully make another call
        if (reply.timeToReset < ruleResult.timeToNextReset) {
          reply.timeToReset = ruleResult.timeToNextReset;
        };
        reply.allowed = false;
        reply.numInvocationsLeft = 0;
        rule._executeCallback(reply, input);
      } else {
        // If this is an allowed attempt and we haven't failed on any of the
        // other rules that match, update the reply field.
        if (rule.options.numRequestsAllowed - numInvocations <
          reply.numInvocationsLeft && reply.allowed) {
          reply.timeToReset = ruleResult.timeToNextReset;
          reply.numInvocationsLeft = rule.options.numRequestsAllowed -
            numInvocations;
        }
        rule._executeCallback(reply, input);
      }
    });
    return reply;
  };
  
  /**
   * Adds a rule to dictionary of rules that are checked against on every call.
   * Only inputs that pass all of the rules will be allowed. Returns unique rule
   * id that can be passed to `removeRule`.
   * @param {object} rule    Input dictionary defining certain attributes and
   * rules associated with them.
   * Each attribute's value can either be a value, a function or null. All
   * functions must return a boolean of whether the input is matched by that
   * attribute's rule or not
   * @param {integer} numRequestsAllowed Optional. Number of events allowed per
   * interval. Default = 10.
   * @param {integer} intervalTime Optional. Number of milliseconds before
   * rule's counters are reset. Default = 1000.
   * @param {function} callback Optional. Function to be called after a
   * rule is executed. Two objects will be passed to this function.
   * The first one is the result of RateLimiter.prototype.check
   * The second is the input object of the rule, it has the following structure:
   * {
   *   'type': string - either 'method' or 'subscription'
   *   'name': string - the name of the method or subscription being called
   *   'userId': string - the user ID attempting the method or subscription
   *   'connectionId': string - a string representing the user's DDP connection
   *   'clientAddress': string - the IP address of the user
   * }
   * @return {string} Returns unique rule id
   */
  RateLimiter.prototype.addRule = function (rule, numRequestsAllowed,
    intervalTime, callback) {
    var self = this;
  
    var options = {
      numRequestsAllowed: numRequestsAllowed || DEFAULT_REQUESTS_PER_INTERVAL,
      intervalTime: intervalTime || DEFAULT_INTERVAL_TIME_IN_MILLISECONDS,
      callback: callback && Meteor.bindEnvironment(callback),
    };
  
    var newRule = new Rule(options, rule);
    this.rules[newRule.id] = newRule;
    return newRule.id;
  };
  
  /**
   * Increment counters in every rule that match to this input
   * @param  {object} input Dictionary object containing attributes that may
   * match to rules
   */
  RateLimiter.prototype.increment = function (input) {
    var self = this;
  
    // Only increment rule counters that match this input
    var matchedRules = self._findAllMatchingRules(input);
    _.each(matchedRules, function (rule) {
      var ruleResult = rule.apply(input);
  
      if (ruleResult.timeSinceLastReset > rule.options.intervalTime) {
        // Reset all the counters since the rule has reset
        rule.resetCounter();
      }
  
      // Check whether the key exists, incrementing it if so or otherwise
      // adding the key and setting its value to 1
      if (_.has(rule.counters, ruleResult.key))
        rule.counters[ruleResult.key]++;
      else
        rule.counters[ruleResult.key] = 1;
    });
  };
  
  // Returns an array of all rules that apply to provided input
  RateLimiter.prototype._findAllMatchingRules = function (input) {
    var self = this;
  
    return _.filter(self.rules, function(rule) {
      return rule.match(input);
    });
  };
  /**
   * Provides a mechanism to remove rules from the rate limiter. Returns boolean
   * about success.
   * @param  {string} id Rule id returned from #addRule
   * @return {boolean} Returns true if rule was found and deleted, else false.
   */
  RateLimiter.prototype.removeRule = function (id) {
    var self = this;
    if (self.rules[id]) {
      delete self.rules[id];
      return true;
    } else {
      return false;
    }
  };
  
  //////////////////////////////////////////////////////////////////////////////////
  
  }).call(this);
  
  
  /* Exports */
  if (typeof Package === 'undefined') Package = {};
  (function (pkg, symbols) {
    for (var s in symbols)
      (s in pkg) || (pkg[s] = symbols[s]);
  })(Package['rate-limit'] = {}, {
    RateLimiter: RateLimiter
  });
  
  })();
  