hyperflow
Version:
A javascript state flow and mutation management toolkit & library for developing universal app.
838 lines (748 loc) • 25.3 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 TreeNodeElement
* @description - An simple implementation of an undirected data tree node.
*
* @author Tuan Le (tuan.t.lei@gmail.com)
*
* @flow
*/
; // eslint-disable-line
import CommonElement from './common-element';
const Hf = CommonElement();
/**
* @description - A tree node prototypes.
*
* TreeNodeElementPrototype
*/
const TreeNodeElementPrototype = Object.create({}).prototype = {
/* ----- Prototype Definitions --------- */
/**
* @description - Helper function to get the head node at this node
*
* @method _getHead
* @return {object}
* @private
*/
_getHead () {
const node = this;
const tree = node._tree;
if (Hf.DEVELOPMENT) {
if (node.isRoot()) {
Hf.log(`error`, `TreeNodeElement._getHead - Cannot get the head node of a root node.`);
}
}
return tree._getNode(node._hPathId);
},
/**
* @description - Helper function to get the tail nodes at this node
*
* @method _getTails
* @return {array}
* @private
*/
_getTails () {
const node = this;
const tree = node._tree;
if (Hf.DEVELOPMENT) {
if (node.isLeaf()) {
Hf.log(`error`, `TreeNodeElement._getTails - Cannot get tail nodes of a leaf node.`);
}
}
return node._tPathIds.map((tPathId) => tree._getNode(tPathId));
},
/**
* @description - Helper function to get all ancestors at this node
*
* @method _getAncestors
* @return {array}
* @private
*/
_getAncestors () {
const node = this;
let aNodes = [];
if (!node.isRoot()) {
const hNode = node._getHead();
aNodes.push(hNode);
if (!hNode.isRoot()) {
aNodes = aNodes.concat(hNode._getAncestors());
}
}
/* note: reserve if want root node is at index 0 */
return aNodes;
},
/**
* @description - Helper function to get all descendants at this node
*
* @method _getDescendants
* @return {array}
* @private
*/
_getDescendants () {
const node = this;
let dNodes = [];
if (!node.isLeaf()) {
const tNodes = node._getTails();
dNodes = dNodes.concat.apply(dNodes, tNodes.concat(tNodes.filter((tNode) => {
return !tNode.isLeaf();
}).map((tNode) => {
return tNode._getDescendants();
})));
}
return dNodes;
},
/**
* @description - Check if this node is an empty root with no tails.
*
* @method isSingular
* @return {boolean}
*/
isSingular () {
const node = this;
return Hf.isEmpty(node._hPathId) && !Hf.isNonEmptyArray(node._tPathIds);
},
/**
* @description - Check if this node is a root.
*
* @method isRoot
* @return {boolean}
*/
isRoot () {
const node = this;
if (node.isSingular()) {
return true;
}
return Hf.isEmpty(node._hPathId) && Hf.isNonEmptyArray(node._tPathIds);
},
/**
* @description - Check if this node is a leaf.
*
* @method isLeaf
* @return {boolean}
*/
isLeaf () {
const node = this;
return !Hf.isEmpty(node._hPathId) && !Hf.isNonEmptyArray(node._tPathIds);
},
/**
* @description - Check if this node is a tail of node at pathId.
*
* @method isTailOf
* @param {string|array} pathId - Node pathId.
* @return {boolean}
*/
isTailOf (pathId) {
const node = this;
/* convert pathId from array format to string format */
pathId = Hf.isArray(pathId) ? Hf.arrayToString(pathId, `.`) : pathId;
return !node.isRoot() && node._hPathId === pathId;
},
/**
* @description - Check if this node is head of node at pathId.
*
* @method isHeadOf
* @param {string|array} pathId - Node pathId.
* @return {boolean}
*/
isHeadOf (pathId) {
const node = this;
/* convert pathId from array format to string format */
pathId = Hf.isArray(pathId) ? Hf.arrayToString(pathId, `.`) : pathId;
return node._tPathIds.every((tPathId) => tPathId === pathId);
},
/**
* @description - Check if this node is a common with node at pathId.
*
* @method isCommonWith
* @param {string|array} pathId - Node pathId.
* @return {boolean}
*/
isCommonWith (pathId) {
const node = this;
/* convert pathId from array format to string format */
pathId = Hf.isArray(pathId) ? Hf.arrayToString(pathId, `.`) : pathId;
if (!node.isRoot()) {
let hNode = node._getHead();
return hNode._tPathIds.every((tPathId) => tPathId === pathId);
} else { // eslint-disable-line
return false;
}
},
/**
* @description - Check if this node is an ancestor of node with pathId.
*
* @method isAncestorOf
* @param {string|array} pathId - Node pathId.
* @return {boolean}
*/
isAncestorOf (pathId) {
const node = this;
/* convert pathId from array format to string format */
pathId = Hf.isArray(pathId) ? Hf.arrayToString(pathId, `.`) : pathId;
return !node.isRoot() && node._getDescendants().every((dNode) => dNode._pathId === pathId);
},
/**
* @description - Check if this node is an descendant of node with pathId.
*
* @method isDescendantOf
* @param {string|array} pathId - Node pathId.
* @return {boolean}
*/
isDescendantOf (pathId) {
const node = this;
/* convert pathId from array format to string format */
pathId = Hf.isArray(pathId) ? Hf.arrayToString(pathId, `.`) : pathId;
return !node.isLeaf() && node._getAncestors().every((aNode) => aNode._pathId === pathId);
},
/**
* @description - Check if this node is in the same hierarchy with node with pathId.
*
* @method isInHierarchyOf
* @param {string|array} pathId - Node pathId.
* @return {boolean}
*/
isInHierarchyOf (pathId) {
const node = this;
/* convert pathId from array format to string format */
pathId = Hf.isArray(pathId) ? Hf.arrayToString(pathId, `.`) : pathId;
return node.isAncestorOf(pathId) || node.isDescendantOf(pathId);
},
/**
* @description - Get node key.
*
* @method getKey
* @return {string|number}
*/
getKey () {
const node = this;
return node._key;
},
/**
* @description - Get node pathId.
*
* @method getPathId
* @return {string}
*/
getPathId () {
const node = this;
return node._pathId;
},
/**
* @description - Get node content type.
*
* @method getContentType
* @return {string}
*/
getContentType () {
const node = this;
return Hf.typeOf(node._content.accessor);
},
/**
* @description - Get node content.
*
* @method getContent
* @return {*}
*/
getContent () {
const node = this;
return node._content.accessor;
},
/**
* @description - Set node content.
*
* @method getContentCacheItem
* @param {*} cachedItem
* @return void
*/
getContentCacheItem (key) {
const node = this;
if (Hf.DEVELOPMENT) {
if (!(Hf.isString(key) || Hf.isInteger(key))) {
Hf.log(`error`, `TreeNodeElement.getContentCacheItem - Input content cache item key is invalid.`);
}
}
if (Hf.isObject(node._content.cache)) {
return node._content.cache.hasOwnProperty(key) ? node._content.cache[key] : undefined;
} else if (Hf.isArray(node._content.cache)) {
return node._content.cache.length > key ? node._content.cache[key] : undefined;
} else { // eslint-disable-line
return undefined;
}
},
/**
* @description - Set node content.
*
* @method setContent
* @param {object} content
* @return void
*/
setContent (content) {
const node = this;
if (Hf.isSchema({
cache: `object|array`,
accessor: `object|array`
}).of(content)) {
node._content = content;
if (!node.isSingular() && !node.isRoot()) {
const hNode = node._getHead();
if (!Hf.isDefined(hNode._content.accessor)) {
hNode._content.accessor = Hf.isString(node._key) ? {} : [];
}
if (!hNode._content.accessor.hasOwnProperty(node._key)) {
Object.defineProperty(hNode._content.accessor, node._key, {
get () {
return node._content.accessor;
},
configurable: false,
enumerable: true
});
}
}
if (!node.isLeaf()) {
const tNodes = node._getTails();
tNodes.forEach((tNode) => {
Object.defineProperty(node._content.accessor, tNode._key, {
get () {
return tNode._content.accessor;
},
configurable: false,
enumerable: true
});
});
}
}
},
/**
* @description - Freeze node content hierarchy and prevent extension.
*
* @method freezeContent
* @return void
*/
freezeContent () {
const node = this;
Object.freeze(node._content);
if (!node.isLeaf()) {
const tNodes = node._getTails();
tNodes.forEach((tNode) => {
Object.freeze(tNode._content);
if (!tNode.isLeaf()) {
tNode.freezeContent();
}
});
}
},
/**
* @description - Delete node content and its children's contents.
*
* @method flushContent
* @return void
*/
// flushContent () {
// const node = this;
//
// if (!node.isLeaf() && !node.isSingular()) {
// node.forEach(`descendants`, (dNode) => {
// dNode._content = {
// cache: undefined,
// accessor: undefined
// }
// });
// }
//
// node._content = {
// cache: undefined,
// accessor: undefined
// }
// },
/**
* @description - Branch out a new tail node to hierarchy and return the new tail node.
*
* @method branch
* @param {string|number} key - Node key.
* @param {*} content - Node content.
* @return {object}
*/
branch (key, content = {
cache: undefined,
accessor: undefined
}) {
const node = this;
if (Hf.DEVELOPMENT) {
if (!(Hf.isString(key) || Hf.isInteger(key))) {
Hf.log(`error`, `TreeNodeElement.branch - Input node key is invalid.`);
}
}
const tree = node._tree;
const pathId = `${node._pathId}.${key}`;
node.sprout(key, content);
return tree.select(pathId);
},
/**
* @description - Sprout out a new tail node to hierarchy and return this node.
*
* @method sprout
* @param {string|number} key - Node key.
* @param {*} content - Node content.
* @return {object}
*/
sprout (key, content = {
cache: undefined,
accessor: undefined
}) {
const node = this;
if (Hf.DEVELOPMENT) {
if (!(Hf.isString(key) || Hf.isInteger(key))) {
Hf.log(`error`, `TreeNodeElement.sprout - Input node key is invalid.`);
}
}
const tree = node._tree;
const pathId = `${node._pathId}.${key}`;
const tNode = tree._createNode(pathId);
tNode._hPathId = node._pathId;
node._tPathIds.push(tNode._pathId);
tNode.setContent(content);
return tree.select(node._pathId);
},
/**
* @description - Graft a tail node and insert it to a branch of a new root hierarchy.
*
* @method graft
* @param {string|number} key - Node key.
* @return {object}
*/
graft (key) {
const node = this;
if (Hf.DEVELOPMENT) {
if (!(Hf.isString(key) || Hf.isInteger(key))) {
Hf.log(`error`, `TreeNodeElement.graft - Input node key is invalid.`);
}
}
const tree = node._tree;
const pathId = `${node._pathId}.${key}`;
if (Hf.DEVELOPMENT) {
if (!node.isHeadOf(pathId)) {
Hf.log(`error`, `TreeNodeElement.graft - Node does not have a tail node key:${key} at pathId:${pathId}.`);
}
}
const tNode = tree._getNode(pathId);
const index = node._tPathIds.indexOf(pathId);
return {
/**
* New node branch to graft onto.
*
* @method on
* @param {string|array} newPathId - Node pathId.
* @return {object}
*/
onto (newPathId) {
if (Hf.DEVELOPMENT) {
if (!tree.hasNode(newPathId)) {
Hf.log(`error`, `TreeNodeElement.graft.onto - Input node pathId is invalid.`);
}
}
node._tPathIds.splice(index, 1);
tNode._hPathId = newPathId;
tNode.rekey(key);
return tree.select(tNode._pathId);
}
};
},
/**
* @description - Cut a tail node and its descendants from hierarchy.
* The node at rootify pathId will become a root node.
*
* @method rootify
* @param {string|number} key - Node key.
* @return {object}
*/
rootify (key) {
const node = this;
if (Hf.DEVELOPMENT) {
if (!(Hf.isString(key) || Hf.isInteger(key))) {
Hf.log(`error`, `TreeNodeElement.rootify - Input node key is invalid.`);
}
}
const tree = node._tree;
const pathId = `${node._pathId}.${key}`;
if (Hf.DEVELOPMENT) {
if (node.isRoot()) {
Hf.log(`error`, `TreeNodeElement.rootify - Cannot rootify a root node key:${node._key}.`);
} else if (!node.isHeadOf(pathId)) {
Hf.log(`error`, `TreeNodeElement.rootify - Node does not have a tail node key:${key} at pathId:${pathId}.`);
}
}
const tNode = tree._getNode(pathId);
const index = node._tPathIds.indexOf(pathId);
node._tPathIds.splice(index, 1);
tNode._hPathId = ``;
tNode.rekey(key);
return tree.select(tNode._pathId);
},
/**
* @description - Content referencing the topology differences of one node descendants to the other.
*
* @method refer
* @param {string|array} pathId - Node pathId.
* @param {object} option - Referal option.
* @return void
*/
refer (pathId, option = {
maxReferDepth: -1,
excludedPathIds: []
}) {
const node = this;
const tree = node._tree;
const {
maxReferDepth,
/* skip referal of pathIds in the exclusion list. */
excludedPathIds
} = Hf.fallback({
maxReferDepth: -1,
excludedPathIds: []
}).of(option);
/* convert pathId from array format to string format */
pathId = Hf.isArray(pathId) ? Hf.arrayToString(pathId, `.`) : pathId;
const depth = pathId.length - pathId.replace(/\./g, ``).length;
if (Hf.DEVELOPMENT) {
if (!tree.hasNode(pathId)) {
Hf.log(`error`, `TreeNodeElement.refer - Input node pathId is invalid.`);
}
}
const rNode = tree._getNode(pathId);
if (!rNode.isSingular() && !rNode.isLeaf()) {
let rtdNodes = [];
let rtsNodes = [];
if (!node.isLeaf()) {
const tNodes = node._getTails();
const rtNodes = rNode._getTails();
const tNodeKeys = tNodes.map((tNode) => tNode._key);
rtdNodes = rtNodes.filter((rtNode) => !tNodeKeys.includes(rtNode._key) && !excludedPathIds.includes(rtNode._pathId));
rtsNodes = rtNodes.filter((rtNode) => tNodeKeys.includes(rtNode._key) && !excludedPathIds.includes(rtNode._pathId));
rtsNodes.forEach((rtsNode) => {
const [ tsNode ] = tNodes.filter((tNode) => tNode._key === rtsNode._key);
if ((maxReferDepth === -1 || depth <= maxReferDepth)) {
tsNode.refer(rtsNode._pathId, option);
}
});
} else {
rtdNodes = rNode._getTails();
}
rtdNodes.forEach((rtdNode) => {
// let ts = new Date().getTime();
// TODO: needs optimization. Performance issue with deep object & array, especially for large array of objects.
if ((maxReferDepth === -1 || depth <= maxReferDepth)) {
node.branch(rtdNode._key, rtdNode._content).refer(rtdNode._pathId, option);
} else {
node.branch(rtdNode._key, rtdNode._content);
}
// let te = new Date().getTime();
// Hf.log(`debug`, `TreeNodeElement.refer - execution time: ${te - ts} ms.`);
// if (te - ts > 20) {
// Hf.log(`debug`, `TreeNodeElement.refer - pathId: ${rtdNode._pathId}.`);
// }
});
}
},
/**
* @description - Helper function to rekey node key.
*
* @method rekey
* @param {string|number} newKey
* @return void
*/
rekey (newKey) {
const node = this;
if (Hf.DEVELOPMENT) {
if (!(Hf.isString(newKey) || Hf.isInteger(newKey))) {
Hf.log(`error`, `TreeNodeElement.rekey - Input node key is invalid.`);
}
}
const tree = node._tree;
const oldPathId = node._pathId;
let newPathId = ``;
if (node.isRoot()) {
newPathId = newKey;
} else {
const hNode = node._getHead();
newPathId = `${hNode._pathId}.${newKey}`;
}
if (oldPathId !== newPathId) {
tree._changeNodePathId(oldPathId, newPathId);
}
node._key = newKey;
node._pathId = newPathId;
if (!node.isLeaf()) {
const tNodes = node._getTails();
Hf.clear(node._tPathIds);
tNodes.forEach((tNode) => {
tNode._hPathId = node._pathId;
tNode.rekey(tNode._key);
node._tPathIds.push(tNode._pathId);
});
}
return tree.select(node._pathId);
},
/**
* @description - Loop through the hierarchy and apply the callback iterator.
*
* @method forEach
* @param {string} flag - A filter flag.
* @param {function} iterator - Iterator function.
* @param {object} context - Object to become context (`this`) for the iterator function.
* @return void
*/
forEach (flag, iterator, context) {
const node = this;
if (Hf.DEVELOPMENT) {
if (!Hf.isString(flag)) {
Hf.log(`error`, `TreeNodeElement.forEach - Input flag is invalid.`);
} else if (!Hf.isFunction(iterator)) {
Hf.log(`error`, `TreeNodeElement.forEach - Input iterator callback is invalid.`);
} else if (node.isSingular()) {
Hf.log(`error`, `TreeNodeElement.forEach - Node pathId:${node._pathId} is singular.`);
}
}
const fromTails = !node.isLeaf() ? flag === `tails` : false;
const fromCommons = !node.isRoot() ? flag === `commons` : false;
const fromAncestors = !node.isRoot() ? flag === `ancestors` : false;
const fromDescendants = !node.isLeaf() ? flag === `descendants` : false;
const revealFrozen = Hf.compose(Hf.reveal, Object.freeze);
if (fromTails) {
Hf.forEach(node._getTails().map((_node) => revealFrozen(_node)), iterator, context);
} else if (fromCommons) {
Hf.forEach(node._getHead()._getTails().map((_node) => revealFrozen(_node)), iterator, context);
} else if (fromAncestors) {
Hf.forEach(node._getAncestors().map((_node) => revealFrozen(_node)), iterator, context);
} else if (fromDescendants) {
Hf.forEach(node._getDescendants().map((_node) => revealFrozen(_node)), iterator, context);
} else {
Hf.log(`error`, `TreeNodeElement.forEach - Input flag is invalid.`);
}
}
/**
* @description - Return node`s as a string for debuging.
*
* @method DEBUG_LOG
* @return void
*/
// DEBUG_LOG () {
// const node = this;
//
// if (node.isRoot()) {
// Hf.log(`debug`, JSON.stringify({
// type: `--Root--`,
// pathId: node._pathId,
// key: node._key,
// tails: node._tPathIds,
// content: node._content
// }, null, `\t`));
// } else if (node.isLeaf()) {
// Hf.log(`debug`, JSON.stringify({
// type: `--Leaf--`,
// pathId: node._pathId,
// key: node._key,
// head: node._hPathId,
// content: node._content
// }, null, `\t`));
// } else {
// Hf.log(`debug`, JSON.stringify({
// type: `--Branch--`,
// pathId: node._pathId,
// key: node._key,
// head: node._hPathId,
// tails: node._tPathIds,
// content: node._content
// }, null, `\t`));
// }
// }
};
/**
* @description - An undirected data tree node element module.
*
* @module TreeNodeElement
* @param {string} tree - undirected tree element.
* @param {string|array} pathId - Node pathId.
* @return {object}
*/
export default function TreeNodeElement (tree, pathId) {
if (Hf.DEVELOPMENT) {
if (!Hf.isObject(tree)) {
Hf.log(`error`, `TreeNodeElement - Input undirected tree element instance is invalid.`);
}
if (!(Hf.isString(pathId) || Hf.isArray(pathId))) {
Hf.log(`error`, `TreeNodeElement - Input tree node pathId is invalid.`);
}
}
let key = ``;
/* get the key from pathId */
if (Hf.isString(pathId)) {
[ key ] = Hf.stringToArray(pathId, `.`).reverse();
}
if (Hf.isArray(pathId)) {
[ key ] = pathId.slice(0).reverse();
pathId = Hf.arrayToString(pathId, `.`);
}
const element = Object.create(TreeNodeElementPrototype, {
_pathId: {
value: pathId,
writable: true,
configurable: false,
enumerable: false
},
_hPathId: {
value: ``,
writable: true,
configurable: false,
enumerable: false
},
_tPathIds: {
value: [],
writable: false,
configurable: true,
enumerable: false
},
_key: {
value: key,
writable: true,
configurable: false,
enumerable: false
},
// TODO: Used es6 proxy.
_content: {
value: {
cache: undefined,
accessor: undefined
},
writable: true,
configurable: true,
enumerable: false
},
_tree: {
value: tree,
writable: false,
configurable: false,
enumerable: false
}
});
if (Hf.DEVELOPMENT) {
if (!Hf.isObject(element)) {
Hf.log(`error`, `TreeNodeElement - Unable to create a tree node element instance.`);
}
}
return element;
}