In this blog post we are going to write an extended version of Javascript Promise, to add new functionalities like progress & cancel so that it can be used as follow:


const request = new ResponsePromise(()=> {....executor....});

request.onProgress((event)=> {
  console.log('onProgress callback.. maybe update the progress-bar ?')
});

request.then((data)=> {
  console.log('finally!!');
});

setTimeout(()=> {
  request.cancel(); // need to cancel
}, 250);

This requirement occurred when I was writing a HTTP Client Interface   for one of my react-native project. This Interface was written over axios package. I will write about this interface in some other blog post, For now Lets focus on the requirements on this Promise Wrapper:

  • Promise should be cancelable (we should be able to cancel the network request)
  • Promise should provide a way: so that subscriber can also get update on progress (to display upload/download progress-bar).

RxJS was the first thing that strike my mind immediately, as it provides stream like observables that subscriber can listen to, also these observables are cancellable, So it fits the use-case  but it was not "that perfect" also introducing a whole new pattern was a big drawback. So i decided to extend the `Promise` & its functionality.

Iteration 1: Override executor

class ResponsePromise extends Promise {
  constructor(executor) {
    const oExecutor = (resolve, reject) => {
      executor(resolve, reject);
    }
  super(oExecutor);  
  }
}

Here I am simply extending the Promise class and creating an overridden version of executor function so that I can monkey-patch it  and tweak the arguments passed as per the need.

Iteration 2:  Add onProgress

class ResponsePromise extends Promise {
  constructor(executor) {
  
    const progress = (event) => {
      this.progressCBQueue.forEach((cb)=> {
        cb(event);
      })
    };
    
    const oExecutor = (resolve, reject) => {
      executor(resolve, progress, reject);
    }
    
    super(oExecutor);
    this.progressCBQueue = [];
  }
  
  onProgress(cb) {
    this.progressCBQueue.push(cb);
    console.log('onProgress called');
  }
}

//Usages:

const request = new ResponsePromise((resolve, progress, reject)=> {
  setTimeout(progress, 100)
  setTimeout(progress, 200)
  setTimeout(progress, 300)
  setTimeout(resolve, 500);
});

request.onProgress((event)=> {
  console.log('onProgress callback.. maybe update the progress-bar ?')
});

Here a new instance function called `onProgress` is defined that takes the callback and stores it inside an array,  also  in constructor  `progress` function  is defined that is passed to executor, whenever executor calls the progress it loop through the array of in progress callback and execute each callback.

Iteration 3: Cancellable Promise

class ResponsePromise extends Promise {
  constructor(executor) {

    const progress = (event) => {
      this.progressCBQueue.forEach((cb)=> {
        cb(event);
      })
    };
    const onCancel =  (cb) => {
      //using nextTick because we cant use "this" before super()
      setTimeout(()=> {
        this.cancelCb = cb;
      })
    }

    const oExecutor = (resolve, reject) => {
      executor(resolve, progress,  reject, onCancel);
    }

    super(oExecutor);
    this.progressCBQueue = [];
    //this.cancelCb = null;
  }
  onProgress(cb) {
    this.progressCBQueue.push(cb);
  }
  cancel() {
    if (this.cancelCb) {
      this.cancelCb();
    } else {
      console.warn('onCancel not provided');
    }
  }
}
let p = new ResponsePromise((resolve, progress, reject, onCancel)=> {
  onCancel(()=> {
    console.log('OnCancel');
    //logic to cancel things
  })
  setTimeout(progress, 100)
  setTimeout(progress, 200)
  setTimeout(progress, 300)
  setTimeout(resolve, 500);

});

setTimeout(()=> {
  p.cancel();
}, 250)

p.then((data)=> {
  console.log('then called');
})

p.onProgress(()=> {
  console.log('onProgress callback')
})

Similar to progress, we have added onCancel function that will be passed to executor so that executor can register onCancel callback, and if any subscriber calls Promise.cancel then this callback will be executed.

In above code you will find some setTimeout hacks in constructor, they are there to avoid usages of this before super

Following is the codepen of above implementation: