-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #411 from smartprocure/flowAsync
`flowAsync`
- Loading branch information
Showing
8 changed files
with
231 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import _ from 'lodash/fp' | ||
import { resolveOnTree } from './tree' | ||
|
||
/** | ||
* Like `Promise.all`, but for objects. Polyfill bluebird Promise.props. Takes an object with promise values and returns a promise that resolves with an object with resolved values instead. | ||
* | ||
* @signature { a: Promise, b: Promise} => Promise<{a: value, b: value}> | ||
*/ | ||
export let promiseProps = | ||
Promise.props || | ||
(async (x) => _.zipObject(_.keys(x), await Promise.all(_.values(x)))) | ||
|
||
// Calls then conditionally, allowing flow to be used synchronously, too | ||
let asyncCall = (value, f) => (value.then ? value.then(f) : f(value)) | ||
let asyncCallShallow = (prop, f) => { | ||
if (_.some('then', prop)) { | ||
if (_.isArray(prop)) return Promise.all(prop).then(f) | ||
if (_.isPlainObject(prop)) return promiseProps(prop).then(f) | ||
} | ||
return asyncCall(prop, f) | ||
} | ||
let asyncCallDeep = (prop, f) => { | ||
prop = resolveOnTree()(prop) | ||
return asyncCall(prop, f) | ||
} | ||
// This implementation of `flow` spreads args to the first method and takes a method to determine how to combine function calls | ||
let flowWith = | ||
(call) => | ||
(fn0, ...fns) => | ||
(...x) => | ||
[...fns, (x) => x].reduce(call, fn0(...x)) | ||
|
||
/** | ||
* Like `_.flow`, but supports flowing together async and non async methods. | ||
* If nothing is async, it *stays synchronous*. | ||
* Also, it handles awaiting arrays of promises (e.g. from _.map) with `Promise.all` and objects of promises (e.g. from _.mapValues) with `promiseProps`. | ||
* This method generally solves most issues with using futil/lodash methods asynchronously. It's like magic! | ||
* NOTE: Main gotchas are methods that require early exit like `find` which can't be automatically async-ified. Also does not handle promises for keys. | ||
* Use `F.resolveOnTree` to await more complexly nested promises. | ||
* | ||
* @signature (f1, f2, ...fn) -> (...args) => fn(f2(f1(...args))) | ||
*/ | ||
export let flowAsync = flowWith(asyncCallShallow) | ||
|
||
/** | ||
* Just like `F.flowAsync`, except it recurses through return values using `F.resolveOnTree` instead of just `Promise.all` or `promise.props` | ||
* _CAUTION_ Just like `resolveOnTree`, this will mutate intermediate results to resolve promises. This is generally safe (and more performant) but might not always be what you expect. | ||
* | ||
* @signature (f1, f2, ...fn) -> (...args) => fn(f2(f1(...args))) | ||
*/ | ||
export let flowAsyncDeep = flowWith(asyncCallDeep) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import _ from 'lodash/fp' | ||
import chai from 'chai' | ||
import Promise from 'bluebird' | ||
import * as F from '../src' | ||
|
||
chai.expect() | ||
const expect = chai.expect | ||
|
||
describe('Async Functions', () => { | ||
it('promiseProps', async () => { | ||
let slow = _.curryN(2, async (f, ...args) => { | ||
await Promise.delay(10) | ||
return f(...args) | ||
}) | ||
let slowAdd = slow((x, y) => x + y) | ||
let slowDouble = slow((x) => x * 2) | ||
let x = { | ||
a: slowAdd(3, 4), | ||
b: slowDouble(5), | ||
} | ||
expect(x.a).to.not.equal(7) | ||
expect(x.b).to.not.equal(10) | ||
let y = await F.promiseProps(x) | ||
expect(y.a).to.equal(7) | ||
expect(y.b).to.equal(10) | ||
}) | ||
it('flowAsync', async () => { | ||
let slow = _.curryN(2, async (f, ...args) => { | ||
await Promise.delay(10) | ||
return f(...args) | ||
}) | ||
let slowAdd = slow((x, y) => x + y) | ||
let slowDouble = slow((x) => x * 2) | ||
let add1 = slow((x) => x + 1) | ||
|
||
// Mixes sync and async | ||
let testMidAsync = F.flowAsync(_.get('a'), slowDouble, (x) => x * 3) | ||
expect(await testMidAsync({ a: 4 })).to.equal(24) | ||
|
||
// Stays sync when there's no async | ||
let testSync = F.flowAsync(_.get('a'), (x) => x + 2) | ||
expect(testSync({ a: 1 })).to.equal(3) | ||
|
||
// Handles mixed sync/async for arrays | ||
let testArrayAsync = F.flowAsync( | ||
_.map(slowDouble), | ||
_.map((x) => x * 2) | ||
) | ||
expect(await testArrayAsync([1, 2])).to.deep.equal([4, 8]) | ||
|
||
// Handles pure async | ||
let f = F.flowAsync(slowAdd, slowDouble) | ||
let result = await f(2, 3) | ||
expect(result).to.equal(10) | ||
|
||
// Doesn't handle promise keys because the key becomes a string | ||
let testAsyncObj = F.flowAsync( | ||
_.mapValues(slowDouble), | ||
_.mapKeys(slowDouble) | ||
) | ||
expect(await testAsyncObj({ a: 1 })).to.deep.equal({ | ||
'[object Promise]': 2, | ||
}) | ||
|
||
// Can be used as mapAsync | ||
let mapAsync = F.flowAsync(_.map) | ||
expect(await mapAsync(add1, [1, 2, 3])).to.deep.equal([2, 3, 4]) | ||
expect(await mapAsync(add1, { a: 1, b: 2, c: 3 })).to.deep.equal([2, 3, 4]) | ||
|
||
// Can be used as mapValuesAsync | ||
let mapValuesAsync = F.flowAsync(_.mapValues) | ||
expect(await mapValuesAsync(add1, { a: 1, b: 2, c: 3 })).to.deep.equal({ | ||
a: 2, | ||
b: 3, | ||
c: 4, | ||
}) | ||
}) | ||
it('resolveOnTree', async () => { | ||
let slow = _.curryN(2, async (f, ...args) => { | ||
await Promise.delay(10) | ||
return f(...args) | ||
}) | ||
let slowAdd = slow((x, y) => x + y) | ||
let slowDouble = slow((x) => x * 2) | ||
|
||
let Tree = F.tree() | ||
let tree = { | ||
a: { b: slowAdd(1, 2) }, | ||
c: [slowAdd(3, 4), { d: 5, e: slowDouble(2) }], | ||
} | ||
let expected = { a: { b: 3 }, c: [7, { d: 5, e: 4 }] } | ||
// Tree is resolved with Promises replaced with results | ||
expect(await Tree.resolveOn(tree)).to.deep.equal(expected) | ||
// Original tree is mutated | ||
expect(tree).to.deep.equal(expected) | ||
// No need to await when there are no promises, original tree is returned | ||
expect(Tree.resolveOn(expected)).to.deep.equal(expected) | ||
}) | ||
it('flowAsyncDeep', async () => { | ||
let slow = _.curryN(2, async (f, ...args) => { | ||
await Promise.delay(10) | ||
return f(...args) | ||
}) | ||
let add1 = slow((x) => x + 1) | ||
|
||
expect( | ||
await F.flowAsyncDeep(_.update('a.b', add1))({ a: { b: 1, c: 2 } }) | ||
).to.deep.equal({ a: { b: 2, c: 2 } }) | ||
}) | ||
}) |