While standard JavaScript Promises lack a built-in cancellation mechanism, Bluebird.js offers a robust cancellation mechanism. Let’s explore a simplified approach to cancellation in async functions, inspired by .NET’s Cancellation Token.
Instead of aiming for full chain cancellation like Bluebird, we’ll focus on stopping an async function “as soon as possible” – after the current async call. We’ll achieve this with a checkCancelation function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| function checkCancelation(cancelationToken){
if (cancelationToken && cancelationToken.reject){
console.log("throwing");
throw new Error("Rejection forced");
}
if (cancelationToken && cancelationToken.cancel){
console.log("cancelling");
//return a Promise that never resolves
return new Promise(()=>{});
}
return false;
}
//to be used like this:
//let result = await (checkCancelation(cancelationToken)
// || getResultAsync());
|
This function allows both rejection (throwing an error) and “abandoning” (returning a never-resolving Promise). Here’s how it’s used:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| function getLastMessageId(){
return new Promise(res => {
setTimeout(() => res(111), 1500);
});
}
function getMessageText(id){
return new Promise(res => {
setTimeout(() => res("this is the last message"), 1500);
});
}
function formatText(txt){
let formattedTxt = `[[${txt}]]`;
return new Promise(res => {
setTimeout(() => res(formattedTxt), 1500);
});
}
async function getLastMessageFormatted(cancelationToken){
let id = await (checkCancelation(cancelationToken)
|| getLastMessageId());
console.log("ID obtained");
let txt = await (checkCancelation(cancelationToken)
|| getMessageText(id));
console.log("Message obtained");
let msg = await (checkCancelation(cancelationToken)
|| formatText(txt));
console.log("Message formatted");
return msg;
}
(async () => {
console.log("-- test 1");
let msg = await getLastMessageFormatted();
console.log("message: " + msg);
console.log("-- test 2");
let cancellationToken = {};
//reject after 1 second
setTimeout(() => cancellationToken.reject = true, 1000);
try{
msg = await getLastMessageFormatted(cancellationToken);
console.log("message: " + msg);
}
catch (ex){
console.error("Exception: " + ex.message);
}
console.log("-- test 3");
cancellationToken = {};
//cancel after 1 second
setTimeout(() => cancellationToken.cancel = true, 1000);
//when cancelling we return a simple Promise, that won't keep the program running
//(it's real IO-timeout calls, not the Promise itself, what keeps the node.js loop running)
//If I just want to keep it running longer, just use this keep alive timeout
setTimeout(() => console.log("keep alive finished"), 10000);
try{
msg = await getLastMessageFormatted(cancellationToken);
console.log("message: " + msg);
}
catch (ex){
console.error("Exception: " + ex.message);
}
})();
|
This code is available on to a gist.
Additionally, here’s an implementation for preventing “then” handlers from executing on a Promise, using two different strategies for “cancellable Promise” creation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| function formatTextAsync(txt){
let formattedTxt = `[[${txt}]]`;
return new Promise(res => {
setTimeout(() => res(formattedTxt), 1500);
});
}
function createCancellablePromiseStrategy1(pr){
let cancelled = false;
let cancellablePromise = new Promise(res => {
pr.then(value => {
if (!cancelled){
res(value);
}
//else we never resolve
else{
console.log("promise has been cancelled");
}
})
});
cancellablePromise.cancel = () => cancelled = true;
return cancellablePromise;
}
function createCancellablePromiseStrategy2(pr){
let cancelled = false;
//if the function ran by "then" returns a promise, the promise initially returned by "then" is resolved
// when that other promise is resolved
let cancellablePromise = pr.then((value) => {
return new Promise(res => {
if (!cancelled){
res(value);
}
//else we never resolve
else{
console.log("promise has been cancelled");
}
});
});
cancellablePromise.cancel = () => cancelled = true;
return cancellablePromise;
}
//let creationStrategy = createCancellablePromiseStrategy1;
let creationStrategy = createCancellablePromiseStrategy2;
let pr1;
const operation = async () =>{
pr1 = creationStrategy(formatTextAsync("hi"));
let result = await pr1; //if cancelling pr1 will never resolve, so the next line won't run
console.log("result: " + result);
};
operation();
pr1.cancel();
|
This approach focuses on cancelling the initial Promise, differing from Bluebird’s chain-based cancellation.