Search code examples
javascriptcallbackes6-promisemethod-chaining

How to do Method Chaining in ProtoType with Promises *around* callbacks


I am trying to impliment "Method Chaining" using this tutorial ( https://schier.co/blog/2013/11/14/method-chaining-in-javascript.html )

My issue is that I have to be creating an Object that uses call back API that looks like the following

function Cook() {};

Cook.prototype.readOrder = function readOrder(callback) {
  setTimeout(function() {
    callback()
    console.log('Cook has read the order');
 }, 666);
};
Cook.prototype.createOrder = function createOrder(callback) {
  setTimeout(function() {
    callback()
    console.log('Cook has created the order');
  }, 1337);
};

I want to be able to do Method chaining without altering the code above...

My approach has been to create a new Cook and wrap the callbacks in Promises.

Something like this.

function NewCook(cook){
  self = this;
  this.readOrder = function() {
      var readOrderPromise = new Promise(function(resolve, reject) {
        cook.readOrder(resolve)
      })
      return self
    };

  this.createOrder = function() {
      var createOrder = new Promise(function(resolve, reject) {
        cook.createOrder(resolve)
      })
      return self
    };
};


var original = new Cook();
var newCook = new newCook(cook)

When I chain my methods together however, the second method does not wait for the first to resolve.

  newCook.readOrder().createOrder()

I tried altering my code to return a promise with "Then" like the following

this.readOrder = function() {
  var readOrderPromise = new Promise(function(resolve, reject) {
    cook.readOrder(resolve)
  }).then(function(){return self})

  return readOrderPromise
};

With this code, however, I get an error saying

TypeError: Cannot read property 'createOrder' of undefined

Which makes me think that the "createOrder" method is not waiting for the "readOrder" to resolve before firing...

How do I, using Native Promises and Method Chaining, make "createOrder" wait for the promise to resolve?


Solution

  • To get something like:

    newCook.readOrder().createOrder()
    

    Where each method still returns the original object (for regular chaining), then you can do something like jQuery does for animations. It allows things like:

    $("someSelector").slideDown(500).fade(500);
    

    Where the .fade(500) operation does not start executing until after the .slideDown(500) operation has completed. Both are async (so the functions return immediately).

    The way it works is that it creates a set of methods that can be used in a chain in this fashion and they all work through a queue. The object itself maintains a queue and every method that wishes to participate in this auto-sequencing, must queue itself and its parameters rather than directly execute. You can monitor all your operations with promises and the queue in this case can just be an actual promise chain. So, each new operation just adds itself to the end of the promise chain.

    Here's an example using ES6 syntax:

    class Queue {
        constructor() {
            this.init();
        }
        init() {
            let oldP = this.p;
            this.p = Promise.resolve();
            return oldP;
        }
        promise() {
            return this.p;
        }
        // takes a function that returns a promise
        add(fn, ...args) {
            // add this operation into our current promise chain
            this.p = this.p.then(() => {
                return fn.call(...args);
            });
        }
    }
    
    class NewCook {
        constructor(cook) {
            this.cook = cook;
            this.q = new Queue();
        }
        // get current promise chain
        promise() {
            return this.q.promise();
        }
        // starts new promise chain, returns prior one
        // this branches a new promise chain so new operations
        // are not affected by the prior state of the promise chain
        // Note that rejects are "latched" so this is required if the previous
        // promise chain rejected.
        init() {
            return this.q.init();
        }
        readOrder() {
            this.q.add(() => {
                return new Promise((resolve, reject) => {
                    this.cook.readOrder(resolve);
                });
            });
            return this;
        }
        createOrder(data) {
            this.q.add(() => {
                return new Promise((resolve, reject) => {
                    this.cook.createOrder(data, resolve);
                });
            });
            return this;
        }
    }
    
    var cook = new Cook();
    var newCook = new NewCook(cook);
    

    In real life where I knew more about the existing Cook, callback-based object, I'd probably "promisify" that object first in some automated fashion so I could just call promisified methods on it directly rather that repeating the promisfy code in every NewCook method.

    For example, if the Cook callbacks all follow the node.js calling convention, then they can be promisified somewhat automatically using Bluebird's PromisifyAll method. If they all followed some other calling convention, I'd probably write a generic promisify method for that style that could be used instead of manually promisifying every method you want to use.

    // promisify the Cook methods using Bluebird
    Promise.promisifyAll(Cook);
    
    class NewCook {
        constructor(cook) {
            this.cook = cook;
            this.q = new Queue();
        }
        // get current promise chain
        promise() {
            return this.q.promise();
        }
        // starts new promise chain, returns prior one
        init() {
            return this.q.init();
        }
        readOrder() {
            this.q.add(this.cook.readOrderAsync.bind(this.cook));
            return this;
        }
        createOrder(data) {
            this.q.add(this.cook.createOrderAsync.bind(this.cook), data);
            return this;
        }
    }
    
    var cook = new Cook();
    var newCook = new newCook(cook);