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-catchAyncCheckpoint.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)
npm i @kaisukez/cancellation-token
This package ships both ESM and CJS formats
import {
CancellationToken,
CancellationError,
SyncCheckpoint,
AsyncCheckpoint,
Task,
} from '@kaisukez/cancellation-token'
const {
CancellationToken,
CancellationError,
SyncCheckpoint,
AsyncCheckpoint,
Task,
} = require('@kaisukez/cancellation-token')
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
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
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 ]
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