6

I have a problem with my promise return code, I have a function getTagQuotes which contains a for loop which can make multiple calls an API to return data into an array.

How my code for this begins below:

// If there are tags, then wait for promise here:
if (tags.length > 0) {

    // Setting promise var to getTagQuotes:
    var promise = getTagQuotes(tags).then(function() {
        console.log('promise =',promise);

        // This array should contain 1-3 tags:
        console.log('tweetArrayObjsContainer =',tweetArrayObjsContainer);

        // Loop through to push array objects into chartObj:
        for (var i=0; i<tweetArrayObjsContainer.length; i++) {
            chartObj.chartData.push(tweetArrayObjsContainer[i]);
        }

        // Finally draw the chart:
        chartDirective = ScopeFactory.getScope('chart');
        chartDirective.nvd3.drawChart(chartObj.chartData);
    });
}

My getTagQuotes function with the promise return:

function getTagQuotes(tags) {
    var deferred = $q.defer(); // setting the defer
    var url      = 'app/api/social/twitter/volume/';

    // My for loop, which only returns ONCE, even if there are 3 tags
    for (var i=0; i<tags.length; i++) {
        var loopStep = i;
        rawTagData   = [];

        // The return statement
        return GetTweetVolFactory.returnTweetVol(url+tags[i].term_id)
            .success(function(data, status, headers, config) {
                rawTagData.push(data);

                // One the last loop, call formatTagData
                // which fills the tweetArrayObjsContainer Array
                if (loopStep === (rawTagData.length - 1)) {
                    formatTagData(rawTagData);
                    deferred.resolve();
                    return deferred.promise;
                }
            });
    }

    function formatTagData(rawData) {

        for (var i=0; i<rawData.length; i++) {
            var data_array = [];
            var loopNum = i;

            for (var j=0; j<rawData[loopNum].frequency_counts.length; j++) {
                var data_obj = {};
                data_obj.x = rawData[loopNum].frequency_counts[j].start_epoch;
                data_obj.y = rawData[loopNum].frequency_counts[j].tweets;
                data_array.push(data_obj);
            }

            var tweetArrayObj = {
                "key" : "Quantity"+(loopNum+1), "type" : "area", "yAxis" : 1, "values" : data_array
            };

            tweetArrayObjsContainer.push(tweetArrayObj);
        }
    }
}

Take notice of this line

return GetTweetVolFactory.returnTweetVol(url+tags[i].term_id)

it's inside my for loop:

for (var i=0; i<tags.length; i++)

Everything works great if I only have to loop through once. However as soon as there is another tag (up to 3) it still only returns the first loop/data. It does not wait till the for loop is done. Then return the promise. So my tweetArrayObjsContainer always only has the first tag.

Ben Wilde
  • 5,552
  • 2
  • 39
  • 36
Leon Gaban
  • 36,509
  • 115
  • 332
  • 529
  • The `return` inside the for loop returns from the entire `getTagQuotes` function. `return` will return from the current function, doesn't matter if you are in for loop or while loop or whatever. – Ben Wilde Oct 07 '15 at 16:52
  • I'm played around with this, if I remove return from `getTagQuotes` then I get `.then of function undefined` error on `getTagQuotes(tags).then(function()` – Leon Gaban Oct 07 '15 at 16:54
  • Of course, because you then returned nothing. Looks like you have 3 items, all of which return promises. And you want to wait for all promises to complete before taking an action. – Ben Wilde Oct 07 '15 at 16:58
  • You should return deferred.promise; instead of GetTweetVolFactory.returnTweetVol – masimplo Oct 07 '15 at 16:59

6 Answers6

2

Three issues:

  1. you didn't return the deferred promise from the getTagQuotes method.
  2. you were looking at i to see if you were through the loop, and the for loop is already completed (i == (tags.length - 1)) before the first success is even called.
  3. you called return in the first iteration of the loop so that you didn't even get to the 2nd item.

Here's corrected code (didn't test it yet)

function getTagQuotes(tags) {
    var deferred = $q.defer(); // setting the defer
    var url      = 'app/api/social/twitter/volume/';
    var tagsComplete = 0;

    for (var i=0; i<tags.length; i++) {
        rawTagData   = [];
        GetTweetVolFactory.returnTweetVol(url+tags[i].term_id)
            .success(function(data, status, headers, config) {
                rawTagData.push(data);
                tagsComplete++;

                if (tagsComplete === tags.length) {
                    formatTagData(rawTagData);
                    deferred.resolve();
                }
            });
    }

    return deferred.promise;
}
Ben Wilde
  • 5,552
  • 2
  • 39
  • 36
  • Yes this is the solution! :D my `tweetArrayObjsContainer` is being filled, then after promise, it's pushed into `chartObj.chartData` then I can draw my chart. – Leon Gaban Oct 07 '15 at 18:02
2

return deferred.promise; should be the return value of your function, not the GetTweetVolFactory.returnTweetVol(), because that's what you intend to promisify.

Your problem is that you are calling several GetTweetVolFactory.returnTweetVol(), and then you need to merge all those async calls to resolve your promise. In order to do that, you should promisify just one GetTweetVolFactory.returnTweetVol() call:

function promisifiedTweetVol(rawTagData, urlStuff) {
    var deferred = $q.defer(); // setting the defer

    GetTweetVolFactory.returnTweetVol(urlStuff)
        .success(function(data, status, headers, config) {
            rawTagData.push(data);

            // One the last loop, call formatTagData
            // which fills the tweetArrayObjsContainer Array
            if (loopStep === (rawTagData.length - 1)) {
                formatTagData(rawTagData);
                deferred.resolve();
            }
        });

    return deferred.promise;
}

And then call each promise in a loop and return the promise that resolves when all promises are completed:

function getTagQuotes(tags) {
    var url      = 'app/api/social/twitter/volume/';
    var promises = [];

    // My for loop, which only returns ONCE, even if there are 3 tags
    for (var i=0; i<tags.length; i++) {
        var loopStep = if;
        rawTagData   = [];

        promises.push( promisifiedTweetVol(rawTagData, url+tags[i].term_id) );
    }

    // ...

    return $.when(promises);
}

There are a few more issues with your code, but you should be able to get this working with my tip.

Tiago Fael Matos
  • 2,077
  • 4
  • 20
  • 34
  • Thanks for your answer, looks like it's similar to Ben's I'm having trouble with another solution, will try both of yours here in a bit. – Leon Gaban Oct 07 '15 at 17:36
1

You should return an array of promises here, which means that you should change getTagsQuotes like this:

function getTagQuotes(tags) {

    var url      = 'app/api/social/twitter/volume/',
        promises = [];

    for (var i=0; i<tags.length; i++) {

       promises.push( GetTweetVolFactory.returnTweetVol( url+tags[i].term_id ) );

    }

    return promises;
}

And then loop through this promises like this:

if (tags.length > 0) {

    var promises = getTagQuotes(tags);

    promises.map( function( promise ) {

         promise.then( function( data ) { 

            //Manipulate data here

         });

    });
}

Edit: In case you want all promises to be finished as outlined in the comment you should do this:

if (tags.length > 0) {

    Promise.all( getTagQuotes(tags) ).then( function( data ) { 

        //Manipulate data here

    });
}

Edit: Full data manipulation:

Promise.all( getTagQuotes(tags) ).then( function( allData ) {

allData.map( function( data, dataIndex ){

    var rawData = data.data,
        dataLength = rawData.frequency_counts.length,
        j = 0,
        tweetArrayObj = {
            // "key"    : "Quantity"+(i+1),
            // "color"  : tagColorArray[i],
            "key"    : "Quantity",
            "type"   : "area",
            "yAxis"  : 1,
            "values" : []
        };

    for ( j; j < dataLength; j++ ) {

        rawData.frequency_counts[j].start_epoch = addZeroes( rawData.frequency_counts[j].start_epoch );

        tweetArrayObj.values.push( { x:rawData.frequency_counts[j].start_epoch, y:rawData.frequency_counts[j].tweets  } );

    }

    tweetArrayObjsContainer.push( tweetArrayObj );

});

for ( var i= 0,length = tweetArrayObjsContainer.length; i < length; i++ ) {

    chartObj.chartData.push( tweetArrayObjsContainer[ i ] );

}

chartDirective = ScopeFactory.getScope('chart');
chartDirective.nvd3.drawChart(chartObj.chartData);

});
guramidev
  • 2,228
  • 1
  • 15
  • 19
  • In this case you should remove the deferred promise – masimplo Oct 07 '15 at 17:03
  • Ah brilliant! Trying this now – Leon Gaban Oct 07 '15 at 17:09
  • This doesn't solve it. He wants to run *one* block of code, after *all* the promises have returned. – Ben Wilde Oct 07 '15 at 17:17
  • It's easy to change the code to accomodate to that, but just in case i added the solution to that also – guramidev Oct 07 '15 at 17:22
  • Still working on it, the code in the function `formatTagData` has to be run up to 3 times. I've since moved that out and into the `promise.then( function( data ) { ` line – Leon Gaban Oct 07 '15 at 17:31
  • i could help you if i knew what you want to do with the data, it is just not very clear to me – guramidev Oct 07 '15 at 17:35
  • Sorry I wish I could post a plunkr with this, but I can't without real API data, here is my full gist atm, I need to iterate the `i` inside of the `tweetArrayObj` but that's the least of my problems. https://gist.github.com/leongaban/d5f119bd39ca213c7c45 – Leon Gaban Oct 07 '15 at 17:40
  • It seems that you really need an event when all promises finished, because now you are drawing chart in a loop. I guess you want to collect all data and then draw a chart? – guramidev Oct 07 '15 at 17:52
  • Yes I need to wait till `tweetArrayObjsContainer` is filled, then push it into `chartObj.chartData` then draw the chart. – Leon Gaban Oct 07 '15 at 18:00
1

Put every promise in an array then do:

$q.all(arrayOfPromises).then(function(){
  // this runs when every promise is resolved.
});
Fernando Pinheiro
  • 1,796
  • 2
  • 17
  • 21
0

Using deferreds is widely considered to be an anti-pattern. If your promise library supports a promise contstructor, that's an easier way to create your own promises.

Rather than trying to resolve all of the promises in one, I usually use a promise implementation that has an all function. Then I create one function that returns a promise for a thing, and then another function that returns a promise for all the things.

Using a map() function is also usually a lot cleaner than using a for loop.

Here's a generic recipe. Assuming your promise implementation has some flavor of an all function:

var fetchOne = function(oneData){
 //Use a library that returns a promise
 return ajax.get("http://someurl.com/" + oneData);
};

var fetchAll = function(allData){
  //map the data onto the promise-returning function to get an
  //array of promises. You could also use `_.map` if you're a 
  //lodash or underscore user.
  var allPromises = myData.map(fetchOne);
  return Promise.all(allPromises);
};

var allData = ["a", "b", "c"];
var promiseForAll = fetchAll(allData);

//Handle the results for all of the promises.
promiseForAll.then(function(results){
  console.log("All done.", results);
});
Chris Jaynes
  • 2,868
  • 30
  • 29
0

With reference to this question and the earlier question :

  • the code in general will be a lot cleaner with array.map() in lieu of for loops, in several places.
  • getTagQuotes() will be made cleaner by building an array of promises, submitting it to $q.all() and returning an aggregate promise.
  • formatTagData(), and its relationship with its caller, will be made cleaner by returning the transformed rawData.

With a few assumptions, the code should simplify to something like this :

getTagQuotes(tags).then(function(tweetArrayObjsContainer) {
    chartObj.chartData = chartObj.chartData.concat(tweetArrayObjsContainer); // concat() ...
    // chartObj.chartData = tweetArrayObjsContainer;                         // ... or simply assign??
    chartDirective = ScopeFactory.getScope('chart');
    chartDirective.nvd3.drawChart(chartObj.chartData);
});

function getTagQuotes(tags) {
    var url = 'app/api/social/twitter/volume/';
    var promises = tags.map(function(tag) {
        var deferred = $q.defer();
        GetTweetVolFactory.returnTweetVol(url + tag.term_id)
        .success(function(data, status, headers, config) {
            deferred.resolve(data);
        })
        .error(function(data, status) {
            console.log(tag.term_id + ': error in returning tweet data');
            deferred.resolve(null); // resolve() here effectively catches the error
        });
        return deferred.promise;
    });
    return $q.all(promises).then(formatTagData); //this is much much cleaner than building an explicit data array and resolving an outer deferred.

    function formatTagData(rawData) {
        return rawData.filter(function(data) {
            return data || false; // filter out any nulls
        }).map(function(item, i) {
            return {
                'key': 'Quantity' + (i+1),
                'type': 'area',
                'yAxis': 1,
                'color': tagColorArray[i],
                'values': item.frequency_counts.reverse().map(function(c) {
                    return {
                        x: addZeroes(c.start_epoch),
                        y: c.tweets,
                    };
                })
            };
        });
    }
}
Community
  • 1
  • 1
Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • Interesting approach, I just tried this out.. but looks like I'll need to add another promise chain inside of my `returnTweetVol` function as well now. – Leon Gaban Oct 07 '15 at 22:00
  • That sounds like (part of?) a good idea. Ideally, `returnTweetVol()` will return a promise such that you can write `GetTweetVolFactory.returnTweetVol(url + tag.term_id).then(...)`, avoiding the need to promisify on-the-fly, as in my answer. – Roamer-1888 Oct 07 '15 at 22:39