2

I have a pretty straight-forward problem where I'm :

  1. Iterating through a series of dashboard "widgets" using _.each().
  2. Calling a function to refresh the current widget, and returning a $q promise.

Now, my issue is that I would like each iteration to WAIT prior to continuing to the next iteration.

My first version was this, until I realized that I need to wait for updateWidget() to complete:

_.each(widgets, function (wid) {
  if (wid.dataModelOptions.linkedParentWidget) {
      updateWidget(wid, parentWidgetData);
  }
});

My second version is this one, which returns a promise. But of course, I still have the problem where the iteration continues without waiting :

_.each(widgets, function (wid) {
  if (wid.dataModelOptions.linkedParentWidget) {
    updateWidget(wid, parentWidgetData).then(function(data){
      var i = 1;
    });
  }
});

and the called function which returns a deferred.promise object (then makes a service call for widget data) :

function updateWidget(widget, parWidData) {
    var deferred = $q.defer();

    // SAVE THIS WIDGET TO BE REFRESHED FOR THE then() SECTION BELOW 
    $rootScope.refreshingWidget = widget;

    // .. SOME OTHER VAR INITIALIZATION HERE...       
    
    var url = gadgetDataService.prepareAggregationRequest(cubeVectors, aggrFunc, typeName, orderBy, numOrderBy, top, filterExpr, having, drillDown);
    
    return gadgetDataService.sendAggGetRequest(url).then(function (data) {
 var data = data.data[0];
 var widget = {};
 if ($rootScope.refreshingWidget) {       
     widget = $rootScope.refreshingWidget;
 }
 // BUILD KENDO CHART OPTIONS
 var chartOptions = chartsOptionsService.buildKendoChartOptions(data, widget);                

 // create neOptions object, then use jquery extend()
 var newOptions = {};
 $.extend(newOptions, widget.dataModelOptions, chartOptions);
 widget.dataModelOptions = newOptions;

 deferred.resolve(data);
    });
    
    return deferred.promise;
}

I would appreciate your ideas on how to "pause" on each iteration, and continue once the called function has completed.

thank you, Bob

******* UPDATED ************

My latest version of the iteration code include $q.all() as follows :

// CREATE ARRAY OF PROMISES !!
var promises = [];
_.each(widgets, function (wid) {
  if (wid.dataModelOptions.linkedParentWidget) {
    promises.push(updateWidget(wid, parentWidgetData));
  }
});
$q.all(promises)
.then(function () {
  $timeout(function () {
    // without a brief timeout, not all Kendo charts will properly refresh.
    $rootScope.$broadcast('childWidgetsRefreshed');
  }, 100);              
});    
bob.mazzo
  • 5,183
  • 23
  • 80
  • 149
  • Do you "need" to pause after each or would it be acceptable to queue them up and execute them all together? – jbrown Dec 08 '15 at 20:32
  • @jbrown it would be acceptable to queue up as you suggested, however, I'm having an issue with the scope of my `widget` parameter sent into the `updateWidget` function. I need to see the contents of `widget` down in the `.then (data)` section of `gadgetDataService.sendAggGetRequest()`; however, I lose its scope. Hence, I've assigned it on rootscope this way: `$rootScope.refreshingWidget = widget;` – bob.mazzo Dec 08 '15 at 20:41
  • @jbrown In other words, look at `$rootScope.refreshingWidget = widget;` , then further down you'll see how I pull that scope var like this `widget = $rootScope.refreshingWidget;` – bob.mazzo Dec 08 '15 at 20:42

2 Answers2

4

By chaining promises

The easiest is the following:

var queue = $q.when();
_.each(widgets, function (wid) {
  queue = queue.then(function() {
    if (wid.dataModelOptions.linkedParentWidget) {
      return updateWidget(wid, parentWidgetData);
    }
  });
});
queue.then(function() {
  // all completed sequentially
});

Note: at the end, queue will resolve with the return value of the last iteration

If you write a lot of async functions like this, it might be useful to wrap it into a utility function:

function eachAsync(collection, cbAsync) {
  var queue = $q.when();
  _.each(collection, function(item, index) {
    queue = queue.then(function() {
      return cbAsync(item, index);
    });
  });
  return queue;
}

// ...
eachAsync(widgets, function(wid) {
  if (wid.dataModelOptions.linkedParentWidget) {
    return updateWidget(wid, parentWidgetData);
  }
}).then(function() {
  // all widgets updated sequentially
  // still resolved with the last iteration
});

These functions build a chain of promises in the "preprocessing" phase, so your callback is invoked sequentially. There are other ways to do it, some of them are more efficient and use less memory, but this solution is the simplest.

By delayed iteration

This method will hide the return value even of the last iteration, and will not build the full promise chain beforehands. The drawback is that, it can be only used on array like objects.

function eachAsync(array, cbAsync) {
  var index = 0;
  function next() {
    var current = index++;
    if (current < array.length) {
      return $q.when(cbAsync(array[current], current), next);
    }
    // else return undefined
  }
  // This will delay the first iteration as well, and will transform
  // thrown synchronous errors of the first iteration to rejection.
  return $q.when(null, next); 
}

This will iterate over any iterable:

function eachAsync(iterable, cbAsync) {
  var iterator = iterable[Symbol.iterator]();
  function next() {
    var iteration = iterator.next();
    if (!iteration.done) {
      // we do not know the index!
      return $q.when(cbAsync(iteration.value), next);
    } else {
      // the .value of the last iteration treated as final
      // return value
      return iteration.value;
    }
  }
  // This will delay the first iteration as well, and will transform
  // thrown synchronous errors of the first iteration to rejection.
  return $q.when(null, next); 
}

Keep in mind that these methods will behave differently when the collection changes during iteration. The promise chaining methods basically build a snapshot of the collection at the moment it starts iteration (the individual values are stored in the closures of the chained callback functions), while the latter does not.

Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97
  • I believe the queuing idea is exactly what I need. Both answers are related to this, but yours appears to be closer to what I need. – bob.mazzo Dec 08 '15 at 21:07
  • The other problem I'm facing with this iteration is in the `gadgetDataService.sendAggGetRequest(url).then` section of `updateWidget(widget, parWidData)`. In the .then section I need some information about the`widget` parameter. Putting on the `$rootScope` is not the best solution because the next iteration overwrites the $rootScope var. – bob.mazzo Dec 08 '15 at 21:26
  • 1
    @bob I have good news for you! You already have `widget` in your scope, but you are hiding it with `var widget = {};`. Delete that line and you have acces to your widget, as it is the parameter of the outer function. You dont need `$rootScope.refreshingWidget` at all. – Tamas Hegedus Dec 08 '15 at 21:28
  • I appreciate your input on this. I also tested the other answer re: `$q.all()`, which appears to serve me better in my scenario. – bob.mazzo Dec 09 '15 at 17:00
  • @bob Don't forget using `$q.all` will execute your tasks in *parallel*. In your question you stated that you want sequential execution. If parallel execution is what you want, then certainly `$q.all` is the way to go. – Tamas Hegedus Dec 09 '15 at 18:14
  • yes it's true; however, I expected that the `queue.then` section would be triggered only AFTER all my iterations were completed. But it got triggered immediately, even before all of my updateWidget() calls were completed. So after testing `$q.all()`, the `$q.all(promises).then` section triggered as I expected. I think there's something I'm not seeing clearly on the `q.when` implementation. – bob.mazzo Dec 09 '15 at 21:47
  • @bob the final `queue.then` section is invoked after all the tasks finished. You must be missing something. Please take a look at this fiddle: http://jsfiddle.net/U3pVM/20742/ what `$q.all` does it returns with a promise that aggregates the resolution values of the parallel pending promises given as parameter, or with the first rejection if there is any. – Tamas Hegedus Dec 10 '15 at 09:24
1

Instead of trying to resolve each promise in your _.each(), I would build out an array of promises in your _.each to get an array like:

promises = [gadgetDataService.sendAggGetRequest(url1), gadgetDataService.sendAggGetRequest(url2)....]

Then resolve them all at once, iterate through the results and set your models:

$q.all(promises).then(function(results){ // iterate through results here })
jbrown
  • 3,025
  • 1
  • 15
  • 22
  • cool idea, but I think I would have to move it up one level because each `widget` is unique (i.e. I need to run through `updateWidget()` for each one). Then at the end of the entire iteration, I trigger the `$broadcast`. – bob.mazzo Dec 08 '15 at 20:45
  • There's an extended post on this scope issue I'm facing - http://stackoverflow.com/questions/28250680/how-do-i-access-previous-promise-results-in-a-then-chain – bob.mazzo Dec 08 '15 at 20:48
  • 1
    @bob the post you linked is rather about the basic code style and patterns when using promises. While that is an excellent post, does not cover the case when iterating over arrays. However, `async-await` combined with `for..of` can be useful for you. `async-await` makes branching and looping easy. – Tamas Hegedus Dec 08 '15 at 21:26
  • @jbrown - I'm trying your `$q.all` idea as well; however the functions execute immediately inside my iteration as soon as I push() them : `promises.push(updateWidget(wid, parentWidgetData));` . I thought it would defer the call until I run `$q.all(promises)`. Am I doing something wrong ? – bob.mazzo Dec 09 '15 at 15:29
  • @bob - has your updateWidget function changed from your original post? – jbrown Dec 09 '15 at 16:57
  • @jbrown - No, it hasn't. Please see my *** UPDATE *** section at the end of my original post. It shows my updated iteration with q.all(). In fact your `$q.all()` is working good for me. And it's consistently entering the .then() as expected. The only thing I did was add a 100ms timer inside .then() function before broadcasting my refresh event. Works good now all the way through. – bob.mazzo Dec 09 '15 at 17:02