@danscode/futures
v2.1.2
Published
Implementation of a Future object for vanilla javascript
Maintainers
Readme
About
This is an implementation of a Future object for vanilla javascript.
A Future extends Promise and partially unwraps itself once it is settled.
It improves the reusability of Promise objects, without the need to reassign variables or re-await it.
Overview
The initialization is very similar to a Promise and has some QoL improvements.
A Future extends Promise for 2 reasons:
- to inherit all the
Promisemethods (e.g..then()or.catch()), even from future ES specs. - because they are very similar concepts.
[!IMPORTANT]
Requires ES2022 and ESM imports.
Available to install via npm i @danscode/futures or deno install npm:@danscode/futures.
You can grab a test file here.
Initialization and Syntax
The accepted function is run immediately, and a Future is returned.
A Future accepts both regular and async executor functions.
const futuro = new Future((resolve, reject, signal, ...) => {
/*your code*/
});const futuroAsincrono = new Future(async (signal, ...) => {
/*your code*/
});Some handles will be passed to the executor, it depends on the kind of function used as executor:
- These args are always sent, they can be called anything.
- You can pass any extra args to the executor like so
new Future(executor, 0, a, b, c, ...). - Regular executor is wrapped in a
Promiseand you can manuallyreject()orresolve()it. - Async executor implicitly returns a
Promiseso you cannot manually settle the asyncPromise.
A Future accepts an options object:
| Property | Description |
| :-------: | :-------------------------------------------------------------- |
| .signal | accepts an AbortSignal object (is always passed to executors) |
Structure
A Future has exposed, readonly properties (the object itself remains extensible):
| Property | Description |
| :----------: | :---------------------------------------------------------------------- |
| .value | is the settled value of the underlying Promise |
| abort() | is a reference to the method of an AbortController |
| .state | mimicks internal slot [[PromiseState]] |
| .isPending | returns false for resolved or rejected Future |
Details:
- initially
null - if resolved it is the resolved value
- if rejected it is the rejected value
- if thrown it is the error object
- if no
signalwas passed to theFutureconstructor, it is present butundefined.- this prevents accidental abort of other dependants when you only meant to abort the one
Future; - also
AbortSignalhas no reference toAbortController.abort(), so I can't grab it.
- this prevents accidental abort of other dependants when you only meant to abort the one
- once the
Futureis settled, it isnull(for memory cleanup). - see abort example
Usage
The main use case is to achieve some performance gain by avoiding frequent use of await.
In other words it's like a let data = new Promise() with more secure automatic unwrapping.Promise always has to be unwrapped (even if already resolved) with await or .then() otherwise the value is inaccessible.
But await schedules a microtask just like .then().
This means every await surrenders this iteration of the event loop, because a microtask is only executed between iterations of the event loop.
Also an async function will be frozen untill it resolves its first await and then the next, and the next...
This is an example of a busy program with the hot path in red:
The following diagram implies that promises might also call/contain long tasks
%% if you see this text try https://mermaid.live/
flowchart LR
NAME["Event Loop (simplified)"]
stack@{ shape: lin-cyl, label: "Check Call Stack" }
stack--- checkT{"Has a task"}
checkT--- |Yes| busyT@{shape: processes, label: "Execute code..."}
checkT--- |No| mTstack@{ shape: lin-cyl, label: "Check Microtask Queue" }
busyT-- "Build the stack up and down" ---stack
mTstack--- checkmT{"Has a microtask"}-.-x |Yes| busyT
%% Highlight the edges in the hot path
linkStyle 0,1,3 stroke:#ff1f1f,stroke-width:5px
%% cold path
linkStyle 2,4 stroke:#3464ff,stroke-width:2pxAs you can see, every time you have to await, your code takes a passing loop.
It will only execute after the entire call stack has been emptied.
There is one trick to await the value at the very last moment, letting the function start other Promises and complete some synchronous setup.
async function longTask() {
const file1 = fileAsync('bunny.png');
const file2 = fileAsync('carrot.png');
let number = numberAsync(1);
// long setup
for (let i = 0; i < 100000; i++) {
const x = i * 3;
}
// actual use of data
number = await number;
return (await file1) + (await file2) + number;
}I find several problems with this approach, specifically in more complicated production grade code:
- We still can only get the value out by
awaitingagain.- what if we need to use
const file1more than once?
- what if we need to use
- We could reassign
let numberto the onceawaitedvalue.- very easy to reassign it somewhere else by accident.
- We could start all
Promises withPromise.all()which still has to be unwrapped with anawait.- and possibly destructured into more variables;
- what if there are more
Promises dependent on earlierPromises? - what if
Promisesare best awaited at different points in the function?
Future removes the need for tricks, you only need to await it once to receive the value.
Any later access is direct and synchronous via <future>.value.
A Future also integrates with AbortSignal in two ways:
- You can pass an
AbortController.signalobject in theFutureconstructor options. - Or an
AbortControllerwill be created.- this makes sure the
AbortSignalis always passed into executors; - and exposes the
.abort()method on theFutureitself.
- this makes sure the
Here is an example of how to use it:
const cancelledFuture = new Future((res, rej, signal) => {
signal.addEventListener(
'abort',
() => {
rej(signal.reason);
},
{ once: true }
);
// never resolves
setTimeout(() => {
res(200);
}, 1500);
});
try {
cancelledFuture.abort(`I don't want this`);
console.log(await cancelledFuture);
} catch (e) {
console.log(e);
}Contributors
Dan: Code author
Changelog
- improved natural async function detection
- changed
<future>.vto<future>.value
