Hamed
Hamed

Reputation: 869

Return array from callback function

After a few hours of thinking and googleing, I seem to be unable to find a solution to this stupid simple problem!

I'm trying to a create an array of JSON objects in order to insert them into DB. But due to Async aspect of Nodejs it has become a little bit complicated.

What I have:

function foo (arg) {
    request({url: arg}, function(err, response, results){
        var chunk = [];
        var r = JSON.parse(results);
        (r.bar).forEach(function(item){
            geocoder.reverse({lat: item.lat, lon: item.lng}, function(err, res){
                chunk.push(res[0].formattedAddress;
            });
        });
        console.log(chunk); \\ This is eventually empty!
        insertData(chunk); \\ useless due to chunk being empty!
    });

I was wondering how could I make this happen (construct an array and insert it into DB)?

I'm desperate please help.
Thanks in advance,

Upvotes: 0

Views: 1333

Answers (3)

jfriend00
jfriend00

Reputation: 708206

Here's a solution using promises to manage the async operations and retain order of multiple results:

var Promise = require('bluebird');
var requestAsync = Promise.promisify(request);
var geocoder.reverseAsync = Promise.promisify(geocoder.reverse);

function foo(arg) {
    return requestAsync({url: arg}).spread(function(response, results) {
        var r = JSON.parse(results);
        return Promise.map(r.bar, function(item) {
            return geocoder.reverseAsync({lat: item.lat, lon: item.lng}).then(function(res) {
                return res[0].formattedAddress;
            });
        });
    });
}

foo(something).then(function(addresses) {
    // addresses will be an array of addresses
    insertData(addresses);
}, function(err) {
    // some error occurred
});

Here's an explanation for how it works:

  1. Load the bluebird promise library
  2. Create promisified versions of the two async functions used here and give them the "Async" suffix to distinguish them.
  3. Call requestAsync() and use .spread() instead of .then() to more easily separate out the two values returned
  4. Use Promise.map() to iterate the array of data for us and return a single promise that represents all the sub-promises so we know when all the geocoder operations are done.
  5. Inside the Promise.map() iterator callback, call geocoder.reverseAsync() and use .then() to access the result.
  6. Reach into the geocoder result to return the formattedAddress which will become the returned value from each promise in Promise.map()
  7. Then, when calling foo() use .then() to set up your success and error handlers. In the success handler, you can access the array of addresses in order.

FYI, if you are concerned about running into rate limiting issues about how many simultaneous geocoder calls you may have running at the same time, you can add a concurrency option to Promise.map() like this:

        return Promise.map(r.bar, function(item) {
           ...
        }, {concurrency: 3});

Upvotes: 1

Georgette Pincin
Georgette Pincin

Reputation: 292

I recommend installing the async module, as its invaluable for node.js

First:

npm install --save async

Then:

var async = require('async');

function foo (arg) {
    request({url: arg}, function(err, response, results){
        var r = JSON.parse(results);
        async.map(r.bar, function eachItem (item, cb) {
            geocoder.reverse({lat: item.lat, lon: item.lng}, function(err, res){
                cb(null, res[0].formattedAddress);
            });
        }, 
        function done (err, result) {
            console.log(result); \\ this will be your ordered array
            insertData(result); \\ no longer useless!
        });
    });

Upvotes: 2

tymeJV
tymeJV

Reputation: 104805

You have to wait for the geocoder.reverse function to finish for each r.bar - create a counter, increment on each complete call, then when the final call is done, insertData!

function foo (arg) {
    request({url: arg}, function(err, response, results){
        var chunk = [];
        var r = JSON.parse(results);
        var totalCalls = r.bar.length;
        var completedCalls = 0; 

        (r.bar).forEach(function(item){
            geocoder.reverse({lat: item.lat, lon: item.lng}, function(err, res){
                chunk.push(res[0].formattedAddress); //missing ) here in your code
                completedCalls++;
                if (completedCalls == totalCalls) {
                    insertData(chunk);
                }
            });
        });
    });
}

Upvotes: 1

Related Questions