Promises, Promises in Node.js

In an earlier BLOG post, I talked about rewriting a deployment script to use promises.

Let me flash back to the problem earlier: I was using rsync to copy files remotely and was encountering network problems when too many copies were happening simultaneously. It was even causing my hosting provider to lock me out for a few minutes. rsync will not be more efficient by running parallel instances as it is already as efficient as it will be. Because the rsyncwrapper library I’m using ran things asynchronously, I ended up making a synchronous version of their rsync function using the Q library and supporting callbacks:

var Q = require('q'); // Installed via $ npm install q

// Load the rsync module
var rsync = require("rsyncwrapper"); // Installed via $ npm install rsyncwrapper
// Convert the rsync function into an asynchronous function that returns a promise.
function rsyncAsync(lRsyncOpts, reject, resolve)
{
?????? // Using the Q defer pattern 
?????? var deferred = Q.defer();

?????? rsync(lRsyncOpts, function(err, stderr, out, cmd) {
???? ??if (err) { 
???? ???????? reject(err, stderr, out, cmd);
???? ???????? deferred.reject(new Error(err));
???? ??} else {
???? ???????? resolve(err, stderr, out, cmd);
???? ???????? deferred.resolve();
???? ??}
?????? });

?????? return deferred.promise;
}

As you can see, the async version takes two callbacks, one for reject and one for resolve for the above signature. If this were a library function, I may have made those optional but this is just for my deploy script so I just refactored what I needed.

Good Advice on Promises

In order to get the promises to work, I had to do quite a bit of writing and rewriting of my script. But now the script is much better and easier to read and debug than ever before. And, I now understand promises.

The article that was the biggest help was Nolan Lawson’s We have a problem with promises. He boils promises down to three things you should do in .then():

There are three things:

  1. return another promise

  2. return a synchronous value (or undefined)

  3. throw a synchronous error

If you don’t do one of those in .then(), that’s when things get wacky. My code was still going asynchronous because of some of these mistakes (see the end of Lawson’s article for timing charts). Also, you pass .then() a function that returns a promise, not the promise itself!

Ironically, in his article, he talks about not using deferred. But, this contradicts the Q documentation (see?? and??https://github.com/bellbind/using-promise-q) which says to use deferred. I understand his point: If a library already returns a promise, you don’t need to use deferred to create a new promise. This is because you already have a promise to return. (As I was writing this, I noticed that my calls to rsyncAsync were not returning it’s promise, but were using deferred. So once something creates a promise, you don’t need to create a promise when you call it! My code keeps getting cleaner.) Also, in ES6, things should work more easily.

Using Promises to Do the Work

So to copy a folder, I wrote these functions:

function RsyncOpts()
{
    this.src = ".";
    this.dest = rsyncuser+"@"+rsyncserver+":";
    this.ssh = true;
    this.port = rsyncport;

    this.getOpts = GetOpts;

    function GetOpts(source, destPath, bLocal, sshport) 
    {
        var newObj = new RsyncOpts();

        newObj.src = source;
        // Only set the destination server if remote.
        newObj.dest = (bLocal?"":newObj.dest) + destPath;
        // Only use SSH for remote.
        newObj.ssh = !bLocal;
        // Local port: Undefined, or use the default sshport or the one passed.
        newObj.port = (bLocal?undefined:(!sshport?this.port:sshport));

        return newObj;
    }
}
// Create global/singular for RsyncOpts factory.
var rsyncOpts = new RsyncOpts();
var path = require('path');
var ds = path.sep; // Environment directory separator.

function CopyFolder(source, dest, bLocal, rsInclude)
{

?????? // Get the fully qualified source path
?????? var sSrcRoot = path.resolve(source);

?????? var dRsyncOpts = rsyncOpts.getOpts(sSrcRoot + ds, dest + ds, bLocal);
?????? // Archive mode, Include folders recursively, Exclude .git dir, Exclude all files
?????? dRsyncOpts.args = ['-a', '-f"+ */"', '-f"- .git/"', '-f"- *"'];
?????? // Include files as passed (overrides exclude all for only these files)
?????? dRsyncOpts.include = rsInclude;
?????? dRsyncOpts.recursive = true;

?????? // Return the promise to copy the folder. 
    // (hRsyncErr and ResolveRsync are functions to handle the error or success).
?????? return rsyncAsync(dRsyncOpts, hRsyncErr, ResolveRsync);
}

Notice how the return value of rsyncAsync is simply returned. We already have a promise here, so we don’t need to create another one.

I completely eliminated the use of fs-extra copy command. Rsync is much more efficient and works either locally or remotely. Also, the command required me to walk the tree because the include/exclude functionality didn’t work on a file-by-file basis.

This is much better than in my last BLOG post and much cleaner. .then() (ALLCAPS are constant folder names), we can do this:

function CopyFile(source, dest, bLocal)
{
 var lRsyncOpts = rsyncOpts.getOpts(source, dest, bLocal);

 return rsyncAsync(lRsyncOpts, hRsyncErr, ResolveRsync);
}

var rsPHPFile = ["*.php"];
var bLocal = ?; // Boolean for local or not.
var destRoot = "/home/user/public_html";
?????? CopyFile(BOWER_COMPONENTS+ds+ANGULAR+ds+ANGULAR+JS, 
???? ?????????? JOURNAL+ds+SCRIPTS+ds+ANGULAR+JS, true)
???? ??.then(function () {
???? ???????? return CopyFile(BOWER_COMPONENTS+ds+ANGULAR_RESOURCE+ds+ANGULAR_RESOURCE+JS, 
???? ?????? ?????????? JOURNAL+ds+SCRIPTS+ds+ANGULAR_RESOURCE+JS, true); })
???? ??.then(function () {
???? ???????? return EnsureFolder(PHPAR, destRoot, bLocal); } )
???? ??.then(function () {
???? ???????? return CopyFolder(PHPAR, destRoot+ds+PHPAR, bLocal, rsPHPFile); })
???? ??.then(function () {
???? ???????? return EnsureFolder(MODELS, destRoot, bLocal); } )
???? ??.then(function () {
???? ???????? return CopyFolder(MODELS, destRoot+ds+MODELS, bLocal, rsPHPFile); })
...
?????? .then(function () { console.log('Deployment Complete'); })
???? ??.done();

// Ideally, put a .catch(error handling function) here!

So with this, each copy happens in sequence. The EnsureFolder function makes sure that the folder exists first rsync doesn’t work if the target folder doesn’t exist! (Remember that .then returns a promise, so pay close attention to the function return value below.)

// Use rsync to ensure that the root folder is created.
function EnsureFolder(src, destRoot, bLocal, index)
{

?????? // Go through an array of path variables.
?????? if (Array.isArray(src)) {
    ???? ??var sPaths = src;
?????? } else {
???? ??    var sPaths = src.split(ds);
    ???? ??index = 0;
?????? }

?????? // Get the first/current destination folder path.
?????? var destFolder = "";
?????? for (i = 0 ; i <= index ; i++) {
???? ??    destFolder += sPaths[i] + ds;
?????? }

?????? // Use rsync opts to only create a folder.
?????? var lRsyncOpts = rsyncOpts.getOpts(destFolder, destRoot + ds + destFolder, bLocal);
?????? lRsyncOpts.args = ['-f"+ */"', '-f"- *"'];
?????? lRsyncOpts.recursive = false;

?????? //Rsync this folder and then recursively ensure the next folder (if required)
?????? return rsyncAsync(lRsyncOpts, hRsyncErr, ResolveRsync)
???? ??.then(function () {
???? ???????? if (index+1 < sPaths.length)
???? ?????? ??    return EnsureFolder(src, destRoot, bLocal, index+1);
???? ??});
}

So far, deployments using node.js are not as straightforward as using a shell script, but it will be cross platform running on node (I’ve yet to try to run it on Windows, though!) Also, it allows me to sharpen my JavaScript skills 🙂

Promises are allowing me to run things asynchronously when I need to, and this should increase the effectiveness of my deployments.