I'm playing with [Promise Extensions for JavaScript (prex)](https://github.com/rbuckton/prex#readme) 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](https://github.com/tc39/proposal-cancellation) and it uses the familiar cancellation token approach, popularized by .NET Task API.

I want to extend the standard [Promise class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with cancellation support, similar to how it is [implemented in Bluebird](http://bluebirdjs.com/docs/api/cancellation.html) (i.e., with an optional `oncancel` callback) but using [prex.CancellationToken](https://github.com/rbuckton/prex/blob/master/docs/cancellation.md#class-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));