Vittorio Romeo
Vittorio Romeo

Reputation: 93264

Chaining callbacks in a custom-made for-each loop, supporting both synchronous and asynchronous functions

I have a for_users function that gets an array of users from a web service, executes a passed function f on the received array, then calls a continuation f_then callback.

// Execute f on every user, then f_then.
function for_users(f, f_then)
{
    // Get all users from the database, in user_array
    db.get_all_users(function(user_array)
    {
        // Execute f on every user
        user_array.forEach(f);

        // Call continuation callback
        f_then();
    });
}

When calling for_users, passing an asynchronous function as the f parameter, I would like all the f callbacks to end before calling f_then. This is obviously not happening in the current code, as user_array.forEach(f) does not wait for f to finish before starting the next iteration.

Here's an example of a problematic situation:

function example_usage()
{
    var temp_credentials = [];

    for_users(function(user)
    {
        // Get credentials is an asynchronous function that will
        // call the passed callback after getting the credential from
        // the database
        database.get_credentials(user.ID, function(credential)
        {
            // ...
        });
    }, function()
    {
        // This do_something call is executed before all the callbacks
        // have finished (potentially)

        // temp_credentials could be empty here!
        do_something(temp_credentials);
    });
}

How can I implement for_users such that if f is an asynchronous function, f_then is called only when all f functions are completed?

Sometimes, though, the passed f to for_users is not asynchronous and the above implementation could suffice. Is there a way to write a generic for_users implementation that would work as intended both for asynchronous and synchronous f functions?

Upvotes: 1

Views: 77

Answers (3)

MinusFour
MinusFour

Reputation: 14423

var getCredentials = function(step){
    return function(user){
        database.get_credentials(user.ID, function(credential) {
            step(credential);
        });
    };
};

var allFinish = function(f){
    return function(step) {
        return function(arr){
            var finished = 0;
            var values = new Array(arr.length);
            if(arr.length){
                arr.forEach(function(el, i){
                    f(function(value){
                        if(finished === arr.length){
                            step(values);
                        } else {
                            values[i] = value;
                            finished++;
                        }
                    })(el);
                });
            } else {
                step(values);
            }
        };
    };  
};

var forEachUser = function(doSomething){
    db.get_all_users(allFinish(getCredentials)(doSomething));
}

And then you can just simply do:

forEachUser(function(tempCredentials){
    //tempCredentials === allCredentials
});

There's probably better ways to handle the order of values inserted in the array in allFinish. allFinish works by taking a function that takes a step and calling it with a step function that will call another step function when all calls are finished. I curried the functions, but it isn't really necessary. It's just a convenience.

Upvotes: 0

BenG
BenG

Reputation: 15154

this should work for you:-

function for_users(f, f_then) {

  db.get_all_users(function(user_array) {
      var promises = [];

      user_array.forEach(function(user) {
        promises.push(new Promise(function(resolve, reject) {
          f(user);
          resolve();
        }));
      });

      if (f_then)
        Promise.all(promises).then(f_then);
      else
        Promise.all(promises);
    }
  });
}

Simple Test below:-

function for_users(f, f_then) {
  var users = [{ID: 1}, {ID: 2}, {ID: 3}];
  var promises = [];

  users.forEach(function(user) {
    var promise = new Promise(function(resolve, reject) {
      f(user);
      resolve();
    });
    promises.push(promise);
  })

  if (f_then)
    Promise.all(promises).then(f_then);
  else
    Promise.all(promises)
}

for_users(function(user) {
  console.log(user.ID);
}, function() {
  console.log('finshed')
})

Upvotes: 1

jcubic
jcubic

Reputation: 66478

You can add next continuation callback to f function like this:

function for_users(f, f_then) {
    // Get all users from the database, in user_array
    db.get_all_users(function(user_array) {
        // Execute f on every user
        (function recur(next) {
            var user = user_array.shift();
            if (user) {
                f(user, function() {
                    recur(next);
                });
            } else {
                // Call continuation callback
                next();
            }
        })(f_then);
    });
}

and then you will be able to call this function using this:

for_users(function(user, next) {
    // Get credentials is an asynchronous function that will
    // call the passed callback after getting the credential from
    // the database
    database.get_credentials(user.ID, function(credential) {
        next();
    });
}, function() {
    // This do_something call is executed before all the callbacks
    // have finished (potentially)

    // temp_credentials could be empty here!
    do_something(temp_credentials);
});

Upvotes: 1

Related Questions