I'm playing with Promise Extensions for JavaScript (prex) library and I like it a lot. Amongst other things, this library appears to be a prototype behind the current ECMA TC39 proposal for cancellation and it uses the familiar cancellation token approach, popularized by .NET Task API.
I want to extend the standard Promise class with cancellation support, similar to how it is implemented in Bluebird (i.e., with an optional oncancel
callback) but using prex.CancellationToken.
Here is a draft that can be run with NodeJS:
const prex = require('prex');
class CancellablePromise extends Promise {
constructor(executor, token) {
if (!token) {
// if no token supplied, just delegate to the parent class
super(executor);
return;
}
const observeCancellation = async () => {
// prex.Deferred is similar to TaskCompletionSource in .NET
const deferred = new prex.Deferred();
executor(
deferred.resolve,
deferred.reject,
cancelListener =>
deferred.cancelListener = cancelListener);
const registration = token.register(() => {
try {
// capture the CancelError
token.throwIfCancellationRequested();
}
catch (cancelError) {
try {
// the token cancellation callback is synchronous,
// and so is the executor-provided cancelListener callback
deferred.cancelListener && deferred.cancelListener(cancelError);
// reject here if cancelListener has not resolved/rejected it
deferred.reject(cancelError);
}
catch (error) {
// in case cancelListener throws
deferred.reject(error);
}
}
});
try {
return await deferred.promise;
}
finally {
registration.unregister();
}
};
super((resolve, reject) =>
observeCancellation().then(resolve, reject));
}
}
// delayWithCancellation
function delayWithCancellation(timeoutMs, token) {
console.log(`delayWithCancellation: ${timeoutMs}`);
return new CancellablePromise((resolve, reject, setCancelListener) => {
token.throwIfCancellationRequested();
const id = setTimeout(resolve, timeoutMs);
setCancelListener(e => clearTimeout(id));
}, token);
}
// main
async function main() {
const tokenSource = new prex.CancellationTokenSource();
setTimeout(() => tokenSource.cancel(), 2000); // cancel after 1500ms
const token = tokenSource.token;
await delayWithCancellation(1000, token);
console.log("successfully delayed."); // we should reach here
await delayWithCancellation(1500, token);
console.log("successfully delayed."); // we should not reach here
}
main().catch(error => console.log(error));