In AJAX based applications its common that user might end up breaking your AJAX calls by clicking on numerous places in very short interval of time. Let us assume there is a page where there are several of hyperlinks which make WebService calls and do some stuffs on callback. If user clicks on five hyperlinks being impatient or may be just for fun, there will be five different WebService calls made. All of those calls had the same parameters or UI state while they were invoked. But on completion of one or more WebService calls it may happen that the UI state or data passed to the rest of the WebServices calls no longer exist or expired, thus will be result in inconsistent UI behaviour and/or invalid data. This is one of the important scenarios an AJAX developer should consider when he designs an application.
The Specification
To address the scenario, I would prefer implementing a Sequential WebService Calls Queue which will be able to schedule the tasks/WebService calls and help keeping UI and data consistent over the AJAX calls. We should achieve the following features from this queue:
- Enqueue any WebService call anytime in the application.
- Dequeue any previously queued call regardless of currently executing call and location in the application.
- Each WebService call should have an identifier so that we can track the call and dequeue anytime later by SSQ.dq(call_id).
- Each call should have a timeout value which will determine the maximum amount of time we will consider for that particular call before we invoke the next call, after that we will remove from the queue.
- A timer will act as scheduler but will not run forever. It should run only when necessary.
- Each call should be able to declare its completion at any time by notifyCompleted, so that the scheduler timer will not wait for the prior task and should dequeue the next call.
- notifyCompleted should also be optional. The currently running call should automatically be dequeued from the scheduler queue after the timeout of its own.
- Each call should be able to mark as replaceIfExists so that if user`s any activity already enqueued this call, should be replaced by the current one.
- The queue instance should be exclusively available to the user and all over the page, meaning that the same queue class will be used to serve the functionality in one page per user basis.
The Usage
You should be able to use this library as follows:
- Include GenericQueue.js and SequentialServiceQueue.js to your project
- Add the reference in the pages that you want them to be used
We will be naming the class as SequentialServiceQueue and in short SSQ. Let us have a look at the WebService calls in Service Queue fashion:
var id1 = SSQ.nq('SomeMethod1', false, 1000,
function()
{
// Do some stuffs
SomeWebService1.SomeMethod1(SomeParameters, onSomeMethodCallCompleted);
});
function onSomeMethodCallCompleted(result)
{
// Do stuffs
SSQ.notifyCompleted(id1);
}
var id2 = SSQ.nq('SomeMethod2', false, 1000,
function()
{
// Do some stuffs
SomeWebService2.SomeMethod2(SomeParameters,
function(result)
{
// Do stuffs
SSQ.notifyCompleted(id2);
});
});
You can not only queue WebService calls, but also regular JavaScript codeblock:
var service_id1 = SSQ.nq('Service1', false, 1000,
function()
{
// Do some stuffs
SSQ.notifyCompleted(service_id1);
});
The GenericQueue
To accomplish the SequentialServiceQueue, first of all we should define an all purpose GenericQueue class which will able to handle any queue requirement out of the box. The queue is fairly simple, just like old Computer Science data structure class. Here are few of the functions from the class:
this.nq = function(element)
{
array.push(element);
++rear;
}
this.dq = function()
{
var element = undefined;
if (!this.is_empty())
{
element = array.shift();
--rear;
}
return element;
}
this.for_each = function(func)
{
for (var i = 0; i < rear; ++i)
func(i, array[i]);
}
this.delete_at = function(index)
{
delete array[index];
var i = index;
while (i < array.length)
array[i] = array[++i];
array = array.slice(0, --rear);
}
The SequentialServiceQueue
The following is how this class starts. You will notice here that the timer_id is for our scheduler timer, running_task indicates the currently executing call, interval is a variable for the timer_id which you can determine as your wish. interval is the knob of how fast or slow you want the scheduler to run. queue as you can understand is an GenericQueue instance we have just created above. Note that the GenericQueue is not a static class rather its a instance class unlike the SSQ. You have also noticed that the ms_when_last_call_made and ms_elapsed_since_last_call are pretty self-describing. get_random_id is reponsible for preparing new id for the newly enqueued call.
var SequentialServiceQueue =
{
timer_id: null,
ms_when_last_call_made: 0, // milliseconds (readonly)
ms_elapsed_since_last_call: 0, // milliseconds (readonly)
running_task: null,
interval: 10, // milliseconds
queue: new GenericQueue(),
get_random_id: function() {
var min = 1;
var max = 10;
return new Date().getTime() + (Math.round((max - min) * Math.random() + min));
},
From the code below, as soon as any new call is enqueued, we check for if it is allowed to replace if already exists in the queue with the same name. If found any, we just update that, otherwise we create a brand new task and enqueue and start our updater which is the scheduler in our case.
nq: function(name, replaceIfExists, timeout, code) {
var id = this.get_random_id();
if (replaceIfExists) {
var isFound = false;
this.queue.for_each(
function(index, element) {
if (element !== undefined && element !== 'undefined' && element.name == name) {
element.id = id;
element.replaceIfExists = replaceIfExists;
element.timeout = timeout;
element.code = code;
isFound = true;
}
});
}
// Enqueue new task
if (!isFound || !replaceIfExists) {
this.queue.nq(
{
id: id,
name: name,
replaceIfExists: replaceIfExists,
timeout: timeout,
code: code
});
}
// We have got new tasks, start the updater
this.startUpdater();
return id;
},
The following is the core part of the class which is the scheduler. Inside startUpdater, it executes the block of code in every interval we defined before. And inside the looping code, we check for if there is already any running task, if yes we check for the timeout whether it should make a good timeout or not. Otherwise we let it run as it was. However, if there is no running task at this moment, we dequeue a task and start executing the code and set a starting time for that to keep track of how long it is being running.
detachTask: function(id) {
this.dq(id);
this.running_task = null;
this.ms_when_last_call_made = 0;
this.ms_elapsed_since_last_call = 0;
// See if we are done with the queued tasks
if (this.queue.is_empty())
this.stopUpdater();
},
startUpdater: function() {
var _self = this;
if (this.timer_id == null) {
this.timer_id = setInterval(
function() {
if (_self.running_task == null) {
// We dont have any running task, lets make the first one
_self.running_task = _self.queue.dq();
if (_self.running_task != null) {
_self.ms_when_last_call_made = new Date().getTime();
_self.running_task.code();
}
}
else {
// We have a running task already
_self.ms_elapsed_since_last_call = new Date().getTime() - _self.ms_when_last_call_made;
// Should the current task be skipped?
if (_self.ms_elapsed_since_last_call > _self.running_task.timeout)
// Time's up. leave the task alone. Let other tasks start executing.
_self.detachTask(_self.running_task.id);
}
}, _self.interval);
}
},
stopUpdater: function() {
if (this.timer_id != null) {
clearInterval(this.timer_id)
this.timer_id = null;
}
this.queue = new GenericQueue();
},
Keep an eye on my blog for continued development and improvements, and download CallQueue from: http://code.msdn.microsoft.com/callqueue