diff --git a/lib/adapters.js b/lib/adapters.js index 31c946dd..8857e1c6 100644 --- a/lib/adapters.js +++ b/lib/adapters.js @@ -1,5 +1,7 @@ 'use strict'; +const { Future } = require('./future'); + // Convert Promise to callback-last // promise // callback @@ -67,10 +69,23 @@ const promisifySync = fn => (...args) => { return Promise.resolve(result); }; +// Convert callback contract to Future-returning function +// fn callback-last function +// +// Returns: Future-returning function +const futurify = fn => (...args) => + new Future((resolve, reject) => { + fn(...args, (err, data) => { + if (err) reject(err); + else resolve(data); + }); + }); + module.exports = { callbackify, asyncify, promiseToCallbackLast, promisify, promisifySync, + futurify, }; diff --git a/lib/future.js b/lib/future.js new file mode 100644 index 00000000..2e563531 --- /dev/null +++ b/lib/future.js @@ -0,0 +1,46 @@ +'use strict'; + +class Future { + constructor(executor) { + this.executor = executor; + } + + static of(value) { + return new Future(resolve => resolve(value)); + } + + static err(error) { + return new Future((resolve, reject) => reject(error)); + } + + chain(fn) { + return new Future((resolve, reject) => + this.run(value => fn(value).run(resolve, reject), error => reject(error)) + ); + } + + map(fn) { + return this.chain( + value => + new Future((resolve, reject) => { + try { + resolve(fn(value)); + } catch (error) { + reject(error); + } + }) + ); + } + + run(successed, failed) { + this.executor(successed, failed); + } + + promise() { + return new Promise((resolve, reject) => { + this.run(value => resolve(value), error => reject(error)); + }); + } +} + +module.exports = { Future }; diff --git a/metasync.js b/metasync.js index a51f620a..97d4e68e 100644 --- a/metasync.js +++ b/metasync.js @@ -12,6 +12,7 @@ const submodules = [ 'control', // Control flow utilities 'do', // Simple chain/do 'fp', // Async utils for functional programming + 'future', // Future stateless and lazy Promise analogue 'memoize', // Async memoization 'poolify', // Create pool from factory 'queue', // Concurrent queue diff --git a/test/future.js b/test/future.js new file mode 100644 index 00000000..59e2ea9a --- /dev/null +++ b/test/future.js @@ -0,0 +1,133 @@ +'use strict'; + +const { Future, futurify } = require('..'); +const metatests = require('metatests'); + +metatests.test('Future map/run', async test => { + Future.of(3) + .map(x => x ** 2) + .run(value => { + test.strictSame(value, 9); + test.end(); + }); +}); + +metatests.test('Future lazy', async test => { + Future.of(3).map(test.mustNotCall()); + test.end(); +}); + +metatests.test('Future resolve', async test => { + new Future(resolve => { + setTimeout(() => { + resolve(5); + }, 0); + }).run(value => { + test.strictSame(value, 5); + test.end(); + }, test.mustNotCall()); +}); + +metatests.test('Future reject', async test => { + new Future((resolve, reject) => { + reject(new Error('msg')); + }).run(test.mustNotCall(), error => { + test.strictSame(error.message, 'msg'); + test.end(); + }); +}); + +metatests.test('Future error', async test => { + Future.err(new Error('msg')).run(test.mustNotCall(), error => { + test.strictSame(error.message, 'msg'); + test.end(); + }); +}); + +metatests.test('Future promise', async test => { + const value = await Future.of(6) + .map(x => ++x) + .map(x => x ** 3) + .promise(); + + test.strictSame(value, 343); + test.end(); +}); + +metatests.test('Future catch', async test => { + Future.of(6) + .map(() => { + throw new Error('msg'); + }) + .run(test.mustNotCall, error => { + test.strictSame(error.message, 'msg'); + test.end(); + }); +}); + +metatests.test('Future stateless', async test => { + const f1 = Future.of(3); + const f2 = f1.map(x => ++x); + const f3 = f2.map(x => x ** 3); + const f4 = f1.map(x => x * 2); + + f1.run(value => { + test.strictSame(value, 3); + }); + + f1.run(value => { + test.strictSame(value, 3); + }); + + f2.run(value => { + test.strictSame(value, 4); + }); + + f2.run(value => { + test.strictSame(value, 4); + }); + + f3.run(value => { + test.strictSame(value, 64); + }); + + f4.run(value => { + test.strictSame(value, 6); + }); + + test.end(); +}); + +metatests.test('Future futurify success', async test => { + const f1 = (a, b, callback) => { + if (typeof a !== 'number' || typeof b !== 'number') { + callback(new Error('Arguments must be numbers')); + return; + } + callback(null, a + b); + }; + + const f2 = futurify(f1); + + f2(10, 20).run(value => { + test.strictSame(value, 30); + test.end(); + }); +}); + +metatests.test('Future futurify fail', async test => { + const f1 = (a, b, callback) => { + if (typeof a !== 'number' || typeof b !== 'number') { + callback(new Error('Arguments must be numbers')); + return; + } + callback(null, a + b); + }; + + const f2 = futurify(f1); + + f2('10', '20').run(test.mustNotCall, error => { + test.strictSame(error.message, 'Arguments must be numbers'); + test.end(); + }); +});