src/DryadTree.js
import * as _ from 'underscore';
/**
* Manages the tree structure, contexts for nodes,
* utilities to walk the tree, collect commands and
* call command middleware.
*/
export default class DryadTree {
/**
* @param {Dryad} rootDryad
* @param {Function} getClass - lookup function
*/
constructor(rootDryad, getClass, rootContext={}) {
this.root = rootDryad;
this.dryads = {};
this.contexts = {};
this.getClass = getClass;
this.rootContext = rootContext;
this.tree = this._makeTree(this.root);
}
/**
* Depth first traversal of the Dryad tree
*
* The function is given arguments:
* node {id type children}
* Dryad
* context
* memo
*
* @param {Function} fn - called with (dryad, context, node)
* @param {Object} node - data object
* @param {Object} memo - for usage during recursion
*/
walk(fn, node, memo) {
if (!node) {
node = this.root;
}
memo = fn(node, this.dryads[node.id], this.contexts[node.id], memo);
node.children.forEach((child) => {
memo = this.walk(fn, child, memo);
});
return memo;
}
/**
* Collect a tree of command objects from each node for a given method.
* eg. 'add' 'remove' 'prepareForAdd'
*
* @param {String} methodName
* @param {Dryad} node
* @returns {Object}
*/
collectCommands(methodName, node, player) {
let dryad = this.dryads[node.id];
let context = this.contexts[node.id];
let commands = dryad[methodName](player);
return {
commands: commands,
context: context,
id: node.id,
children: node.children.map((child) => this.collectCommands(methodName, child, player))
};
}
/**
* Construct a command tree for a single commandObject to be executed
* with a single node's context.
*
* This is for runtime execution of commands,
* called from streams and async processes initiated during Dryad's .add()
*/
makeCommandTree(nodeId, command) {
return {
commands: command,
context: this.contexts[nodeId],
id: nodeId,
children: []
};
}
/**
* Update the context for a node.
*
* @param {String} dryadId
* @param {Object} update
*/
updateContext(dryadId, update) {
return this.contexts[dryadId] = _.assign(this.contexts[dryadId], update);
}
/**
* Get a representation of current state of the tree.
* Contains add|remove|prepared and may hold errors.
*/
getDebugState() {
const formatState = (s) => {
if (!s) {
return s;
}
if (s.error) {
return `ERROR: ${s.error}`;
}
if (s.add) {
return 'running';
}
if (s.remove) {
return 'removed';
}
if (s.prepared) {
return 'prepared';
}
};
const dbug = (node) => {
const r = {
class: this.dryads[node.id].constructor.name,
// props: this.dryads[node.id].properties,
state: formatState(this.contexts[node.id].state)
};
if (node.children.length) {
r.children = node.children.map(dbug);
}
return r;
};
return dbug(this.tree);
}
/**
* Create and return initial context for a Dryad.
*
* Each context inherits from it's parent's context.
*
* @returns {Object}
*/
_createContext(dryad, dryadId, parentId, rootContext={}) {
let cc = _.assign({id: dryadId}, rootContext, dryad.initialContext());
if (parentId) {
let parent = this.dryads[parentId];
let childContext = parent.childContext(this.contexts[parentId]);
return _.create(this.contexts[parentId], _.assign(childContext, cc));
}
return cc;
}
/**
* Given a Dryad (possibily with children), construct a tree of Objects
* {id type children}
*
* Dryad classes may use requireParent() and subgraph() to replace themselves
* with a different graph. So this tree is not a direct representation of the input
* graph, but may be expanded through the use of requireParent() and subgraph()
*
* Generates ids for each Dryad
* Stores each by its id
* Creates context for each
*
* This method calls itself recursively for children.
*
* @param {Dryad} dryad
* @param {String} parentId
* @param {Integer} childIndex
* @param {Object} memo - for internal usage during recursion
* @returns {Object}
*/
_makeTree(dryad, parentId, childIndex=0, memo={}) {
if (!dryad.isDryad) {
console.error('Not a dryad', dryad);
throw new Error('Not a Dryad:' + dryad);
}
// Copy seenTypes, pass it to your children
// Each branch sees a different descendent list
memo.seenTypes = memo.seenTypes ? memo.seenTypes.slice() : [];
if (memo.skipRequireParentOf === dryad) {
delete memo.skipRequireParentOf;
} else {
let rq = dryad.requireParent();
if (rq) {
if (!_.contains(memo.seenTypes, rq)) {
// fetch the parent class from dryadTypes registery by name
if (!this.getClass) {
throw new Error('A getClass lookup was not provided to DryadTree and ' + dryad.constructor.name + ' needs one for requireParent()');
}
let requiredParent = new (this.getClass(rq))({}, [dryad]);
memo.skipRequireParentOf = dryad;
return this._makeTree(requiredParent, parentId, childIndex, memo);
}
}
}
let id = parentId ? parentId + '.' + childIndex : '0';
let context = this._createContext(dryad, id, parentId, this.rootContext);
this.dryads[id] = dryad;
this.contexts[id] = context;
let makeSubgraph = (dr) => {
let subgraph = dr.subgraph();
if (subgraph) {
context.subgraph = {};
let subMemo = _.clone(memo);
// When and if this dryad appears in its own subgraph
// then do not call subgraph() on that. It will just
// do prepare/add/remove on its own self.
subMemo.skipSubgraphOf = dryad;
// objects in subgraph will store references to themselves
// in this dryad's context because of this memo flag:
subMemo.subgraphOfId = id;
// if its an array then should have been supplied in a Branch
if (Array.isArray(subgraph)) {
throw new Error('Dryad subgraph should return a single Dryad with children.' + dr + subgraph);
}
return this._makeTree(subgraph, id, 'subgraph', subMemo);
}
};
if (memo.skipSubgraphOf) {
if (memo.skipSubgraphOf === dryad) {
// may still be subgraph children to come
delete memo.skipSubgraphOf;
} else {
// This dryad is in a subgraph of another
// store self and context in that parent's context
// under the dryad.tag or create a unique id
if (memo.subgraphOfId) {
this.contexts[memo.subgraphOfId].subgraph[dryad.tag || id] = {
dryad: dryad,
context: context
};
}
let subgraph = makeSubgraph(dryad);
if (subgraph) {
return subgraph;
}
}
} else {
let subgraph = makeSubgraph(dryad);
if (subgraph) {
return subgraph;
}
}
let dryadType = dryad.constructor.name;
memo.seenTypes.push(dryadType);
return {
id: id,
type: dryadType,
// don't want Dryads from properties
// they should be in subgraph
// but there should be some comparable data dump of them
// for diffing
// properties: _convertObject(dryad.properties, dryad.id, 0, memo),
// parent: this.contexts[parentId],
children: this._convertObject(dryad.children, id, childIndex, memo)
};
}
/**
* private.
*
* Calls the appropriate method on the dryad.children
* Currently it can only be an Array and all the children
* must be a Dryad.
*
* It will be used for including the properties in the tree
* for use in diffing. If there are any Dryad in the properties
* then the Dryad class is currently responsible for returning those
* in subgraph() so they get launched.
*
* @param {Dryad|Array|Object|String|Number|undefined} obj
*/
_convertObject(obj, parentId, childIndex=0, memo={}) {
if (obj.isDryad) {
return this._makeTree(obj, parentId, childIndex, memo);
}
if (_.isArray(obj)) {
return _.map(obj, (pp, ii) => {
return this._convertObject(pp, parentId, ii, memo);
});
}
if (_.isObject(obj)) {
return _.mapObject(obj, (pp, key) => {
return this._convertObject(pp, parentId, key, memo);
});
}
// should check that its a primitive type
return obj;
}
}