diff --git a/src/async.js b/src/async.js new file mode 100644 index 0000000000..98c0a1dd73 --- /dev/null +++ b/src/async.js @@ -0,0 +1,61 @@ +import { curryN, bind } from 'ramda'; +import curry1 from 'ramda/src/internal/_curry1'; + +import resolveP from './resolveP'; +import rejectP from './rejectP'; + +/** + * Takes a generator function and returns an async function. + * The async function returned is a curried function whose arity matches that of the generator function. + * + * Note: This function is handy for environments that does support generators but doesn't support async/await. + * + * @func async + * @memberOf RA + * @since {@link https://char0n.github.io/ramda-adjunct/2.16.0|v2.16.0} + * @category Function + * @sig Promise c => (a, b, ...) -> a -> b -> ... -> c + * @param {Function} generatorFn The generator function + * @return {Function} Curried async function + * @see {@link https://www.promisejs.org/generators/} + * @example + * + * const asyncFn = RA.async(function* generator(val1, val2) { + * const a = yield Promise.resolve(val1); + * const b = yield Promise.resolve(val2); + * + * return a + b; + * }); + * + * asyncFn(1, 2); //=> Promise(3) + * + */ +const async = curry1(generatorFn => { + function asyncWrapper(...args) { + const iterator = bind(generatorFn, this)(...args); + + const handle = result => { + const resolved = resolveP(result.value); + + return result.done + ? resolved + : resolved.then( + value => handle(iterator.next(value)), + error => handle(iterator.throw(error)) + ); + }; + + try { + return handle(iterator.next()); + } catch (error) { + return rejectP(error); + } + } + + if (generatorFn.length > 0) { + return curryN(generatorFn.length, asyncWrapper); + } + return asyncWrapper; +}); + +export default async; diff --git a/src/index.d.ts b/src/index.d.ts index 33aad9e64f..37c26f6d19 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,3 +1,5 @@ +import { GeneratorFn } from 'jsverify'; + declare var RA: RamdaAdjunct.Static; declare namespace RamdaAdjunct { @@ -1085,6 +1087,14 @@ declare namespace RamdaAdjunct { */ sign(val: number): number; + /** + * Takes a generator function and returns an async function. + * The async function returned is a curried function whose arity matches that of the generator function. + * + * Note: This function is handy for environments that does support generators but doesn't support async/await. + */ + async(generatorFn: Function): Function; + /** * Identity type. */ diff --git a/src/index.js b/src/index.js index 0fb91926f7..c3beb924d0 100644 --- a/src/index.js +++ b/src/index.js @@ -96,6 +96,7 @@ export { default as Y } from './Y'; export { default as seq } from './seq'; export { default as sequencing } from './seq'; export { default as dispatch } from './dispatch'; +export { default as async } from './async'; // List export { default as mapIndexed } from './mapIndexed'; export { default as reduceIndexed } from './reduceIndexed'; diff --git a/test/async.js b/test/async.js new file mode 100644 index 0000000000..72f830d8dd --- /dev/null +++ b/test/async.js @@ -0,0 +1,146 @@ +import * as R from 'ramda'; +import { assert } from 'chai'; + +import eq from './shared/eq'; +import * as RA from '../src'; + +describe('async', function() { + context('given wrapping of generator', function() { + specify('should mimic async/await behavior', async function() { + const asyncFn = RA.async(function* generator(val1, val2) { + const a = yield RA.resolveP(val1); + const b = yield RA.resolveP(val2); + + return a + b; + }); + const expected = await asyncFn(1, 2); + + eq(expected, 3); + }); + + context('and the generator throw Error', function() { + specify('should resolve with rejection', async function() { + const asyncFn = RA.async(function* generator(val1, val2) { + yield RA.resolveP(val1); + yield RA.resolveP(val2); + + throw new Error('generator error'); + }); + + try { + await asyncFn(1, 2); + throw new Error('fulfilling should fail'); + } catch (error) { + assert.instanceOf(error, Error); + eq(error.message, 'generator error'); + } + }); + }); + }); + + it('should support yield delegation', async function() { + const foo = function* generator(val1, val2) { + const a = yield RA.resolveP(val1); + const b = yield RA.resolveP(val2); + + return a + b; + }; + const bar = RA.async(function* generator(val1, val2) { + const a = yield RA.resolveP(val1); + const b = yield RA.resolveP(val2); + const c = yield* foo(a, b); + + return c + 3; + }); + + eq(await bar(1, 2), 6); + }); + + it('should support async delegation', async function() { + const foo = RA.async(function* generator(val1, val2) { + const a = yield RA.resolveP(val1); + const b = yield RA.resolveP(val2); + + return a + b; + }); + const bar = RA.async(function* generator(val1, val2) { + const a = yield RA.resolveP(val1); + const b = yield RA.resolveP(val2); + const c = yield foo(a, b); + + return c + 3; + }); + + eq(await bar(1, 2), 6); + }); + + it('should support recursion delegation', async function() { + const async = RA.async(function* generator(val) { + let newVal = val; + + if (val > 1) { + newVal = yield async(val - 1); + } + + return yield newVal; + }); + + eq(await async(10), 1); + }); + + it('should curry', async function() { + const async = RA.async(R.__); + const asyncFn = async(function* generator() { + yield RA.resolveP(1); + return 2; + }); + const expected = await asyncFn(); + + eq(expected, 2); + }); + + context('given wrapping of generator with arity of 2', function() { + let asyncFn; + + beforeEach(function() { + // eslint-disable-next-line require-yield + asyncFn = RA.async(function* generator(a, b) { + return a + b; + }); + }); + + specify('should translate generator arity to wrapper', function() { + eq(asyncFn.length, 2); + }); + + specify('should curry wrapper to appropriate arity', async function() { + eq(await asyncFn(1, 2), 3); + eq(await asyncFn(1)(2), 3); + }); + }); + + context('given wrapping of generator with arity of 0', function() { + context('then the resulting wrapper', function() { + let asyncFn; + + beforeEach(function() { + // eslint-disable-next-line require-yield + asyncFn = RA.async(function* generator() { + return 1; + }); + }); + + specify('should not support placeholder', async function() { + const expected = await asyncFn(R.__); + + eq(expected, 1); + }); + + specify('should support call without arguments', async function() { + const expected = await asyncFn(); + + eq(expected, 1); + }); + }); + }); +});