0.0.1 • Published 6 years ago

regrettable v0.0.1

Weekly downloads
6
License
MIT
Repository
github
Last release
6 years ago

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';

// Normal async function cannot be canceled:
async function randomAsync() {
    // Line I:
    console.log("Generating a random number...");

    try {
        const randomNumber = await Math.random();
        // Line II:
        console.log("Generated a random number:", randomNumber);

        return randomNumber;
    }
    finally {
        // Line III:
        console.log("Cleaning up...");
    }
}

// Same as randomAsync but can be canceled:
const cancelableRandomAsync = cancelable(randomAsync);

// Line I will be executed:
const cancelableRandomPromise = cancelableRandomAsync();

// Only line III will be executed:
cancel(cancelableRandomPromise);

// Line I will be executed:
const randomPromise = randomAsync();

// Does nothing (TBD - or throws, or issues warning):
cancel(randomPromise);
// Lines II and III will both eventually be executed.

Propagation

When cancelable functions are composed, the cancel signal propagates upstream:

const delegate = cancelable(async doSomethingAsync => {
    await doSomethingAsync();
});

// Line I will be executed:
const cancelablePropagatingRandomPromise = delegate(cancelableRandomAsync);

// Only line III will be executed:
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:

// Line I will be executed:
const cancelableNonPropagatingRandomPromise = delegate(/* non-cancelable version: */randomAsync);

cancelableNonPropagatingRandomPromise.then(randomNumber => {
    // Line IV:
    console.log("Got random number:", randomNumber);
});

// Lines II and III will both eventually be executed, but not line IV:
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 => {
    // Line V:
    console.log("Got random number:", randomNumber);
});

// Lines II and III will both eventually be executed, but not line V:
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