aico

abort in coroutines (promises)

Usage no npm install needed!

<script type="module">
  import aico from 'https://cdn.skypack.dev/aico';
</script>

README

A.I.C.O 🦄

Abort In COroutines (promises)

npm npm

aico was inspired by redux-saga's Task cancellation. I wanted to use it in promises and found several alternatives. But they are a little bit verbose or lacking. aico writes less and does more. And it supports AbortController and typescript (Although not enough).

aico

(I enjoyed watching A.I.C.O on Netflix)

Example

import aico from 'aico'

const promise = aico(function * (signal) {
    try {
        yield fetch('/delay/100', { signal }) // <= This api takes 100ms.
        console.log('1. This is printed.')

        yield fetch('/delay/100', { signal }) // <= This api takes 100ms.
        console.log('2. This is not printed.')
    } finally {
        if (signal.aborted) {
            console.log('3. aborted!')
        }
    }
})

promise.catch(err => {
    console.log(`4. message: ${err.message}`)
    console.log(`5. isAborted: ${err.isAborted}`)
})

setTimeout(() => {
    promise.abort() // <= After 150ms
}, 150)
> output
1. This is printed.
3. aborted!
4. message: Aborted
5. isAborted: true

Install

npm install aico

API

new AbortInCoroutines(generator, options?)

Create an abortable promise using a generator. In a generator, yield is the same as async function's await. Likewise, wait for a promise result.

import { AbortInCoroutines } from 'aico'

const promise = new AbortInCoroutines(function * () {
    const result = yield asyncTask() // <= result is `{ status: 'complete' }`.

    return result.status
})

promise.then(val => {
    console.log(val) // => complete
})

the parameter signal is AbortSignal that can cancel DOM requests such as fetch.

const promise = new AbortInCoroutines(function * (signal) {
    const response = yield fetch('/api/request', { signal })

    console.log('This is not printed.')
})

promise.abort() // <= Abort `/api/request` request.

signal has an aborted that indicates whether the promise was aborted or not.

const promise = new AbortInCoroutines(function * (signal) {
    try {
        /* ... */
    } finally {
        if (signal.aborted) {
            console.log('aborted!')
        } else {
            console.log('not aborted.')
        }
    }
})

promise.abort() // => aborted!

If the yielded promise is created by aico, the abortion is propagated.

const subTask = () => new AbortInCoroutines(function * (signal) {
    try {
        /* ... */
    } finally {
        if (signal.aborted) {
            console.log('subTask is aborted!')
        }
    }
})

const promise = new AbortInCoroutines(function * () {
    yield subTask()
})

promise.abort() // => subTask is aborted!

options

AbortController

This is an option for AbortController ponyfill instead of polyfill. (aico uses AbortController internally)

import AbortController from 'abort-controller'

new AbortInCoroutines(function * (signal) { /* ... */ }, { AbortController })
signal

This is an option to abort a promise with the signal of the external controller.

const controller = new AbortController()

const promise = new AbortInCoroutines(function * (signal) {
    try {
        /* ... */
    } finally {
        if (signal.aborted) {
            console.log('aborted!')
        }
    }
 }, { signal: controller.signal }) // 👈 Here, the external controller's signal is used.

 controller.abort() // => aborted!

aico(generator, options?)

This function can be used instead of the verbose new AbortInCoroutines().

import aico from 'aico'

//or
import { aico } from 'aico'

promise.isAborted

Returns whether the promise is aborted or not.

const promise = new AbortInCoroutines(/* ... */)

console.log(promise.isAborted) // => false

promise.abort()

console.log(promise.isAborted) // => true

promise.abort()

Abort the promise.

all(values)

This is an abortable Promise.all().

import { aico, all } from 'aico'

const fetchData = url => aico(function * (signal) {
    try {
        /* ... */
    } finally {
        if (signal.aborted) {
            console.log(`aborted : ${url}`)
        }
    }
})

const promise = all([
    fetchData('/api/1'),
    fetchData('/api/2'),
    fetchData('/api/3')
])

promise.abort()
// => aborted : /api/1
// => aborted : /api/2
// => aborted : /api/3

If one is rejected, the other promise created by aico is automatically aborted.

const promise = all([
    fetchData('/api/1'),
    fetchData('/api/2'),
    fetchData('/api/3'),
    Promise.reject('fail')
])
// (This is printed immediately)
// => aborted : /api/1
// => aborted : /api/2
// => aborted : /api/3

race(values)

This is an abortable Promise.race().

import { race } from 'aico'

const timeout = ms => new Promise((_, reject) => setTimeout(reject, ms))

const promise = race([
    fetchData('/delay/600'), // <= This api takes 600ms.
    timeout(500)
])

// (After 500ms)
// => aborted : /delay/600

Likewise, if one is rejected, the other promise created by aico is automatically aborted.

abortify(fn)

This function wraps a custom function that handles multiple promises to make it abortable.

import { abortify } from 'aico'

const any = abortify(Promise.any.bind(Promise)) // <= `Promise.any` is experimental and not fully supported.

const promise = any([ /* ... */ ])

promise.abort()

Tips

Type inference of yielded promise

const promise = aico(function * () {
    const result = yield asyncTask() // <= result type is `any` or `unknown`.

    /* ... */
})

In TypeScript, type inference of yielded promise is difficult. So do type assertions explicitly, or using AsyncResult helper.

import { aico, AsyncResult } from 'aico'

const promise = aico(function * () {
    const data = (yield asyncTask()) as { value: string }

    // or
    const data = (yield asyncTask()) as AsyncResult<typeof asyncTask>
})

License

MIT © skt-t1-byungi