No Thanks!

Fine-grained cancellation for promises

The no-thanks package provides a set of utilities to cancel a promise that can be used as primitives to compose more complex pipelines with the ability to stop execution at any time

npm i --save no-thanks

20 kB source, 5.8 kB minified, 1.8 kB gzipped

Quick links:

In case the website is down all the documentation can be found in the doc directory of the source repository

Motivation

It's a very common situation when a javascript program has to run some expensive or long-running task and then the result of the task or the task itself is no longer needed.

As an example, an application may start a network request (or a group of requests) and then the user clicks "Cancel" button or changes some parameters so we need to abort the request and drop the result.

Standard javascript APIs such as Promise or AsyncFunction has no way to stop a task, neither has a tool to split the task into smaller ones and stop them in a more granular way.

This library provides such tools, along with a cancellation callbacks to free up resources after cancel.

Getting Started

const {
    CancellablePromise, 
    cancellable, 
    interruptible, 
    coroutine, 
    compose, 
    decompose } = require('no-thanks')

If the package is included in a browser with <script> tag, it can be found at window.noThanks key

Basic Usage

CancellablePromise is a Promise that can be canceled. It extends native Promise for full compatibility

class CancellablePromise (task: Function|Promise, finalizer: ?Function = null) extends Promise

The very basic usage of it:

let job = new CancellablePromise(someTask);

// or alternativelly:
job = cancellable(someTask);

/* ... */

job.cancel()

When the cancel method is called while the someTask is not finished, fulfillment value or the rejection of it will be muted and will not be propagated further.

If someTask has finished execution before the cancel method was called, the cancellation has no effect, but the finalizer (if given) will be called anyway.

Note CancellablePromise constructor and cancellable function are interchangeable except the fineGrained parameter that will be discussed a bit later

Finalization

CancellablePromise constructor or cancellable function can be called with a callback that will be called after cancel.

This callback is responsible for a finalization of the task, it's often used to do some cleanup when a task was canceled.

The finalizer will be called with the results of promises that are already fulfilled at the time of cancel

// CancellablePromise constructor can also be used here
const job = cancellable(someTask, taskResult => { 
    console.log('finalization')
})

/* ... */

job.cancel()

// outputs:
// > finalization

Because finalizer is called asynchronously cancel method of the CancellablePromise returns a Promise with the result of finalizer call. If the finalizer returns promise it will be automatically chained into a cancellation result promise

const job = cancellable(someTask, () => Promise.resolve(42))

job.cancel()
    .then(answer => console.log(answer))

// prints: 42

Note that the finalizer will be called after cancel() in any case, even if the wrapped promise was successfully fulfilled with a value or rejected

Fine-grained cancellation

To create a cancellable pipeline of promises just call .then and .catch methods of CancellablePromise, then the resulting promise can be canceled in the middle of execution

// CancellablePromise constructor can also be used here
const job = cancellable(task1, (result1, resul2, result3) => {
        console.log('canceled', result1, result2, result3)
    })
    .then(() => task2)
    .then(() => task3)

/* ... wait for task1 to complete and task2 to start */

job.cancel()

// outputs:
// > canceled resul1 result2 undefined

Note that only first two tasks were executed

Async Function

The cancellation of an AsyncFunction is controlled with special argument "fineGrained": boolean in the cancellable signature:

cancellable (task: AsyncFunction|Promise, finalizer: ?Function = null, fineGrained: boolean = true) => CancellablePromise

When it's set to true (default) additional argument will be given to the task function, this argument is used to wrap inner tasks into "grains" to make them cancellable. The argument will be provided at the first position and it's usually named "grain"

const job = cancellable(async (grain, ...args) => {
    await grain(task1)
    /* do other stuff */
    await grain(task2)
    await grain(42) // any value can be wrapped into grain, and passed to the finalizer
    await grain(task3)

    console.log(...agrs)
    /// prints: the answer is 42
})

// run the job
const running = job('the', 'answer', 'is', 42)

/* ... */

running.cancel()

Coroutine

To avoid unnecessary verbosity with a grain argument from the previous example, a generator can be used instead of AsyncFunction. The library provides the coroutine method for that. It takes a generator function as an argument and wraps it into a CancellablePromise.

This approach сould be used to run a set of independent tasks sequentially, but if the next task in pipeline somehow depends on the result of the previous one, then cancellable method should be used.

coroutine (generator: Generator Function, finalizer: ?Function = null, fineGrained: boolean = true) => CancellablePromise

Optionally it takes finalizer as the second argument, and fineGrained as the third, if the fineGrained is set to true (default) then all the Promises that generator yields will be wrapped into grains, like in the previous example:

const job = coroutine(function* (...args) {
    yield task1
    /* do other stuff */
    yield task2
    yield task3

    console.log(...args)
    // prints: the answer is 42
})

const running = job('the', 'answer', 'is', 42)

/* ... */

running.cancel()

Interruptible

All the functions described above cancel a task and run finalizer only after the currently executing task is fulfilled or rejected, but sometimes it's necessary to interrupt a task immediately for example, when a network request was canceled by the user it should be aborted before the response arrives.

For that reason the interruptible method can be used:

interruptible (task: AsyncFunction|Promise, finalizer: ?Function = null, fineGrained: boolean = true) => CancellablePromise

const job = interruptible(async (grain, ...args) => {
    await grain(task1)
    await grain(task2)
    await grain(task3)

    console.log(...agrs)
    /// prints: the answer is 42
})

// run the job
const running = job('the', 'answer', 'is', 42)

/* ... */

running.cancel()

The interface of this method is identical to the cancellable method, except it runs finalize immediatelly after cancel()

Note Use interruptible with care and only if your tasks (grains) support immediate abortion, in other case the state of the current task will be unpredictable.

Check network request recipe for more real-life example

Compose & Decompose

There are two more utility functions in the public API

compose (promise: Promise|Object) => Promise

and

decompose (promise: Object) => Promise

They are used to avoid automatic chaining when returning Promises from promise resolvers and async functions.


(async () => compose(Promise.resolve(42)))()
    .then(decompose)
    .then(value => value === 42)

More examples and recipes can be found here

Also, take a look at the test directory, it contains a lot of examples of different semantics of the library methods

License

MIT

See LICENSE to see the full text.


All notable changes are documented in the CHANGELOG.md file.

This package can also be found at:

About the Author

I'm a web-developer, based in ~700 km south of the Arctic Circle in the beautiful city of Saint-Petersburg.

Feel free to contact me by email

results matching ""

    No results matching ""