Regrettable
A JS/TS async operations cancellation module over native promises (does not exist yet).
Background
Itay is using Bluebird promises in his project. While he enjoys the API in general, the main
reason for not switching to native promises is its sound and useful cancellation API.
He loves using async/await but cannot use Node's native support for async functions -
it breaks the cancellation propagation of Bluebird promises.
Since he is also using TypeScript (for many reasons), a solution was found in
cancelable-awaiter that makes Bluebird
cancellation work in conjunction with async functions - as long as they are transpiled by
TypeScript instead of being used natively.
There is willingness from Node/v8 Citation Needed to expose hooks that enable
experimentation with cancellation APIs for (at least) async functions over native promises
(i.e. without a dependency on Bluebird).
Motivation
Itay would like to switch to native promises as long as he gets reasonable cancellation
features. His idea is to create a module that depends on a minimal set of hooks that may
be later exposed by Node and provide a minimal viable API for canceling async operations.
An additional requirement is that the hooks could be added to TypeScript today
(similar to the way the cancelable-awaiter
module enabled cancellation propagation)
so that he can switch to native promises right away and, hopefully, in the future switch to
native async functions when similar hooks will be exposed by Node.
API by example
This is a draft of how the API should work, using simple examples.
Opt-in
Cancellation of async functions is opt-in:
import {cancelable, cancel} from 'regrettable';
async function randomAsync() {
console.log("Generating a random number...");
try {
const randomNumber = await Math.random();
console.log("Generated a random number:", randomNumber);
return randomNumber;
}
finally {
console.log("Cleaning up...");
}
}
const cancelableRandomAsync = cancelable(randomAsync);
const cancelableRandomPromise = cancelableRandomAsync();
cancel(cancelableRandomPromise);
const randomPromise = randomAsync();
cancel(randomPromise);
Propagation
When cancelable functions are composed, the cancel signal propagates upstream:
const delegate = cancelable(async doSomethingAsync => {
await doSomethingAsync();
});
const cancelablePropagatingRandomPromise = delegate(cancelableRandomAsync);
cancel(cancelablePropagatingRandomPromise);
However, since cancellations are opt-in, the cancel signal does not propagate
to non-cancellable functions. Instead, it simply suppresses subsequent onFulfilled
and
onRejected
(and onFinally
) callbacks:
const cancelableNonPropagatingRandomPromise = delegate(randomAsync);
cancelableNonPropagatingRandomPromise.then(randomNumber => {
console.log("Got random number:", randomNumber);
});
cancel(cancelableNonPropagatingRandomPromise);
Promises are not cancelable
Though, in the examples above, we repeatedly invoked statements such as cancel(promise)
the fact is that the promises themselves were not canceled (as they are merely placeholders
for values) but the underlying async function that produced them directly was canceled.
By default, calling cancel(promise)
with an arbitrary promise instance would have no
effect (TBD - perhaps a warning or an error). As a convenience, one can wrap a
promise with a cancelable async function in the following way:
const wrappedRandomPromise = cancelable(randomPromise);
wrappedRandomPromise.then(randomNumber => {
console.log("Got random number:", randomNumber);
});
cancel(wrappedRandomPromise);
Note that the above code does not make randomPromise
truly cancelable in any sense,
it simply wraps the promise in order to allow the consumer to "unregister" from it.
Required Hooks
TBD
FAQ
TBD