fngraph
Version:
A utility for composing function graphs.
683 lines (603 loc) • 18.4 kB
JavaScript
function _typeof(obj) {
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function (obj) {
return typeof obj;
};
} else {
_typeof = function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _objectSpread(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === 'function') {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function (key) {
_defineProperty(target, key, source[key]);
});
}
return target;
}
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest();
}
function _toArray(arr) {
return _arrayWithHoles(arr) || _iterableToArray(arr) || _nonIterableRest();
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
return arr2;
}
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _iterableToArray(iter) {
if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
}
function _iterableToArrayLimit(arr, i) {
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"] != null) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance");
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
/**
* A special placeholder value used to specify "gaps" within curried functions,
* allowing partial application of any combination of arguments, regardless of
* their positions.
*
* If `g` is a curried ternary function and `_` is `R.__`, the following are
* equivalent:
*
* - `g(1, 2, 3)`
* - `g(_, 2, 3)(1)`
* - `g(_, _, 3)(1)(2)`
* - `g(_, _, 3)(1, 2)`
* - `g(_, 2, _)(1, 3)`
* - `g(_, 2)(1)(3)`
* - `g(_, 2)(1, 3)`
* - `g(_, 2)(_, 3)(1)`
*
* @name __
* @constant
* @memberOf R
* @since v0.6.0
* @category Function
* @example
*
* const greet = R.replace('{name}', R.__, 'Hello, {name}!');
* greet('Alice'); //=> 'Hello, Alice!'
*/
function _isPlaceholder(a) {
return a != null && typeof a === 'object' && a['@@functional/placeholder'] === true;
}
/**
* Optimized internal one-arity curry function.
*
* @private
* @category Function
* @param {Function} fn The function to curry.
* @return {Function} The curried function.
*/
function _curry1(fn) {
return function f1(a) {
if (arguments.length === 0 || _isPlaceholder(a)) {
return f1;
} else {
return fn.apply(this, arguments);
}
};
}
/**
* Optimized internal two-arity curry function.
*
* @private
* @category Function
* @param {Function} fn The function to curry.
* @return {Function} The curried function.
*/
function _curry2(fn) {
return function f2(a, b) {
switch (arguments.length) {
case 0:
return f2;
case 1:
return _isPlaceholder(a) ? f2 : _curry1(function (_b) {
return fn(a, _b);
});
default:
return _isPlaceholder(a) && _isPlaceholder(b) ? f2 : _isPlaceholder(a) ? _curry1(function (_a) {
return fn(_a, b);
}) : _isPlaceholder(b) ? _curry1(function (_b) {
return fn(a, _b);
}) : fn(a, b);
}
};
}
/**
* Private `concat` function to merge two array-like objects.
*
* @private
* @param {Array|Arguments} [set1=[]] An array-like object.
* @param {Array|Arguments} [set2=[]] An array-like object.
* @return {Array} A new, merged array.
* @example
*
* _concat([4, 5, 6], [1, 2, 3]); //=> [4, 5, 6, 1, 2, 3]
*/
function _arity(n, fn) {
/* eslint-disable no-unused-vars */
switch (n) {
case 0:
return function () {
return fn.apply(this, arguments);
};
case 1:
return function (a0) {
return fn.apply(this, arguments);
};
case 2:
return function (a0, a1) {
return fn.apply(this, arguments);
};
case 3:
return function (a0, a1, a2) {
return fn.apply(this, arguments);
};
case 4:
return function (a0, a1, a2, a3) {
return fn.apply(this, arguments);
};
case 5:
return function (a0, a1, a2, a3, a4) {
return fn.apply(this, arguments);
};
case 6:
return function (a0, a1, a2, a3, a4, a5) {
return fn.apply(this, arguments);
};
case 7:
return function (a0, a1, a2, a3, a4, a5, a6) {
return fn.apply(this, arguments);
};
case 8:
return function (a0, a1, a2, a3, a4, a5, a6, a7) {
return fn.apply(this, arguments);
};
case 9:
return function (a0, a1, a2, a3, a4, a5, a6, a7, a8) {
return fn.apply(this, arguments);
};
case 10:
return function (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) {
return fn.apply(this, arguments);
};
default:
throw new Error('First argument to _arity must be a non-negative integer no greater than ten');
}
}
/**
* Internal curryN function.
*
* @private
* @category Function
* @param {Number} length The arity of the curried function.
* @param {Array} received An array of arguments received thus far.
* @param {Function} fn The function to curry.
* @return {Function} The curried function.
*/
function _curryN(length, received, fn) {
return function () {
var combined = [];
var argsIdx = 0;
var left = length;
var combinedIdx = 0;
while (combinedIdx < received.length || argsIdx < arguments.length) {
var result;
if (combinedIdx < received.length && (!_isPlaceholder(received[combinedIdx]) || argsIdx >= arguments.length)) {
result = received[combinedIdx];
} else {
result = arguments[argsIdx];
argsIdx += 1;
}
combined[combinedIdx] = result;
if (!_isPlaceholder(result)) {
left -= 1;
}
combinedIdx += 1;
}
return left <= 0 ? fn.apply(this, combined) : _arity(left, _curryN(length, combined, fn));
};
}
/**
* Returns a curried equivalent of the provided function, with the specified
* arity. The curried function has two unusual capabilities. First, its
* arguments needn't be provided one at a time. If `g` is `R.curryN(3, f)`, the
* following are equivalent:
*
* - `g(1)(2)(3)`
* - `g(1)(2, 3)`
* - `g(1, 2)(3)`
* - `g(1, 2, 3)`
*
* Secondly, the special placeholder value [`R.__`](#__) may be used to specify
* "gaps", allowing partial application of any combination of arguments,
* regardless of their positions. If `g` is as above and `_` is [`R.__`](#__),
* the following are equivalent:
*
* - `g(1, 2, 3)`
* - `g(_, 2, 3)(1)`
* - `g(_, _, 3)(1)(2)`
* - `g(_, _, 3)(1, 2)`
* - `g(_, 2)(1)(3)`
* - `g(_, 2)(1, 3)`
* - `g(_, 2)(_, 3)(1)`
*
* @func
* @memberOf R
* @since v0.5.0
* @category Function
* @sig Number -> (* -> a) -> (* -> a)
* @param {Number} length The arity for the returned function.
* @param {Function} fn The function to curry.
* @return {Function} A new, curried function.
* @see R.curry
* @example
*
* const sumArgs = (...args) => R.sum(args);
*
* const curriedAddFourNumbers = R.curryN(4, sumArgs);
* const f = curriedAddFourNumbers(1, 2);
* const g = f(3);
* g(4); //=> 10
*/
var curryN = /*#__PURE__*/_curry2(function curryN(length, fn) {
if (length === 1) {
return _curry1(fn);
}
return _arity(length, _curryN(length, [], fn));
});
/**
* Tests whether or not an object is an array.
*
* @private
* @param {*} val The object to test.
* @return {Boolean} `true` if `val` is an array, `false` otherwise.
* @example
*
* _isArray([]); //=> true
* _isArray(null); //=> false
* _isArray({}); //=> false
*/
/**
* Determine if the passed argument is an integer.
*
* @private
* @param {*} n
* @category Type
* @return {Boolean}
*/
/**
* Returns a curried equivalent of the provided function. The curried function
* has two unusual capabilities. First, its arguments needn't be provided one
* at a time. If `f` is a ternary function and `g` is `R.curry(f)`, the
* following are equivalent:
*
* - `g(1)(2)(3)`
* - `g(1)(2, 3)`
* - `g(1, 2)(3)`
* - `g(1, 2, 3)`
*
* Secondly, the special placeholder value [`R.__`](#__) may be used to specify
* "gaps", allowing partial application of any combination of arguments,
* regardless of their positions. If `g` is as above and `_` is [`R.__`](#__),
* the following are equivalent:
*
* - `g(1, 2, 3)`
* - `g(_, 2, 3)(1)`
* - `g(_, _, 3)(1)(2)`
* - `g(_, _, 3)(1, 2)`
* - `g(_, 2)(1)(3)`
* - `g(_, 2)(1, 3)`
* - `g(_, 2)(_, 3)(1)`
*
* @func
* @memberOf R
* @since v0.1.0
* @category Function
* @sig (* -> a) -> (* -> a)
* @param {Function} fn The function to curry.
* @return {Function} A new, curried function.
* @see R.curryN, R.partial
* @example
*
* const addFourNumbers = (a, b, c, d) => a + b + c + d;
*
* const curriedAddFourNumbers = R.curry(addFourNumbers);
* const f = curriedAddFourNumbers(1, 2);
* const g = f(3);
* g(4); //=> 10
*/
var curry = /*#__PURE__*/_curry1(function curry(fn) {
return curryN(fn.length, fn);
});
// Based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
/**
* Polyfill from <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString>.
*/
var makeError = function makeError(message, data) {
return {
'ERROR': {
message: message,
data: data
}
};
};
var getBadValueTypes = function getBadValueTypes(nodes) {
return Object.entries(nodes).filter(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
_name = _ref2[0],
value = _ref2[1];
return typeof value !== 'number' && !Array.isArray(value);
}).map(function (_ref3) {
var _ref4 = _slicedToArray(_ref3, 2),
name = _ref4[0],
_value = _ref4[1];
return name;
});
};
var hasBadValueTypes = function hasBadValueTypes(nodes) {
var badValueTypeNames = getBadValueTypes(nodes);
if (badValueTypeNames.length) return makeError('nodes with bad value types', badValueTypeNames);
return {};
};
var getBadArgRefs = function getBadArgRefs(nodes) {
var argNames = Object.keys(nodes);
return Object.values(nodes).filter(function (value) {
return Array.isArray(value);
}).reduce(function (refs, value) {
var _value2 = _toArray(value),
_fn = _value2[0],
argRefs = _value2.slice(1);
return [].concat(_toConsumableArray(refs), _toConsumableArray(argRefs));
}, []).filter(function (ref) {
return !argNames.includes(ref);
});
};
var hasBadArgRefs = function hasBadArgRefs(nodes) {
var badArgRefs = getBadArgRefs(nodes);
if (badArgRefs.length) return makeError('bad graph node refs', badArgRefs);
return {};
};
var keyExists = function keyExists(key, obj) {
return key in obj;
};
var makeFwdNode = function makeFwdNode(name) {
return {
name: name,
successors: []
};
};
var addPropIfMissing = function addPropIfMissing(obj, key, value) {
return keyExists(key, obj) ? obj : _objectSpread({}, obj, _defineProperty({}, key, value));
};
var addArgsAsPredecessorNodes = function addArgsAsPredecessorNodes(obj, nodeName, args) {
var res = _objectSpread({}, obj);
args.forEach(function (predecessorName) {
res = addPropIfMissing(res, predecessorName, makeFwdNode(predecessorName));
res[predecessorName].successors.push(nodeName);
});
return res;
};
var nodeToFwdLinks = function nodeToFwdLinks(accum, _ref5) {
var _ref6 = _slicedToArray(_ref5, 2),
nodeName = _ref6[0],
value = _ref6[1];
if (Array.isArray(value)) {
var _value3 = _toArray(value),
_fn = _value3[0],
args = _value3.slice(1);
return addArgsAsPredecessorNodes(accum, nodeName, args);
} else {
var successors = [].concat(_toConsumableArray(accum['ENTRY'].successors), [nodeName]);
var newEntry = _objectSpread({}, accum['ENTRY'], {
successors: successors
});
return _objectSpread({}, accum, {
'ENTRY': newEntry
});
}
};
var findCycle = function findCycle(graph, nodeName, history) {
if (nodeName === 'RETURN') return [false, []];
if (history.includes(nodeName)) return [true, [].concat(_toConsumableArray(history), [nodeName])]; //console.log(`findCycle: looking at ${nodeName}`);
var node = graph[nodeName];
if (node.successors.length == 0) return [false, []];
var result = node.successors.reduce(function (error, successorName) {
var _findCycle = findCycle(graph, successorName, [].concat(_toConsumableArray(history), [nodeName])),
_findCycle2 = _slicedToArray(_findCycle, 2),
found = _findCycle2[0],
path = _findCycle2[1];
if (found) return [found, path];
return error;
}, []);
return result;
};
var detectCycles = function detectCycles(nodes) {
var graph = Object.entries(nodes).reduce(nodeToFwdLinks, {
'ENTRY': makeFwdNode('ENTRY')
});
var _findCycle3 = findCycle(graph, 'ENTRY', []),
_findCycle4 = _slicedToArray(_findCycle3, 2),
found = _findCycle4[0],
path = _findCycle4[1];
if (found) return makeError('fn graph contains a cycle', path);
return {};
};
var validate = function validate(nodes) {
var result;
if (_typeof(nodes) !== 'object' || Array.isArray(nodes)) return makeError('graph is not an object', _typeof(nodes));
if (!keyExists('RETURN', nodes)) return makeError('graph is missing RETURN node', '');
result = hasBadValueTypes(nodes);
if ('ERROR' in result) return result;
result = hasBadArgRefs(nodes);
if ('ERROR' in result) return result;
result = detectCycles(nodes);
if ('ERROR' in result) return result;
return {};
};
var makeNode = curry(function (isAsync, argArr, _ref) {
var _ref2 = _slicedToArray(_ref, 2),
k = _ref2[0],
v = _ref2[1];
if (typeof v === 'number') {
var value = isAsync ? Promise.resolve(argArr[v]) : argArr[v];
return {
key: k,
ready: true,
value: value
};
} else {
var _v = _toArray(v),
fn = _v[0],
nodeArgNames = _v.slice(1);
return {
key: k,
ready: false,
function: fn,
args: nodeArgNames
};
}
});
var someNodeIsNotReady = function someNodeIsNotReady(nodes) {
return nodes.some(function (node) {
return !node.ready;
});
};
var getNodeByName = curry(function (nodes, arg) {
return nodes.find(function (node) {
return node.key === arg;
});
});
var getPrereqNodes = function getPrereqNodes(nodes, node) {
return node.args.map(getNodeByName(nodes));
};
var getPrereqValues = function getPrereqValues(nodes, node) {
return getPrereqNodes(nodes, node).map(function (n) {
return n.value;
});
};
var allPrereqsReady = function allPrereqsReady(nodes, node) {
return getPrereqNodes(nodes, node).every(function (n) {
return n.ready;
});
};
var getRunnableNode = function getRunnableNode(nodes) {
return nodes.find(function (node) {
return !node.ready && allPrereqsReady(nodes, node);
});
};
var makeNodePromise = function makeNodePromise(nodes, node) {
var promises = getPrereqValues(nodes, node);
return Promise.all(promises).then(function (args) {
return node.function.apply(node, _toConsumableArray(args));
});
};
var executeNodeFn = function executeNodeFn(nodes, node) {
var args = getPrereqValues(nodes, node);
return node.function.apply(node, _toConsumableArray(args));
};
var _fngraph = function _fngraph(graph, isAsync) {
var validation = validate(graph);
if ('ERROR' in validation) {
console.error('fngraph: invalid input graph:', validation.ERROR);
return validation;
}
return function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var nodes = Object.entries(graph).map(makeNode(isAsync, args));
while (someNodeIsNotReady(nodes)) {
var node = getRunnableNode(nodes);
node.value = isAsync ? makeNodePromise(nodes, node) : executeNodeFn(nodes, node);
node.ready = true;
}
return getNodeByName(nodes, 'RETURN').value;
};
};
var IS_ASYNC = true;
var IS_SYNC = false;
var fngraph = function fngraph(graph) {
return _fngraph(graph, IS_ASYNC);
};
var fngraphSync = function fngraphSync(graph) {
return _fngraph(graph, IS_SYNC);
};
var ifAll = function ifAll(fn, altRes) {
return function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return args.some(function (item) {
return item == undefined;
}) ? altRes : fn.apply(void 0, args);
};
};
var ifAny = function ifAny(fn, altRes) {
return function () {
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
return args.every(function (item) {
return item == undefined;
}) ? altRes : fn.apply(void 0, args);
};
};
export { fngraph, fngraphSync, ifAll, ifAny };