Stats Collector Reference Source Repository

src/stats/BaseStats.js


/**
 * A stat collector that initially includes no collector functions.
 * @example
 * const stats = new BaseStats();
 * stats.addCollector({name: 'custom', initialValue: 0, requirements: [], handleUpdate: fn});
 * stats.processAll([1, 2, 3, 4, 5]);
 * stats.get();
 */
export default class BaseStats {
  constructor() {
    this._collectors = {};
    this._collectorNames = [];
    this._processCount = 0;
    this._processFilteredCount = 0;
    this._state = {};
    this._ignore = [];
    this._filters = [];
    this._handleProcessList = [];
    this._handleGetList = [];
  }
  /**
   * Add a collector function that can process data
   * when process() or get() is called.
   * @param {Object} collector An object with the following properties:
   *   - **name** - [String] The name of the collector
   *   - **initialValue** - [any] The initial value if update() has never been called
   *   - **requirements** - [Array] An array of collector names that must be added before this collector
   *   - **handleProcess** - [Function]
   *   - **handleGet** - [Function]
   * @example
   * const stats = new BaseStats();
   * stats.addCollector({
   *   name: 'test-sum',
   *   initialValue: 0,
   *   requirements: [],
   *   handleUpdate: function (prev, state, val) { return prev + val; }
   * });
   * @return {undefined}
   */
  addCollector(collector) {
    const self = this;
    if (self._processCount > 0) {
      throw new Error('Cannot add a new collector unless you call reset() first');
    }
    if (collector.hasOwnProperty('name') &&
      collector.hasOwnProperty('initialValue') &&
      collector.hasOwnProperty('requirements')) {
      let reqIndex;
      let requirements = collector.requirements;
      if (!Array.isArray(requirements)) {
        throw new Error('The collector\'s requirements need to be an array.');
      }
      // make a copy
      requirements = requirements.slice(0);
      // check requirements
      self._collectorNames.forEach(function (collectorName) {
        const currentCollector = self._collectors[collectorName];
        if (currentCollector.name === collector.name) {
          throw new Error('A collector with that name has already been added.');
        }
        reqIndex = requirements.indexOf(currentCollector.name);
        if (reqIndex >= 0) {
          requirements.splice(reqIndex, 1);
        }
      });
      if (requirements.length) {
        throw new Error(`Did not meet the requirements: ${requirements}`);
      }
      // add our collector
      self._collectorNames.push(collector.name);
      self._collectors[collector.name] = collector;
      if (typeof collector.handleProcess === 'function') {
        self._handleProcessList.push(collector.name);
      }
      if (typeof collector.handleGet === 'function') {
        self._handleGetList.push(collector.name);
      }
    } else {
      throw new Error('Collectors have names, initialValues, and requirements');
    }
    self.reset();
  }
  addFilter(filter) {
    if (typeof filter !== 'function') {
      throw new Error('Filter must be a function.');
    }
    this._filters.push(filter);
  }
  addIgnore(name) {
    if (this._ignore.indexOf(name) === -1) {
      this._ignore.push(name);
    }
  }
  get(zeroForUndefined) {
    const self = this;
    const result = {};
    // run handleGet()
    if (this._processCount > 0) {
      for (let i = 0; i < this._handleGetList.length; i++) {
        const collectorName = this._handleGetList[i];
        const collector = this._collectors[collectorName];
        this._state[collectorName] = collector.handleGet(
          this._state
        );
      }
    }
    // sort, ignore, deep-copy, and use zeroForUndefined if neeeded
    Object.keys(this._state).sort().forEach(function (key) {
      if (self._ignore.indexOf(key) === -1) {
        let val = self._state[key];
        if (val) {
          val = JSON.parse(JSON.stringify(val));
        } else if (typeof val === 'undefined' && zeroForUndefined) {
          val = 0;
        }
        result[key] = val;
      }
    });
    return result;
  }
  processAll(arr) {
    for (let i = 0; i < arr.length; i++) {
      this.process(arr[i]);
    }
  }
  process(value) {
    // run filters
    for (let i = 0; i < this._filters.length; i++) {
      if (!this._filters[i](value)) {
        this._processFilteredCount++;
        return;
      }
    }
    this._processCount++;
    // run handleProcess()
    for (let i = 0; i < this._handleProcessList.length; i++) {
      const collectorName = this._handleProcessList[i];
      const collector = this._collectors[collectorName];
      this._state[collectorName] = collector.handleProcess(
        this._state,
        this._state[collectorName] || collector.initialValue,
        value
      );
    }
  }
  reset() {
    const self = this;
    this._state = {};
    this._collectorNames.forEach(function (collectorName) {
      self._state[collectorName] = undefined;
    });
    this._processCount = 0;
    this._processFilteredCount = 0;
  }
}