diff --git a/README.md b/README.md index d815ada..523e2d0 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,48 @@ try { } ``` +## Future + +Future is a stateless chained abstraction for handling asynchronous operations. + +- `constructor(executor: Function)` +- `static of(value: unknown): Future` +- `chain(fn: Function): Future` +- `map(fn: Function): Future` +- `fork(successed: Function, failed?: Function): void` +- `toPromise(): Promise` +- `toThenable(): Thenable` + +Example with `Future` + +```js +const futureFile = (name) => + new Future((resolve, reject) => { + fs.readFile(name, 'utf8', (err, data) => { + if (err) reject(err); + else resolve(data); + }); + }); + +const future = futureFile('file.js'); +const size = future.map((x) => x.length); +const lines = future.map((x) => x.split('\n').length); + +size.fork((x) => console.log('File size:', x)); +lines.fork((x) => console.log('Line count:', x)); +``` + +Using `futurify` + +```js +const readFile = (name, callback) => fs.readFile(name, 'utf8', callback); +const futureFile = futurify(readFile); + +futureFile('file.js') + .map((x) => x.length) + .fork((x) => console.log('File size:', x)); +``` + ## Crypto utilities - `cryptoRandom(min?: number, max?: number): number` diff --git a/dist.js b/dist.js index dbda61b..5c1af92 100644 --- a/dist.js +++ b/dist.js @@ -6,6 +6,7 @@ module.exports = { ...require('./lib/datetime.js'), ...require('./lib/error.js'), ...require('./lib/events.js'), + ...require('./lib/future.js'), ...require('./lib/http.js'), ...require('./lib/objects.js'), ...require('./lib/pool.js'), diff --git a/lib/future.js b/lib/future.js new file mode 100644 index 0000000..2b21db6 --- /dev/null +++ b/lib/future.js @@ -0,0 +1,67 @@ +'use strict'; + +class Future { + #executor; + + constructor(executor) { + this.#executor = executor; + } + + static of(value) { + return new Future((resolve) => resolve(value)); + } + + chain(fn) { + return new Future((resolve, reject) => + this.fork( + (value) => fn(value).fork(resolve, reject), + (error) => reject(error), + ), + ); + } + + map(fn) { + return new Future((resolve, reject) => + this.fork( + (value) => + new Future((resolve, reject) => { + try { + resolve(fn(value)); + } catch (error) { + reject(error); + } + }).fork(resolve, reject), + (error) => reject(error), + ), + ); + } + + fork(successed, failed) { + this.#executor(successed, failed); + } + + toPromise() { + return new Promise((resolve, reject) => { + this.fork(resolve, reject); + }); + } + + toThenable() { + const then = (resolve, reject) => { + this.fork(resolve, reject); + }; + return { then }; + } +} + +const futurify = + (fn) => + (...args) => + new Future((resolve, reject) => { + fn(...args, (err, data) => { + if (err) reject(err); + else resolve(data); + }); + }); + +module.exports = { Future, futurify }; diff --git a/metautil.js b/metautil.js index c748ee4..14fc374 100644 --- a/metautil.js +++ b/metautil.js @@ -9,6 +9,7 @@ module.exports = { ...require('./lib/error.js'), ...require('./lib/events.js'), ...require('./lib/fs.js'), + ...require('./lib/future.js'), ...require('./lib/http.js'), ...require('./lib/network.js'), ...require('./lib/objects.js'), diff --git a/test/future.js b/test/future.js new file mode 100644 index 0000000..60af7ee --- /dev/null +++ b/test/future.js @@ -0,0 +1,99 @@ +'use strict'; + +const metatests = require('metatests'); +const { Future, futurify } = require('..'); + +metatests.test('Future: executor', async (test) => { + const future = new Future((resolve) => resolve(24)); + const future2 = future.map((value) => value * 2); + future.fork( + (value) => test.strictSame(value, 24), + (error) => test.error(error), + ); + future2.fork( + (value) => test.strictSame(value, 48), + (error) => test.error(error), + ); + test.end(); +}); + +metatests.test('Future: of', async (test) => { + const future = new Future((resolve) => resolve(24)); + future.fork( + (value) => test.strictSame(value, 24), + (error) => test.error(error), + ); + test.end(); +}); + +metatests.test('Future: map', async (test) => { + const future = Future.of(24) + .map((value) => value * 2) + .map((value) => value + 1); + future.fork( + (value) => test.strictSame(value, 49), + (error) => test.error(error), + ); + + test.end(); +}); + +metatests.test('Future: chain', async (test) => { + const future = Future.of(24) + .chain((value) => Future.of(value * 2)) + .chain((value) => Future.of(value + 1)); + future.fork( + (value) => test.strictSame(value, 49), + (error) => test.error(error), + ); + + test.end(); +}); + +metatests.test('Future: toPromise', async (test) => { + const future = new Future((resolve) => { + setTimeout(() => { + resolve(24); + }, 50); + }); + const futurePromise = future.toPromise(); + const res = await futurePromise; + test.strictSame(res, 24); + test.end(); +}); + +metatests.test('Future: toThenable', async (test) => { + const future = new Future((resolve) => { + setTimeout(() => { + resolve(24); + }, 50); + }); + const thenable = future.toThenable(); + thenable.then((value, err) => { + if (err) return void test.error(err); + test.strictSame(value, 24); + }); + test.end(); +}); + +metatests.test('Future: futurify', async (test) => { + const asyncFunction = (value, callback) => { + setTimeout(() => { + if (value > 0) callback(null, value * 2); + else callback(new Error('Negative value')); + }, 50); + }; + const futureFunction = futurify(asyncFunction); + const future = futureFunction(24); + future.fork( + (value) => test.strictSame(value, 48), + (error) => test.error(error), + ); + const rejectedFuture = futureFunction(-1); + rejectedFuture.fork( + () => test.error(new Error('Should not be executed')), + (error) => test.strictSame(error.message, 'Negative value'), + ); + + test.end(); +});