Updated, below is a more current and simple version of this. Also, if you track the progress of TC39's "ECMAScript Cancellation" proposal, this thread might be worth checking out.
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));
prex
forCancellationTokenSource
, you have a bug. The constructor takes an iterable of tokens, not a single token. \$\endgroup\$CancellationTokenSource
andCancellationToken
than what Prex does. \$\endgroup\$CancellationTokenSource
class constructor doesn't accept a token at all... \$\endgroup\$CancellationTokenSource.CreateLinkedTokenSource
, so I've made it an option to pass a token to the token source constructor. Do you think it is not a good idea? \$\endgroup\$