Wednesday, March 4, 2015

Sharpening the JavaScript Saw by Building a Promise API

I’ve been working with JavaScript a lot lately and wanted to set myself a small challenge where I would aim to build something small, but meaningful within 60 minutes. One thing that I had wondered about how they actually worked was JavaScript promises. I’ve used them with Angular.js and node.js and thought that building my own basic implementation of a promise would be a suitable challenge. With the task decided, I felt that I needed to put together a few constraints that would drive my implementation and give me a set of criteria for the challenge. These I derived from my experience with using promises and what I had read about them. Here are the constraints that I decided on:

-    The promise can only be resolved once--be this keeping or breaking it
-    An optional callback for failure can be supplied with a success callback
-    Multiple callbacks can be supplied and they will be executed in the same order when resolving the promise


With everything laid out, I started my timer and began coding. It didn’t take long to get something up and running that allowed a single set of success and failure callbacks to be queued but the requirement to be able to supply multiple callbacks was a bit more of a challenge. It took a little bit of a design change and some refactoring before I ended up with a solution that I was happy with. This is what I came up with:

(function (promiseApi) {

    promiseApi.Promise = function () {
        
        var isResolved = false;
        var successCallbacks = [];
        var failureCallbacks = [];
        
        var keep = function (data) {
            
            resolve(successCallbacks, data);
        };
        
        var abandon = function (error) { // "break" is a keyword unfortunately
            
            resolve(failureCallbacks, error);
        };
        
        var resolve = function (queue, state) {
            
            if (!isResolved) {
                isResolved = true;
                
                queue.forEach(function (callback) {
                    
                    state = callback(state);
                });
                
                successCallbacks = [];
                failureCallbacks = [];
            }
        };
        
        var when = function (success, failure) {
            
            if (success !== undefined) {
                successCallbacks.push(success);
            }
            
            if (failure !== undefined) {
                failureCallbacks.push(failure);
            }
            
            return Object.freeze({ when : when });
        };

        return Object.freeze({
            keep: keep,
            abandon: abandon,
            promise: Object.freeze({ when: when })
        });
    };

}(module.exports));

Here are some notes about my solution:

-    I decided to include an “isResolved” Boolean even though I am emptying the arrays as a guard against the promise being resolved again before all the callbacks in the appropriate queue have been invoked. This ensures that any additional calls to resolve the promise will result in no further action.
-    The arrays are being re-initialized as a housekeeping measure to release any resources that they are consuming. As a promise can only be resolved once, it doesn’t make sense to hold on to any of the callbacks in its queues. Originally, I was setting the length of the array to 0, but I learned that this actually blocks the garbage collector until the items in the array are overwritten by new items. In normal use of a promise, you wouldn't add additional success and failure callbacks after the promise had been resolved, so the original callbacks would remain in memory.
-    The use of the “setTimeout” function with a timeout of 0 is to ensure that the callbacks are invoked immediately. However, as this is a node.js project it probably would be more efficient to use process.nextTick().
-    To prevent the behavior of a promise being changed, I have used Object.freeze() to make it immutable.
-    In an attempt to keep things clean, I used the revealing module pattern.


This is a very basic implementation of a promise API. I’m sure that there is much that doesn’t adhere to the promise specification, but as I mentioned above, this was just a challenge that I set myself to exercise my JavaScript muscles. I wouldn’t recommend that anyone use this code in production even though—technically—it does work. For one, there’s no error handling and if anything were to go wrong when calling a callback, it would continue to bubble up the call stack. The start of a solution would be to wrap the callback’s invocation in a try-catch block, but what would the appropriate course of action be once an exception has been caught? I think I’d have to refer to the specification for guidance on that. Below is a simple example of how to use the promise API:

carService.js:
(function (carService) {
    
    var promiseApi = require('./promise.js');
    
    carService.get = function (id) {
        
        var promise = new promiseApi.Promise();        

        setTimeout(function () {
            
            var data =  {
                id: id,
                model: 'Ford Mustang ' + id,
                year: 2014
            };

            promise.keep(data);
            //promise.abandon({ message: "Something went wrong" });
        }, 1000);
        
        return promise.promise;
    };

}(module.exports));

app.js:
(function () {

    var carService = require('./fakeCarService.js');

    carService.get(1)
    .when(function (data) {

        console.log(data.model);

        return data.model;
    }, function (error) {

        console.log("Error: " + error.message);

        return error;
    })
    .when(function (data) {

        console.log("Second CB: " + data);

        return data;
    })
    .when(function (data) {

        console.log("Third CB: " + data);
    }, function (error) {

        console.log("Second Error CB: " + error.message);
    });

}()); 


As a challenge to help me practice JavaScript, this was a good problem to solve. The complete node.js project can be found here.

No comments:

Post a Comment