ciril
Version:
A javascript data binding library
763 lines (658 loc) • 25.1 kB
JavaScript
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
var _bluebird = require('bluebird');
var _bluebird2 = _interopRequireDefault(_bluebird);
var _remove = require('lodash/array/remove');
var _remove2 = _interopRequireDefault(_remove);
var _cloneDeep = require('lodash/lang/cloneDeep');
var _cloneDeep2 = _interopRequireDefault(_cloneDeep);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// renaming
var hasProp = require('lodash/object/has');
/**
* Maintains bindings between various nodes, and
* manages all node updates. The nodes themselves
* are unaware of their bindings -- this information
* is kept within the FlowGraph.
*
* The FlowGraph structure is meant for internal
* use. The public API is exposed through the Ciril
* object.
*/
var _FlowGraph = function () {
function _FlowGraph() {
_classCallCheck(this, _FlowGraph);
// uuid --> node
this.nodes = new Map([]);
// uuid --> Set<uuid>
this.bindings = new Map([]);
// uuid --> [uuid]
this.inputs = new Map([]);
// Set<Promise>
this.pending = new Set([]);
}
/**
* Register the node with this FlowGraph.
* @param node
* the node
* @return
* true iff the node doesn't already
* exist in the store.
*/
_createClass(_FlowGraph, [{
key: 'register',
value: function register(node) {
if (!hasProp(node, 'uuid')) throw new Error('register(node): node must ' + 'have a uuid.');
var uuid = node.uuid;
if (this.nodes.has(uuid)) throw new Error('register(node): a node with ' + 'the given uuid is already registered, ' + 'try generating a new one.');
this.nodes.set(uuid, node);
this.bindings.set(uuid, new Set([]));
this.inputs.set(uuid, []);
}
/**
* Remove the given nodes from the FlowGraph.
* Connected edges are removed as well.
* @param nodes
* the nodes to remove
*/
}, {
key: 'remove',
value: function remove() {
for (var _len = arguments.length, nodes = Array(_len), _key = 0; _key < _len; _key++) {
nodes[_key] = arguments[_key];
}
this.removeAll(nodes);
}
/**
* Remove the given nodes from the FlowGraph.
* Connected edges are removed as well.
* @param nodes
* the nodes to remove
*/
}, {
key: 'removeAll',
value: function removeAll(nodes) {
var _this = this;
nodes.forEach(function (node) {
var uuid = node.uuid;
_this.inputs.get(uuid).map(function (id) {
return _this.nodeFromUuid(id);
}).forEach(function (inp) {
return _this.unbind(inp, _this);
});
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = _this.bindings.get(uuid)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var id = _step.value;
var dest = _this.nodeFromUuid(id);
_this.unbind(node, dest);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
node.onRemove();
_this.nodes.delete(uuid);
});
}
/**
* Create a one-way data binding between
* the source and each of the destinations.
* @param source
* the source node
* @param destinations
* the destination nodes
* @return
* true iff source and destinations
* are registered
*/
}, {
key: 'bind',
value: function bind(source) {
for (var _len2 = arguments.length, destinations = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
destinations[_key2 - 1] = arguments[_key2];
}
return this.bindAll(source, destinations);
}
/**
* Create a one-way data binding between
* the source and each of the destinations.
* @param source
* the source node
* @param destinations
* the destination nodes
* @return true iff source and destinations
* are registered
*/
}, {
key: 'bindAll',
value: function bindAll(source, destinations) {
if (!this.isRegistered(source)) throw new Error('bindAll(...): source not registered.');
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = destinations[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var node = _step2.value;
if (!this.isRegistered(node)) throw new Error('bindAll(...): destination not registered.');
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = destinations[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var node = _step3.value;
this.inputs.get(node.uuid).push(source.uuid);
this.bindings.get(source.uuid).add(node.uuid);
node.onBindInput(source);
source.onBind(node);
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
return true;
}
/**
* Bind the input nodes to the given node, in
* the given order.
* @param node
* the output node
* @param inputs
* the input nodes
*/
}, {
key: 'bindInputs',
value: function bindInputs(node) {
for (var _len3 = arguments.length, inputs = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
inputs[_key3 - 1] = arguments[_key3];
}
this.bindAllInputs(node, inputs);
}
/**
* Bind the input nodes to the given node, in
* the given order.
* @param node
* the output node
* @param inputs
* the input nodes
*/
}, {
key: 'bindAllInputs',
value: function bindAllInputs(node, inputs) {
var _this2 = this;
var uuid = node.uuid;
var _inputs = this.inputs.get(uuid);
if (_inputs.length > 0) {
console.warn("bindAllInputs(...): Overwriting existing inputs");
_inputs.map(function (id) {
return _this2.nodeFromUuid(id);
}).forEach(function (inp) {
return _this2.unbind(inp, node);
});
}
var _iteratorNormalCompletion4 = true;
var _didIteratorError4 = false;
var _iteratorError4 = undefined;
try {
for (var _iterator4 = inputs[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
var inp = _step4.value;
this.bind(inp, node);
}
} catch (err) {
_didIteratorError4 = true;
_iteratorError4 = err;
} finally {
try {
if (!_iteratorNormalCompletion4 && _iterator4.return) {
_iterator4.return();
}
} finally {
if (_didIteratorError4) {
throw _iteratorError4;
}
}
}
}
/**
* Remove bindings between the source
* and destinations.
* @param source
* the source node
* @param destinations
* the destination nodes
* @return true iff source and destinations
* are registered
*/
}, {
key: 'unbind',
value: function unbind(source) {
for (var _len4 = arguments.length, destinations = Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
destinations[_key4 - 1] = arguments[_key4];
}
return this.unbindAll(source, destinations);
}
/**
* Remove bindings between the source
* and destinations.
* @param source
* the source node
* @param destinations
* the destination nodes
* @return true iff source and destinations
* are registered
*/
}, {
key: 'unbindAll',
value: function unbindAll(source, destinations) {
if (!this.nodes.has(source.uuid)) return false;
var _iteratorNormalCompletion5 = true;
var _didIteratorError5 = false;
var _iteratorError5 = undefined;
try {
for (var _iterator5 = destinations[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
var node = _step5.value;
if (!this.nodes.has(node.uuid)) {
console.warn("unbindAll(...): Attempting to unbind unregistered node.");
continue;
}
node.onUnbindInput(source);
source.onUnbind(node);
(0, _remove2.default)(this.inputs.get(node.uuid), function (id) {
return id === source.uuid;
});
this.bindings.get(source.uuid).delete(node.uuid);
// remove direct reference to input state objects.
node.state = (0, _cloneDeep2.default)(node.state);
}
} catch (err) {
_didIteratorError5 = true;
_iteratorError5 = err;
} finally {
try {
if (!_iteratorNormalCompletion5 && _iterator5.return) {
_iterator5.return();
}
} finally {
if (_didIteratorError5) {
throw _iteratorError5;
}
}
}
return true;
}
/**
* Create a cycle of bindings within nodes.
* @param nodes
* the nodes to synchronize
*/
}, {
key: 'synchronize',
value: function synchronize() {
for (var _len5 = arguments.length, nodes = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
nodes[_key5] = arguments[_key5];
}
this.synchronizeAll(nodes);
}
/**
* Create a cycle of bindings within nodes.
* @param nodes
* a list of the nodes to synchronize
*/
}, {
key: 'synchronizeAll',
value: function synchronizeAll(nodes) {
var _this3 = this;
if (nodes.length <= 1) return false;
nodes.reduce(function (p, c, i, a) {
var next = (i + 1) % nodes.length;
return _this3.bind(c, a[next]);
});
}
/**
* Inverse of this.synchronize(...nodes).
* @param nodes
* the nodes to desynchronize
*/
}, {
key: 'desynchronize',
value: function desynchronize() {
for (var _len6 = arguments.length, nodes = Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
nodes[_key6] = arguments[_key6];
}
this.desynchronizeAll(nodes);
}
/**
* Inverse of this.synchronizeAll(nodes).
* @param nodes
* a list of the nodes to desynchronize
*/
}, {
key: 'desynchronizeAll',
value: function desynchronizeAll(nodes) {
var _this4 = this;
if (nodes.length <= 1) return;
nodes.reduce(function (p, c, i, a) {
_this4.unbind(p, c);
return c;
}, nodes[nodes.length - 1]);
}
/**
* Get the node keyed by uuid.
* @param uuid
* the node's uuid
* @return
* the node keyed by uuid
*/
}, {
key: 'nodeFromUuid',
value: function nodeFromUuid(uuid) {
return this.nodes.has(uuid) ? this.nodes.get(uuid) : null;
}
/**
* Get the state of the node keyed
* by uuid.
* @param uuid
* the node's uuid
* @return
* the node's state
*/
}, {
key: 'getNodeState',
value: function getNodeState(uuid) {
return this.nodeFromUuid(uuid).getState();
}
/**
* Should be called when a node's data changes.
* Recursively updates bound nodes asynchronously.
* @param nodes
* the nodes to update
* @return
* a Promise for the update
*/
}, {
key: 'update',
value: function update() {
for (var _len7 = arguments.length, nodes = Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
nodes[_key7] = arguments[_key7];
}
return this.updateAll(nodes);
}
/**
* Should be called when a node's data changes.
* Recursively updates bound nodes asynchronously.
* @param nodes
* the nodes to update
* @return
* a Promise for the update
*/
}, {
key: 'updateAll',
value: function updateAll(nodes) {
var _this5 = this;
var p = _bluebird2.default.map(nodes, function (node) {
var uuid = node.uuid;
return new _bluebird2.default(function (resolve, reject) {
resolve(_this5._terminalsFrom(uuid));
}).map(function (id) {
return _this5._update(id);
}).all();
}).all(); // TODO: test with inconsistent updates
this.pending.add(p);
return p.then(function (res) {
return _this5.pending.delete(p);
});
}
/**
* Synchronous version of update(node).
* Assumes setState() implementations on
* dependent nodes are synchronous.
* @param nodes
* the nodes to update
*/
}, {
key: 'updateSync',
value: function updateSync() {
for (var _len8 = arguments.length, nodes = Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {
nodes[_key8] = arguments[_key8];
}
this.updateAllSync(nodes);
}
/**
* Synchronous version of update(node).
* Assumes setState() implementations on
* dependent nodes are synchronous.
* @param nodes
* the nodes to update
*/
}, {
key: 'updateAllSync',
value: function updateAllSync(nodes) {
var _this6 = this;
nodes.forEach(function (node) {
var uuid = node.uuid;
_this6._terminalsFrom(uuid).forEach(function (id) {
return _this6._updateSync(id);
});
});
}
/**
* Removes all nodes and bindings. Completes
* Pending updates first.
* @param safe
* if safe, pending updates will
* be completed before FlowGraph is
* cleared.
* @return
* a Promise for the clear, void if !safe
*/
}, {
key: 'clear',
value: function clear() {
var _this7 = this;
var safe = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0];
if (safe) {
// Finish remaining updates.
return this.flush().then(function (res) {
return _this7._clear();
}).caught(function (e) {
console.warn(e.stack);
_this7._clear();
});
} else {
this._clear();
}
}
/**
* Return a promise for the completion of all pending
* promises.
* @return
* a promise for the completion.
*/
}, {
key: 'flush',
value: function flush() {
return _bluebird2.default.all(Array.from(this.pending));
}
/**
* Clear this FlowGraph.
*/
}, {
key: '_clear',
value: function _clear() {
this.nodes.clear();
this.bindings.clear();
this.inputs.clear();
this.pending.clear();
}
/**
* Updates terminal node keyed by uuid, recursively
* updating dirty nodes when necessary.
* @param uuid
* the terminal uuid
* @return
* a Promise for the update
* @api private
*/
}, {
key: '_update',
value: function _update(uuid) {
var _this8 = this;
var node = this.nodeFromUuid(uuid);
if (!node.isDirty()) return _bluebird2.default.resolve(false);
node.markDirty(false);
return _bluebird2.default.map(this.inputs.get(uuid).filter(function (uuid) {
return _this8.nodeFromUuid(uuid).isDirty();
}), function (id) {
return _this8._update(id);
}).all()
// then update node
.then(function (res) {
var inputs = _this8.inputs.get(uuid);
var changed = inputs.length > 0 ? inputs.reduce(function (p, uuid, i, a) {
return p || _this8.nodeFromUuid(uuid).changed;
}) : false;
if (changed) {
var args = inputs.map(function (id) {
return _this8.getNodeState(id);
});
// supporting asynchronous setState() functions
return _bluebird2.default.resolve(node.setState.apply(node, args));
}
node.changed = false;
return _bluebird2.default.resolve(false);
});
}
/**
* Synchronous version of _updateSync(node).
* Assumes setState() implementations on
* dependent nodes are synchronous.
* @param node
* the node to update
* @api private
*/
}, {
key: '_updateSync',
value: function _updateSync(uuid) {
var _this9 = this;
var node = this.nodeFromUuid(uuid);
if (!node.isDirty()) return false;
node.markDirty(false);
var upstream = this.inputs.get(uuid).filter(function (id) {
return _this9.nodeFromUuid(id).isDirty();
}).map(function (id) {
return _this9._updateSync(id);
});
var inputs = this.inputs.get(uuid);
var changed = inputs.length > 0 ? inputs.reduce(function (p, uuid, i, a) {
return p || _this9.nodeFromUuid(uuid).changed;
}) : false;
if (changed) {
var args = inputs.map(function (id) {
return _this9.getNodeState(id);
});
return node.setState.apply(node, _toConsumableArray(args));
}
node.changed = false;
return false;
}
/**
* Get terminal nodes from node keyed by uuid
* @param uuid
* the node uuid
* @return
* the terminal nodes
* @api private
*/
}, {
key: '_terminalsFrom',
value: function _terminalsFrom(uuid) {
var visited = new Set();
var stack = [uuid];
var terminals = [];
// Depth-first search from uuid
while (stack.length > 0) {
var current = stack.pop();
var terminal = true;
visited.add(current);
var _iteratorNormalCompletion6 = true;
var _didIteratorError6 = false;
var _iteratorError6 = undefined;
try {
for (var _iterator6 = this.bindings.get(current)[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
var child = _step6.value;
if (!visited.has(child)) {
terminal = false;
stack.push(child);
this.nodeFromUuid(child).markDirty(true);
}
}
} catch (err) {
_didIteratorError6 = true;
_iteratorError6 = err;
} finally {
try {
if (!_iteratorNormalCompletion6 && _iterator6.return) {
_iterator6.return();
}
} finally {
if (_didIteratorError6) {
throw _iteratorError6;
}
}
}
if (terminal) terminals.push(current);
}
return terminals;
}
}, {
key: 'isRegistered',
value: function isRegistered(node) {
return this.nodes.has(node.uuid);
}
}]);
return _FlowGraph;
}();
var FlowGraph = new _FlowGraph();
exports.default = FlowGraph;