richie
richie

Reputation: 467

Multiple Promise Chains in Single Function

I have some code that will dynamically generate an AJAX request based off a scenario that I'm retrieving via an AJAX request to a server.

The idea is that:

  1. A server provides a "Scenario" for me to generate an AJAX Request.
  2. I generate an AJAX Request based off the Scenario.
  3. I then repeat this process, over and over in a Loop.

I'm doing this with promises here: http://jsfiddle.net/3Lddzp9j/11/

However, I'm trying to edit the code above so I can handle an array of scenarios from the initial AJAX request.

IE:

{
"base": {
    "frequency": "5000"
},
"endpoints": [
    {
        "method": "GET",
        "type": "JSON",
        "endPoint": "https://api.github.com/users/alvarengarichard",
        "queryParams": {
            "objectives": "objective1, objective2, objective3"
        }
    },
    {
        "method": "GET",
        "type": "JSON",
        "endPoint": "https://api.github.com/users/dkang",
        "queryParams": {
            "objectives": "objective1, objective2, objective3"
        }
    }
]

This seems like it would be straight forward, but the issue seems to be in the "waitForTimeout" function.

I'm unable to figure out how to run multiple promise chains. I have an array of promises in the "deferred" variable, but the chain only continues on the first one--despite being in a for loop.

Could anyone provide insight as to why this is? You can see where this is occuring here: http://jsfiddle.net/3Lddzp9j/10/

Upvotes: 0

Views: 619

Answers (4)

JLRishe
JLRishe

Reputation: 101680

You have a return statement in the loop in your waitForTimeout function. This means that the function is going to return after the first iteration of the loop, and that is where you are going wrong.

You're also using the deferred antipattern and are using promises in places where you don't need them. You don't need to return a promise from a then handler unless there's something to await.

The key is that you need to map each of your instructions to a promise. Array#map is perfect for this. And please use a proper promise library, not jQuery promises (edit but if you absolutely must use jQuery promises...):

var App = (function ($) {
    // Gets the scenario from the API
    // NOTE: this returns a promise
    var getScenario = function () {
        console.log('Getting scenario ...');
        return $.get('http://demo3858327.mockable.io/scenario');
    };

    // mapToInstructions is basically unnecessary. each instruction does
    // not need its own timeout if they're all the same value, and you're not
    // reshaping the original values in any significant way

    // This wraps the setTimeout into a promise, again
    // so we can chain it
    var waitForTimeout = function(data) {
        var d = $.Deferred();
        setTimeout(function () {
            d.resolve(data.endpoints);
        }, data.base.frequency);
        return d.promise();
    };

    var callApi = function(instruction) {
        return $.ajax({
            type: instruction.method,
            dataType: instruction.type,
            url: instruction.endPoint
        });
    };

    // Final step: call the API from the 
    // provided instructions
    var callApis = function(instructions) {
        console.log(instructions);
        console.log('Calling API with given instructions ...');
        return $.when.apply($, instructions.map(callApi));
    };

    var handleResults = function() {
        var data = Array.prototype.slice(arguments);
        console.log("Handling data ...");
    };

    // The 'run' method
    var run = function() {
        getScenario()
        .then(waitForTimeout)
        .then(callApis)
        .then(handleResults)
        .then(run);
    };

    return {
        run : run
    }
})($);

App.run();

Upvotes: 1

guest271314
guest271314

Reputation: 1

Try utilizing deferred.notify within setTimeout and Number(settings.frequency) * (1 + key) as setTimeout duration; msg at deferred.notify logged to console at deferred.progress callback , third function argument within .then following timeout

    var App = (function ($) {
    
        var getScenario = function () {
            console.log("Getting scenario ...");
            return $.get("http://demo3858327.mockable.io/scenario2");
        };
        
        var mapToInstruction = function (data) {
            var res = $.map(data.endpoints, function(settings, key) {
                return {
                    method:settings.method,
                    type:settings.type,
                    endpoint:settings.endPoint,
                    frequency:data.base.frequency
                }
            });
            
            console.log("Instructions recieved:", res);
    
            return res
        };
        
        var waitForTimeout = function(instruction) {        
               var res = $.when.apply(instruction, 
                  $.map(instruction, function(settings, key) {
                    return new $.Deferred(function(dfd) {                    
                        setTimeout(function() {  
                          dfd.notify("Waiting for " 
                                    + settings.frequency 
                                    + " ms")
                         .resolve(settings);                 
                        }, Number(settings.frequency) * (1 + key));
                    }).promise()
                 })
               )
               .then(function() {
                 return this
               }, function(err) {
                 console.log("error", err)
               }
               , function(msg) {
                 console.log("\r\n" + msg + "\r\nat " + $.now() + "\r\n")
               });
               return res
        };
    
        var callApi = function(instruction) {
            console.log("Calling API with given instructions ..."
                       , instruction);
            var res = $.when.apply(instruction, 
              $.map(instruction, function(request, key) {
                return request.then(function(settings) {
                  return $.ajax({
                    type: settings.method,
                    dataType: settings.type,
                    url: settings.endpoint
                  }); 
                })                                                       
              })
            )
            .then(function(data) {
                return $.map(arguments, function(response, key) {              
                    return response[0]
                })
            })
            return res
        };
        
        var handleResults = function(data) {
            console.log("Handling data ..."
                        , JSON.stringify(data, null, 4));
            return data
        };
        
        var run = function() {
            getScenario()
            .then(mapToInstruction)
            .then(waitForTimeout)
            .then(callApi)
            .then(handleResults)
            .then(run);
        };
        
        return {
            // This will expose only the run method
            // but will keep all other functions private
            run : run
        }
    })($);
    
    // ... And start the app
    App.run();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>

jsfiddle http://jsfiddle.net/3Lddzp9j/13/

Upvotes: 1

Roamer-1888
Roamer-1888

Reputation: 19288

The main problems are that :

  • waitForTimeout isn't passing on all the instructions
  • even if waitForTimeout was fixed, then callApi isn't written to perform multiple ajax calls.

There's a number of other issues with the code.

  • you really need some data checking (and associated error handling) to ensure that expected components exist in the data.
  • mapToInstruction is an unnecessary step - you can map straight from data to ajax options - no need for an intermediate data transform.
  • waitForTimeout can be greatly simplified to a single promise, resolved by a single timeout.
  • synchronous functions in a promise chain don't need to return a promise - they can return a result or undefined.

Sticking with jQuery all through, you should end up with something like this :

var App = (function ($) {
    // Gets the scenario from the API
    // sugar for $.ajax with GET as method - NOTE: this returns a promise
    var getScenario = function () {
        console.log('Getting scenario ...');
        return $.get('http://demo3858327.mockable.io/scenario2');
    };

    var checkData = function (data) {
        if(!data.endpoints || !data.endpoints.length) {
            return $.Deferred().reject('no endpoints').promise();
        }
        data.base = data.base || {};
        data.base.frequency = data.base.frequency || 1000;//default value
    };

    var waitForTimeout = function(data) {
        return $.Deferred(function(dfrd) {
            setTimeout(function() {
                dfrd.resolve(data.endpoints);
            }, data.base.frequency);
        }).promise();
    };

    var callApi = function(endpoints) {
        console.log('Calling API with given instructions ...');
        return $.when.apply(null, endpoints.map(ep) {
            return $.ajax({
                type: ep.method,
                dataType: ep.type,
                url: ep.endpoint
            }).then(null, function(jqXHR, textStatus, errorThrown) {
                return textStatus;
            });
        }).then(function() {
            //convert arguments to an array of results
            return $.map(arguments, function(arg) {
                return arg[0];
            });
        });
    };

    var handleResults = function(results) {
        // results is an array of data values/objects returned by the ajax calls.
        console.log("Handling data ...");
        ...
    };

    // The 'run' method
    var run = function() {
        getScenario()
        .then(checkData)
        .then(waitForTimeout)
        .then(callApi)
        .then(handleResults)
        .then(null, function(reason) {
            console.error(reason);
        })
        .then(run);
    };

    return {
        run : run
    }
})(jQuery);

App.run();

This will stop on error but could be easily adapted to continue.

Upvotes: 1

Mauricio Poppe
Mauricio Poppe

Reputation: 4876

I'll try to answer your question using KrisKowal's q since I'm not very proficient with the promises generated by jQuery.

First of all I'm not sure whether you want to solve the array of promises in series or in parallel, in the solution proposed I resolved all of them in parallel :), to solve them in series I'd use Q's reduce

function getScenario() { ... }

function ajaxRequest(instruction) { ... }

function createPromisifiedInstruction(instruction) {
  // delay with frequency, not sure why you want to do this :(
  return Q.delay(instruction.frequency)
    .then(function () {
      return this.ajaxRequest(instruction);
    });
}

function run() {
  getScenario()
    .then(function (data) {
      var promises = [];
      var instruction;
      var i;
      for (i = 0; i < data.endpoints.length; i += 1) {
        instruction = {
          method: data.endpoints[i].method,
          type: data.endpoints[i].type,
          endpoint: data.endpoints[i].endPoint,
          frequency: data.base.frequency
        };
        promises.push(createPromisifiedInstruction(instruction));   
      }
      // alternative Q.allSettled if all the promises don't need to
      // be fulfilled (some of them might be rejected)
      return Q.all(promises);
    })
    .then(function (instructionsResults) {
      // instructions results is an array with the result of each
      // promisified instruction
    })
    .then(run)
    .done();
}

run();

Ok let me explain the solution above:

  1. first of all assume that getScenario gets you the initial json you start with (actually returns a promise which is resolved with the json)
  2. create the structure of each instruction
  3. promisify each instruction, so that each one is actually a promise whose resolution value will be the promise returned by ajaxRequest
  4. ajaxRequest returns a promise whose resolution value is the result of the request, which also means that createPromisifiedInstruction resolution value will be the resolution value of ajaxRequest
  5. Return a single promise with Q.all, what it actually does is fulfill itself when all the promises it was built with are resolved :), if one of them fails and you actually need to resolve the promise anyways use Q.allSettled
  6. Do whatever you want with the resolution value of all the previous promises, note that instructionResults is an array holding the resolution value of each promise in the order they were declared

Reference: KrisKowal's Q

Upvotes: 1

Related Questions