UNPKG

fngraph

Version:

A utility for composing function graphs.

683 lines (603 loc) 18.4 kB
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 };