Home Manual Reference Source Repository

src/stack.js

/** Requires Immutable */
import Immutable from 'immutable';

/**
 * A Stack isn't a stream, but can be used in conjunction with streams, actions, and
 * transformers to make debugging much easier. Using `pushAction`, 'pushState`,
 * `pushTransformer`, and `pushLog`, you can create a log of state changes and
 * their causes. Calling `dump`, `dumpToLog`, and `dumpWhen` will give you control
 * over when to view to stack.
 */
class Stack {

  /**
   * constructor - A stack takes a size for the stack and a boolean to determine
   * whether the stack should be enabled on creation.
   *
   * @access public
   * @param  {number} size = 20     The size of the stack.
   * @param  {boolean} debug = false
   * @return {Stack}
   */
  constructor (size = 20, debug = false) {


    /**
     * @access private
     */
    this.stack = Immutable.List().setSize(size);

    /**
     * @access public
     */
    this.debug = debug;
  }

  /**
   * addToStack - Removes the oldest item from the stack,
   * and adds a new item to the stack.
   *
   * @access private
   * @param  {Immutable.Map} item The new item to add to the stack.
   */
  addToStack (item) {
    if (this.debug) {
      this.stack = this.stack.shift().push(item);
    }
  }

  /**
   * pushAction - Adds an action to the stack.
   *
   * @access public
   * @param  {string} fnName Name of the action called.
   * @param  {object} args Arguments to the action called.
   */
  pushAction (fnName, args) {
    this.addToStack(Immutable.Map({type: 'ACTION', name: fnName, args: args}));
  }

  /**
   * pushState - Adds a state Map to the stack.
   *
   * @access public
   * @param  {string} name     Name of the stream that's adding state
   * @param  {Immutable.Map} state      The state object to add to the stack.
   * @param  {string} streamType The type of stream (View, Funnel, Stream) adding the state.
   */
  pushState (name, state, streamType) {
    this.addToStack(Immutable.Map({ type: 'STATE', name , state, streamType }));
    if (this.testFn && this.testFn(state)) {
      this.dumpWhenCb(this.dump());
    }
  }

  /**
   * pushTransformer - Adds a transformer to the stack.
   *
   * @access public
   * @param  {string} fnName Name of the transformer called.
   * @param  {object} args Arguments to the transformer called.
   */
  pushTransformer (fnName, args) {
    this.addToStack(Immutable.Map({ type: 'TRANSFORMER', name: fnName, args: args}));
  }

  /**
   * pushLog - Adds a text log to the stack.
   *
   * @param  {string} text The text to be added to the stack.
   */
  pushLog (text) {
    this.addToStack(Immutable.Map({ type: 'LOG', text }));
  }

  /**
   * dump - Returns the stack, removing any undefined items.
   *
   * @return {Immutable.List}  the stack.
   */
  dump () {
    return this.stack.filter(item => item);
  }

  /**
   * dumpWhen - adds a test function to the stack that will run whenever
   * new state is added to the stack. The test function takes the state
   * as an argument and returns a boolean. If the test function returns
   * true, it will then run the callback function, passing the current
   * stack as an argument.
   *
   * @param  {function} testFn takes state as an argument and returns a boolean.
   * @param  {function} callbackFn takes the current stack as an argument
   */
  dumpWhen (testFn, callbackFn) {

    /**
     * @access private
     */
    this.testFn = testFn;

    /**
     * @access = private
     */
    this.dumpWhenCb = callbackFn;
  }

  /**
   * dumpToLog - When dumpToLog is called, it logs out the current stack to
   * the console, formatting the different types of items in the stack for
   * clarity. It takes an optional array for only logging a particular
   * part of the state objects.
   *
   * @param  {array} stateAccessor takes an array of strings for logging only a certain part of the state
   */
  dumpToLog (stateAccessor) {
    const toLog = {
      ACTION: item => console.log('%cACTION ' + '%c'+item.get('name') + '\n',
                                  'font-weight: bold; color: #629456', 'font-style: italic; color: #004',
                                  item.get('args')),
      LOG: item => console.log('%cLOG\n' + item.get('text'), 'background: #eee; color: #555'),
      TRANSFORMER: item => console.log('%cTRANSFORMER ' + '%c' + item.get('name') + '\n',
                                       'font-weight: bold; color: #D08A10', 'font-style: italic; color: #004',
                                       item.get('args')),
      STATE: (item, stateAccessor = []) => console.log('%cSTATE ' + (item.streamType ? '(' + item.streamType + ')' : '') + '%c' + item.get('name') + '\n',
                                            'font-weight: bold; color: #3B74C4', 'font-style: italic; color: #004',
                                            item.getIn(['state'].concat(stateAccessor)).toJS())
    };

    if (this.debug) {
      this.stack.map(item => {
        if(item){
          toLog[item.get('type')](item, stateAccessor);
        }
      });
    }
  }

  /**
   * set size - sets the size of the stack.
   *
   * @access public
   * @param  {number} newSize new size for the stack.
   */
  set size(newSize) {
    this.stack.setSize(newSize);
  }

  /**
   * get size - getter for returning the current size of the stack.
   *
   * @return {number}  returns size of the stack
   */
  get size() {
    return this.stack.size;
  }
}

/** @ignore Export the Stack class. */
export default Stack;