Slava Fomin II Slava Fomin II - 4 months ago 8
Node.js Question

Sequentially iterate list of asynchronous functions until at least one returns truthy value in Node.js

Given an array containing references to asynchronous functions (which return promises or simple values), I want to sequentially iterate over it resolving each element, until at least one of the elements resolves to the truthy value.

The idea came from this simple synchronous code:

var hasAccess = (user.isAdmin() || user.isManager() || entity.isOwnedBy(user));


The code above works for synchronous functions well, but will break for asynchronous ones. I'm looking for an asynchronous replacement of the code above.

I expect it to be called like this (in ES6 syntax for brevity):

atLeastOneTruthy([
() => user.isAdmin(), // Called first
() => user.isManager(), // Called second if first returned falsy value
() => entity.isOwnedBy(user) // Called third if all previous returned falsy values
]).then(function (result) {
// result === (true || false)
}).catch(function () {
// Promise chain is rejected when at least one element in rejected
});


All the functions in the example above return either promises or simple values directly.

Is there an out-of-the box solution for this? I'm using Bluebird promises, however, I couldn't find anything suitable in it's API. I can't use e.g.
Promise.any()
, because it only checks for resolved promises and doesn't check returned values. I can't use
Promise.map()
either, because it will execute all functions, but I wan't them to be executed only when necessary (i.e. when previous element failed).

A node module recommendation would be great (I couldn't find one), however, a concept of how
atLeastOneTruthy()
could be coded would also be nice because I have some doubts about its implementation.

Answer

Because a then handler can return a promise, you can just make a recursive solution.

function atLeastOneTruthy(arrayOfCallables) {
  // Keep a numeric index to avoid mutating the input array.
  // This implementation is not safe for item removal, but appending is OK.
  let nextCallableIndex = 0;
  // Start with a false value as a base case. Use a "named function expression"
  // to allow for recursion.
  return Promise.resolve(false).then(function innerHandler(returnValue) {
    if (returnValue) {
      // If truthy, return immediately. Nothing else is evaluated.
      return returnValue;
    } else if (nextCallableIndex < arrayOfCallables.length) {
      // If falsy, get the next iterable...
      let nextCallable = arrayOfCallables[nextCallableIndex++];
      // ...then call it and try again, regardless of whether nextCallable
      // returns a simple value or a promise.
      return Promise.resolve(nextCallable()).then(innerHandler);
    } else {
      // We're out of callables, and the last one returned a falsy value.
      // Return false. (You could also return returnValue to parallel ||.)
      return false;
    }
  });
}

See also: Named function expressions. You could also refactor to use a simple nested function.