/**
* MultiAjax class provides a convenient way to smart work with AJAX.
* It supports timeout, queue, session limits and batch mode.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*/
/**
* Serialize object.
*/
Object.prototype.serialize = function() {
var s = '', i = 0;
for (var key in this) {
if (typeof this[key] != 'function') {
s += ':' + key.serialize() + ':' +
(this[key] ? this[key].serialize() : 'n');
i++;
}
}
return 'o:' + i + s;
}
/**
* Serialize array.
*/
Array.prototype.serialize = function() {
while (this.length > 0 && typeof this[this.length - 1] == 'undefined') {
this.length--;
}
var s = '', i = 0;
for (; i < this.length; i++) {
s += ':' + (this[i] ? this[i].serialize() : 'n');
}
return 'a:' + i + s;
}
/**
* Serialize number.
*/
Number.prototype.serialize = function() {
return 's:' + this;
}
/**
* String serialization separator.
*/
String.prototype.SERIALIZATION_SEPARATOR = "__\t__";
/**
* Serialize string
*/
String.prototype.serialize = function() {
return String(this) ? 's:' + encodeURIComponent(this.replace(/:/g,
String.prototype.SERIALIZATION_SEPARATOR)).replace(/%20/g, '+'):'n';
}
/**
* Replace all occurrences of the search string with the replacement string.
*
* @param fromstr Searched string.
* @param tostr Replaced string.
*
* @return Result string
*/
String.prototype.replaceAll = function(fromstr, tostr) {
var s = String(this), i;
while ((i = s.indexOf(fromstr)) >= 0) {
s = s.replace(fromstr, tostr);
}
return s;
}
/**
* Unserialize data.
*
* @param value Serialized string.
*
* @return Unserialized data.
*/
String.prototype.unserialize = function() {
// Internal function for unserialization.
function _unserialize_internal() {
switch (arr[idx++]) {
case 's':
// String
return decodeURIComponent(arr[idx++].replace(/\+/g, ' ')).replaceAll(String.prototype.SERIALIZATION_SEPARATOR, ':');
case 'a':
// Array
var a = [];
for (var i = arr[idx++]; i > 0; i--) {
a.push(_unserialize_internal());
}
return a;
case 'o':
// Object
var obj = new Object();
for (var i = arr[idx++]; i > 0; i--) {
obj[_unserialize_internal()] = _unserialize_internal();
}
return obj;
default:
return '';
}
}
// Divide lexemes of input serialized string.
var arr = this.split(':'), idx = 0;
return _unserialize_internal();
}
/**
* =============================================================================
* XMLHttpRequest pool class
* =============================================================================
*
* @param maxSize Maximal pool size.
*/
function RequestPool(maxSize) {
this.maxSize = maxSize;
this.pool = [];
}
/**
* Get XMLHttpRequest from pool.
*
* @return Return XMLHttpRequest object or null.
*/
RequestPool.prototype.getRequest = function() {
if (this.pool.length >= this.maxSize) {
return null;
}
var rec = null;
if (typeof XMLHttpRequest != 'undefined') {
rec = new XMLHttpRequest();
} else {
var ms = ['Msxml2.XMLHTTP.6.0',
'Msxml2.XMLHTTP.4.0',
'Msxml2.XMLHTTP.3.0',
'Msxml2.XMLHTTP',
'Microsoft.XMLHTTP'];
for (var i in ms) {
try {
rec = ActiveXObject(ms[i]);
break;
} catch(e) {
}
}
}
if (rec) {
this.pool.push(rec);
}
return rec;
}
/**
* Close XMLHttpRequest from pool.
*
* @param request XMLHttpRequest object.
*/
RequestPool.prototype.closeRequest = function(request) {
for (var i in this.pool) {
if (this.pool[i] === request) {
this.pool.splice(i, 1);
delete request.ontimeout;
delete request.onreadystatechange;
break;
}
}
}
/**
* =============================================================================
* Queue class
* =============================================================================
*
* @param maxSize Maximal queue size.
*/
function Queue(maxSize) {
this.maxSize = maxSize;
this.queue = [];
}
/**
* Push new element into the queue.
*
* @param value New element.
*
* @return Result 0 if the queue is overflowed, other number if the value
* is added.
*/
Queue.prototype.push = function(value) {
if (this.queue.length >= this.maxSize) {
return 0;
}
return this.queue.push(value);
}
/**
* Get element from the queue without deletion.
*
* @param i Number of element, this parameter is not required, 0 (first element)
* by default.
*
* @return Result first element from the queue or undefined value if queue
* is empty.
*/
Queue.prototype.get = function(i) {
if (typeof i != 'number') {
i = 0;
}
if (i < 0 || i >= this.queue.length) {
return null;
}
return this.queue[i];
}
/**
* Shift element from the queue.
*
* @param i Number of element, this parameter is not required, 0 (first element)
* by default.
*
* @return Result first element from the queue or undefined value if queue
* is empty.
*/
Queue.prototype.shift = function(i) {
if (typeof i != 'number') {
i = 0;
}
if (i < 0 || i >= this.queue.length) {
return null;
}
value = this.queue[i];
this.queue.splice(i, 1);
return value;
}
/**
* =============================================================================
* MultiAjax object
* =============================================================================
*/
var multiajax = {
/**
* Default request data. It is possible methods: 'GET', 'POST' and 'AUTO'
*/
_request: {
url: '/multiajax',
method: 'AUTO',
request_parameter: 'q',
timeout: 20,
method_get_maxsize: 200
},
/**
* AJAX sessions
*/
_sessions: new RequestPool(5),
/**
* Queue of AJAX queries
*/
_queue: new Queue(100),
/**
* Activity flag for _checkQueue method
*/
_isActive: 0,
/**
* Check queue of AJAX requests
*/
_checkQueue: function() {
this._isActive = 1;
// Get request from query
var maxi = this._queue.queue.length;
if (maxi == 0) {
this._isActive = 0;
return;
}
// Get HTTP request from pool
var req = this._sessions.getRequest();
if (req != null) {
// Prepare request
var sessions = this._sessions;
var r = this._queue.shift();
// Build batch package
var batch = [[r.data, r.handler]];
var callbacks = [r.callback];
for (var i = 0; i < maxi - 1; i++) {
var x = this._queue.get(i);
if (r.url == x.url && r.timeout == x.timeout && r.method == x.method) {
this._queue.shift(i);
maxi--;
i--;
batch.push([x.data, x.handler]);
callbacks.push(x.callback);
}
}
// Prepare request parameters
if (!r.url) {
r.url = this._request.url;
}
if (!r.timeout) {
r.timeout = this._request.timeout;
}
if (!r.method) {
r.method = this._request.method;
}
// Build query string
var reqstr = this._request.request_parameter + '=' + batch.serialize();
var geturl = r.url + (r.url.indexOf('?') < 0 ? '?' : '&') + reqstr;
if (r.method == 'AUTO') {
r.method = geturl.length > this._request.method_get_maxsize ? 'POST' : 'GET';
}
// Set timeout
req.ontimeout = function() {
req.abort();
for (var i in callbacks) {
callbacks[i]({error: 'TIMEOUT'});
}
sessions.closeRequest(req);
}
var timeoutid = setTimeout(req.ontimeout, r.timeout * 1000);
// Prepare callback method for result
req.onreadystatechange = function() {
if (req.readyState == 4) {
if (req.status) {
clearTimeout(timeoutid);
var data = req.responseText.unserialize();
for (var i in callbacks) {
callbacks[i](req.status == 200 && data instanceof Array ? {data: data[i]} : {error: 'REQUEST_ERROR'});
}
}
sessions.closeRequest(req);
}
}
// Send Request
if (r.method == 'POST') {
req.open('POST', r.url, true);
req.setRequestHeader(
'Content-type',
'application/x-www-form-urlencoded; charset=UTF-8');
req.send(reqstr);
} else {
req.open('GET', geturl, true);
req.send(null);
}
}
setTimeout(function() {
multiajax._checkQueue();
}, 1000);
},
/**
* Send AJAX request.
* The structure of request:
* - url - URL of server-side resource
* - handler - request function name ("/multiajax" by default)
* - data - request data
* - callback - JavaScript callback function name
* - timeout - request timeout, seconds (30 seconds by default)
* - method - HTTP request method, POST, GET or AUTO (AUTO by default)
*
* The structure of response:
* - data - response data
* - error - response error code
*
* @param r AJAX request.
*/
send: function(r) {
if (!this._queue.push(r)) {
r.callback({error: 'QUEUE_OVERFLOW'});
return;
}
if (!this._isActive) {
this._checkQueue();
}
}
}
|