Luke Tan
Luke Tan

Reputation: 2128

Returning value from multiple promises within Meteor.method

After a bunch of looking into Futures, Promises, wrapAsync, I still have no idea how to fix this issue

I have this method, which takes an array of images, sends it to Google Cloud Vision for logo detection, and then pushes all detected images with logos into an array, where I try to return in my method.

Meteor.methods({
  getLogos(images){
    var logosArray = [];
    images.forEach((image, index) => {
        client
        .logoDetection(image)
        .then(results => {
            const logos = results[0].logoAnnotations;
            if(logos != ''){
                logos.forEach(logo => logosArray.push(logo.description));
            }
        })
    });
    return logosArray;      
  },
});

However, when the method is called from the client:

Meteor.call('getLogos', images, function(error, response) {
  console.log(response);
});

the empty array is always returned, and understandably so as the method returned logosArray before Google finished processing all of them and returning the results.

How to handle such a case?

Upvotes: 3

Views: 631

Answers (1)

ghybs
ghybs

Reputation: 53290

  1. With Meteor methods you can actually simply "return a Promise", and the Server method will internally wait for the Promise to resolve before sending the result to the Client:
Meteor.methods({
  getLogos(images) {
    return client
      .logoDetection(images[0]) // Example with only 1 external async call
      .then(results => {
        const logos = results[0].logoAnnotations;
        if (logos != '') {
          return logos.map(logo => logo.description);
        }
      }); // `then` returns a Promise that resolves with the return value
          // of its success callback
  }
});
  1. You can also convert the Promise syntax to async / await. By doing so, you no longer explicitly return a Promise (but under the hood it is still the case), but "wait" for the Promise to resolve within your method code.
Meteor.methods({
  async getLogos(images) {
    const results = await client.logoDetection(images[0]);
    const logos = results[0].logoAnnotations;
    if (logos != '') {
      return logos.map(logo => logo.description);
    }
  }
});
  1. Once you understand this concept, then you can step into handling several async operations and return the result once all of them have completed. For that, you can simply use Promise.all:
Meteor.methods({
  getLogos(images) {
    var promises = [];
    images.forEach(image => {
      // Accumulate the Promises in an array.
      promises.push(client.logoDetection(image).then(results => {
        const logos = results[0].logoAnnotations;
        return (logos != '') ? logos.map(logo => logo.description) : [];
      }));
    });
    return Promise.all(promises)
      // If you want to merge all the resulting arrays...
      .then(resultPerImage => resultPerImage.reduce((accumulator, imageLogosDescriptions) => {
        return accumulator.concat(imageLogosDescriptions);
      }, [])); // Initial accumulator value.
  }
});

or with async/await:

Meteor.methods({
  async getLogos(images) {
    var promises = [];
    images.forEach(image => {
      // DO NOT await here for each individual Promise, or you will chain
      // your execution instead of executing them in parallel
      promises.push(client.logoDetection(image).then(results => {
        const logos = results[0].logoAnnotations;
        return (logos != '') ? logos.map(logo => logo.description) : [];
      }));
    });
    // Now we can await for the Promise.all.
    const resultPerImage = await Promise.all(promises);
    return resultPerImage.reduce((accumulator, imageLogosDescriptions) => {
      return accumulator.concat(imageLogosDescriptions);
    }, []);
  }
});

Upvotes: 5

Related Questions