0

I have the following:

https://jsfiddle.net/qofbvuvs/

var allItems = ["1", "2", "3"]
var allPeople = ["A", "B"]

var testFoo = function(itemValue, peopleValue) {
  setTimeout(function(){
      return itemValue == "3" && peopleValue == "B"
  }, 200)
}

allItems.forEach(function(itemValue) {
  allPeople.forEach(function(peopleValue) {
    // I want to iterate through each object, completing testFoo before moving on to the next object, without recursion.  TestFoo has a few instances of setTimeout.  I've tried using promises to no avail.
    if (testFoo(itemValue, peopleValue)){
        alert("success")
    } else{
        // nothing
    };
  })
})
alert("complete")

My goal is to iterate through each item, one by one, in order, while waiting for the results of testFoo. If testFoo passes, then I should stop execution.

I've tried to use promises (https://jsfiddle.net/qofbvuvs/2/) but can't get the behavior I'm looking for. Success should be called before Complete. TestFoo has a couple setTimeouts that I need to work around (it's a library I can't modify). How can this be achieved?

SB2055
  • 12,272
  • 32
  • 97
  • 202
  • since the call to testFoo() executes immediately (without waiting for the timeout) it won't have a return value like you're expecting. If you're using async you will have to get used to not using return, and to handling results within callbacks. – James May 01 '17 at 21:20
  • @James I did introduce a Deferred into `testFoo` to pass the result, but the loops still continue without waiting for the result. Any way I can use deferred callbacks AND have the loops wait? – SB2055 May 01 '17 at 21:22
  • @James my attempt: https://jsfiddle.net/qofbvuvs/2/ – SB2055 May 01 '17 at 21:30
  • Are they supposed to be processed in order or in parallel? – nem035 May 01 '17 at 21:35
  • @nem035 in order - and ideally, I could stop execution on success. – SB2055 May 01 '17 at 21:38
  • With a setTimeout you can accomplish your goal with an incrementally increasing delay. Otherwise, if you're not actually using setTimeout in your real code, recursion is pretty much your only option outside of moving to async/await and transpiling. You can't make a forEach wait. – Kevin B May 01 '17 at 21:40
  • @SB2055 Try my solution, I think it is what you're looking for. It executes in order, and you can stop execution at will – mhodges May 01 '17 at 22:01
  • @KevinB I like the async/await approach as it leaves existing code mostly untouched. Would you have any reservations around recommending that approach? It seems cleanest to me. – SB2055 May 01 '17 at 23:41
  • just use a for loop. http://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop – Kevin B May 02 '17 at 00:08
  • @KevinB thanks - I just discovered that I need to transpile if I want to use async/await which stinks. – SB2055 May 02 '17 at 00:16
  • yup. that be the tradeoff. good news is you can replicate that funcitonality by using promises, it just isn't as pretty – Kevin B May 02 '17 at 00:17

4 Answers4

1

One way you can do it is through jQuery deferreds and manually stepping through your arrays, rather than using the built in loops, so you can control if/when you proceed. I guess it uses recursion in a way, but for nothing more than to invoke the next iteration - there is no crazy recursive return value unraveling or anything that makes recursion complex. Let me know if this works for you:

var allItems = ["1", "2", "3"]
var allPeople = ["A", "B"]

var testFoo = function(itemValue, peopleValue) {
  var deferredObject = $.Deferred();
  setTimeout(function() {
    deferredObject.resolve(itemValue == "3" && peopleValue == "B")
  }, 200)
  return deferredObject;
}
var currentItemIndex = 0;
var currentPeopleIndex = 0;

var testDeferred = $.Deferred();

function testAll() {
  testFoo(allItems[currentItemIndex], allPeople[currentPeopleIndex]).done(function(result) {
    if (result) {
      // found result - stop execution
      testDeferred.resolve("success");
    } else {
      currentPeopleIndex++;
      if (currentPeopleIndex >= allPeople.length) {
        currentPeopleIndex = 0;
        currentItemIndex++;
      }
      if (currentItemIndex >= allItems.length) {
        // result not found - stop execution
        testDeferred.resolve("fail");
      } else {
        // check next value pair
        testAll();
      }
    }
  });
  return testDeferred;
}

testAll().done(function resolveCallback (message) {
  alert(message);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
mhodges
  • 10,938
  • 2
  • 28
  • 46
  • What are your thoughts on the async/await approach below? – SB2055 May 01 '17 at 23:39
  • 1
    @SB2055 Well, it depends on the environment you'll be running this in. Async/await is not even an ES6 feature - it's actually a part of the ES2016+ spec. It has decent support on the latest versions of the major browsers, but even a browser slightly out of date will choke on it. You could use a transpiler, like Sasang mentioned, but currently, they have even worse support for async functions. What's nice about the jQuery implementation of deferred objects is that it has polyfills/fallbacks and legacy browser support built in so you never have to worry about it. – mhodges May 02 '17 at 15:28
  • @SB2055 I'd say in about a year, the async/await approach will have much better support and will be much more widely used. Until then, stick with features that are supported, imo. [Here is the compatibility table for async functions](http://kangax.github.io/compat-table/es2016plus/#test-async_functions) – mhodges May 02 '17 at 15:29
1

Since there's already a bunch of solutions out, I figured i'd add a solution using async/await. I personally like this one because it keeps the code similar to your original and more compact, where you don't have to keep track of any intermediate states or resolve all promises. The solution is just to wrap the for loops inside an async function and change the forEach to a basic for(var x...) because await wont work correctly within a forEach statement because you are defining an inner non async function and calling await within that is invalid. Additionally change the testFoo function to return a promise.

Since the for loops are inside a function you an easily exit it once a match has been found to skip further checks. I also added a return statement at the end of the loops to indicate nothing has been found, which can be handy.

Finally, since async functions themselves return a promise, all you have to do is add the final alerts inside the then of the returned promise to evaluate if a match has been found or not.

async/await example:

var allItems = ["1", "2", "3"]
var allPeople = ["A", "B"]

var testFoo = function(itemValue, peopleValue) {
   return new Promise(function(resolve, reject){
    setTimeout(function(){
        resolve(itemValue == "3" && peopleValue == "B");
    }, 200)
  });
}

async function checkItemPeople(){
  for(var itm=0; itm<allItems.length; itm++){
    for(var ppl=0; ppl<allPeople.length; ppl++){
      var result = await testFoo(allItems[itm], allPeople[ppl]);
      if (result) {
        alert("success");
        return true;
      } else {
        // nothing
      };
    }
  }
  return false;
}

checkItemPeople().then(function(resp){
  resp ? alert("Found Something") : alert("Theres nothing");
});
Sasang
  • 1,261
  • 9
  • 10
  • This is my favorite if it works - I'll test it out and mark as complete if it does. I wonder why nobody else recommended this? – SB2055 May 01 '17 at 23:39
  • 1
    I think some of it might have to do with the environment you'll be running it on. If async/await is not natively supported you'll have to transpile it, so that could be added work. – Sasang May 02 '17 at 00:42
  • @Sasang Babel, typescript, core-js, traceur, etc. only support 3 of 15 async/await features, so (at this current moment in time) even a transpiler is not a sure-fire answer. – mhodges May 02 '17 at 15:35
0

Used Promise.all to solve your problem. Hope this helps.

var allItems = ["1", "2", "3"];
var allPeople = ["A", "B"];

var testFoo = function(itemValue, peopleValue) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve(itemValue === "3" && peopleValue === "B");
    }, 200);
  });
}

var promises = [];
allItems.forEach(function(itemValue) {
  allPeople.forEach(function(peopleValue) {
    promises.push(testFoo(itemValue, peopleValue).then(function(result) {
      console.log(result?'success':'fail');
      return result;
    }));
  })
});

Promise.all(promises).then(function(results) {
  console.log('Result from all promise: ' + results);
  console.log("complete");
});
ajai Jothi
  • 2,284
  • 1
  • 8
  • 16
0

If you want to construct a chain of promises in such a way where each promise isn't created until it's predecessor promise in the chain resolves, you should create an array of functions that return your promises and use reduction to create the needed promise chain.

var allItems = ["1", "2", "3"];
var allPeople = ["A", "B"];

// Async requests aren't made once we reach "2" and "B"
var resultToStopOn = function(itemValue, peopleValue) {
  return itemValue === "2" && peopleValue === "B"
}

var testFoo = function(itemValue, peopleValue) {
  return new Promise(function(resolve) {
    console.log('Starting promise for: ', itemValue, peopleValue);
    setTimeout(function() {
      console.log('Resolving promise for: ', itemValue, peopleValue);
      resolve(resultToStopOn(itemValue, peopleValue));
    }, 200);
  });
}

var functionsThatReturnPromises = [];
allItems.forEach(function(itemValue) {
  allPeople.forEach(function(peopleValue) {
    functionsThatReturnPromises.push(function() {
      return testFoo(itemValue, peopleValue);
    });
  });
});

functionsThatReturnPromises
  .reduce(function(chainOfPromises, fnReturningAPromise) {
    return chainOfPromises.then(function(result) {
      // if result is false, we continue down the chain
      // otherwise we just propagate the result and don't chain anymore
      return !result
        ? fnReturningAPromise()
        : result;
    });
  }, Promise.resolve())
  .then((result) => {
    console.log('Result: ', result);
  });
nem035
  • 34,790
  • 6
  • 87
  • 99