/**
* @summary store
* @version 2.0.0
* @since 1.0.0
* @author Arian Khosravi <arian.khosravi@aofl.com>
*/
import {deepFreeze} from '@aofl/object-utils';
import {RegisterCallback} from '@aofl/register-callback';
import {generateMutations} from './generate-basic-mutations';
/**
* Store is a built on the same principles as redux and attempts to simplify
* some of Redux's concepts. It also incorporates ideas from other centralized
* state management implementations.
*
* @memberof module:@aofl/store/legacy
* @deprecated
*/
class Store {
/**
* Creates an instance of Store.
* @param {Boolean} debug
*/
constructor(debug) {
this.debug = debug;
this.state = {};
this.decorators = [];
this.namespaces = {};
this.purgeList = [];
this.registerCallbackInstance = new RegisterCallback();
this.pending = {
any: false,
};
if (debug === true || /* istanbul ignore next */typeof window.aoflDevtools !== 'undefined') {
this.debug = true;
this.state = deepFreeze(this.state);
if (!Array.isArray(window.aoflDevtools.storeInstances)) {
window
.aoflDevtools
.storeInstances = [];
}
window.aoflDevtools.storeInstances.push(this);
}
}
/**
* subscribe() register the callback function with registerCallbackInstance and returns
* the unsubscribe function.
*
* @param {Furtion} callback
* @return {Function}
*/
subscribe(callback) {
return this.registerCallbackInstance.register(callback);
}
/**
* getState() return the current state.
*
* @return {Object}
*/
getState() {
return this.state;
}
/**
* setPending() updates the pending state of store when an async operation is in process.
*
* @private
* @param {String} namespace
* @param {String} mutationId
* @param {Boolean} status
*/
setPending(namespace, mutationId, status = true) {
this.pending[namespace][mutationId] = status;
const setPendingAny = (obj, root = false) => {
let anyPending = false;
for (const key in obj) {
/* istanbul ignore next */
if (key === 'any' || !obj.hasOwnProperty(key)) continue;
if (obj[key] === true || (root && obj[key].any === true)) {
anyPending = true;
break;
}
}
obj.any = anyPending;
};
setPendingAny(this.pending[namespace]);
setPendingAny(this.pending, true);
}
/**
* addDecorators() adds an array decorators to the docorators list and calls forceCommit to
* invoke the newly added decorators.
*
* @param {Array} decorators
*/
addDecorators(decorators) {
this.decorators = this.decorators.concat(decorators);
this.forceCommit();
}
/**
* applyDecorators() loops through the decorators array and executes each decorator against the
* modified state.
*
* @private
* @param {Object} state
* @return {Object}
*/
applyDecorators(state) {
let nextState = state;
for (let i = 0; i < this.decorators.length; i++) {
nextState = this.decorators[i](nextState);
}
return nextState;
}
/**
* execAsyncMutations() loops through the asyncMutations and invokes the condition function of
* each asyncMutation. It only ivokes the method if the condition evaluates to true.
*
* @private
* @param {Object} nextState
*/
execAsyncMutations(nextState) {
const ns = this.namespaces;
for (const key in ns) {
/* istanbul ignore next */
if (!ns.hasOwnProperty(key)) continue;
for (const mutationId in ns[key].asyncMutations) {
/* istanbul ignore next */
if (!ns[key].asyncMutations.hasOwnProperty(mutationId)) continue;
if (ns[key].asyncMutations[mutationId].condition(nextState)) {
this.setPending(key, mutationId);
ns[key].asyncMutations[mutationId].method(nextState)
.then(function(pendingMutationId, namespace, payload) {
this.setPending(namespace, pendingMutationId, false);
this.commit({
namespace,
mutationId: pendingMutationId,
payload,
});
}.bind(this, mutationId, key))
.catch(function(pendingMutationId, namespace) {
this.setPending(namespace, pendingMutationId, false);
this.forceCommit();
}.bind(this, mutationId, key));
}
}
}
}
/**
* applyMutations() loops through the mutations array and executes each mutation funcion against
* the subState.
*
* @private
* @param {Array} mutations
* @param {Object} state
* @return {Object}
*/
applyMutations(mutations, state) {
let nextState = state;
for (let i = 0; i < mutations.length; i++) {
if (typeof this.namespaces[mutations[i].namespace] === 'undefined') {
throw new TypeError(`${mutations[i].namespace} is not a valid namespace`);
}
const mutation = mutations[i];
const ns = this.namespaces[mutation.namespace];
nextState = Object.assign({}, nextState, {
[ns.namespace]: ns.mutations[mutation.mutationId](nextState[ns.namespace], mutation.payload),
});
}
return nextState;
}
/**
* addState() adds an sdo to the store and invokes the SDO.mutations.init() method to set the
* initial state of the sub-state. Additionaly a payload can be supplied to the init funciton
* to instantiate the sub-state with a modifed inital state.
*
* @param {Object} sdo
* @param {*} payload
*/
addState(sdo, payload) {
if (typeof this.namespaces[sdo.namespace] !== 'undefined') return;
const initState = sdo.mutations.init(payload);
const mutations = Object.assign(generateMutations(initState), sdo.mutations);
this.namespaces[sdo.namespace] = {
namespace: sdo.namespace,
mutations,
asyncMutations: {},
};
if (sdo.hasOwnProperty('asyncMutations')) {
this.namespaces[sdo.namespace].asyncMutations = sdo.asyncMutations;
this.pending[sdo.namespace] = Object.keys(sdo.asyncMutations).reduce((acc, item) => {
acc[item] = false;
return acc;
}, {any: false});
}
this.state = Object.assign({}, this.state, {
[sdo.namespace]: initState,
});
this.purgeList.push({namespace: sdo.namespace, mutationId: 'init'});
if (Array.isArray(sdo.decorators)) {
this.addDecorators(sdo.decorators);
} else {
this.forceCommit();
}
}
purge() {
this.commit(...this.purgeList);
}
/**
* commit() accepts variadit mutation objects as arguments and applies the mutations agains the
* current state to generate the next state of the application.
*
* @param {*} mutations
* @param {*} forceCommit
* @throws {TypeError}
*/
commit(...mutations) {
if (mutations.length === 0) {
throw new TypeError('Failed to execute \'commit\' on \'Store\': at least 1 argument required, but only 0 present.');
}
let nextState = this.applyMutations(mutations, this.state);
/* istanbul ignore else */
if (nextState !== this.state) {
nextState = this.applyDecorators(nextState);
this.execAsyncMutations(nextState);
this.replaceState(nextState);
}
}
/**
* forceCommit() applies decorators and asyncMutations against the current state and executes
* subscribed callback functions even if state did not change.
*/
forceCommit() {
let nextState = this.state;
nextState = this.applyDecorators(nextState);
this.execAsyncMutations(nextState);
this.replaceState(nextState);
}
/**
* replaceState() take a state object, replaces the state property and notifies subscribers.
*
* @param {Object} state
*/
replaceState(state) {
this.state = state;
if (this.debug) {
this.state = deepFreeze(this.state);
}
this.registerCallbackInstance.next();
}
}
export default Store;