Matt K
Matt K

Reputation: 4948

Meteor - Run code after for-loop of async callbacks on the client

In Meteor, I've got a for-loop full of asynchronous geocoding requests sent off to the google maps api. After all the geocodes complete, I want to display a table with all the failed geocode attempts. What's the best pattern for this? From what I've learned, I've got 3 options:

  1. Promises - I can set the google maps callback to a promise & use a thenable to return the error boolean, probably using the bluebird polyfill.

  2. NPM async - I can call async.each on each iteration & if it's the last one, return the error boolean like this: How to call a function after an asynchronous for loop of Object values finished executing

  3. Use a session variable like so:

    Deps.autorun(function() { console.log(Session.get('hasError')); });

  4. Create a meteor method & call it using Meteor.wrapasync???

Any guidance on Meteor best practices would be great, thank you!

Upvotes: 2

Views: 903

Answers (2)

stubailo
stubailo

Reputation: 6147

This is how I would do it:

// Local collection, not synced to server
var geocodeErrors = new Mongo.Collection();

// Reactive variable to track completion
var geocodingComplete = new ReactiveVar(false);

var lookupThings = function (addresses) {
  // How many are there?
  var count = addresses.length;

  // Count how many requests are complete
  var complete = 0;

  // Use underscore to make iteration easier
  _.each(addresses, function (address) {
    geocode(address, function (results, status) {
      if (status === "OK") {
        // do whatever
      } else {
        geocodeErrors.insert({
          address: address,
          otherData: "whatever"
        });
      }

      complete++;
      if (complete === count) {
        geocodingComplete.set(true);
      }
    });
  });
}

Template.results.helpers({
  geocodeErrors: function () {
    // Use an if statement to only display the errors when everything is done
    if (geocodingComplete.get()) {
      return geocodeErrors.find();
    }
  }
});

You could use this template to show a list:

<template name="results">
  <ul>
    {{#each geocodeErrors}}
      <li>Address failed: {{address}}</li>
    {{/each}}
  </ul>
</template>

I think the main takeaway is that the Meteor style is to use reactive variables and collections instead of callbacks.

Upvotes: 3

Matt K
Matt K

Reputation: 4948

Until another answer comes along (please!), I solved this by adding a callback handler to the looping function and when I call that loop, I pass in my callback.

lookupAddress(function(hasError, errArr) {
  if (hasError === false) {
    console.log("no errors");
  } else {
    console.log (errArr);
  }
});

And here are the good parts hacked out of the actual function...

function lookupAddress(callback) {
  var hasError = false;
  var errArr = [];
  var cursorClosed = false;
  var outstandingCalls = 0;

  for (i = 0; i < n; i++) {
    (function (j) { //wrap in an iffe to get 'i' byVal, not byRef
      if (j === n - 1) cursorClosed = true; //check if I'm on the last iteration
      geocoder.geocode({'address': address}, function (results, status) {
        if (status === "OK") {
          doSomethingFancy();
        } else {
          hasError = true;
          errArr.push(arr[j]);
        }
        outstandingCalls--; //reduce queue for each completed async call
        if (cursorClosed && outstandingCalls === 0) { //if no more iters and we're on the last async call
          return callback(hasError, errArr)
        }
      });
    })(i);
    outstandingCalls++; //for each loop iter add 1 to the async queue
  }
}

Upvotes: 1

Related Questions