Michael Tallino
Michael Tallino

Reputation: 821

$.when.apply on array of deferreds runs before all resolved

Trying to build a prerequisite chain for some classes. Because of unknown recursion depth, I have an ajax call which runs every time a new course is created. DFDS is a global array to store deferreds in. COURSES is a global array of courses.

var DFDS = []; //define global DFDS
var course = function( options ) {
  ...
  var self = this;
  this.p = { //prereqs object
    a: [], // array for prereqs
    rgx: /([A-Z]\w+[\ ])\w+/g, 
    parse: function() {
      if (self.prereqs == '') return true;
      self.prereqs.replace(this.rgx,function(m) {
        //search through prereq string (self.prereqs) and run ajax for each match
        var id = m.split(' ');
        var dfd2 = $.Deferred();
        DFDS.push(dfd2);
        $.getJSON('/ajax/ajaxPrereqs.php',{subj:id[0],crs:id[1]},function(d) {
          var d = d[0];
          self.p.a.push(new course({
            ... //create new course in self.p.a[]
          }));
          dfd2.resolve();
        });
      });
    }
  };
  ...
  //run parse function when created
  this.p.parse();
  return this;
}

I'm able to get the structure I expect with all the right courses loaded into all the right self.p.a[] arrays.

So to initialize the top-level courses:

$.getJSON('/ajax/ajaxPrereqs.php',{subj:$('#subj').val()}, function(data) {

  $.each(data,function() {
    var d = this;
    COURSES.push(new course({
      s: d.subj,
      c: d.crs,
      t: d.titleshrt,
      crd: d.credits,
      chr: d.contacthrs,
      prereqs: d.prereqs
    }));
  });


  console.log(DFDS.length); //displays 10
  $.when.apply($, DFDS).then(function() {
    console.log('DFDS all done???');
    console.log(DFDS.length);
    $.each(DFDS,function() {console.log(this.state())})
    $.each(COURSES,function() {
      this.render();
    })
  });

});

PROBLEM: $.when.apply is running BEFORE all deferreds resolve. My console says:

24
(10) resolved
(14) pending

I also tried running this logging function every 5 milliseconds:

var interval = setInterval(function() {
  console.log('--- intv ---');
  var pending = 0;
  var resolved = 0;
  $.each(DFDS,function() {
    switch(this.state()) {
      case 'pending': pending++; break;
      case 'resolved': resolved++; break;
      default: console.log('NOT PENDING OR RESOLVED');
    }
  });
  console.log(pending+' pending');
  console.log(resolved+' resolved');
},5);

The last console entry right before $.when.apply runs is 14 pending, 8 resolved.

There are 92 ajax calls when this completes. All 92 return good data (no errors / fails).

How can I tell it to run .then() after all DFDs in array (not just ones in array when $.when.apply defined?)

Upvotes: 0

Views: 856

Answers (2)

jfriend00
jfriend00

Reputation: 707496

Here's a general idea. I had several goals here.

  1. Get rid of the global variable that you were accumulating promises into.
  2. Make all nested promises be chained onto their parent promises so that you can just wait on the parent promises and everything will just work.
  3. To chain promises, I replace the completion callback in $.getJSON() with a .then() handler and then return the embedded promises from the .then() handler. This automatically chains them onto the parent so the parent won't itself get resolved until the embedded ones are also resolved. And, this works to an arbitrary depth of embedded promises.
  4. Get rid of the anti-pattern of creating and resolving new deffereds when promises already exist that can just be used.

In the process of getting rid of the globals, I had to remove the creation of promises from the course constructor because it can't return the promises. So, I created a course.init() where the promises are created and returned. Then, we can accumulate promises in a local variable and avoid the global.

So, here's the general idea:

var course = function( options ) {
  ...
  var self = this;

  this.p = { //prereqs object
    a: [], // array for prereqs
    rgx: /([A-Z]\w+[\ ])\w+/g, 
    parse: function() {
      var promises = [];
      // check if we have any prereqs to process
      if (self.prereqs !== '') {
          self.prereqs.replace(this.rgx,function(m) {
            //search through prereq string (self.prereqs) and run ajax for each match
            var id = m.split(' ');
            promises.push($.getJSON('/ajax/ajaxPrereqs.php',{subj:id[0],crs:id[1]}).then(function(d) {
              var d = d[0];
              var c = new course({
                ... //create new course in self.p.a[]
              }));
              self.p.a.push(c);
              // chain all the new promises created by c.init() onto our master promise
              // by returning a new promise from the .then() handler
              return(c.init());
            }));        
          });
      }
      // return a master promise that is resolve when all the sub promises 
      // created here are all done
      return $.when.apply($, promises);
    }
  };

  // call this to run the initial parse
  // returns a single promise that is resolve when all the promises are done
  this.init = function() {
      return this.p.parse();
  };

  ...
  return this;
}

$.getJSON('/ajax/ajaxPrereqs.php',{subj:$('#subj').val()}, function(data) {

  var promises = [];
  var COURSES = [];
  $.each(data,function() {
    var d = this;
    var c = new course({
      s: d.subj,
      c: d.crs,
      t: d.titleshrt,
      crd: d.credits,
      chr: d.contacthrs,
      prereqs: d.prereqs
    });
    COURSES.push(c);
    promises.push(c.init());    
  });
  $.when.apply($, promises).then(function() {
    console.log('promises all done');
    console.log(promises.length);
    $.each(promises,function() {console.log(this.state())})
    $.each(COURSES,function() {
      this.render();
    })
  });
});

Upvotes: 1

Michael Tallino
Michael Tallino

Reputation: 821

This worked. I appreciate everyone's comments. It helped me realize the issue was $.when.apply was watching only the deferreds in DFDS when $.when.apply was defined.

I put this function in the $.getJSON callback in course constructor .parse().

function checkDFDS() {
  var completed = 0;
  $.each(DFDS,function() {
    if (this.state()=='resolved') completed++;
  });
  if (completed == DFDS.length) {
    $.each(COURSES,function() {
      this.render();
    })
  }
}

Upvotes: 0

Related Questions