Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Q.js: How can I rewrite an async series flow in Q.js?

In an attempt to grasp Q.js, I'd like to convert the following code using async.series in Q.js. Basically I create a folder if it doesn't exist (using mkdirp), move a file into a backup folder and save a file into a main folder.

var async = require('async');
var fs = require('fs');
var path = require('path');
var sessiondId = new Date().getTime() % 2 == 0 ? new Date().getTime().toString() : '_1234';
var backupFolder = path.join(__dirname,sessiondId);
var backupFullPath = path.join(backupFolder,'a.txt');
var fullPath = path.join(__dirname,'main','a.txt');
var mkdirp = require('mkdirp');

async.series({
    createOrSkip: function(callback) {
        mkdirp(backupFolder, function (err, dir) {
            if(err) {
                callback(err, null);
            } else {
                callback(null, {created: !!dir, folderAt: backupFolder});
            }
        }); 
    },
    move: function(callback) {
        fs.rename(fullPath, backupFullPath, function(err) {
            if(err) {
                callback(err, null);
            } else {
                callback(null, {backupAt: backupFullPath});
            }
        });
    },
    write: function(callback) {
        fs.writeFile(fullPath, 'abc', function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, {saveAt: fullPath});
            }
        });
    }
}, function(err, result) {
    console.log(result);
});

Actually I don't know where to start. Thanks for your help.

R.

like image 635
roland Avatar asked Aug 29 '13 11:08

roland


2 Answers

The key is to convert the node.js functions to return promises using Q.denodeify before you start, this means the header of your file should look like:

var Q = require('q')
var fs = require('fs');
var path = require('path');
var sessiondId = new Date().getTime() % 2 == 0 ? new Date().getTime().toString() : '_1234';
var backupFolder = path.join(__dirname,sessiondId);
var backupFullPath = path.join(backupFolder,'a.txt');
var fullPath = path.join(__dirname,'main','a.txt');

var mkdirp = Q.denodeify(require('mkdirp'));
var rename = Q.denodeify(fs.rename);
var writeFile = Q.denodeify(fs.writeFile);

That change wouldn't be needed if node.js natively supported promises.

Option 1

// createOrSkip
mkdirp(backupFolder)
    .then(function (dir) {
        // move
        return rename(fullPath, backupFullPath);
    })
    .then(function () {
        // write
        return writeFile(fullPath, 'abc');
    })
    .done(function () {
        console.log('operation complete')
    });

I don't think it gets much simpler than that. Like @Bergi said though, it's more similar to "waterfall". If you want the exact behavior of series (but with promises) you'll have to use something like Option 2 or Option 3.

Option 2

You could write out the code manually to save the results. I usually find that, although this requires a little extra writing, it's by far the easiest to read:

var result = {}
mkdirp(backupFolder)
    .then(function (dir) {
        result.createOrSkip = {created: !!dir, folderAt: backupFolder};
        return rename(fullPath, backupFullPath);
    })
    .then(function () {
        result.move = {backupAt: backupFullPath};
        return writeFile(fullPath, 'abc');
    })
    .then(function () {
        result.write = {saveAt: fullPath};
        return result;
    })
    .done(function (result) {
        console.log(result);
    });

Option 3

If you find yourself using this sort of code all the time, you could write a very simple series helper (I've never found the need to do this personally):

function promiseSeries(series) {
    var ready = Q(null);
    var result = {};
    Object.keys(series)
        .forEach(function (key) {
            ready = ready.then(function () {
                return series[key]();
            }).then(function (res) {
                result[key] = res;
            });
        });
    return ready.then(function () {
        return result;
    });
}
promiseSeries({
    createOrSkip: function () {
        return mkdirp(backupFolder).then(function (dir) {
            return {created: !!dir, folderAt: backupFolder};
        });
    },
    move: function () {
        return rename(fullPath, backupFullPath)
            .thenResolve({backupAt: backupFullPath});
    },
    write: function () {
        return writeFile(fullPath, 'abc')
            .thenResolve({saveAt: fullPath});
    }
}).done(function (result) {
    console.log(result);
});

I'd say once you've written the helper, the code is a lot clearer for promises than with all the error handling cruft required to work with callbacks. I'd say it's clearer still when you either write it by hand or don't keep track of all those intermediate results.

Summing Up

You may or may not think these examples are clearer than the async.series version. Consider how well you might know that function though. It's actually doing something pretty complex in a very opaque manner. I initially assumed that only the last result would be returned (ala waterfall) and had to look it up in the documentation of Async. I almost never have to look something up int the documentation of a Promise library.

like image 94
ForbesLindesay Avatar answered Oct 30 '22 07:10

ForbesLindesay


Make each of your functions return a promise. Construct them with a Deferred:

function createOrSkip(folder) {
    var deferred = Q.defer();
    mkdirp(folder, function (err, dir) {
        if(err) {
            deferred.reject(err);
        } else {
            deferred.resolve({created: !!dir, folderAt: backupFolder});
        }
    });
    return deferred.promise;
}

However, there are helper functions for node-style callbacks so that you don't need to check for the err yourself everytime. With Q.nfcall it becomes

function createOrSkip(folder) {
    return Q.nfcall(mkdirp, folder).then(function transform(dir) {
        return {created: !!dir, folderAt: backupFolder};
    });
}

The transform function will map the result (dir) to the object you expect.

If you have done this for all your functions, you can chain them with then:

createOrSkip(backupfolder).then(function(createResult) {
    return move(fullPath, backupFullPath);
}).then(function(moveResult) {
    return write(fullPath, 'abc');
}).then(function(writeResult) {
    console.log("I'm done");
}, function(err) {
    console.error("Something has failed:", err);
});

Notice that this works like async's waterfall, not series, i.e. the intermediate results will be lost. To achieve that, you would need to nest them:

createOrSkip(backupfolder).then(function(createResult) {
    return move(fullPath, backupFullPath).then(function(moveResult) {
        return write(fullPath, 'abc');.then(function(writeResult) {
            return {
                createOrSkip: createResult,
                move: moveResult,
                write: writeResult
            };
        });
    });
}).then(function(res){
    console.log(res);
}, function(err) {
    console.error("Something has failed:", err);
});
like image 36
Bergi Avatar answered Oct 30 '22 08:10

Bergi