acdlite/flummox

View on GitHub
src/Flux.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * Flux
 *
 * The main Flux class.
 */

import Store from './Store';
import Actions from './Actions';
import { Dispatcher } from 'flux';
import EventEmitter from 'eventemitter3';
import assign from 'object-assign';

export default class Flux extends EventEmitter {

  constructor() {
    super();

    this.dispatcher = new Dispatcher();

    this._stores = {};
    this._actions = {};
  }

  createStore(key, _Store, ...constructorArgs) {

    if (!(_Store.prototype instanceof Store)) {
      const className = getClassName(_Store);

      throw new Error(
        `You've attempted to create a store from the class ${className}, which `
      + `does not have the base Store class in its prototype chain. Make sure `
      + `you're using the \`extends\` keyword: \`class ${className} extends `
      + `Store { ... }\``
      );
    }

    if (this._stores.hasOwnProperty(key) && this._stores[key]) {
      throw new Error(
        `You've attempted to create multiple stores with key ${key}. Keys must `
      + `be unique.`
      );
    }

    const store = new _Store(...constructorArgs);
    const token = this.dispatcher.register(store.handler.bind(store));

    store._waitFor = this.waitFor.bind(this);
    store._token = token;
    store._getAllActionIds = this.getAllActionIds.bind(this);

    this._stores[key] = store;

    return store;
  }

  getStore(key) {
    return this._stores.hasOwnProperty(key) ? this._stores[key] : undefined;
  }

  removeStore(key) {
    if (this._stores.hasOwnProperty(key)) {
      this._stores[key].removeAllListeners();
      this.dispatcher.unregister(this._stores[key]._token);
      delete this._stores[key];
    } else {
      throw new Error(
        `You've attempted to remove store with key ${key} which does not exist.`
      );
    }
  }

  createActions(key, _Actions, ...constructorArgs) {
    if (!(_Actions.prototype instanceof Actions) && _Actions !== Actions) {
      if (typeof _Actions === 'function') {
        const className = getClassName(_Actions);

        throw new Error(
          `You've attempted to create actions from the class ${className}, which `
        + `does not have the base Actions class in its prototype chain. Make `
        + `sure you're using the \`extends\` keyword: \`class ${className} `
        + `extends Actions { ... }\``
        );
      } else {
        const properties = _Actions;
        _Actions = class extends Actions {};
        assign(_Actions.prototype, properties);
      }
    }

    if (this._actions.hasOwnProperty(key) && this._actions[key]) {
      throw new Error(
        `You've attempted to create multiple actions with key ${key}. Keys `
      + `must be unique.`
      );
    }

    const actions = new _Actions(...constructorArgs);
    actions.dispatch = this.dispatch.bind(this);
    actions.dispatchAsync = this.dispatchAsync.bind(this);

    this._actions[key] = actions;

    return actions;
  }

  getActions(key) {
    return this._actions.hasOwnProperty(key) ? this._actions[key] : undefined;
  }

  getActionIds(key) {
    const actions = this.getActions(key);

    if (!actions) return;

    return actions.getConstants();
  }

  removeActions(key) {
    if (this._actions.hasOwnProperty(key)) {
      delete this._actions[key];
    } else {
      throw new Error(
        `You've attempted to remove actions with key ${key} which does not exist.`
      );
    }
  }

  getAllActionIds() {
    let actionIds = [];

    for (let key in this._actions) {
      if (!this._actions.hasOwnProperty(key)) continue;

      const actionConstants = this._actions[key].getConstants();

      actionIds = actionIds.concat(getValues(actionConstants));
    }

    return actionIds;
  }

  dispatch(actionId, body) {
    this._dispatch({ actionId, body });
  }

  dispatchAsync(actionId, promise, actionArgs) {
    const payload = {
      actionId,
      async: 'begin'
    };

    if (actionArgs) payload.actionArgs = actionArgs;

    this._dispatch(payload);

    return promise
      .then(
        body => {
          this._dispatch({
            actionId,
            body,
            async: 'success'
          });

          return body;
        },
        error => {
          this._dispatch({
            actionId,
            error,
            async: 'failure'
          });
        }
      )
      .catch(error => {
        this.emit('error', error);

        throw error;
      });
  }

  _dispatch(payload) {
    this.dispatcher.dispatch(payload);
    this.emit('dispatch', payload);
  }

  waitFor(tokensOrStores) {

    if (!Array.isArray(tokensOrStores)) tokensOrStores = [tokensOrStores];

    const ensureIsToken = tokenOrStore => {
      return tokenOrStore instanceof Store
        ? tokenOrStore._token
        : tokenOrStore;
    };

    const tokens = tokensOrStores.map(ensureIsToken);

    this.dispatcher.waitFor(tokens);
  }

  removeAllStoreListeners(event) {
    for (let key in this._stores) {
      if (!this._stores.hasOwnProperty(key)) continue;

      const store = this._stores[key];

      store.removeAllListeners(event);
    }
  }

  serialize() {
    const stateTree = {};

    for (let key in this._stores) {
      if (!this._stores.hasOwnProperty(key)) continue;

      const store = this._stores[key];

      const serialize = store.constructor.serialize;

      if (typeof serialize !== 'function') continue;

      const serializedStoreState = serialize(store.state);

      if (typeof serializedStoreState !== 'string') {
        const className = store.constructor.name;

        if (process.env.NODE_ENV !== 'production') {
          console.warn(
            `The store with key '${key}' was not serialized because the static `
          + `method \`${className}.serialize()\` returned a non-string with type `
          + `'${typeof serializedStoreState}'.`
          );
        }
      }

      stateTree[key] = serializedStoreState;

      if (typeof store.constructor.deserialize !== 'function') {
        const className = store.constructor.name;

        if (process.env.NODE_ENV !== 'production') {
          console.warn(
            `The class \`${className}\` has a \`serialize()\` method, but no `
          + `corresponding \`deserialize()\` method.`
          );
        }
      }

    }

    return JSON.stringify(stateTree);
  }

  deserialize(serializedState) {
    let stateMap;

    try {
      stateMap = JSON.parse(serializedState);
    } catch (error) {
      const className = this.constructor.name;

      if (process.env.NODE_ENV !== 'production') {
        throw new Error(
          `Invalid value passed to \`${className}#deserialize()\`: `
        + `${serializedState}`
        );
      }
    }

    for (let key in this._stores) {
      if (!this._stores.hasOwnProperty(key)) continue;

      const store = this._stores[key];

      const deserialize = store.constructor.deserialize;

      if (typeof deserialize !== 'function') continue;

      const storeStateString = stateMap[key];
      const storeState = deserialize(storeStateString);

      store.replaceState(storeState);

      if (typeof store.constructor.serialize !== 'function') {
        const className = store.constructor.name;

        if (process.env.NODE_ENV !== 'production') {
          console.warn(
            `The class \`${className}\` has a \`deserialize()\` method, but no `
          + `corresponding \`serialize()\` method.`
          );
        }
      }
    }
  }

}

// Aliases
Flux.prototype.getConstants = Flux.prototype.getActionIds;
Flux.prototype.getAllConstants = Flux.prototype.getAllActionIds;
Flux.prototype.dehydrate = Flux.prototype.serialize;
Flux.prototype.hydrate = Flux.prototype.deserialize;

function getClassName(Class) {
  return Class.prototype.constructor.name;
}

function getValues(object) {
  let values = [];

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

    values.push(object[key]);
  }

  return values;
}

const Flummox = Flux;

export {
  Flux,
  Flummox,
  Store,
  Actions,
};