UNPKG

@constantiner/fun-ctional

Version:

The library brings most of the familiar functional techniques (like functional composition) to asynchronous world with shining Promises

486 lines (456 loc) 23.3 kB
/** * @constantiner/fun-ctional * The library brings most of the familiar functional techniques (like functional composition) to asynchronous world with shining Promises * * @author Konstantin Kovalev <constantiner@gmail.com> * @version v0.6.6 * @link https://github.com/Constantiner/fun-ctional#readme * @date 30 May 2019 * * MIT License * * Copyright (c) 2018-2019 Konstantin Kovalev * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * */ const customPromiseHandlingSupportSupport = Symbol.for("Custom Promise Handling Support"); const addCustomPromiseHandlingSupport = (fn, customVersion) => ( (fn[customPromiseHandlingSupportSupport] = customVersion), fn ); const supportsCustomPromiseHandling = fn => fn[customPromiseHandlingSupportSupport]; /** * Composable version of catch method for promises. * * It gets a value (a promise or not), resolves it and if resulting promise was rejected calls catch function. * * It allows to handle errors within acompose or apipe asynchronous composition chains to restore broken state etc. * * A sample with acompose: * * <pre><code>const resultOrFallback = await acompose(acatch(handleAndRecoverFn), canFailFn)(someInput);</code></pre> * * Standalone usage: * * <pre><code>const resultOrFallback = await acatch(handleAndRecoverFn)(requestDataAndReturnPromise());</code></pre> * * It is the same as the following: * * <pre><code>requestDataAndReturnPromise().catch(handleAndRecoverFn).then(resultOrFallback => console.log(resultOrFallback));</code></pre> * * @param {function} catchFn Is function to handle Promise's rejection. * @returns {any => Promise} A function which expects any value as input (Promise or not) and returns a Promise. */ var acatch = catchFn => { const handler = value => Promise.resolve(value).catch(catchFn); return addCustomPromiseHandlingSupport(handler, promise => promise.catch(catchFn)); }; /** * Checks if parameter is an iterable. * * @param {any} obj is target to check. * @returns {boolean} Result of checking. */ const isIterable = obj => Array.isArray(obj) || ((obj || obj === "") && typeof obj[Symbol.iterator] === "function"); /** * Returns an array of values from arguments of some function. * Arguments can be passed as arguments list ar as single iterable parameter. * * @param {array} args are input array (just arguments of some other function). * If if consists of one element and this element is an iterable returns array from it. * @returns {Promise} Resulting array of arguments. */ const extractResolvedArguments = args => (args ? (args.length === 1 && isIterable(args[0]) ? [...args[0]] : args) : []); /** * Asynchronous compose function (acompose stays for async-compose). * * The main purpose is to replace a Promise handling code like this: * <pre><code>somePromise.then(normalize).then(upperCase).then(insertGreetings);</code></pre> * * with point-free style of functional compose syntax like the following: * <pre><code>acompose(insertGreetings, upperCase, normalize)(somePromise);</code></pre> * * It is lazy and allows of reusing of promise handling chains. * * You can run acompose with Promise instance (for true asynchronous execution) * or with any other object to use as usual functional composition. * It produces a Promise and can be used in async/await context: * * <pre><code>const message = await acompose(insertGreetings, upperCase, normalize)(somePromise);</code></pre> * * It also allows to handle errors like for traditional Promise but only in the tail position of the chain: * * <pre><code>acompose(insertGreetings, upperCase, normalize)(somePromise).catch(e => console.error(e));</code></pre> * * @param {...function|Iterable.<*>} fns Are functions to compose chains of promises. * @returns {(promise : Promise|any) => Promise} A function which expects any value as input (resolving to Promise) and returns a Promise. */ var acompose = (...fns) => async promise => extractResolvedArguments(fns).reduceRight((promise, fn) => { const promiseHandler = supportsCustomPromiseHandling(fn); if (promiseHandler) { return promiseHandler(promise); } return promise.then(fn); }, Promise.resolve(promise)); /* eslint-disable unicorn/prefer-spread */ /** * Returns a promise which resolved to array of values from a passed iterable. * An iterable can be a promise to resolve to iterable and then to array. * * @param {Promise|Iterable.<*>} arrayLike Is iterable or promise to resolve to. * @returns {Promise} A promise to resolve to resulting array. */ const extractArrayFromArgument = async arrayLike => Array.from(await Promise.resolve(arrayLike)); const filterMergeMap = filterFn => async (element, index, array) => { const filterResult = !!(await filterFn(element, index, array)); return { filterResult, element }; }; const filterResultsReducer = (filteredArray, { filterResult, element }) => { if (filterResult) { filteredArray.push(element); } return filteredArray; }; const getFilteredInParallel = async (filterFn, array) => { const filterMergeMapFn = filterMergeMap(filterFn); const filterValues = await Promise.all(array.map(filterMergeMapFn)); return filterValues.reduce(filterResultsReducer, []); }; const getFilteredInSequence = async (filterFn, array) => { const result = []; for (let i = 0; i < array.length; i++) { const filterResult = !!(await filterFn(array[i], i, array)); if (filterResult) { result.push(array[i]); } } return result; }; var afilterGeneric = (sequence = false) => filterFn => async iterable => { const sourceArray = await extractArrayFromArgument(iterable); const array = await Promise.all(sourceArray); return sequence ? await getFilteredInSequence(filterFn, array) : await getFilteredInParallel(filterFn, array); }; /** * An asynchronous version of filter over an iterable (afilter stays for async-filter). * * It gets an iterable of values (or promises) as input (or promise to resolve to iterable), * resolves them, iterates over them with filter function * (which returns boolean where true means current value will be included in resulting array) * and returns a promise which resolves to an array of values (filtered input iterable). * * It allows asynchronous filtering point-free way and can be used with asynchronous compose functions. * * It uses Promise.all() under the hood. * So if filtering function is asynchronous (returns a promise) all promises are being generated at once * and then resolved with Promise.all(). * So if any of promises will produce error (promise rejection) all the other promises will be invoked anyway. * The advantage of this method of invoking promises it will finish earlier than sequential filter (because of Promise.all()) * but it may perform some fetches or even state modifications even in case of fail on some previous filtering steps. * * <pre><code>const [ first, third ] = await afilter(fetchPermissions)([somePromise1, someValue2, somePromise3]);</code></pre> * * It first resolves a promises passed and then pass resolutions value to the filtering function. * * Input iterable's values are not restricted to promises but can be any value to pass as input to functions. * * It also allows to handle errors like for traditional Promise: * * <pre><code>afilter(fetchPermissions)(somePromise1, someValue2, somePromise3).catch(e => console.error(e));</code></pre> * * @param {function} filterFn Is filtering function which can produce a promise (but not restricted to this). * Function can return a promise (asynchronous filtering) or may just perform some synchronous filtering. * So you can use it in synchronous code taking in mind it returns promise so can't be resolved immediately. * It has three parameters (currentValue, currentIndex, array) which are already resolved (not promises). * @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects any values as input (resolving to Promise) * and returns a Promise. */ var afilter = afilterGeneric(); /** * An asynchronous version of filter over an iterable (afilterSeq stays for async-filter). * * It gets an iterable of values (or promises) as input (or promise to resolve to iterable), * resolves them, iterates over them with filter function * (which returns boolean where true means current value will be included in resulting array) * and returns a promise which resolves to an array of values (filtered input iterable). * * It allows asynchronous filtering point-free way and can be used with asynchronous compose functions. * * The difference from regular afilter is if filter function is asynchronous (returns a promise) * every new invocation of filter function performs sequentially after resolving previous promise. * So if any of promises produces error (promise rejection) afilterSeq will not produce new promises and they won't be invoked. * * <pre><code>const [ first, third ] = await afilterSeq(fetchPermissions)([somePromise1, someValue2, somePromise3]);</code></pre> * * It first resolves a promises passed and then pass resolutions value to the filtering function. * * Input iterable's values are not restricted to promises but can be any value to pass as input to functions. * * It also allows to handle errors like for traditional Promise: * * <pre><code>afilterSeq(fetchPermissions)(somePromise1, someValue2, somePromise3).catch(e => console.error(e));</code></pre> * * @param {function} filterFn Is filtering function which can produce a promise (but not restricted to this). * Function can return a promise (asynchronous filtering) or may just perform some synchronous filtering. * So you can use it in synchronous code taking in mind it returns promise so can't be resolved immediately. * It has three parameters (currentValue, currentIndex, array) which are already resolved (not promises). * @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects any values as input (resolving to Promise) * and returns a Promise. */ var afilterSeq = afilterGeneric(true); const getMappedInParallel = (mapFn, array) => Promise.all(array.map(mapFn)); const getMappedInSequence = async (mapFn, array) => { const result = []; for (let i = 0; i < array.length; i++) { const mapResult = await mapFn(array[i], i, array); result.push(mapResult); } return result; }; var amapGeneric = (sequence = false) => mapFn => async iterable => { const sourceArray = await extractArrayFromArgument(iterable); const array = await Promise.all(sourceArray); return sequence ? getMappedInSequence(mapFn, array) : getMappedInParallel(mapFn, array); }; /** * An asynchronous version of map over an iterable (amap stays for async-map). * * It gets an iterable of values (or promises) as input (or promise to resolve to iterable), * resolves them, maps over map function and returns a promise which resolves to an array of values. * * It allows asynchronous mapping point-free way and can be used with asynchronous compose functions. * * It uses Promise.all() under the hood. * So if mapping function is asynchronous (returns a promise) all promises are being generated at once * and then resolved with Promise.all(). * So if any of promises will produce error (promise rejection) all the other promises will be invoked. * The advantage of this method of invoking promises it will finish earlier than sequential map (because of Promise.all()) * but it may perform some fetches or even state modifications even in case of fail on some previous mapping steps. * * <pre><code>const [ first, second, third ] = await amap(getDataFromServer)([somePromise1, someValue2, somePromise3]);</code></pre> * * It first resolves a promises passed and then pass resolutions value to the mapping function. * * Input iterable's values are not restricted to promises but can be any value to pass as input to functions. * * It also allows to handle errors like for traditional Promise: * * <pre><code>amap(getDataFromServer)(somePromise1, someValue2, somePromise3).catch(e => console.error(e));</code></pre> * * @param {function} mapFn Is mapping function which can produce a promise (but not restricted to this). * Function can return a promise (asynchronous mapping) or may just perform some synchronous mapping. * So you can use it in synchronous code taking in mind it returns promise so can't be resolved immediately. * It has three parameters (currentValue, currentIndex, array) which are resolved (not promises). * @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects any values as input (resolving to Promise) * and returns a Promise. */ var amap = amapGeneric(); /** * An asynchronous version of map over an iterable (amap stays for async-map). * * It gets an iterable of values (or promises) as input (or promise to resolve to iterable), * resolves them, maps over map function and returns a promise which resolves to an array of values. * * It allows asynchronous mapping point-free way and can be used with asynchronous compose functions. * * The difference from regular amap is if map function is asynchronous (returns a promise) * every new invocation of map function performs sequentially after resolving previous promise. * So if any of promises produces error (promise rejection) amapSeq will not produce new promises and they won't be invoked. * * <pre><code>const [ first, second, third ] = await amap(getDataFromServer)([somePromise1, someValue2, somePromise3]);</code></pre> * * It first resolves a promises passed and then pass resolutions value to the mapping function. * * Input iterable's values are not restricted to promises but can be any value to pass as input to functions. * * It also allows to handle errors like for traditional Promise: * * <pre><code>amap(getDataFromServer)(somePromise1, someValue2, somePromise3).catch(e => console.error(e));</code></pre> * * @param {function} mapFn Is mapping function which can produce a promise (but not restricted to this). * Function can return a promise (asynchronous mapping) or may just perform some synchronous mapping. * So you can use it in synchronous code taking in mind it returns promise so can't be resolved immediately. * It has three parameters (currentValue, currentIndex, array) which are resolved (not promises). * @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects any values as input (resolving to Promise) * and returns a Promise. */ var amapSeq = amapGeneric(true); /** * Asynchronous pipe function (apipe stays for async-pipe). * * The main purpose is to replace a Promise handling code like this: * <pre><code>somePromise.then(normalize).then(upperCase).then(insertGreetings);</code></pre> * * with point-free style of functional pipe syntax like the following: * <pre><code>apipe(normalize, upperCase, insertGreetings)(somePromise);</code></pre> * * It is lazy and allows of reusing of promise handling chains. * * You can run apipe with Promise instance (for true asynchronous execution) * or with any other object to use as in usual functional composition. * It produces a Promise and can be used in async/await context: * * <pre><code>const message = await apipe(normalize, upperCase, insertGreetings)(somePromise);</code></pre> * * It also allows to handle errors like for traditional Promise but only in the tail position of the chain: * * <pre><code>apipe(normalize, upperCase, insertGreetings)(somePromise).catch(e => console.error(e));</code></pre> * * @param {...function|Iterable.<*>} fns Are functions to pipe chains of promises. * @returns {(promise : Promise|any) => Promise} A function which expects any value as input (resolving to Promise) and returns a Promise. */ var apipe = (...fns) => async promise => extractResolvedArguments(fns).reduce((promise, fn) => { const promiseHandler = supportsCustomPromiseHandling(fn); if (promiseHandler) { return promiseHandler(promise); } return promise.then(fn); }, Promise.resolve(promise)); /** * A kind of composable version of Promise.all(). * * It gets some value or promise as input, pass it to the functions list * and produces the array of results after resolving all the functions which can return promises as well. * * It allows to use Promise.all() point-free way. * * <pre><code>const [ first, second ] = await applyFns(squareRoot, getDataFromServer)(somePromise);</code></pre> * * It first resolves a promise passed and then pass resolution value to all the functions. * * Input value is not restricted to promise but can be any value to pass as input to functions. * * It also allows to handle errors like for traditional Promise: * * <pre><code>applyFns(squareRoot, getDataFromServer)(somePromise).catch(e => console.error(e));</code></pre> * * @param {...function|Iterable.<*>} fns Are functions to handle input value in parallel. * Functions can return promises or may just perform some mapping. * So you can use it in synchronous code taking in mind it returns promise so can't be resolved immediately. * @returns {(value : Promise|any) => Promise} A function which expects any value as input (resolving to Promise) and returns a Promise. */ var applyFns = (...fns) => async value => { const resolvedValue = await Promise.resolve(value); return Promise.all(extractResolvedArguments(fns).map(fn => fn(resolvedValue))); }; /** * Composable version of promise.then(mapFn).catch(catchFn). * * It gets a value (a promise or not), resolves it and handles as promise.then(mapFn).catch(catchFn) returning resulting promise. * * It allows to handle errors within acompose or apipe asynchronous composition chains to restore broken state etc. * * A sample with acompose: * * <pre><code>const resultOrFallback = await acompose(applySafe(canFailFn, handleAndRecoverFn), canFailTooFn)(someInput);</code></pre> * * Standalone usage: * * <pre><code>const resultOrFallback = await applySafe(canFailFn, handleAndRecoverFn)(requestDataAndReturnPromise());</code></pre> * * Or even: * * <pre><code>const resultOrFallback = await applySafe(acompose(handlerFn2, handlerFn1), handleAndRecoverFn)(requestDataAndReturnPromise());</code></pre> * * It is the same as the following: * * <pre><code>requestDataAndReturnPromise().then(canFailFn).catch(handleAndRecoverFn).then(resultOrFallback => console.log(resultOrFallback));</code></pre> * * @param {function} mapFn Is function to handle Promise's resolution (then). * @param {function} catchFn Is function to handle Promise's rejection (catch). * @returns {any => Promise} A function which expects any value as input (Promise or not) and returns a Promise. */ var applySafe = (mapFn, catchFn) => { const handler = value => Promise.resolve(value) .then(mapFn) .catch(catchFn); return addCustomPromiseHandlingSupport(handler, promise => promise.then(mapFn).catch(catchFn)); }; const resolveArrayFromInput = async iterable => Promise.all(await extractArrayFromArgument(iterable)); const reducer = reduceFn => (acc, current, index, array) => Promise.resolve(acc).then(acc => reduceFn(acc, current, index, array)); const getReducerArguments = args => { const effectiveReduceFn = reducer(args[0]); const effectiveArguments = [...args]; effectiveArguments[0] = effectiveReduceFn; return effectiveArguments; }; /** * Asynchronous composable version of reduce method for iterables ("a" stays for "asynchronous"). * * It gets a list of values (or list of promises, or promise to resolve to list) and performs standard reduce on them. * * Reduce function may be asynchronous to return a promise (to fetch some data etc). * * Initial value of reducer also could be a promise. * * A sample usage is: * * <pre><code>const sum = async (currentSum, invoiceId) => { * const { total:invoiceTotal } = await fetchInvoiceById(invoiceId); * return currentSum + invoiceTotal; * }; * const paymentTotal = await areduce(sum, 0)(fetchInvoiceIds(userId));</code></pre> * * Or the same with acompose: * * <pre><code>const paymentTotal = await acompose(areduce(sum, 0), fetchInvoiceIds)(userId);</code></pre> * * @param {function} callback Function to execute on each element in the array, taking four arguments * (accumulator, currentValue, currentIndex, array). * @param {any} initialValue (optional) Value to use as the first argument to the first call of the callback. * @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects an iterable * (or promise resolved to iterable) and returns a Promise. */ var areduce = (...args) => async iterable => Array.prototype.reduce.apply(await resolveArrayFromInput(iterable), getReducerArguments(args)); /** * Asynchronous composable version of reduceRight method for iterables ("a" stays for "asynchronous"). * * It gets a list of values (or list of promises, or promise to resolve to list) and performs standard reduce on them. * * Reduce function may be asynchronous to return a promise (to fetch some data etc). * * Initial value of reducer also could be a promise. * * A sample usage is: * * <pre><code>const sum = async (currentSum, invoiceId) => { * const { total:invoiceTotal } = await fetchInvoiceById(invoiceId); * return currentSum + invoiceTotal; * }; * const paymentTotal = await areduceRight(sum, 0)(fetchInvoiceIds(userId));</code></pre> * * Or the same with acompose: * * <pre><code>const paymentTotal = await acompose(areduceRight(sum, 0), fetchInvoiceIds)(userId);</code></pre> * * @param {function} callback Function to execute on each element in the array, taking four arguments * (accumulator, currentValue, currentIndex, array). * @param {any} initialValue (optional) Value to use as the first argument to the first call of the callback. * @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects an iterable * (or promise resolved to iterable) and returns a Promise. */ var areduceRight = (...args) => async iterable => Array.prototype.reduceRight.apply(await resolveArrayFromInput(iterable), getReducerArguments(args)); export { acatch, acompose, afilter, afilterSeq, amap, amapSeq, apipe, applyFns, applySafe, areduce, areduceRight }; //# sourceMappingURL=index.js.map