Search This Blog

Showing posts with label JavaScript. Show all posts
Showing posts with label JavaScript. Show all posts

2011-11-21

Controlling Ajax requests flow

While developing rich internet applications which contain a lot of Ajax requests, sooner or later you come to the idea of controlling requests flow. Users are continuously interacting with site(s) GUI that causes massive Ajax requests which are passed through load balances, firewalls, virtual servers etc. Problem might happen on any layer - from dead lock in database to exceptions in firewall; at the same time, user does not know and care about all that complex issues on the background. 
There are several ways to manage Ajax requests flow and I will show on of them: controlling request queue with JavaScript and jQuery.
The idea is not to bomb overloaded server with tons of requests. Instead, we assume if requests are processed to long - there is some issue on the server side and we should react somehow. With decribed approach we are organizing the queue of requests, but quick ones can still be processed simultaneously.
First, let's synthesize idea with tests. Let's imagine we have some web service that processes one request per two seconds. Our web client is continuously sending requests to the service.
module("Test.RequestController.js");

asyncTest("Test: sending requests", 6, function () {
  var url = "/TestWebService";
  var data = { "testData": "data" };

  var controller = new RequestController();
  controller.ajax({'url':url,'data':data}).done(function () {
    ok("done1");
  });
  controller.ajax({'url':url,'data':data}).done(function () {
    ok("done2");
  });
  setTimeout(function () {
    controller.ajax({'url':url,'data':data}).done(function () {
      ok("done3");
    });
    controller.ajax({'url':url,'data':data}).done(function () {
      ok("done4");
    });
  }, 1000);
  setTimeout(function () {
    controller.ajax({'url':url,'data':data}).done(function () {
      ok("done5");
    });
    controller.ajax({'url':url,'data':data}).done(function () {
      ok("done6");
      start();
    });
  }, 2000);
});
Our test service has two seconds delay. But, let's say after tree seconds delay, user should be warned about the problem and/or ajax requests should be redirected. We can wrap jQuery.ajax function with our Request controller which encapsulates required logic.
First, let's define constructor with preset data.
RequestController = function () {
  this.requestCounter = 0; // Number of too long requests
  this.timeout = 3000; // Default timeout for "long" requests - 3 sec
  this.maxLongRequests = 3; // Max amount of simultaneous long requests 

  this.requestPromise = jQuery(this).promise();
};
Then we should wrap jQuery ajax with our queue:
RequestController.prototype.ajax = function(ajaxParams) {
  var ajaxRequest = function() {
    var isLongRequest = false;

    var ajaxPromise = jQuery.ajax(ajaxParams).always(function() {
      if (isLongRequest) {
        this.requestCounter--;
      }
    }.bind(this));

    setTimeout(function() {
      if (ajaxPromise.state() == "pending") {
        this.requestCounter++;
        isLongRequest = true;
      }
    }.bind(this), this.timeout);

    return ajaxPromise;
  }.bind(this);


  if (this.requestCounter >= this.maxLongRequests) {
    // show warning to the user
    // and warn system admin or redirect request
    return this.requestPromise = this.requestPromise.pipe(ajaxRequest);
  } else {
    return this.requestPromise = ajaxRequest();
  }
};
The idea is simple enough: we have a wrapper function that calls jQuery.ajax; on request complete/fail we check: if request was too "long" we just decrease the pending requests counter. On setTimeout function we are checking whether request is finished; if not - request can be considered as long: we increase the counter and switch the flag of long request.
Finally, we check whether flow reached maximum allowed number of long requests: if not - just return request promise to the caller. If it reached - we can send a warning and put request to the queue described earlier.
The request flow look like this:


2011-10-31

jQuery Deferred queue

Since jQuery 1.5 had been released some very good feature called Deferred Object appeared for better callbacks handling and other kinds of synchronizing calls.
Deferred API has "jQuery.when()" which allows to multiplex deferreds (waiting on multiple deferreds at the same time)... and has "pipe()" to create a pipeline or chain of deferreds (since version 1.6). While this is very nice if all deferreds are available at the same point, it doesn't really convenient if there is a collections of deferreds coming from multiple sources (which may not be aware of one another). This plugin allows to solve the problem at some point. But it was based on previous version... before pipe() feature.
My simple solution also tries to handle multiplexing deferreds from different sources using shared Queue object.
Idea is to have a queue that is based on the principle of FIFO multiple-producers multiple-consumers tasks queues.
Let's start from unit test to describe how it works:
module("Test.Queue.js");

asyncTest("Test of appending to the queue ", 6, function () {
  var queue = new Queue();
  queue.append(function () {
   ok("func 1");

   setTimeout(function () {
      ok("func 1 nested");
      start(); // Continue async test. 
      this.resolve(1); // return some value for the subscribers.
    }.bind(this), 500); // Here I would like to have a control over Deferred through 'this'.
  })
  .done(function (arg) { // on done callback
    ok("func 1 done callback");
    equal(arg, 1);
  });

  queue.append(function (arg) {
    equal(arg, "test arg data");
    this.reject(); // operation was failed.
  }, "test arg data")
  .fail(function (funcArg) { // on fail callback
    ok("func 2 fail callback");
  });
});
So here we have a queue object and two calls of independent functions that might come from the multiple sources. Each function itself might have a callbacks or chain so "queue.append" should return a promise.
The assert trace should look like this: "func 1", "func 1 nested", "func 1 done callback", "func 2" and "func 2 fail callback".
And the code itself:
Queue = function () {
  this.promise = jQuery(this).promise(); // Stub promise
};

// Magic with arguments is needed to pass arguments to the called function
Queue.prototype.append = function () {
  var args = arguments;

  var fn = args[0];
  if (!fn || !jQuery.isFunction(fn)) {
    throw new TypeError('1st parameter should be a function');
  }

  var self = this;
  args = Array.prototype.slice.call(args, 1);
  return this.promise = this.promise.pipe(function () {
    return jQuery.Deferred(function () {
      try {
        return fn.apply(this, args);
      } catch (ex) {
        // log exception
        this.reject(ex);
        return self.promise = jQuery(self).promise();
      }
    }).promise();
  });
};
Implementation idea is very simple: Class just has a reference to the head of a queue and push new function to the Deferred pipeline.

2011-05-17

JavaScript tip: on complete event

Recently I was fixing some issue in the JavaScipt event handler. Task was simple: to save user scroll position setting for the HTML control to the server.
Of course, it is possible to subscribe on the "scroll" event of the jQuery control and add a handler. But while user is scrolling it invokes event handler enormous number of times :-), so I need only "on scroll complete" event. And it took me some time to find one of the solutions.
If you need to have an "on complete" entry for some continuing event (like scrolling), described solution might help you.
Some event handler:

This code will write "scrolling..." to the console a lot of times while scrolling. In order to make it work just "on complete" we can add the timer function. Idea is simple: each time while scrolling it will cancel the timer function and then create it again. After the last scroll event (and after 1 sec) we will have an "on complete" function so it will write some message to the console.
EventHandler = function ($control) {
  if (!$control) {
    throw ("jQuery control is expected");
  }

  this.$control = $control;
  this.scrollCompleteHandling = null;
};

EventHandler.prototype.setupHandlers = function () {
  var self = this;
  this.$control.bind("scroll", function () {
    if (self.scrollCompleteHandling) {
      clearTimeout(self.scrollCompleteHandling);
    }
    self.scrollCompleteHandling = setTimeout(function () {
      console.log("scrolled...");
    }, 1000);
  });
};
It might not be the best option, but still it works :-)