0

I am currently working on a web based time tracking software. I'm developing in grails, but this question is solely related to javascript and asynchronous requests.

The time tracking tool shall enable users to choose a day for the current month, create one or multiple activities for each day and save the entire day. Each activity must be assigned to a project and a contract.

Upon choosing "save", the partial day is saved to the database, the hours are calculated and a table is updated at the bottom of the page, showing an overview of the user's worked hours per month.

Now to my issue: There may be a lot of AJAX request. Patient users might only click the "create activity" button just once and wait until it is created. Others, however, might just keep clicking until something happens.

The main issue here is updating the view, although i also recognized some failed calls because of concurrent database transaction (especially when choosing "save" and "delete" sequentially). Any feedback on that issue -- requests not "waiting" for the same row to be ready again -- will be apreciated as well, yet this is not my question.

I have an updateTemplate(data, day) function, which is invoked onSuccess of respective ajax calls in either of my functions saveRecord(), deleteRecord(), pasteRecords(), makeEditable() (undo save). Here is the example AJAX call in jquery:

$.ajax({
    type: "POST",
    url: "${g.createLink(controller:"controller", action:"action")}",
    data: requestJson,
    contentType:"application/json; charset=utf-8",  
    async: true,
    success: function(data, textstatus) {updateTemplate(data["template"], tag); updateTable(data["table"]);},
}); 

In the controller action, a JSON object is rendered as a response, containing the keys template and table. Each key has a template rendered as a String assigned to it, using g.render.

Now, what happens when I click on create repeatedly in very short intervalls, due to the asynchronous calls, some create (or other) actions are executed concurrently. The issue is that updateTemplate just renders data from the repsonse; the data to render is collected in the create controller action. But the "last" request action only finds the objects created by itself. I think this is because create actions are run concurrently

I figure there is something I'm either overcomplicating or doing something essentially wrong working with a page that refreshs dynamically. The only thing I found that helps are synchronous calls, which works, but the user experience was awful. What options do I have to make this work? Is this really it or am I just looking for the wrong approach? How can I make this all more robust, so that impatient users are not able to break my code?

*********EDIT:********

I know that I could block buttons or keyboard shortcuts, use synchronous calls or similar things to avoid those issues. However, I want to know if it is possible to solve it with multiple AJAX requests being submitted. So the user should be able to keep adding new activities, although they won't appear immediately. There is a spinner for feedback anyway. I just want to somehow make sure that before the "last" AJAX request gets fired, the database is up to date so that the controller action will respond with the up-to-date gsp template with the right objects.

nst1nctz
  • 333
  • 3
  • 23
  • What about blocking the button until you have your response (+ showing any preloader)? I don't understand what you want to to tell in the other parts what's going wrong with your code... – hffmr Sep 09 '14 at 09:27
  • Hi, thank you for your reply. This is a good idea, but you can also use shortcuts. I am just generally wondering if this is a bad approach to update the view or if it can be improved. Half of the tool exists in legacy code and I'm trying to make this work without restarting from scratch with a suitable javascript framework. I basically now what's going wrong, I just don't seem to find anything on how to handle many concurrent AJAX calls. – nst1nctz Sep 09 '14 at 09:31
  • You can also block shortcuts... I guess you call on button-click and on shortcut one function and in this function you can add some condition which checks whether there is a running call or not (set `running = true` before call and onSuccess `running = false`, that's it) – hffmr Sep 09 '14 at 09:33
  • I think I understand what you mean. I already tried to use a global variable `noAjaxRunning` for other ideas. But isn't that basically the same as using synchronous calls? Basically I do not want to block anything. I want the user to be able to do like 100 requests at the same time, I just want to make sure that the row is saved in the database until the next ajax call is fired -- but I don't want synchronous calls (if evitable)! The script should not stop because I know the users won't like it. Not possible? – nst1nctz Sep 09 '14 at 09:45
  • The script won't stop. You will only block the function which would cause the next AJAX call, nothing else. All other actions aren't affected. – hffmr Sep 09 '14 at 12:25
  • To your edit: In fact your blockade won't stay there for 5h or something like that. Check it out and you will see that you have to wait about 2 seconds (max). - You say you want to block the calls but you don't want to. Anyway, one request won't overtake the other, that shouldn't be the problem, so the answer will ever contain the actual response of this call, not the last and not the next, the actual one. – hffmr Sep 09 '14 at 12:31
  • I totally understand your point when you say the lock is not bad. In fact, it IS acceptable, rather than synchronous requests, where the entire script is blocked and it looks to the user as if the app froze. But I want to know how I can manage it without the user recognizing a change, not even a disabled button -- let alone the spinner. I found a solution that works for me, and I will post it as an answer for my own question soon. If my question did not address the actual topic or is poor in general, let me know and I'll update it. – nst1nctz Sep 09 '14 at 13:41
  • @Admin/Editor: I do not understand why you removed the tags `javascript` and `ajax`. I only used jquery syntax (which is also javascript) so far, but my solution, for instance, contains plain javascript as well. To me, this is a javascript question above all. And `ajax` was also an important tag to me, because I was expecting input on how to generally handle multiple asynchronous requests sent with the XMLHttpObject -- which is considered an AJAX call as far as I know. – nst1nctz Sep 09 '14 at 13:54

1 Answers1

0

With help of this Stackoverflow answer, I found a way to ensure that the ajax call -- in the javascript function executed lastly -- always responds with an up-to-date model. Basically, I put the javascript functions containing AJAX calls in a waiting queue if a "critical" AJAX request has been initiated before but not completed yet.

For that I define the function doCallAjaxBusyAwareFunction(callable) that checks if the global variable Global.busy is 'true' prior to executing the callable function. If it's true, the function will be executed again until Global.busy is false, to finally execute the function -- collecting the data from the DOM -- and fire the AJAX request.

Definition of the global Variable:

var Global = {
    ajaxIsBusy = false//,
    //additional Global scope variables
};

Definition of the function doCallAjaxBusyAwareFunction:

function doCallAjaxBusyAwareFunction(callable) {
        if(Global.busy == true){
           console.log("Global.busy = " + Global.busy + ". Timout set! Try again in 100ms!!");  
           setTimeout(function(){doCallAjaxBusyAwareFunction(callable);}, 100);
        }     
        else{                
            console.log("Global.busy = " + Global.busy + ". Call function!!");   
            callable();             
        }
    }

To flag a function containing ajax as critical, I let it set Global.busy = true at the very start and Global.busy = false on AJAX complete. Example call:

function xyz (){
    Global.busy = true;
    //collect ajax request parameters from DOM

    $.ajax({
        //desired ajax settings
        complete: function(data, status){ Global.busy = false; }
    }

Since Global.busy is set to true at the very beginning, the DOM cannot be manipulated -- e.g. by deletes while the function xyz collects DOM data. But when the function was executed, there is still Global.busy === true until the ajax call completes.

Fire an ajax call from a "busy-aware" function:

doCallAjaxBusyAwareFunction(function(){
    //collect DOM data

    $.ajax({/*AJAX settings*/});
});

....or fire an ajax call from a "busy-aware" function that is also marked critical itself (basically what I mainly use it for):

doCallAjaxBusyAwareFunction(function(){
    Global.busy = true;
    //collect DOM data

    $.ajax({
        //AJAX SETTINGS
        complete: function(data, status){ Global.busy = false; }
    });
});

Feedback is welcome and other options too, especially if this approach is bad practice. I really hope somebody finds this post and evaluates it, since I don't know if it should be done like that at all. I will leave this question unanswered for now.

Community
  • 1
  • 1
nst1nctz
  • 333
  • 3
  • 23