0

I have a recursive query that needs to potentially make further queries based on the results. I would ideally like to be able to construct a promise chain so that I know when all of the queries are finally complete.

I've been using the example from this question, and I have the following method:

 this.pLoadEdges = function(id,deferred) {
    if (!deferred) {
        deferred = $q.defer();
    }
    $http.post('/Create/GetOutboundEdges', { id: id }).then(function(response) {
        var data = response.data;

        if (data.length > 0) {
            for (var i = 0; i < data.length; i++) {
                var subID = data[i].EndNode;

                edgeArray.push(data[i]);

                self.pLoadEdges(subID, deferred);
            }
        } else {
            deferred.resolve();
            return deferred.promise;
        }

    });

    deferred.notify();
    return deferred.promise;
}

Which I then start elsewhere using:

 self.pLoadEdges(nodeID).then(function() {
                    var edgedata = edgeArray;
                });

And of course I intend to do some more stuff with the edgeArray.

The problem is that the then() function is trigged whenever any individual path reaches an end, rather than when all the paths are done. One particular pathway might be quite shallow, another might be quite deep, I need to know when all of the pathways have been explored and they're all done.

How do I construct a promise array based on this recursive query, ideally so that I can use $q.all[] to know when they're all done, when the number of promises in the promise array depends on the results of the query?

Community
  • 1
  • 1
RamblerToning
  • 926
  • 2
  • 13
  • 28

3 Answers3

3

I'm not 100% positive what the end result of the function should be, but it looks like it should be a flat array of edges based on the example that you provides. If that's correct, then the following should work

this.pLoadEdges = function(id) {
    var edges = [];

    // Return the result of an IIFE so that we can re-use the function
    // in the function body for recursion
    return (function load(id) {
        return $http.post('/Create/GetOutboundEdges', { id: id }).then(function(response) {
            var data = response.data;

            if (data.length > 0) {
                // Use `$q.all` here in order to wait for all of the child
                // nodes to have been traversed. The mapping function will return
                // a promise for each child node.
                return $q.all(data.map(function(node) {
                    edges.push(node);

                    // Recurse
                    return load(node.EndNode);
                });
            }
        });
    }(id)).then(function() {
        // Change the return value of the promise to be the aggregated collection
        // of edges that were generated
        return edges;
    });
};

Usage:

svc.pLoadEdges(someId).then(function(edgeArray) {
// Use edgeArray here
});
dherman
  • 2,832
  • 20
  • 23
1

You need $q.all function:

Combines multiple promises into a single promise that is resolved when all of the input promises are resolved.


Update 1

Check this demo: JSFiddle

The controller can be like following code (well, you may want to put it in a factory).

It loads a list of users first, then for each user, load the posts of this user. I use JSONPlaceholder to get the fake data.

$q.all accepts an array of promises and combine them into one promise. The message All data is loaded is only displayed after all data is loaded. Please check the console.

angular.module('Joy', [])
    .controller('JoyCtrl', ['$scope', '$q', '$http', function ($scope, $q, $http) {
    function load() {
        return $http.get('http://jsonplaceholder.typicode.com/users')
            .then(function (data) {
            console.log(data.data);
            var users = data.data;
            var userPromises = users.map(function (user) {
                return loadComment(user.id);
            });
            return $q.all(userPromises);
        });
    }

    function loadComment(userId) {
        var deferred = $q.defer();
        $http.get('http://jsonplaceholder.typicode.com/posts?userId=' + userId).then(function (data) {
            console.log(data);
            deferred.resolve(data);
        });
        return deferred.promise;
    }

    load().then(function () {
        console.log('All data is loaded');
    });
}]);

Update 2

You need a recursive function, so, check: JSFiddle.

The code is below. I use round to jump out of the recursion because of the fake API. The key is here: $q.all(userPromises).then(function () { deferred.resolve(); });. That tells: Please resolve this defer object after all promises are resolved.

angular.module('Joy', [])
    .controller('JoyCtrl', ['$scope', '$q', '$http', function ($scope, $q, $http) {
    var round = 0;

    function load(userId) {
        return $http.get('http://jsonplaceholder.typicode.com/posts?userId=' + userId)
            .then(function (data) {
            var deferred = $q.defer();
            console.log(data.data);
            var posts = data.data;
            if (round++ > 0 || !posts || posts.length === 0) {
                deferred.resolve();
            } else {
                var userPromises = posts.map(function (post) {
                    return load(post.userId);
                });
                $q.all(userPromises).then(function () {
                    deferred.resolve();
                });
            }
            return deferred.promise;
        });
    }

    load(1).then(function () {
        console.log('All data is loaded');
    });
}]);
Joy
  • 9,430
  • 11
  • 44
  • 95
  • I know, but how do I build up the promise array when the number of promises depends on the result of the query from the promise itself? – RamblerToning Aug 05 '15 at 15:12
  • Why do you not just return an array of promises, from a minimum length of 1, then wrap them all together within a `.when` statement that operates over an array that is the concatenation of all the returned arrays? That's what I was getting at above. – Alex Lynham Aug 05 '15 at 15:28
  • But how do I even return the array of promises to start with? That's the part I don't understand. I have a query, which is 1 promise. That query may (or may not) return results that require more queries, more promises, which may in turn require more queries, and so on. – RamblerToning Aug 05 '15 at 15:43
  • How would you do that if it's a recursive method? The response from load() calls loadComment() and then it stops, but what if load() has to call load() itself? What do you return? – RamblerToning Aug 05 '15 at 15:53
  • @RamblerToning Please check the answer updated for recursive calling. – Joy Aug 05 '15 at 16:21
  • Wow! Thanks! That was absolutely the key, adding deferred.resolve() within the $q.all .then call! – RamblerToning Aug 05 '15 at 16:54
  • @RamblerToning My pleasure. You picked the answer from @dherman, could you please further verify that the solution works? Maybe we do not need the `$q.defer()` thing. – Joy Aug 05 '15 at 16:59
  • I tried them both and they both work. @dherman's code works just as is, and your answer works when I implement your suggestions into the previous code. – RamblerToning Aug 06 '15 at 08:54
0

You can try building up an array of returned promises and then use the $.when.apply($, <array>) pattern. I've used it before to accomplish a similar thing to what you're describing.

More info on this SO thread.

UPDATE:

You probably also want to read the docs on the apply function, it's pretty neat.

Alex Lynham
  • 1,318
  • 2
  • 11
  • 29