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

2

5

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.

Zone answered 29/8, 2013 at 11:42 Comment(0)
F
10

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.

Frigidaire answered 29/8, 2013 at 15:39 Comment(2)
I agree with you, the latest option is clearer. Without your promiseSeries helper, I don't see the interest to use promise in a control flow fashion. The model behind promise is more difficult to grasp and I need time to get used to. In my humble opinion, flow control with async is much easier to manipulate. I may need time, though :)Zone
The thing is that all the helpers provided by async are so trivial to write for promises, and so rarely what's needed, that they haven't tended to be put in a library.Frigidaire
B
2

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);
});
Barrel answered 29/8, 2013 at 12:13 Comment(1)
Thanks, it's pretty interesting. For waterfall operation, I found async more readable (it is a matter of taste I guess).Zone

© 2022 - 2024 — McMap. All rights reserved.