eozzy
eozzy

Reputation: 68760

Making forEach wait for callback

New to async programming so I just can't figure how to do this:

$results = [];

products.forEach(function (product) {

  // 1. Search ...
  google(keyword, function (err, res) {
    if (err) console.error(err)

    for (var i = 0; i < res.links.length; ++i) {
      var result = res.links[i];
      var obj = {
        title: res.links[i].title,
        href: res.links[i].href,
        description: res.links[i].description
      }
      results.push(obj); // 2. store each result in results Array
    }
  }, processData); // 3. send all results to processData when done

  // 5. NOW, itereate further ...

});

function processData(results) {
  console.log('processing data');
  // 4. save results to DB
}

Since the process requires making HTTP requests, collecting data and then saving to DB which all takes time, so I don't want forEach to advance to the next element until one is done.

Upvotes: 1

Views: 2881

Answers (3)

Pankaj Jatav
Pankaj Jatav

Reputation: 2184

Use async package.

async.eachSeries(docs, function iteratee(product, callback) {
    // 1. Search ...
    google(keyword, function (err, res) {
        if (err) {
           console.error(err)
           callback(results) // this will send a fail callback.
        }
        for (var i = 0; i < res.links.length; ++i) {
          var result = res.links[i];
          var obj = {
            title: res.links[i].title,
            href: res.links[i].href,
            description: res.links[i].description
          }
          results.push(obj); // 2. store each result in results Array
          callback(null, results) // this is a success callback
        }
      }, processData); // 3. send all results to processData when done
});

Note: Callback behaves like return. Once callback meet the value, It won't further proceed. Now it will send request for the next product.

Upvotes: 3

Antonio Val
Antonio Val

Reputation: 3340

You can't wait for asynchronous operations inside Array.prototype.forEach().

Taking into account the library you are using for the requests to Google is not compatible with Promises maybe using Async could be a fast solution in your case (for big projects or Promise compatible libraries I recommend the Promise way).

Async map allows you to use asynchronous operations inside, since it waits for the callback.

In your case it would be something like this I guess:

async.map(products, function(product, callback) {
  var keyword = product['vendor'] + ' "' + product['mpn'] + '"';
  google( keyword, function (err,res) {
    if (err) {
      // if it fails it finish here
      return callback(err);
    }

    // using map here makes it easier to loop through the results
    var results = res.links.map(function(link) {
      return {
        title: link.title,
        href: link.href,
        description: link.description
      };
    });
    callback(null, results);
  });
}, processData);

If you have any question about the above code let me know.

Upvotes: 0

Robba
Robba

Reputation: 8324

Since the forEach is synchronous and the request is asynchronous, there is no way to do it exactly as you describe. What you can do however is to create a function that handles one item from the docs array and removes it, then when you're done processing, go to the next:

var results;
var productsToProcess;
MongoClient.connect( 'mongodb://localhost:27017/suppliers', function ( err, db ) {
  assert.equal( null, err );
  var findDocuments = function ( db ) {
    var collection = db.collection( 'products' );
    collection.find( {
      $and: [ {
        "qty": {
          $gt: 0
        }
      }, {
        "costex": {
          $lte: 1000.0
        }
      } ]
    }, {
      "mpn": 1,
      "vendor": 1,
      "_id": 0
    } ).limit( 1 ).toArray( function ( err, products ) {
      assert.equal( err, null );
      productsToProcess = products;
      getSearching();
      db.close();
    } );
  }
  findDocuments( db );
} );

function getSearching() {
  if ( productsToProcess.length === 0 ) return;
  var product = productsToProcess.splice( 0, 1 )[0];
  var keyword = product[ 'vendor' ] + ' "' + product[ 'mpn' ] + '"';
  google( keyword, function ( err, res ) {
    if ( err ) console.error( err )
    for ( var i = 0; i < res.links.length; ++i ) {
      var result = res.links[ i ];
      var obj = {
        title: res.links[ i ].title,
        href: res.links[ i ].href,
        description: res.links[ i ].description
      }
      results.push( obj );
    }
  }, processData );
}

function processData( results ) {
  MongoClient.connect( 'mongodb://localhost:27017/google', function ( err, db ) {
    assert.equal( null, err );
    // insert document to DB
    var insertDocuments = function ( db, callback ) {
      // Get the documents collection
      var collection = db.collection( 'results' );
      // Insert some documents
      collection.insert( results, function ( err, result ) {
        assert.equal( err, null );
        console.log( "Document inserted" );
        callback( result );
        db.close();
      } );
    }
    insertDocuments( db, getSearching );
  } );
}

EDIT

Moved the products from the database to the productsToProcess variable and changed the getSearching() to no longer require a parameter.

Upvotes: 2

Related Questions