Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: asyncFlow and asyncFlowRight methods #5846

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/asyncFlow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Composes a function that returns the result of invoking the given functions
* with the `this` binding of the created function, where each successive
* invocation is supplied the return value of the previous.
*
* @since 3.0.0
* @category Util
* @param {IncommingFunction[]} [funcs] The functions to invoke.
* @returns {OutcomingFunction} Returns the new composite function.
* @see asyncFlowRight
* @example
*
* import add from 'lodash/add'
*
* async function square(n) {
* const { resolve } = Promise.withResolvers()
* setTimeout(() => {
* resolve()
* }, 3000);
* }
*
* (async () => {
* const addSquare = asyncFlow(add, square)
*
* const result = await addSquare(1, 2)
* console.log(result) => 9
* })()
*/

type IncommingFunction = (...args: any[]) => Promise<any> | any;
type OutcomingFunction<T> = (...args: any[]) => Promise<T>;

export function asyncFlow<T>(funcs: IncommingFunction[]): OutcomingFunction<T> {
const length = funcs.length;
let i = funcs.length;

while (i--) {
if (typeof funcs[i] !== 'function') {
throw new TypeError('Expected a function');
}
}

return async function (this: any, ...args: any[]) {
let j = 0;
let result = length ? await funcs[j].apply(this, args) : args[0];

while (++j < length) {
result = await funcs[j].call(this, await result);
}

return result;
};
}

export default asyncFlow;
39 changes: 39 additions & 0 deletions src/asyncFlowRight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import asyncFlow from './asyncFlow.js';

/**
* Composes a function that returns the result of invoking the given functions
* with the `this` binding of the created function, where each successive
* invocation is supplied the return value of the previous.
*
* @since 3.0.0
* @category Util
* @param {IncommingFunction[]} [funcs] The functions to invoke.
* @returns {OutcomingFunction} Returns the new composite function.
* @see asyncFlow
* @example
*
* import add from 'lodash/add'
*
* async function square(n) {
* const { resolve } = Promise.withResolvers()
* setTimeout(() => {
* resolve()
* }, 3000);
* }
*
* (async () => {
* const addSquare = asyncFlowRight(square, add)
*
* const result = await addSquare(1, 2)
* console.log(result) => 9
* })()
*/

type IncommingFunction = (...args: any[]) => Promise<any> | any;
type OutcomingFunction<T> = (...args: any[]) => Promise<T>;

export function asyncFlowRight<T>(funcs: IncommingFunction[]): OutcomingFunction<T> {
return asyncFlow<T>([...funcs.reverse()]);
}

export default asyncFlowRight;
54 changes: 54 additions & 0 deletions test/asyncFlow-methods.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import lodashStable from 'lodash';
import { add, square, noop, identity } from './utils';
import head from '../src/head';
import map from '../src/map';
import uniq from '../src/uniq';
import asyncFlow from '../src/asyncFlow';
import asyncFlowRight from '../src/asyncFlowRight';

const methods = {
asyncFlow,
asyncFlowRight,
};

const asyncIdentity = Promise.resolve(identity);

describe('asyncFlow methods', () => {
lodashStable.each(['asyncFlow', 'asyncFlowRight'], (methodName) => {
const func = methods[methodName];
const isAsyncFlow = methodName === 'asyncFlow';

it(`\`_.${methodName}\` should supply each function with the return value of the previous`, async () => {
const fixed = function (n) {
return n.toFixed(1);
};
const combined = isAsyncFlow
? func(add, asyncIdentity, square, fixed)
: func(fixed, square, asyncIdentity, add);

await expect(combined(1, 2)).resolves.toBe('9.0');
});

it(`\`_.${methodName}\` should return a new function`, () => {
assert.notStrictEqual(func(noop), noop);
});

it(`\`_.${methodName}\` should work with a curried function and \`_.head\``, async () => {
const curried = lodashStable.curry(asyncIdentity);

const combined = isAsyncFlow ? func(head, curried) : func(curried, head);

await expect(combined([1])).resolves.toBe(1);
});

it(`\`_.${methodName}\` should work with curried functions with placeholders`, () => {
const curried = lodashStable.curry(lodashStable.ary(map, 2), 2);
const getProp = curried(curried.placeholder, (value) => value.a);
const objects = [{ a: 1 }, { a: 2 }, { a: 1 }];

const combined = isAsyncFlow ? func(getProp, uniq) : func(uniq, getProp);

expect(combined(objects)).toEqual([1, 2]);
});
});
});