@kaisukez/cancellation-token
What is this library
This library's idea is based on this proposal tc39/proposal-cancelable-promises which is actually great proposal but it was withdrawn for some reason.
I copied some code from https://github.com/conradreuter/cancellationtoken which is the implementation of the withdrawn proposal.
I also added some useful features that conradreuter/cancellationtoken doesn't have such as
-
new CancellationToken(cancel => {})
Revealing Constructor Pattern (from the withdrawn proposal) -
await CancellationError.ignoreAsync(() => promise)
use to ignore CancellationError in one line without using try-catch -
AyncCheckpoint.before(token, () => yourFunction())
use to throw CancellationError if the specified token is already cancelled before running function, in one line (you can also useAyncCheckpoint.after
andAyncCheckpoint.beforeAfter
) -
await cancel()
async cancel function (so that you can wait all async onCancel functions to finish before you move on)
Installation
npm i @kaisukez/cancellation-token
How to import
This package ships both ESM and CJS formats
ES Modules (ESM)
import {
CancellationToken,
CancellationError,
SyncCheckpoint,
AsyncCheckpoint,
Task,
} from '@kaisukez/cancellation-token'
CommonJS (CJS)
const {
CancellationToken,
CancellationError,
SyncCheckpoint,
AsyncCheckpoint,
Task,
} = require('@kaisukez/cancellation-token')
Examples
1. Basic usage
import {
CancellationToken,
CancellationError,
Task,
} from '@kaisukez/cancellation-token'
async function task(token: CancellationToken) {
let i = 0
while (true) {
console.log(`do task i=${i++}`)
await Task.sleep(500)
if (token.isCancellationRequested) {
console.log('do cleanup before throwing CancelError')
token.throwIfCancellationRequested()
}
}
}
async function main() {
const token = new CancellationToken(cancel => {
setTimeout(async () => await cancel(), 3000)
})
// other variations of instantiation
// const { token, cancel } = CancellationToken.source()
// const [token, cancel] = CancellationToken.sourceArray()
try {
await task(token)
} catch (error) {
if (error instanceof CancellationError) {
console.log('task got canceled')
} else {
throw error
}
}
}
;(async () => {
try {
await main()
} catch (error) {
console.error(error)
}
})()
do task i=0
do task i=1
do task i=2
do task i=3
do task i=4
do task i=5
do cleanup before throwing CancelError
task got canceled
2. Using with AbortController
import fetch from 'node-fetch'
import { AbortController, AbortSignal } from 'node-abort-controller'
import {
CancellationToken,
CancellationError,
Task,
} from '@kaisukez/cancellation-token'
async function fetchData(token: CancellationToken, signal: AbortSignal) {
const result = await fetch('http://www.google.com', { signal })
// do something in between
await Task.sleep(100)
token.throwIfCancellationRequested()
const html = await result.text()
return html.slice(0, 20)
}
async function main() {
const controller = new AbortController()
const { token, cancel } = CancellationToken.source()
const unregister = token.onCancel(() => {
console.log('onCancel -> controller.abort()')
controller.abort()
})
setTimeout(async () => await cancel(), 400)
try {
const html = await fetchData(token, controller.signal)
console.log('html', html)
} catch (error: any) {
if (error instanceof CancellationError) {
console.log('task got canceled')
} else if (error?.constructor?.name === 'AbortError') {
// or you can use (error instanceof AbortError)
console.log('task got aborted')
} else {
throw error
}
} finally {
unregister()
await cancel()
}
}
;(async () => {
try {
await main()
} catch (error) {
console.error(error)
}
})()
abort case
onCancel -> controller.abort()
task got aborted
cancel case
onCancel -> controller.abort()
task got canceled
success case
html <!doctype html><html
3. Combining token
import {
CancellationToken,
CancellationError,
Task,
} from '@kaisukez/cancellation-token'
async function task(token: CancellationToken, id: number) {
let i = 0
while (true) {
console.log(`id=${id} do task i=${i++}`)
await Task.sleep(50)
if (token.isCancellationRequested) {
console.log(`id=${id} do cleanup before throwing CancelError`)
token.throwIfCancellationRequested()
}
}
}
async function main() {
const token1 = new CancellationToken(cancel => {
const timeout = 100
console.log(`token1 timeout = ${timeout}`)
setTimeout(async () => {
console.log('token1 is canceled')
await cancel()
}, timeout)
})
const token2 = new CancellationToken(cancel => {
const timeout = 300
console.log(`token2 timeout = ${timeout}`)
setTimeout(async () => {
console.log('token2 is canceled')
await cancel()
}, timeout)
})
// use CancellationError.ignoreAsync
// so that you don't have to try-catch CancellationError
const result = await Promise.all([
CancellationError.ignoreAsync(() => task(token1, 1)),
CancellationError.ignoreAsync(() => task(token2, 2)),
CancellationError.ignoreAsync(() => task(CancellationToken.race([token1, token2]), 3)),
CancellationError.ignoreAsync(() => task(CancellationToken.all([token1, token2]), 4)),
])
console.log('result', result)
}
;(async () => {
try {
await main()
} catch (error) {
console.error(error)
}
})()
token1 timeout = 100
token2 timeout = 300
id=1 do task i=0
id=2 do task i=0
id=3 do task i=0
id=4 do task i=0
id=1 do task i=1
id=2 do task i=1
id=3 do task i=1
id=4 do task i=1
token1 is canceled
id=1 do cleanup before throwing CancelError
id=2 do task i=2
id=3 do cleanup before throwing CancelError
id=4 do task i=2
id=2 do task i=3
id=4 do task i=3
id=2 do task i=4
id=4 do task i=4
id=2 do task i=5
id=4 do task i=5
token2 is canceled
id=2 do cleanup before throwing CancelError
id=4 do cleanup before throwing CancelError
result [ undefined, undefined, undefined, undefined ]
4. Checkpoint
import {
CancellationToken,
CancellationError,
SyncCheckpoint,
AsyncCheckpoint,
Task,
} from '@kaisukez/cancellation-token'
async function longRunningTask(id: number) {
console.log('longRunningTask', id)
await Task.sleep(500)
}
function longRunningTaskSync(id: number) {
console.log('longRunningTaskSync', id)
// do long running task in sync
}
async function task(token: CancellationToken) {
// #1
token.throwIfCancellationRequested()
await longRunningTask(1)
// #2 - is equivalent to #1
await AsyncCheckpoint.before(token, () => longRunningTask(2))
// # 3
await longRunningTask(3)
token.throwIfCancellationRequested()
// #4 - is equivalent to #3
await AsyncCheckpoint.after(token, () => longRunningTask(4))
// -------------------------------------------
// #5
token.throwIfCancellationRequested()
longRunningTaskSync(5)
token.throwIfCancellationRequested()
// #6 - is equivalent to #5
SyncCheckpoint.beforeAfter(token, () => longRunningTaskSync(6))
// -------------------------------------------
// #7
await AsyncCheckpoint.before(token, () => Promise.resolve())
// #8
SyncCheckpoint.after(token, () => {})
// #9 - is equivalent to #7 and #8
// but #9 is preferred
token.throwIfCancellationRequested()
}
async function main() {
const token = new CancellationToken(cancel => {
setTimeout(async () => await cancel(), 1234)
})
await CancellationError.ignoreAsync(() => task(token))
}
;(async () => {
try {
await main()
} catch (error) {
console.error(error)
}
})()
longRunningTask 1
longRunningTask 2
longRunningTask 3