From 7ddef45ec4c76b7fc710e2c44d2087dfc2e7fa16 Mon Sep 17 00:00:00 2001 From: Timur Shemsedinov Date: Fri, 29 Nov 2024 20:19:59 +0200 Subject: [PATCH 1/6] Add Future implementation --- dist.js | 1 + lib/future.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ metautil.js | 1 + 3 files changed, 65 insertions(+) create mode 100644 lib/future.js diff --git a/dist.js b/dist.js index dbda61ba..5c1af92b 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 00000000..cbfd5969 --- /dev/null +++ b/lib/future.js @@ -0,0 +1,63 @@ +'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); + } + + promise() { + return new Promise((resolve, reject) => { + this.fork( + (value) => resolve(value), + (error) => reject(error), + ); + }); + } +} + +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 c748ee43..14fc374c 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'), From afc5a109cbdf3a82a35e83af14b4a88dd45d5e62 Mon Sep 17 00:00:00 2001 From: Timur Shemsedinov Date: Sat, 30 Nov 2024 23:35:27 +0200 Subject: [PATCH 2/6] fixup! Add Future implementation --- lib/future.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/future.js b/lib/future.js index cbfd5969..0ba5b950 100644 --- a/lib/future.js +++ b/lib/future.js @@ -40,12 +40,9 @@ class Future { this.#executor(successed, failed); } - promise() { + toPromise() { return new Promise((resolve, reject) => { - this.fork( - (value) => resolve(value), - (error) => reject(error), - ); + this.fork(resolve, reject); }); } } From 1726eb3b08eb2e5e770abc935964ca03b9a5eed5 Mon Sep 17 00:00:00 2001 From: Timur Shemsedinov Date: Sat, 30 Nov 2024 23:39:25 +0200 Subject: [PATCH 3/6] Add Future..toThenable() --- lib/future.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/future.js b/lib/future.js index 0ba5b950..2b21db6e 100644 --- a/lib/future.js +++ b/lib/future.js @@ -45,6 +45,13 @@ class Future { this.fork(resolve, reject); }); } + + toThenable() { + const then = (resolve, reject) => { + this.fork(resolve, reject); + }; + return { then }; + } } const futurify = From 348b298c251f145cc5a9a04bf4a4a9415d90825a Mon Sep 17 00:00:00 2001 From: Timur Shemsedinov Date: Sat, 30 Nov 2024 23:41:43 +0200 Subject: [PATCH 4/6] fixup! Add Future implementation --- lib/future.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/future.js b/lib/future.js index 2b21db6e..974318b4 100644 --- a/lib/future.js +++ b/lib/future.js @@ -41,9 +41,9 @@ class Future { } toPromise() { - return new Promise((resolve, reject) => { - this.fork(resolve, reject); - }); + const { promise, resolve, reject } = Promise.withResolvers(); + this.fork(resolve, reject); + return promise; } toThenable() { From ecedd2e759186e5efa408c755c5072adcddac508 Mon Sep 17 00:00:00 2001 From: Alex Buglak Date: Thu, 16 Jan 2025 19:11:41 +0100 Subject: [PATCH 5/6] Fixed future.toPromise * added docs and tests * fixed future * fixed toPromise --- lib/future.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/future.js b/lib/future.js index 974318b4..2b21db6e 100644 --- a/lib/future.js +++ b/lib/future.js @@ -41,9 +41,9 @@ class Future { } toPromise() { - const { promise, resolve, reject } = Promise.withResolvers(); - this.fork(resolve, reject); - return promise; + return new Promise((resolve, reject) => { + this.fork(resolve, reject); + }); } toThenable() { From e5583b0d880a637886f605920374529ba02d81a2 Mon Sep 17 00:00:00 2001 From: Alex Buglak Date: Thu, 16 Jan 2025 19:33:18 +0100 Subject: [PATCH 6/6] Added docs and tests for Future class --- README.md | 42 +++++++++++++++++++++ test/future.js | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 test/future.js diff --git a/README.md b/README.md index d815adae..523e2d01 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/test/future.js b/test/future.js new file mode 100644 index 00000000..60af7eeb --- /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(); +});