hyperflow
Version:
A javascript state flow and mutation management toolkit & library for developing universal app.
661 lines (603 loc) • 32.6 kB
JavaScript
/**
* Copyright 2015-present Tuan Le.
*
* Licensed under the MIT License.
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*------------------------------------------------------------------------
*
* @module CompositeElement
* @description - A composite elements can be resolved into a factory.
*
* @author Tuan Le (tuan.t.lei@gmail.com)
*
* @flow
*/
; // eslint-disable-line
import CommonElement from './common-element';
/* load DataElement */
import DataElement from './data-element';
const Hf = CommonElement();
const PRIVATE_PREFIX = `_`;
const INITIALIZATION_PREFIX = `$`;
const DEFAULT_EXCLUSION_PREFIXES = [ PRIVATE_PREFIX, INITIALIZATION_PREFIX ];
/* number mutations to persist in mutation map before roll-over */
const DEFAULT_MUTATION_HISTORY_DEPTH = 64;
/**
* @description - A composite element prototypes.
*
* CompositeElementPrototype
*/
const CompositeElementPrototype = Object.create({}).prototype = {
/* ----- Prototype Definitions --------- */
/**
* @description - Get a cloned set of composite enclosure.
*
* @method getEnclosure
* @param {array} fnNames - Names of the composed enclosures to retrieve.
* @return {object}
*/
getEnclosure (...fnNames) {
const composite = this;
let clonedEnclosure = Hf.clone(composite._enclosure);
if (!Hf.isNonEmptyArray(fnNames)) {
Hf.log(`warn0`, `CompositeElement.getEnclosure - Input enclosure function name array is empty.`);
return clonedEnclosure;
} else { // eslint-disable-line
if (Hf.DEVELOPMENT) {
if (fnNames.some((fnName) => !Hf.isString(fnName))) {
Hf.log(`error`, `CompositeElement.getEnclosure - Input enclosure function name is invalid.`);
}
}
return fnNames.filter((fnName) => composite._enclosure.hasOwnProperty(fnName)).reduce((enclosure, fnName) => {
enclosure[fnName] = clonedEnclosure[fnName];
return enclosure;
}, {});
}
},
/**
* @description - Get a cloned set of composite template.
*
* @method getTemplate
* @param {array} keys - Names of the template methods and properties to retrieve.
* @return {object}
*/
getTemplate (...keys) {
const composite = this;
let clonedTemplate = Hf.clone(composite._template);
if (!Hf.isNonEmptyArray(keys)) {
Hf.log(`warn0`, `CompositeElement.getTemplate - Input template key array is empty.`);
return clonedTemplate;
} else { // eslint-disable-line
if (Hf.DEVELOPMENT) {
if (keys.some((key) => !Hf.isString(key))) {
Hf.log(`error`, `CompositeElement.getTemplate - Input template key is invalid.`);
}
}
return keys.filter((key) => composite._template.hasOwnProperty(key)).reduce((template, key) => {
template[key] = clonedTemplate[key];
return template;
}, {});
}
},
/**
* @description - Get a cloned of composite exclusion option.
*
* @method getExclusion
* @return {object}
*/
getExclusion () {
const composite = this;
return Hf.clone(composite._exclusion);
},
/**
* @description - Mix self with a set of methods defined by source.
*
* @method mixin
* @param {array} sources
* @return {object}
*/
mixin (...sources) {
const composite = this;
if (Hf.DEVELOPMENT) {
if (!Hf.isNonEmptyArray(sources)) {
Hf.log(`warn0`, `CompositeElement.mixin - Input source array is empty.`);
} else if (sources.filter((source) => !(Hf.isObject(source) || Hf.isFunction(source)))) {
Hf.log(`warn0`, `CompositeElement.mixin - Input source is invalid.`);
}
}
const mixedTemplate = sources.reduce((_mixedTemplate, source) => {
let sourceObj = {};
if (Hf.isObject(source)) {
/* object literal mixins */
sourceObj = source;
}
if (Hf.isFunction(source)) {
/* functional mixins */
source.call(sourceObj);
}
_mixedTemplate = Hf.mix(sourceObj).with(_mixedTemplate);
return _mixedTemplate;
}, composite.getTemplate());
if (Hf.DEVELOPMENT) {
if (!Hf.isObject(mixedTemplate)) {
Hf.log(`error`, `CompositeElement.mixin - Unable to mixin methods of source object.`);
}
}
const definition = {
exclusion: composite.getExclusion(),
enclosure: composite.getEnclosure(),
template: mixedTemplate
};
return CompositeElement(definition); // eslint-disable-line
},
/**
* @description - Compose self with a set of composites into a new composite.
*
* @method compose
* @param {array} composites - A set of composites.
* @return {object}
*/
compose (...composites) {
const composite = this;
if (Hf.DEVELOPMENT) {
if (!Hf.isNonEmptyArray(composites)) {
Hf.log(`warn0`, `CompositeElement.compose - Input composites set is empty.`);
} else if (!composites.every((_composite) => Hf.isSchema({
// NOTE: for composite with internal private state, use enclosure.
getEnclosure: `function`,
// NOTE: for composite with no internal private state, use template.
getTemplate: `function`,
getExclusion: `function`
}).of(_composite))) {
Hf.log(`error`, `CompositeElement.compose - Input composite object is invalid.`);
}
}
const composedExclusion = composites.reduce((_composedExclusion, _composite) => {
const mergeExclusion = Hf.compose(_composite.getExclusion, Hf.merge);
_composedExclusion = mergeExclusion().with(_composedExclusion);
return _composedExclusion;
}, composite.getExclusion());
const composedEnclosure = composites.reduce((_composedEnclosure, _composite) => {
const mixEnclosure = Hf.compose(_composite.getEnclosure, Hf.mix);
_composedEnclosure = mixEnclosure().with(_composedEnclosure);
return _composedEnclosure;
}, composite.getEnclosure());
const composedTemplate = composites.reduce((_composedTemplate, _composite) => {
const mixTemplate = Hf.compose(_composite.getTemplate, Hf.mix);
_composedTemplate = mixTemplate().with(_composedTemplate);
return _composedTemplate;
}, composite.getTemplate());
if (Hf.DEVELOPMENT) {
if (!(Hf.isObject(composedEnclosure) && Hf.isObject(composedTemplate))) {
Hf.log(`error`, `CompositeElement.compose - Unable to compose composites set.`);
}
}
const definition = {
exclusion: composedExclusion,
enclosure: composedEnclosure,
template: composedTemplate
};
return CompositeElement(definition); // eslint-disable-line
},
/**
* @description - Resolve a composite with required initial state values and returns a factory.
*
* @method resolve
* @param {object} initialStatic - The initial constant of a factory.
* @param {object} initialState - The initial state of a factory.
* @return {object}
*/
resolve (initialStatic = {}, initialState = {}) {
const composite = this;
initialStatic = Hf.isObject(initialStatic) ? initialStatic : {};
initialState = Hf.isObject(initialState) ? initialState : {};
/* return a factory function */
return function Factory (state = {}) {
const exclusion = composite.getExclusion();
const enclosure = composite.getEnclosure();
let product;
if (!Hf.isEmpty(initialState)) {
const data = DataElement().read(initialState, `state`).asImmutable(true);
const stateCursor = data.select(`state`);
let originalStateAccessor;
let currentStateAccessor;
let nextStateAccessor;
let originalStateAccessorCache;
/* helper function to deep reduce original state by a reducer. */
const deepStateReduction = (originalState, reducer) => {
if (Hf.isObject(originalState) && Hf.isObject(reducer)) {
const originalStateKeys = Object.keys(originalState);
const reducerKeys = Object.keys(reducer);
if (originalStateKeys.length >= reducerKeys.length && reducerKeys.every((key) => originalStateKeys.includes(key))) {
reducerKeys.filter((key) => {
if (stateCursor.isItemComputable(key)) {
Hf.log(`warn0`, `Factory.deepStateReduction - Ignore mutation of computable key:${key}.`);
return false;
} else if (stateCursor.isItemObservable(key)) {
Hf.log(`warn0`, `Factory.deepStateReduction - Ignore mutation of observable key:${key}.`);
return false;
}
return true;
}).forEach((key) => {
const originalStateItem = originalState[key];
const reducerItem = reducer[key];
if ((Hf.isNonEmptyObject(originalStateItem) && !Hf.isObject(reducerItem) || Hf.isNonEmptyArray(originalStateItem) && !Hf.isArray(reducerItem)) ||
(!Hf.isNonEmptyObject(originalStateItem) && Hf.isObject(reducerItem) || !Hf.isNonEmptyArray(originalStateItem) && Hf.isArray(reducerItem))) {
Hf.log(`warn1`, `Factory.deepStateReduction - Input reducer schema at key:${key} must be a subset of state schema. Use state reconfigurator instead.`);
Hf.log(`debug`, `Factory.deepStateReduction - originalStateItem:${JSON.stringify(originalStateItem, null, `\t`)}`);
Hf.log(`debug`, `Factory.deepStateReduction - reducerItem:${JSON.stringify(reducerItem, null, `\t`)}`);
} else {
if (Hf.isNonEmptyObject(originalStateItem) && Hf.isObject(reducerItem) || Hf.isNonEmptyArray(originalStateItem) && Hf.isArray(reducerItem)) {
deepStateReduction(originalStateItem, reducerItem);
} else {
originalState[key] = reducerItem;
}
}
});
} else {
Hf.log(`warn1`, `Factory.deepStateReduction - Input reducer schema must be a subset of the top level state schema. Use state reconfigurator instead.`);
Hf.log(`debug`, `Factory.deepStateReduction - originalState:${JSON.stringify(originalState, null, `\t`)}`);
Hf.log(`debug`, `Factory.deepStateReduction - reducer:${JSON.stringify(reducer, null, `\t`)}`);
}
} else if (Hf.isArray(originalState) && Hf.isArray(reducer)) {
if (originalState.length === reducer.length) {
originalState.forEach((originalStateItem, key) => {
const reducerItem = reducer[key];
if ((Hf.isNonEmptyObject(originalStateItem) && !Hf.isObject(reducerItem) || Hf.isNonEmptyArray(originalStateItem) && !Hf.isArray(reducerItem)) ||
(!Hf.isNonEmptyObject(originalStateItem) && Hf.isObject(reducerItem) || !Hf.isNonEmptyArray(originalStateItem) && Hf.isArray(reducerItem))) {
Hf.log(`warn1`, `Factory.deepStateReduction - Input reducer schema at key:${key} must be a subset of state schema. Use state reconfigurator instead.`);
Hf.log(`debug`, `Factory.deepStateReduction - originalStateItem:${JSON.stringify(originalStateItem, null, `\t`)}`);
Hf.log(`debug`, `Factory.deepStateReduction - reducerItem:${JSON.stringify(reducerItem, null, `\t`)}`);
} else {
if (Hf.isNonEmptyObject(originalStateItem) && Hf.isObject(reducerItem) || Hf.isNonEmptyArray(originalStateItem) && Hf.isArray(reducerItem)) {
deepStateReduction(originalStateItem, reducerItem);
} else {
originalState[key] = reducerItem;
}
}
});
} else {
Hf.log(`warn1`, `Factory.deepStateReduction - Input reducer must be the same size as the top level state. Use state reconfigurator instead.`);
Hf.log(`debug`, `Factory.deepStateReduction - originalState:${JSON.stringify(originalState, null, `\t`)}`);
Hf.log(`debug`, `Factory.deepStateReduction - reducer:${JSON.stringify(reducer, null, `\t`)}`);
}
} else {
Hf.log(`error`, `Factory.deepStateReduction - Input reducer is invalid.`);
}
};
/* helper function to deep reconfig original state by a reconfigurator. */
const deepStateReconfiguration = (originalState, reconfigurator, parentState, parentKey = ``) => {
let reconfiguratorPathIds = [];
parentKey = Hf.isString(parentKey) ? parentKey : ``;
if (Hf.isObject(originalState) && Hf.isObject(reconfigurator)) {
const originalStateKeys = Object.keys(originalState);
const reconfiguratorKeys = Object.keys(reconfigurator);
if (originalStateKeys.length >= reconfiguratorKeys.length && reconfiguratorKeys.every((key) => originalStateKeys.includes(key))) {
reconfiguratorKeys.filter((key) => {
if (stateCursor.isItemComputable(key)) {
Hf.log(`warn0`, `Factory.deepStateReconfiguration - Ignore mutation of computable key:${key}.`);
return false;
} else if (stateCursor.isItemObservable(key)) {
Hf.log(`warn0`, `Factory.deepStateReconfiguration - Ignore mutation of observable key:${key}.`);
return false;
}
return true;
}).forEach((key) => {
const originalStateItem = originalState[key];
const reconfiguratorItem = reconfigurator[key];
if (Hf.isNonEmptyObject(originalStateItem) && Hf.isObject(reconfiguratorItem) || Hf.isNonEmptyArray(originalStateItem) && Hf.isArray(reconfiguratorItem)) {
reconfiguratorPathIds.concat(deepStateReconfiguration(originalStateItem, reconfiguratorItem, originalState, key));
} else if (Hf.isNonEmptyObject(originalStateItem) && !Hf.isObject(reconfiguratorItem) || Hf.isNonEmptyArray(originalStateItem) && !Hf.isArray(reconfiguratorItem)) {
Hf.log(`warn1`, `Factory.deepStateReconfiguration - Cannot reconfig state object at key:${key} as it is not null or empty initially.`);
} else {
originalState[key] = reconfiguratorItem;
reconfiguratorPathIds.push(`${parentKey}.${key}`);
}
});
} else {
if (Hf.isObject(parentState) && parentState.hasOwnProperty(parentKey)) {
parentState[parentKey] = reconfigurator;
// if (parentState[parentKey] === null || Hf.isEmpty(parentState[parentKey])) {
// parentState[parentKey] = reconfigurator;
// } else {
// Hf.log(`warn1`, `Factory.deepStateReconfiguration - Cannot reconfig state object at key:${parentKey} as it is not null or empty initially.`);
// }
} else {
Hf.log(`warn1`, `Factory.deepStateReconfiguration - Top level state object is non-configurable.`);
Hf.log(`debug`, `Factory.deepStateReconfiguration - originalState:${JSON.stringify(originalState, null, `\t`)}`);
Hf.log(`debug`, `Factory.deepStateReconfiguration - reconfigurator:${JSON.stringify(reconfigurator, null, `\t`)}`);
}
}
} else if (Hf.isArray(originalState) && Hf.isArray(reconfigurator)) {
if (originalState.length === reconfigurator.length) {
originalState.forEach((originalStateItem, key) => {
const reconfiguratorItem = reconfigurator[key];
if (Hf.isNonEmptyObject(originalStateItem) && Hf.isObject(reconfiguratorItem) || Hf.isNonEmptyArray(originalStateItem) && Hf.isArray(reconfiguratorItem)) {
reconfiguratorPathIds.concat(deepStateReconfiguration(originalStateItem, reconfiguratorItem, originalState, key));
} else if (Hf.isNonEmptyObject(originalStateItem) && !Hf.isObject(reconfiguratorItem) || Hf.isNonEmptyArray(originalStateItem) && !Hf.isArray(reconfiguratorItem)) {
Hf.log(`warn1`, `Factory.deepStateReconfiguration - Cannot reconfig state array at key:${key} as it is not null or empty initially.`);
} else {
originalState[key] = reconfiguratorItem;
reconfiguratorPathIds.push(`${parentKey}.${key}`);
}
});
} else {
if (Hf.isObject(parentState) && parentState.hasOwnProperty(parentKey)) {
parentState[parentKey] = reconfigurator;
// if (parentState[parentKey] === null || Hf.isEmpty(parentState[parentKey])) {
// parentState[parentKey] = reconfigurator;
// } else {
// Hf.log(`warn1`, `Factory.deepStateReconfiguration - Cannot reconfig state array at key:${parentKey} as it is not null or empty initially.`);
// }
} else {
Hf.log(`warn1`, `Factory.deepStateReconfiguration - Top level state array is non-configurable.`);
Hf.log(`debug`, `Factory.deepStateReconfiguration - originalState:${JSON.stringify(originalState, null, `\t`)}`);
Hf.log(`debug`, `Factory.deepStateReconfiguration - reconfigurator:${JSON.stringify(reconfigurator, null, `\t`)}`);
}
}
} else {
Hf.log(`error`, `Factory.deepStateReconfiguration - Input reconfigurator is invalid.`);
}
return reconfiguratorPathIds;
};
product = composite.mixin(...Hf.collect(...Object.keys(enclosure)).from(enclosure)).mixin({
/**
* @description - Get original state cursor.
*
* @method getStateCursor
* @return {object}
*/
// TODO: Need to find a way to not expose the state cursor.
getStateCursor () {
return stateCursor;
},
/**
* @description - Get state schema.
*
* @method getStateSchema
* @return {object}
*/
// TODO: Remove if find no use case.
getStateSchema () {
return stateCursor.getSchema();
},
/**
* @description - Get state as a plain object.
*
* @method getStateAsObject
* @return {object}
*/
getStateAsObject () {
return stateCursor.toObject();
},
/**
* @description - Clear all state mutation history.
*
* @method flushState
* @param {object} option
* @return void
*/
flushState (option = {}) {
const {
/* skip referal of pathIds in the exclusion list. */
mutationHistoryDepth
} = Hf.fallback({
mutationHistoryDepth: DEFAULT_MUTATION_HISTORY_DEPTH
}).of(option);
Hf.clear(originalStateAccessorCache);
/* reset all state data element mutation history recorded */
data.setMutationHistoryDepth(mutationHistoryDepth);
data.flush(`state`);
},
/**
* @description - Do a strict mutation of original state. The reducer object must have matching
* property keys/indexes as the original state.
*
* @method reduceState
* @param {object} reducer
* @return {boolean}
*/
reduceState (reducer) {
let mutated = false;
if (Hf.DEVELOPMENT) {
if (!Hf.isObject(reducer)) {
Hf.log(`error`, `Factory.reduceState - Input reducer is invalid.`);
}
}
deepStateReduction(currentStateAccessor, reducer);
nextStateAccessor = stateCursor.getAccessor();
mutated = currentStateAccessor !== nextStateAccessor;
if (mutated) {
/* do update state accessor if mutation did occur */
currentStateAccessor = nextStateAccessor;
}
return mutated;
},
/**
* @description - Do a reconfigurator original state by a reconfigurator. Allows modification inner state schema.
* Top level state schema is still non-configurable.
*
* @method reconfigState
* @param {object} reconfigurator
* @return void
* @return void
*/
reconfigState (reconfigurator) {
if (Hf.DEVELOPMENT) {
if (!Hf.isObject(reconfigurator)) {
Hf.log(`error`, `Factory.reconfigState - Input reconfigurator is invalid.`);
}
}
const reconfiguratorKeys = Object.keys(reconfigurator);
const originalStateKeys = Object.keys(originalStateAccessor);
if (Hf.DEVELOPMENT) {
if (originalStateKeys.length < reconfiguratorKeys.length || !reconfiguratorKeys.every((key) => originalStateKeys.includes(key))) {
Hf.log(`error`, `Factory.reconfigState - Input reconfigurator is invalid.`);
}
}
let reconfiguratorPathIds = [];
reconfiguratorKeys.forEach((key) => {
let stateAccessorAtPath;
const pathId = `state.${key}`;
if (originalStateAccessor[key] !== null && (Hf.isNonEmptyObject(originalStateAccessor[key]) || Hf.isNonEmptyArray(originalStateAccessor[key]))) {
if (!originalStateAccessorCache.hasOwnProperty(pathId)) {
stateAccessorAtPath = data.select(pathId).getAccessor();
originalStateAccessorCache[pathId] = stateAccessorAtPath;
} else {
stateAccessorAtPath = originalStateAccessorCache[pathId];
}
reconfiguratorPathIds = deepStateReconfiguration(stateAccessorAtPath, reconfigurator[key], originalStateAccessor, key);
} else {
reconfiguratorPathIds = deepStateReconfiguration(originalStateAccessor, reconfigurator, null, ``);
}
});
nextStateAccessor = stateCursor.getAccessor({
excludedNonmutatioReferalPathIds: reconfiguratorPathIds.map((pathId) => `state.${pathId}`)
});
/* do update current state accessor after reconfiged state */
currentStateAccessor = nextStateAccessor;
}
}).getTemplate();
if (Hf.isNonEmptyObject(state)) {
originalStateAccessor = stateCursor.getAccessor();
currentStateAccessor = stateCursor.getAccessor();
if (product.reduceState(state)) {
originalStateAccessor = stateCursor.getAccessor();
}
originalStateAccessorCache = {
state: originalStateAccessor
};
} else {
originalStateAccessor = stateCursor.getAccessor();
currentStateAccessor = stateCursor.getAccessor();
originalStateAccessorCache = {
state: originalStateAccessor
};
}
product = stateCursor.getContentItemKeys().reduce((productState, key) => {
Object.defineProperty(productState, key, {
get () {
return currentStateAccessor[key];
},
configurable: false,
enumerable: true
});
return productState;
}, product);
/* reset all state data element mutation history recorded during init */
data.setMutationHistoryDepth(DEFAULT_MUTATION_HISTORY_DEPTH);
data.flush(`state`);
} else {
product = composite.mixin(...Hf.collect(...Object.keys(enclosure)).from(enclosure)).getTemplate();
}
if (!Hf.isEmpty(initialStatic)) {
product = Object.entries(initialStatic).reduce((productStatic, [ key, value ]) => {
Object.defineProperty(productStatic, key, {
get () {
return Hf.freeze(value);
},
configurable: false,
enumerable: true
});
return productStatic;
}, product);
}
let revealedProduct = Hf.reveal(product, {
exclusion
});
/* if a function with initialization prefix is defined, call it once at init */
Object.entries(product).filter(([ fnName, fn ]) => {
return Hf.isFunction(fn) && fnName.charAt(0) === INITIALIZATION_PREFIX;
}).sort(([ fnNameA, fnA ], [ fnNameB, fnB ]) => { // eslint-disable-line
if (fnNameA < fnNameB) {
return -1;
}
if (fnNameA > fnNameB) {
return 1;
}
return 0;
}).forEach(([ fnName, fn ]) => { // eslint-disable-line
fn.call(revealedProduct);
});
/* reveal only the public properties and functions */
return Object.freeze(revealedProduct);
};
}
};
/**
* @description - A composite element module.
*
* @module CompositeElement
* @param {object} definition - Composite definition.
* @return {object}
*/
export default function CompositeElement (definition) {
if (Hf.DEVELOPMENT) {
if (!Hf.isObject(definition)) {
Hf.log(`error`, `CompositeElement - Input composite definition object is invalid.`);
}
}
const {
enclosure,
template,
exclusion
} = Hf.fallback({
enclosure: {},
template: {},
exclusion: {
/* set default exclusion */
prefixes: DEFAULT_EXCLUSION_PREFIXES,
properties: false
}
}).of(definition);
exclusion.prefixes = exclusion.prefixes.concat(DEFAULT_EXCLUSION_PREFIXES.filter((prefix) => {
return !exclusion.prefixes.includes(prefix);
}));
if (Hf.DEVELOPMENT) {
if (!Object.values(enclosure).every((fn) => Hf.isFunction(fn))) {
Hf.log(`error`, `CompositeElement - Input composite definition for enclosure object is invalid.`);
}
}
const element = Object.create(CompositeElementPrototype, {
// TODO: Implement $init sequence for composer method. This will allow the $init method call order to be deterministic.
// _initSequences: [],
/* internal composite enclose functions */
_enclosure: {
value: enclosure,
writable: true,
configurable: false,
enumerable: false
},
/* internal composite method template */
_template: {
value: template,
writable: true,
configurable: false,
enumerable: false
},
/* esolve exclusion option */
_exclusion: {
value: exclusion,
writable: true,
configurable: false,
enumerable: false
}
});
if (Hf.DEVELOPMENT) {
if (!Hf.isObject(element)) {
Hf.log(`error`, `CompositeElement - Unable to create a composite element instance.`);
}
}
/* reveal only the public properties and functions */
return Hf.compose(Hf.reveal, Object.freeze)(element);
}