zordius/fluxex

View on GitHub
lib/fluxex.js

Summary

Maintainability
A
2 hrs
Test Coverage
'use strict';

var react = require('react');
var FluxexObject = require('./fluxobj');
var FluxexStore = require('./fluxstore');
var FluxexContextedHtml = require('./fluxhtml');

var countWeight = function (O, N, names) {
    var weight = 1;

    if (N === '_wait') {
        return 0;
    }

    if (!O) {
        return 1;
    }

    if (O._score) {
        return O._score;
    }

    names[N] = true;
    Object.keys(O).forEach(function (K) {
        if (names[K]) {
            throw new Error('Circular waitFor dependency detected on store `' + N + '` (' + Object.keys(names).concat([K]).join('->') + ') !');
        }
        weight += countWeight(O[K], K, names);
    });
    delete names[N];

    return weight;
};

/**
 * Fluxex object is an isomorphic application
 * @class
 * @augments FluxexObject
 * @param {Object=} state - Serialized Fluxex application state
 */
var Fluxex = function Fluxex() {
    FluxexObject.apply(this, arguments);

    // constructor == check failed in IE8, so we use .match() hack
    if (!this.stores && !this.constructor.toString().match(/__SELF__CHECK__HIT__THIS__/)) {
        throw new Error('Your app should define this.stores !!');
    }

    this._initStore();
    this._initDispatch();
};

Fluxex.prototype = new FluxexObject();

Object.assign(Fluxex.prototype, {
/** @lends Fluxex# */
    constructor: Fluxex,

    /**
     * Return HTML get context ready react element.
     * @return {element} the react element represent whole Html
     */
    getContextedHtml: function () {
        return react.createElement(FluxexContextedHtml, {
            fluxex: this,
            html: react.createFactory(this.HtmlJsx)
        });
    },

    /**
     * Create store instances and keep context sync.
     * @protected
     */
    _initStore: function () {
        var states = this._get('stores');

        if (this.hasOwnProperty('_stores')) {
            throw new Error('._initStore() should not be called externally!');
        }

        if (!states) {
            states = {};
            this._set('stores', states);
        }

        this._stores = {};

        if (!this.stores) {
            return;
        }

        Object.keys(this.stores).forEach(function (I) {
            if (!states[I]) {
                states[I] = {};
            }
            this._stores[I] = this._createStore(this.stores[I], states[I]);
        }.bind(this));
    },

    /**
     * Create and update dispatch queue for all actions
     * @protected
     */
    _initDispatch: function () {
        if (this.hasOwnProperty('_actions')) {
            throw new Error('._initDispatch() should not be called externally!');
        }

        if (!this.stores) {
            return;
        }

        this._actions = {};

        Object.keys(this.stores).forEach(function (I) {
            this._scanDispatch(this.stores[I], I);
        }.bind(this));

        this._updateDispatch();
    },

    /**
     * Create and update dispatch info for all actions
     * @protected
     */
    _scanDispatch: function (store, name) {
        Object.keys(store).concat(['handle_**UPDATEALL**']).forEach(function (N) {
            var M = N.match(/^handle_(.+)$/);
            if (M) {
                if (!this._actions[M[1]]) {
                    this._actions[M[1]] = {
                        wait: {}
                    };
                }

                this._actions[M[1]].wait[name] = {_wait: (store.waitFor && store.waitFor[M[1]]) ? store.waitFor[M[1]] : undefined};
            }
        }.bind(this));
    },

    /**
     * Resolve waitFor and detect deadlocks.
     * @protected
     */
    _updateDispatch: function () {
        Object.keys(this._actions).forEach(function (A) {
            var info = this._actions[A];

            Object.keys(info.wait).forEach(function (W) {
                var L = Array.isArray(info.wait[W]._wait) ? info.wait[W]._wait : [info.wait[W]._wait];
                L.forEach(function (N) {
                    if ((N !== undefined) && (info.wait[N] !== undefined)) {
                        info.wait[W][N] = info.wait[N];
                    }
                });
            });

            Object.keys(info.wait).forEach(function (W) {
                info.wait[W]._score = countWeight(info.wait[W], W, {});
            });
            this._actions[A] = Object.keys(info.wait).sort(function (A, B) {
                return info.wait[A]._score - info.wait[B]._score;
            });
        }.bind(this));
    },

    /**
     * Create a store by store prototype and initial status.
     * @protected
     * @param {Object} store - prototype for the new store instance
     * @param {Object} states - the initial status of the new store
     * @return {FluxexStore} A created store instance
     */
    _createStore: function (store, states) {
        var S = function() {
            FluxexStore.apply(this, arguments);
        };
        S.prototype = Object.assign(new FluxexStore(), store);
        return new S(states);
    },

    /**
     * Get a store by store name
     * @param {String} name - store name
     * @return {FluxexStore} A fluxex store instance
     */
    getStore: function (name) {
        if (!this._stores[name]) {
            throw new Error('no store defined as "' + name + '"!');
        }
        return this._stores[name];
    },

    /**
     * Execute an action creator with provided payload
     * @param {Function} action - the action creator function
     * @param {...*=} payload - payload or arguments for the action
     * @return {Promise} A promise instance
     */
    executeAction: function (action) {
        var A,
            args = Array.prototype.slice.call(arguments, 1);

        if (!action || ('function' !== (typeof action.call))) {
            return Promise.reject(new Error('.executeAction() require a action creator function as first parameter!'));
        }

        try {
            A = action.apply(this, args);
        } catch (E) {
            return Promise.reject(E);
        }

        if (!A || ('function' !== (typeof A.then))) {
            return Promise.reject(new Error('Execute an action creator that do not return a promise!'));
        }

        return A;
    },

    /**
     * Dispatch an action with payload
     * @param {string} name - the action name
     * @param {Object=} payload - payload for the action
     * @return {Promise} A promise instance
     */
    dispatch: function (name, payload) {
        var I = 0;
        var err = [];
        var promises = [];

        if (!name) {
            return Promise.reject(new Error('Can not dispatch without name!'));
        }

        if (!this._actions[name]) {
            return Promise.reject(new Error('No store handled the "' + name + '" action. Maybe you forget to provide "handle_' + name + '" method in a store?'));
        }

        if (this.currentAction !== undefined) {
            return Promise.reject(new Error('Can not dispatch "' + name + '" action when previous "' + this.currentAction + '" action is not done!'));
        }

        this.currentAction = name;

        this._actions[name].forEach(function (N) {
            I++;
            try {
                var P = this._stores[N]['handle_' + name](payload);
                if (P && P.then) {
                    promises.push(P);
                }
            } catch (E) {
                err.push(E);
            }
        }.bind(this));

        this.currentAction = undefined;

        return err.length ? Promise.reject(new Error(err)) : (promises.length ? Promise.all(promises) : Promise.resolve(I));
    }
}, require('./fluxex-server'));

module.exports = Fluxex;