0

I need to collect logs for different devices from a backend, to export it to a csv-file. Problem is the number of devices can vary. So I ask the backend for the amount of devices and loop through the requests for the logs.

The problem is the for... loop runs too faster than I get the responses to $.post, so the loop finishes before I get the responses. After some research I could handle that behaviour, but now I was asked to add data to the request, for which I have no reference to the according device is stored. So I added the device names and spots I need to poll in an external js-file, so I have a defined list to loop through.

I tried to use the index of the for loop to fetch the device names, which didn't work, since the loop was too fast. Now I have created a workaround, by defining a counter and pushing the devices in another variable. This doesn't feel "clean" and there should be a better way to poll the data and keep track for which device it is.

The code so far:

     function collectData() {
        var outString = "";
        var lots of stuff I can pre-fetch

        var logs = function (outString, saveCSV) {
           var postString;
           var devices = [];
           var count = 0;

           for (i = 1; i <= maxDevice; i++) {

              postString = build postString in loop
              devices.push(spots[0][i - 1]);

              $.post('/path/foobar.db',
                      postString,
                      function (data) {
                         outString += "Spotlist for: " + spots[0][count] + "\n";
                         count++;
                         outString += data.replace(/{/g, "").replace(/}/g, "").replace(/:/g, ";").replace(/,/g, "\n").replace(/\"/g, "");
                         outString += "\n\n";
                      });

              postString = "/path/eventlog.csv?device=" + i;
              $.get(postString,
                      function (data) {
                         outString += "Event Log: \n" + data + "\n";
                      });

              postString = "/path/errorlog.csv?device=" + i;
              $.get(postString,
                      function (data) {
                         outString += "Error Log: \n" + data + "\n";
                      });
           }

           $(document).ajaxStop(function () {
              saveCSV(outString, filename);
              $(this).unbind('ajaxStop');
           });
        };

        var saveCSV = function (outString, filename) {
           var tempString = "data:text/csv;charset=utf-8," + outString;
           var encodedUri = encodeURI(tempString);

           var a = document.getElementById("dlLink");

           if (window.navigator.msSaveOrOpenBlob) {
              blobObject = new Blob([outString], {type: 'text/csv;charset=utf-8'});
              window.navigator.msSaveBlob(blobObject, filename);
           } else
           {
              a.setAttribute("href", encodedUri);
              a.setAttribute("download", filename);
              a.click();
           }
        };

        outString = lots of predefined and pre-fetched stuff
        outString += "Device data: \n\n";

        logs(outString, saveCSV);
     }

The part which I am not satisfied with is:

          for (i = 1; i <= maxDevice; i++) {

              postString = "get = {" + i + ":en:[";
              for (j = 0; j < spots[i].length; j++) {
                 postString += '"' + spots[i][j] + '",';
              }
              postString = postString.slice(0, -1) + "]}";
              devices.push(spots[0][i - 1]);

              $.post('/path/foobar.db',
                      postString,
                      function (data) {
                         outString += "Spotlist for: " + spots[0][count] + "\n";
                         count++;
                         outString += data.replace(/{/g, "").replace(/}/g, "").replace(/:/g, ";").replace(/,/g, "\n").replace(/\"/g, "");
                         outString += "\n\n";
                      });

To output the device that I collected the spots for I use the counter, to track device names. I have the hunch this is not the best and "cleanest" method, so I'd like to ask if there is any better way to deal with the asynchronity (sp?) in terms of collecting the right device for which the post is made and also to trigger the DL if everything is done.

Since my question doesn't seem to be clear, perhaps I need to narrow it down. The code works, but it seems just to be tinkered by me and there should be cleaner ways to

A) handle the posts/gets, since the outstring for CSV is just put together in the way the requests are answered, so not device 1 is the first in the csv, but the one which comes first. $(document).ajaxStop waits for everything to be finished, but not to be finished in the right order.

B) I need to relate the index of the for loop to the device I poll the data for. I used additional variables, that I count up to go through an additional array. Is there any better way?

Blind Seer
  • 492
  • 1
  • 5
  • 17
  • 1
    It's very hard to understand your question. I think you can simplify the sample code and show a bit of working code. Please, see this: http://stackoverflow.com/help/mcve. But as far as I can see, you're not using the promises returned from the ajax calls, which is the key piece of handling asynchronous methods. Please, simplify your code show that someone can explain you how to do it. – JotaBe Jan 15 '16 at 09:20

2 Answers2

2

The problem is that you need to run in order the methods that are invoked after you get the response to the AJAX calls.

To do so you must undertand that all jQuery AJAX calls return promises. Instead of passing the code to run as a parameter, you can do the following:

var functionToRunAfterResponse(params) = function(params) {
   // do something with params
};

var promise = $.post(/*...*/); // some kind of ajax call

promise.then(functionToRunAfterResponse);

Note that the functionToRunAfterResponse will receive the response data as parameter. I.e. it's equivalent to:

promise.then(function(reponseData) {
  functionToRunAfterResponse(responseData);
});

This is how it works for a simple call. See $.then and deferred and promise docs.

If you have to make a bunch of calls, you have to do the following:

  • store the promises of the AJAX calls, for example in an array
  • check that all the promises are fulfilled (i.e. that all responses have arrived)
  • run the code in the required order. I.e. run the promise.then in the right order to get the desired result.

To do so, you can use $.when.

The code structure should be like this (pseudocode):

var promises = [];
// Make the calls, and store the promises
for(/**/) {
  promises.push( $.post or some other kind of ajax call );
}
// Ensure that all the responses have arrived
$.when.apply($, promises)  // See below note
.then(function() {
   for each promise in promises
      promise[i].then(function(reponseData) {
         // run the necessary code
      });

NOTE: Here you've got a deeper explanation, but, basically, as $.when expects a series of promises, like this: $.when(promise1, promise2, ...) and you have an array promises[], you have to use apply to make the call, so that the items in the array are passed as individual parameters.

FINAL NOTES:

1) take into account that AJAX calls can fail. In that case, instead of a resolved promise, you get a failed promise. You should check it. If any of the promises fails, $.when will not work as desired:

The method [when] will resolve its master Deferred as soon as all the Deferreds resolve, or reject the master Deferred as soon as one of the Deferreds is rejected.

2) To handle errors, then has two parameters:

$.when.apply().then(function() { /* success */ }, function() { /* error */ });

3) When you make the calls as I've explained, they are all executed in parallell, and the responses will arrive in any order. I.e. there is no warranty that you'll receive all the responses in the order you made the calls. They can even fail, as I explained in 1) That's why you must use when and run then in order again

4) Working with asynchronous methods is great, but you have the responsibility to check that they didn't fail, and run in the right order the code that you need to run after you get the responses.

Community
  • 1
  • 1
JotaBe
  • 38,030
  • 8
  • 98
  • 117
  • Thanks for the answer. I try to work through it and read on promises. I just found the Deferred Object for a single call so far. What about browser compatibility? When I check on caniuse.com it says promise is not supported before Edge on MS sided browsers? – Blind Seer Jan 15 '16 at 10:48
  • Don't worry. You can use jQuery promises in any browser supported by jQuery (vritually any usual browser). caniuse.com referes to *native promises*, which are quite recent, and, as you've seen not available in some browsers. There are other queue implementations like Q.js, which doesn't depend on the browser, but as you're using jQuery, you'd stuck to jQuery promises. – JotaBe Jan 15 '16 at 14:10
  • Works great. Thanks. Will try to work out the background to use it. But may I suggest corrections to your code? the for part needs to be `for(promise in promises){ promises[promise].then(function(responseData){...]});` I worked it out, since I mostly needed to understand the basics, but perhaps someone else will try to C&P it. – Blind Seer Jan 22 '16 at 12:16
  • Don't iterate the array like that! Please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in and specially the section https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Array_iteration_and_for...in You should use a helper library like lodash or underscore, or a simple for(var i=0;i – JotaBe Jan 22 '16 at 14:31
  • Ok, will read through it, but the code as posted did not work. The `for each promise in promises` part throws an exception and is pointed out in Netbeans with a red exclamation mark, stating "Expected ( but found promise});", so I took the first variant I figured out. Too bad it isn't good, it's less to type. Will change that part to a normal for-iteration – Blind Seer Jan 22 '16 at 14:49
  • JavaScript is not famous for letting you write concise code. In modern browsers, however there are some functions in the Array prototype that help you write less code: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach For compatibilty with older browsers you can use https://lodash.com/ or http://underscorejs.org/ as I told you. I also like the `for in`, but in JS doeen't work as expected (well, on most browser works as expected, but it's risky). For more details on support see aray.prototype on this table http://kangax.github.io/compat-table/es5/ – JotaBe Jan 22 '16 at 20:34
0

I can't understand your question, but the main problem is the asynchronity, right?

Try to call the functions asyncronously (summary version):

// Function that manage the data from ajax
var myResponseFunction = function (data) {
  $('#response').html('data: ' + JSON.stringify(data));
};

// Ajax function, like your post with one parameter (add all you need)
function myAJAXFunction(callback) {
    $.ajax({
        type: 'POST',
        url: '/echo/json/',
        dataType: 'json',
        data: {
            json: JSON.stringify({
                'foo': 'bar'
            })
        }
    }).done(function(response) {
      // Done!! Now is time to manage the answer
      callback(response);
    }).fail(function (jqXHR, textStatus, errorThrown) {
      window.console.error('Error ' + textStatus + ': ' + errorThrown);
    });
}

// Usually, this function it's inside "document.ready()".
// To avoid the ajax problem we call the function and "data manage function" as parameter.

for (i = 1; i <= maxDevice; i++) {
    myAJAXFunction(myResponseFunction);
}

https://jsfiddle.net/erknrio/my1jLfLr/

This example is in spanish but in this answer you have the code commented in english :).

Sorry for my english :S.

mrroot5
  • 1,811
  • 3
  • 28
  • 33