Question

I have an array queue that I push objects to it when they are modified. If the user presses save, then I will loop through the queue and apply the appropriate API call for them.

If the API call goes through successfully, I want to remove the item from the queue, otherwise keep it inside and notify the user that some items were not successfully saved. I currently have this (in AngularJS)

var unsuccessfulItems = [];
var promise = queue.map(function(item) {
    var defer = $q.defer();
    myCallFunction( item
           , function( response ) {} // Success
           , function( response ) {  // Error
               unsuccessfulItems.push(item);
           }
    )
    defer.resolve();
    return defer.promise;
})
// Once all items have been processed
$q.all( promise ).then( function() {
    queue = unsuccessfulItems;
});

Is there a better way of doing this?

Was it helpful?

Solution

You're already using promises, you might want to do it end-to-end. Also, you're resolving the promise too early.

Assuming the suboptimal case where you don't want to promisify myCallFunction itself, you should still promisify it.

function myCall(item){
    var d = $q.defer();
    myCallFunction(item,function(r){ d.resolve({val:r,item:item});}
                       ,function(r){ d.reject(r);});
    return d.promise;
}

Note, we are resolving the defer after the asynchronous function is done, not before it.

Now, we need to implement a "Settle" function, that resolves when all promises are done no matter what. This is like $q.all but will wait for all promises to resolve and not fulfill.

function settle(promises){
     var d = $q.defer();
     var counter = 0;
     var results = Array(promises.length);
     promises.forEach(function(p,i){ 
         p.then(function(v){ // add as fulfilled
              results[i] = {state:"fulfilled", promise : p, value: v};
         }).catch(function(r){ // add as rejected
              results[i] = {state:"rejected", promise : p, reason: r};
         }).finally(function(){  // when any promises resolved or failed
             counter++; // notify the counter
             if (counter === promises.length) {
                d.resolve(results); // resolve the deferred.
             }
         });
     });
}

This sort of settle function exists in most promise implementations but not in $q. We could have also done this with rejections and $q.all, but that would mean exceptions for flow control which is a bad practice.

Now, we can settle:

 settle(queue.map(myCall)).then(function(results){
     var failed = results.filter(function(r){ return r.state === "rejected"; });
     var failedItems = failed.map(function(i){ return i.value.item; });
 });

OTHER TIPS

Here's a concise solution that works around the limitations of the very limited $q, without the need to augment its methods with bulky functions/polyfills.

In particular,

  • $q's promises don't include a simple mechanism for enquiring their status
  • $q has an .all() method but not allSettled().

The tricks I employ here are :

  • to keep promises in one array and a record of their (eventual) success in a congruent second array
  • to resolve promises on both success and failure, thus allowing $q.all() to behave like the missing $q.allSettled().
function saveQueue() {
    //First some safety
    if(queue.saving) {
        return $q.defer().resolve(-1).promise;
    }
    queue.saving = true;

    var settled = [],//the sole purpose of this array is to allow $q.all() to be called. All promises place in  this array will be resolved.
        successes = [];//an array to be (sparsely) populated with `true` for every item successfully saved. This helps overcome the lack of a simple test of a $q promise's state (pending/fulfilled/rejected).

    queue.forEach(function(item, i) {
        var defer = $q.defer(); 
        settled[i]  = defer.promise;
        myCallFunction(item, function(response) {
            //here do awesome stuff with the response
            //`item`, if required, is in scope
            successes[i] = true;//register the promise's success
            defer.resolve();//as you would expect
        }, function(error) {
            //here do awesome stuff with the error (eg log it).
            //`item`, if required, is in scope
            defer.resolve();//here we *resolve*, not reject, thus allowing `$q.all(settled)` to reflect the settling of all promises regardless of whether they were fulfilled or rejected.
        });
    });

    // Once all items have been processed
    return $q.all(settled).then(function() {
        queue = queue.filter(function(val, i) {
            return !successes[i];
        });
        queue.saving = false;
        return queue.length;
    });
}

saveQueue() will return :

  • a promise of -1 if a previous saveQueue() is still in progress, or
  • a promise of the queue length after all saves are settled.

Purists will undoubtedly deem this solution an "antipattern" (yuk!) due to the need to resolve promises on both success and error, however the nature of the problem and the limitations of $q encourage us in this direction.

In addition to all this, you will probably need a mechanism to ensure that items placed in the queue are unique. Duplicates would be at best wasteful, and at worst may cause errors.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top