Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Loop through tasks waterfall- promises bluebird

I'm looking to loop through some tasks with bluebird, just using timeout as a experimental mechanism. [not looking to use async or any other library]

var Promise = require('bluebird');

var fileA = {
    1: 'one',
    2: 'two',
    3: 'three',
    4: 'four',
    5: 'five'
};


function calculate(key) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(fileA[key]);
        }, 500);
    });
}

Promise.map(Object.keys(fileA), function (key) {
    calculate(key).then(function (res) {
        console.log(res);
    });
}).then(function () {
    console.log('finish');
});

result is

finish,
one,
two,
three,
four,
five,

I need the loop to only iterate once each timeout as completed, then fire the last thenable with finish.

like image 492
John O'Donnell Avatar asked Apr 12 '15 13:04

John O'Donnell


1 Answers

  1. In the function object passed to Promise.map, you need to return a Promise object, so that all the Promises will be resolved and the array of resolved values can be passed on to the next then function. In your case, since you are not returning anything explicitly, undefined will be returned, by default, not a promise. So, the thenable function with finish gets executed as the Promises of Promises.map were resolved with undefineds. You can confirm that like this

    ...
    }).then(function (result) {
        console.log(result);
        console.log('finish');
    });
    

    would print

    [ undefined, undefined, undefined, undefined, undefined ]
    finish
    one
    two
    three
    four
    five
    

    So, your code should have a return statement like this

    Promise.map(Object.keys(fileA), function (key) {
        return calculate(key).then(function (res) {
            console.log(res);
        });
    }).then(function () {
        console.log('finish');
    });
    

    Now, you will see that code prints the things on order, as we return the Promise objects and the thenable function with finish is invoked after all the Promises are resolved. But they all don't get resolved sequentially. If that had happened, every number would be printed after the specified time elapses. That brings us to the second part.

  2. Promise.map will execute the function passed as parameter, as soon as the Promises in the array are resolved. Quoting the documentation,

    The mapper function for a given item is called as soon as possible, that is, when the promise for that item's index in the input array is fulfilled.

    So, all the values in the array are converted to Promises which are resolved with the corresponding values and the function will be called immediately for each and every value. So, they all wait 500 ms at the same time and resolve at once. This doesn't happen sequentially.

    Since you want them to execute sequentially, you need to use Promise.each. Quoting the docs,

    Iteration happens serially. .... If the iterator function returns a promise or a thenable, the result for the promise is awaited for before continuing with next iteration.

    Since the Promises are created serially and resolution is awaited before continuing, order of the result is guaranteed. So your code should become

    Promise.each(Object.keys(fileA), function (key) {
        return calculate(key).then(function (res) {
            console.log(res);
        });
    }).then(function () {
        console.log('finish');
    });
    

    Additional Note:

    If the Order does not matter, as suggested by Benjamin Gruenbaum, you can use Promise.map itself, with the concurrency limit, like this

    Promise.map(Object.keys(fileA), function (key) {
        return calculate(key).then(function (res) {
            console.log(res);
        });
    }, { concurrency: 1 }).then(function () {
        console.log('finish');
    });
    

    The concurrency option basically limits the number of Promises which can be created and resolved before creating more promises. So, in this case, since the limit is 1, it will create the first promise and as the limit is reached, it will wait till the created Promise resolves, before moving on to the next Promise.


If the whole point of using calculate is to introduce delay, then I would recommend Promise.delay, which can be used like this

Promise.each(Object.keys(fileA), function (key) {
    return Promise.delay(500).then(function () {
        console.log(fileA[key]);
    });
}).then(function () {
    console.log('finish');
});

delay can transparently chain the resolved value of a Promise to the next thenable function, so the code can be shortened to

Promise.each(Object.keys(fileA), function (key) {
    return Promise.resolve(fileA[key]).delay(500).then(console.log);
}).then(function () {
    console.log('finish');
});

Since Promise.delay accepts a dynamic value, you can simply write the same as

Promise.each(Object.keys(fileA), function (key) {
    return Promise.delay(fileA[key], 500).then(console.log);
}).then(function () {
    console.log('finish');
});

If the Promise chain ends here itself, its better to use .done() method to mark it, like this

...
}).done(function () {
    console.log('finish');
});

General Note: If you are not going to do any processing in a thenable function but you are using that just to track the progress or to follow the process, then you can better change them to Promise.tap. So, your code would become

Promise.each(Object.keys(fileA), function (key) {
    return Promise.delay(fileA[key], 500).tap(console.log);
}).then(function () {
    // Do processing
    console.log('finish');
});
like image 170
thefourtheye Avatar answered Oct 02 '22 20:10

thefourtheye