acdlite/flummox

View on GitHub
src/Store.js

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * Store
 *
 * Stores hold application state. They respond to actions sent by the dispatcher
 * and broadcast change events to listeners, so they can grab the latest data.
 * The key thing to remember is that the only way stores receive information
 * from the outside world is via the dispatcher.
 */

import EventEmitter from 'eventemitter3';
import assign from 'object-assign';

export default class Store extends EventEmitter {

  /**
   * Stores are initialized with a reference
   * @type {Object}
   */
  constructor() {
    super();

    this.state = null;

    this._handlers = {};
    this._asyncHandlers = {};
    this._catchAllHandlers = [];
    this._catchAllAsyncHandlers = {
      begin: [],
      success: [],
      failure: [],
    };
  }

  setState(newState) {
    // Do a transactional state update if a function is passed
    if (typeof newState === 'function') {
      const prevState = this._isHandlingDispatch
        ? this._pendingState
        : this.state;

      newState = newState(prevState);
    }

    if (this._isHandlingDispatch) {
      this._pendingState = this._assignState(this._pendingState, newState);
      this._emitChangeAfterHandlingDispatch = true;
    } else {
      this.state = this._assignState(this.state, newState);
      this.emit('change');
    }
  }

  replaceState(newState) {
    if (this._isHandlingDispatch) {
      this._pendingState = this._assignState(undefined, newState);
      this._emitChangeAfterHandlingDispatch = true;
    } else {
      this.state = this._assignState(undefined, newState);
      this.emit('change');
    }
  }

  getStateAsObject() {
    return this.state;
  }

  static assignState(oldState, newState) {
    return assign({}, oldState, newState);
  }

  _assignState(...args){
    return (this.constructor.assignState || Store.assignState)(...args);
  }

  forceUpdate() {
    if (this._isHandlingDispatch) {
      this._emitChangeAfterHandlingDispatch = true;
    } else {
      this.emit('change');
    }
  }

  register(actionId, handler) {
    actionId = ensureActionId(actionId);

    if (typeof handler !== 'function') return;

    this._handlers[actionId] = handler.bind(this);
  }

  registerAsync(actionId, beginHandler, successHandler, failureHandler) {
    actionId = ensureActionId(actionId);

    const asyncHandlers = this._bindAsyncHandlers({
      begin: beginHandler,
      success: successHandler,
      failure: failureHandler,
    });

    this._asyncHandlers[actionId] = asyncHandlers;
  }

  registerAll(handler) {
    if (typeof handler !== 'function') return;

    this._catchAllHandlers.push(handler.bind(this));
  }

  registerAllAsync(beginHandler, successHandler, failureHandler) {
    const asyncHandlers = this._bindAsyncHandlers({
      begin: beginHandler,
      success: successHandler,
      failure: failureHandler,
    });

    Object.keys(asyncHandlers).forEach((key) => {
      this._catchAllAsyncHandlers[key].push(
        asyncHandlers[key]
      );
    });
  }

  _bindAsyncHandlers(asyncHandlers) {
    for (let key in asyncHandlers) {
      if (!asyncHandlers.hasOwnProperty(key)) continue;

      const handler = asyncHandlers[key];

      if (typeof handler === 'function') {
        asyncHandlers[key] = handler.bind(this);
      } else {
        delete asyncHandlers[key];
      }
    }

    return asyncHandlers;
  }

  waitFor(tokensOrStores) {
    this._waitFor(tokensOrStores);
  }

  handler(payload) {
    const {
      body,
      actionId,
      'async': _async,
      actionArgs,
      error
    } = payload;

    const _allHandlers = this._catchAllHandlers;
    const _handler = this._handlers[actionId];

    const _allAsyncHandlers = this._catchAllAsyncHandlers[_async];
    const _asyncHandler = this._asyncHandlers[actionId]
      && this._asyncHandlers[actionId][_async];

    if (_async) {
      let beginOrFailureHandlers = _allAsyncHandlers.concat([_asyncHandler]);

      switch (_async) {
        case 'begin':
          this._performHandler(beginOrFailureHandlers, actionArgs);
          return;
        case 'failure':
          this._performHandler(beginOrFailureHandlers, [error]);
          return;
        case 'success':
          this._performHandler(_allAsyncHandlers.concat([
            (_asyncHandler || _handler)
          ].concat(_asyncHandler && [] || _allHandlers)), [body]);
          return;
        default:
          return;
      }
    }

    this._performHandler(_allHandlers.concat([_handler]), [body]);
  }

  _performHandler(_handlers, args) {
    this._isHandlingDispatch = true;
    this._pendingState = this._assignState(undefined, this.state);
    this._emitChangeAfterHandlingDispatch = false;

    try {
      this._performHandlers(_handlers, args);
    } finally {
      if (this._emitChangeAfterHandlingDispatch) {
        this.state = this._pendingState;
        this.emit('change');
      }

      this._isHandlingDispatch = false;
      this._pendingState = undefined;
      this._emitChangeAfterHandlingDispatch = false;
    }
  }

  _performHandlers(_handlers, args) {
    _handlers.forEach(_handler =>
      (typeof _handler === 'function') && _handler.apply(this, args));
  }
}

function ensureActionId(actionOrActionId) {
  return typeof actionOrActionId === 'function'
    ? actionOrActionId._id
    : actionOrActionId;
}