0

I'm trying to iterate a list of objects that I have, and want to check if it has 'Add to Cart' text, then get it's productID and then break out of the loop (since I only want the first available item).

However, I've tried breaking it but it runs through all of the elements regardless, and can't seem it figure out why. Does it have to do with async await? I'm using puppeteer.

    let putterID;
    
    await putters.every(async (putter) => {
        let inStock = await putter.$eval('.product-details .tocart a', el => el.innerHTML);
        inStock = inStock.trim();

        if (inStock == "Add To Cart") {
            putterID = await putter.$eval('.product-details .price', el => el.getAttribute('data-publishproductid'));
            console.log(`Found the first available putter with ID: ${putterID}`);
            return false;
        }

    });
  • What is `putters`? Is it an array? If so, you cannot pass `.every` an async function and expect it to work. – evolutionxbox Mar 22 '22 at 22:07
  • 1
    Does this answer your question? [Using async/await with a forEach loop](https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop) – evolutionxbox Mar 22 '22 at 22:08
  • Use a regular `for` loop to get full control over when you break out of the loop and to get legit `async/await` support. `.every()` is not async aware. – jfriend00 Mar 22 '22 at 22:23

2 Answers2

3

The built-in higher order functions don't play nice with asynchronous functions. You aren't going to be able to get .every() to do what you want like that.

If you want to do a for loop, why not just do a for loop?

for (const putter of putters) {
  let inStock = await putter.$eval('.product-details .tocart a', el => el.innerHTML);
  inStock = inStock.trim();

  if (inStock == "Add To Cart") {
      putterID = await putter.$eval('.product-details .price', el => el.getAttribute('data-publishproductid'));
      console.log(`Found the first available putter with ID: ${putterID}`);
      break;
  }
           
}

The reason why .every() isn't working the way you want is because of the following: .every() takes a synchronous function, calls it, and if all of the returned values are truthy, .every() will return true, otherwise it will return false. You passed in an asyncrounous function, which, remember, an asyncrounous function is really the same as a syncrounous function that returns a promise, that's it. So, for each item in putters, the callback will be called, and the callback is always going to return a promise, which is truthy, causing .every() to return true (which you then await for no reason - true isn't a promise, so awaiting it does nothing). Note that nothing actually waited for these callbacks to finish executing. They'll finish later on, at their own time.

Scotty Jamison
  • 10,498
  • 2
  • 24
  • 30
0

The use of the every method in Array seems to be fully explained by Scotty Jamison. Since the async function returns a return value enclosed in Promise, the return value of the function that is a parameter of the every method in your code is Promise<void|false>, which is evaluated as true.

One more thing, if you use for loop and async/await, you can only handle one task at a time. So you can't benefit from Concurrency. Therefore, you can use the counter as follows to check every end point and get the first result you want.

function getPutterID(putters) {
  return new Promise((res, rej) => {
    let count = putters.length;
    let complete = false;
    let putterID = '';

    async function checkPutter(putter) {
      let inStock = await putter.$eval('.product-details .tocart a', el => el.innerHTML);
      inStock = inStock.trim();
      count--;

      if (inStock == "Add To Cart" && !complete) {
        complete = true;

        console.log(`Found the first available putter with ID: ${putterID}`);  
        res(putter.$eval('.product-details .price', el => el.getAttribute('data-publishproductid')));          
      }
      if (!count) rej();
    }

    putters.forEach(putter => checkPutter(putter));
  });
}

const putterID = await getPutterID(putters);

I created this code assuming that the putterID will be obtained unconditionally, but it is not complete, so it needs to be modified according to the situation. For example, if there are too many putters, you may need to control the number of async functions that run simultaneously on the checkPutter.

lowfront
  • 639
  • 1
  • 5
  • To run them in parallel, an even simpler solution would be `const didAllOfThemComplete = await Promise.all(putters.map(async () => {...the O.P.'s original callback...})).then(results => results.every(x => x))`. Of course, with both this and your solution there's no short-circuiting happening, since we're firing all of the async tasks at once. – Scotty Jamison Mar 23 '22 at 00:02
  • `Promise.all` is resolved only when all functions that meet the condition and all functions that do not meet the condition are completed. That's the reason I didn't use `Promise.all` is because want to get the first result that includes "Add To Cart" among the resolve of the promise. – lowfront Mar 23 '22 at 00:20
  • Ah, I see. In that case you could use `Promise.race()`. – Scotty Jamison Mar 23 '22 at 00:44
  • `Promise.race` gets the first resolved value, but the first resolved putter may not include "Add To Cart". So `Promise.race` is not possible either :D. Therefore, after one async is completed, whether or not to return the result depends on whether putter have an "Add To Cart" or not. – lowfront Mar 23 '22 at 02:20
  • Oh, wait, yeah you're right about `race()` (durp on my part). Though, I think `Promise.all()` would actually do what you want if I understand what you're doing correctly. You're just trying to make it wait until a single promise rejects, and then you immediately stop waiting? `Promise.all()` does that, it stops waiting as soon as one promise rejects, even if there's other pending promises. – Scotty Jamison Mar 23 '22 at 05:02
  • Of course, `Promise.all` is also possible, but there is a difference in the time of resolved the promise. If you don't reject the async function, it will all return the resolve regardless of whether putter have "Add To Cart", so there will be no problem with the operation. The problem is that `Promise.all` is not resolved until all async functions are resolved, even though a value that meets the first condition is found(have "Add To Cart"). Your `Promise.all` solution will work as well, but I think it's a matter of how well can resolve it. – lowfront Mar 23 '22 at 06:04
  • Oh, oh, sorry. So, generally, when I see the Promise constructor being used for some other purpose besides converting a callback-based API to a promise-based one, I see that as a potential problem that can be improved. Now, I actually took the time to set down with your code and figure out how to use a built-in functions to solve it. Turns out it's `Promise.any()`. Here's [the code](https://pastebin.com/gwUAR4cJ). (Also, while combing through your code, I noticed you never actually set the putterID variable to anything). Ok, glad I got that figured out, sorry I kept making bad suggestions. – Scotty Jamison Mar 23 '22 at 14:30
  • 1
    Oh, I don't think it's a bad suggestion. No problem. I have confirmed that the code works correctly with my code. Okay, I missed it :D. If you slightly modify the code you created, you can implement it the same way using `Promise.all`. But I'm not sure which one is the cleanest code to use(because use free variable). Furthermore, when find the first "Add To Cart", can cancel all pending Promises(like $eval) and immediately resolves them. So I agree with the Promise method, but I still don't know the best way. – lowfront Mar 23 '22 at 20:43