Sun May 18 2014

Websites != CMS Platform - Promises

This post is part of a series where I'm hoping to prove to myself that building a dynamic website with NodeJS is much more fun than using a CMS platform. See the first post for an explanation of why

The code can be found on GitHub

Previous Post

A promise represents the eventual result of an asynchronous operation.

The basic idea is that you can swap in a promise where you would normally pass in a callback.

The primary interaction is that you call a method which returns a promise which will eventually return a result (it can immediately return the result if it's available) and you chain a call to .then() onto that method call.

The call to then is equivalent to passing in the callback function.

Clear as mud?

There's (much) more at the Promises specification website.

Bluebird

This is JavaScript so there are a bazillion npm packages that could be used to switch the project's code to using promises. A (relatively small) bit of googling research suggested that the Bluebird library was a good bet.

In their words:

Bluebird is a fully featured promise library with focus on innovative features and performance

Before

Here's the code from the previous Post which shows the smelly, arrow anti-pattern

var bcrypt = require('bcrypt');
var SALT_WORK_FACTOR = 10;

module.exports = function(db) {
  return {
    create: function(username, password, callback) {
        bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
            if (err) {
                callback(err);
                return;
            }
            bcrypt.hash(password, salt, function(err, hash) {
                if (err) {
                    callback(err);
                    return;
                }
                db.users.save({
                    username:username,
                    password:hash
                }, function(err, result) {
                    if(err) {
                        callback(err.err);
                    } else {
                        callback('user created');
                    }
                });
            });
        });
    }
  };
};

This took a bit of faff to translate to promises almost entirely as a result of this being the first ever promises code I've written and I didn't RTFM.

I did have this code covered by tests so I could leave mocha running in the background and poke the code with a stick (Yay TDD!)

After

The first pass at implementing promises generated:

var SALT_WORK_FACTOR = 10;

var bcrypt = require('bcrypt');
var Promise = require('bluebird');

var genSalt = Promise.promisify(bcrypt.genSalt);
var genHash = Promise.promisify(bcrypt.hash);

module.exports = function(db) {
    var users = Promise.promisifyAll(db.users);

  return {
    create: function(username, password, callback) {
        genSalt(SALT_WORK_FACTOR).then(function(salt) {
	        return genHash(password, salt);
	    }).then(function(hash) {
            return users.saveAsync({
                    username:username,
                    password:hash
            });
        }).then(function() {
            callback('user created');
        }).error(function (e) {
            callback(e.message);
        });
    }
  };
};

All tests still pass at this point and there's fewer levels of arrow to wade through but it still feels a bit pointy and not massively clear so…

var SALT_WORK_FACTOR = 10;

var bcrypt = require('bcrypt');
var Promise = require('bluebird');

var genSalt = Promise.promisify(bcrypt.genSalt);
var genHash = Promise.promisify(bcrypt.hash);

module.exports = function(db) {
    var users = Promise.promisifyAll(db.users);

  return {
    create: function(username, password, callback) {
        var hashPassword = function() {
            return genSalt(SALT_WORK_FACTOR).then(function(salt) {
                return genHash(password, salt);
            });
        };

        var persistUser = function(hashedPassword) {
            return users.saveAsync({
                    username:username,
                    password:hashedPassword
            });
        };

        hashPassword()
        .then(persistUser)
        .then(function() {
            callback('user created');
        }).error(function (e) {
            callback(e.message);
        });
    }
  };
};

skipping over the setup code you can get to the meat of the module

hashPassword()
.then(persistUser)
.then(function() {
    callback('user created');
}).error(function (e) {
    callback(e.message);
});

Which is a huge amount clearer than the starting point! I do like a method to be a sentence! 'Hash password then persist user'!

A very high count of exclamation marks in this post but that was much easier and more fun than I anticipated - winner!

At this point I either need to pass through the code to implement promises more widely… or I could choose to leave everything as it is and improve each code file as it's touched.

As it is it's nearly midnight and my alarm goes off at 5:50am so…