1. #1
    Sencha User ThorstenSuckow's Avatar
    Join Date
    Sep 2007
    Location
    Aachen, Germany
    Posts
    597
    Vote Rating
    2
    ThorstenSuckow is on a distinguished road

      0  

    Default [3.0.2] Queue for concurrent Ajax calls

    [3.0.2] Queue for concurrent Ajax calls


    Edit 12-October-2009

    Code for Ext 3.0.2

    PHP Code:
    /**
     * Overrides ext-adapter behavior for allowing queuing of AJAX requests.
     *
     */
    /*
     * Portions of this code are based on pieces of Yahoo User Interface Library
     * Copyright (c) 2007, Yahoo! Inc. All rights reserved.
     * YUI licensed under the BSD License:
     * http://developer.yahoo.net/yui/license.txt
     */
    Ext.lib.Ajax = function() {

        
    /**
         * @type {Array} _queue A FIFO queue for processing pending requests
         */
        
    var _queue = [];

        
    /**
         * @type {Number} _activeRequests The number of requests currently being processed.
         */
        
    var _activeRequests 0;

        
    /**
         * @type {Number} _concurrentRequests The number of max. concurrent requests requests allowed.
         */
        
    var _concurrentRequests 2;

        switch (
    true) {
            case 
    Ext.isIE8:
                
    _concurrentRequests window.maxConnectionsPerServer;
            break;
            case 
    Ext.isIE:
                
    _concurrentRequests 2;
            break;
            case 
    Ext.isSafari:
            case 
    Ext.isChrome:
            case 
    Ext.isGecko3:
                
    _concurrentRequests 4;
            break;
        }

        var 
    activeX = [
            
    'MSXML2.XMLHTTP.3.0',
            
    'MSXML2.XMLHTTP',
            
    'Microsoft.XMLHTTP'
        
    ], CONTENTTYPE 'Content-Type';

        
    // private
        
    function setHeader(o)
        {
            var 
    conn o.connprop;

            function 
    setTheHeaders(connheaders) {
                for (
    prop in headers) {
                    if (
    headers.hasOwnProperty(prop)) {
                        
    conn.setRequestHeader(propheaders[prop]);
                    }
                }
            }

            if (
    pub.defaultHeaders) {
                
    setTheHeaders(connpub.defaultHeaders);
            }

            if (
    pub.headers) {
                
    setTheHeaders(connpub.headers);
                
    delete pub.headers;
            }
        }

        
    // private
        
    function createExceptionObject(tIdcallbackArgisAbortisTimeout)
        {
            return {
                
    tId tId,
                
    status isAbort ? -0,
                
    statusText isAbort 'transaction aborted' 'communication failure',
                
    isAbortisAbort,
                
    isTimeoutisTimeout,
                
    argument callbackArg
            
    };
        }

        
    // private
        
    function initHeader(labelvalue)
        {
            (
    pub.headers pub.headers || {})[label] = value;
        }

        
    // private
        
    function createResponseObject(ocallbackArg)
        {
            var 
    headerObj = {},
                
    headerStr,
                
    conn o.conn,
                
    t,
                
    s;

            try {
                
    headerStr o.conn.getAllResponseHeaders();
                
    Ext.each(headerStr.replace(/\r\n/g'\n').split('\n'), function(v){
                    
    v.indexOf(':');
                    if(
    >= 0){
                        
    v.substr(0t).toLowerCase();
                        if(
    v.charAt(1) == ' '){
                            ++
    t;
                        }
                        
    headerObj[s] = v.substr(1);
                    }
                });
            } catch(
    e) {}

            return {
                
    tId o.tId,
                
    status conn.status,
                
    statusText conn.statusText,
                
    getResponseHeader : function(header){return headerObj[header.toLowerCase()];},
                
    getAllResponseHeaders : function(){return headerStr},
                
    responseText conn.responseText,
                
    responseXML conn.responseXML,
                
    argument callbackArg
            
    };
        }

        
    // private
        
    function releaseObject(o)
        {
            
    //console.log(o.tId+" releasing");
            
    _activeRequests--;

            
    o.conn null;
            
    null;

            
    _processQueue();
        }

        
    // private
        
    function handleTransactionResponse(ocallbackisAbortisTimeout)
        {
            if (!
    callback) {
                
    releaseObject(o);
                return;
            }

            var 
    httpStatusresponseObject;

            try {
                if (
    o.conn.status !== undefined && o.conn.status != 0) {
                    
    httpStatus o.conn.status;
                }
                else {
                    
    httpStatus 13030;
                }
            }
            catch(
    e) {
                
    httpStatus 13030;
            }

            if ((
    httpStatus >= 200 && httpStatus 300) || (Ext.isIE && httpStatus == 1223)) {
                
    responseObject createResponseObject(ocallback.argument);
                if (
    callback.success) {
                    if (!
    callback.scope) {
                        
    callback.success(responseObject);
                    }
                    else {
                        
    callback.success.apply(callback.scope, [responseObject]);
                    }
                }
            }
            else {
                switch (
    httpStatus) {
                    case 
    12002:
                    case 
    12029:
                    case 
    12030:
                    case 
    12031:
                    case 
    12152:
                    case 
    13030:
                        
    responseObject createExceptionObject(o.tIdcallback.argument, (isAbort isAbort false), isTimeout);
                        if (
    callback.failure) {
                            if (!
    callback.scope) {
                                
    callback.failure(responseObject);
                            }
                            else {
                                
    callback.failure.apply(callback.scope, [responseObject]);
                            }
                        }
                    break;

                    default:
                        
    responseObject createResponseObject(ocallback.argument);
                        if (
    callback.failure) {
                            if (!
    callback.scope) {
                                
    callback.failure(responseObject);
                            }
                            else {
                                
    callback.failure.apply(callback.scope, [responseObject]);
                            }
                        }
                }
            }

            
    releaseObject(o);
            
    responseObject null;
        }

        
    // private
        
    function handleReadyState(ocallback)
        {
            
    callback callback || {};
            var 
    conn o.conn,
                
    tId o.tId,
                
    poll pub.poll,
                
    cbTimeout callback.timeout || null;

            if (
    cbTimeout) {
                
    pub.timeout[tId] = setTimeout(function() {
                    
    pub.abort(ocallbacktrue);
                }, 
    cbTimeout);
            }

            
    poll[tId] = setInterval(
                function() {
                    if (
    conn && conn.readyState == 4) {
                        
    clearInterval(poll[tId]);
                        
    poll[tId] = null;

                        if (
    cbTimeout) {
                            
    clearTimeout(pub.timeout[tId]);
                            
    pub.timeout[tId] = null;
                        }

                        
    handleTransactionResponse(ocallback);
                    }
                },
                
    pub.pollInterval);
        }

        
    /**
         * Pushes the request into the queue if a connection object can be created
         * na dimmediately processes the queue.
         *
         */
        
    function asyncRequest(methoduricallbackpostData)
        {
            var 
    getConnectionObject();

            if (!
    o) {
                return 
    null;
            } else {
                
    _queue.push({
                   
    o        o,
                   
    method   method,
                   
    uri      uri,
                   
    callback callback,
                   
    postData postData
                
    });
                
    //console.log(o.tId+" was put into the queue");
                
    var head _processQueue();

                if (
    head) {
                    
    //console.log(o.tId+" is being processed a the  head of queue");
                    
    return head;
                } else {
                    
    //console.log(o.tId+" was put into the queue and will be processed later on");
                    
    return o;
                }
            }
        }

        
    /**
         * Initiates the async request and returns the request that was created,
         * if, and only if the number of currently active requests is less than the number of
         * concurrent requests.
         */
        
    function _processQueue()
        {
            var 
    to _queue[0];
            if (
    to && _activeRequests _concurrentRequests) {
                
    to _queue.shift();
                
    _activeRequests++;
                return 
    _asyncRequest(to.methodto.urito.callbackto.postDatato.o);
            }
        }


        
    // private
        
    function _asyncRequest(methoduricallbackpostDatao)
        {
            if (
    o) {
                
    o.conn.open(methoduritrue);

                if (
    pub.useDefaultXhrHeader) {
                    
    initHeader('X-Requested-With'pub.defaultXhrHeader);
                }

                if(
    postData && pub.useDefaultHeader && (!pub.headers || !pub.headers[CONTENTTYPE])){
                    
    initHeader(CONTENTTYPEpub.defaultPostHeader);
                }

                if (
    pub.defaultHeaders || pub.headers) {
                    
    setHeader(o);
                }

                
    handleReadyState(ocallback);
                
    o.conn.send(postData || null);
            }
            return 
    o;
        }

        
    // private
        
    function getConnectionObject()
        {
            var 
    o;

            try {
                
    //console.log(pub.transactionId+" is the current transaction id");
                
    if (createXhrObject(pub.transactionId)) {
                    
    pub.transactionId++;
                }
            } catch(
    e) {
            } 
    finally {
                return 
    o;
            }
        }

        
    // private
        
    function createXhrObject(transactionId)
        {
            var 
    http;

            try {
                
    http = new XMLHttpRequest();
            } catch(
    e) {
                for (var 
    0activeX.length; ++i) {
                    try {
                        
    http = new ActiveXObject(activeX[i]);
                        break;
                    } catch(
    e) {}
                }
            } 
    finally {
                return {
    conn httptId transactionId};
            }
        }

        var 
    pub = {

            
    request : function(methoduricbdataoptions) {
                if(
    options){
                    var 
    me this,
                        
    xmlData options.xmlData,
                        
    jsonData options.jsonData,
                        
    hs;

                    
    Ext.applyIf(meoptions);

                    if(
    xmlData || jsonData){
                        
    hs me.headers;
                        if(!
    hs || !hs[CONTENTTYPE]){
                            
    initHeader(CONTENTTYPExmlData 'text/xml' 'application/json');
                        }
                        
    data xmlData || (Ext.isObject(jsonData) ? Ext.encode(jsonData) : jsonData);
                    }
                }
                return 
    asyncRequest(method || options.method || "POST"uricbdata);
            },

            
    serializeForm : function(form)
            {
                var 
    fElements form.elements || (document.forms[form] || Ext.getDom(form)).elements,
                    
    hasSubmit false,
                    
    encoder encodeURIComponent,
                    
    element,
                    
    options,
                    
    name,
                    
    val,
                    
    data '',
                    
    type;

                
    Ext.each(fElements, function(element) {
                    
    name element.name;
                    
    type element.type;

                    if (!
    element.disabled && name){
                        if(/
    select-(one|multiple)/i.test(type)){
                            
    Ext.each(element.options, function(opt) {
                                if (
    opt.selected) {
                                    
    data += String.format("{0}={1}&",
                                                         
    encoder(name),
                                                         
    encoder((opt.hasAttribute opt.hasAttribute('value') : opt.getAttribute('value') !== null) ? opt.value opt.text));
                                }
                            });
                        } else if(!/
    file|undefined|reset|button/i.test(type)) {
                            if(!(/
    radio|checkbox/i.test(type) && !element.checked) && !(type == 'submit' && hasSubmit)){

                                
    data += encoder(name) + '=' encoder(element.value) + '&';
                                
    hasSubmit = /submit/i.test(type);
                            }
                        }
                    }
                });
                return 
    data.substr(0data.length 1);
            },

            
    useDefaultHeader true,
            
    defaultPostHeader 'application/x-www-form-urlencoded; charset=UTF-8',
            
    useDefaultXhrHeader true,
            
    defaultXhrHeader 'XMLHttpRequest',
            
    poll : {},
            
    timeout : {},
            
    pollInterval 50,
            
    transactionId 0,


            
    abort : function(ocallbackisTimeout)
            {
                var 
    me this,
                    
    tId o.tId,
                    
    isAbort false;

                
    //console.log(o.tId+" is aborting - was "+o.tId+" in progress?: "+me.isCallInProgress(o));

                
    if (me.isCallInProgress(o)) {
                    
    o.conn.abort();
                    
    clearInterval(me.poll[tId]);
                    
    me.poll[tId] = null;

                    
    clearTimeout(pub.timeout[tId]);
                    
    me.timeout[tId] = null;

                    
    // @ext-bug 3.0.2 why was this commented out? if the request is aborted
                    // programmatically, the timeout for the "timeout"-handler is never destroyed,
                    // thus this method would at least be called once, if the initial reason is
                    // that no timeout occured.
                    //if (isTimeout) {
                    //    me.timeout[tId] = null;
                    //}

                      
    handleTransactionResponse(ocallback, (isAbort true), isTimeout);
                } else {
                    
    // check here if the current call was in progress. This might not be the case
                    // if the connection was put into the queue, waiting to get triggered
                    
    for (var 0max_i _queue.lengthmax_ii++) {
                        if (
    _queue[i].o.tId == o.tId) {
                            
    _queue.splice(i1);
                            
    //console.log(o.tId+" was not a call in progress, thus removed from the queue at "+i);
                            
    break;
                        }
                    }

                }

                return 
    isAbort;
            },

            
    isCallInProgress : function(o) {
                
    // if there is a connection and readyState is not 0 or 4
                
    return o.conn && !{0:true,4:true}[o.conn.readyState];
            }
        };
        return 
    pub;
    }(); 

    -------

    I have this application where a huge amount of server interaction is going on (update views, send data, periodically update data etc.).
    There may be cases when more than 2 ajax calls are being made to the server. The Internet Explorer, for example, states itself as standard compliant and does not allow to open more than 2 simultaneously connections from the browser to the server (that goes for HTTP1.1. Using HTTP1.0, IE will allow 4 concurrent connections) - thus, any previously made XMLHttpRequest may be cancelled without even triggering some sort of exception, AFAIK. Mozilla Firefox does indeed allow more than 2 concurrent connections being made to the server, but still, clients do want to use the browser they are used to, and in the end, there is no way to predict that there are never more than - let's say - 4 requests.

    Anyway, I have extended Ext.lib.Ajax (ext adapter) to put requests into a queue and then processes it using the FIFO prinicple. It guarantees that there will be never more than 2 connections opened from the client's browser to the server at one time. Once a request is finished, the implementation will peek into the queue and look if there are still requests pending, and process them subsequently.


    Code:
    /**
     * The queue that will store all XMLHttpRequests
     */
    Ext.lib.Ajax._queue = [];
    
    /**
     * Stores the number of XMLHttpRequests being processed
     */
    Ext.lib.Ajax._activeRequests = 0;
    
    /**
     * Overwritten so pending XMLHttpRequests in the queue will be removed 
     */
    Ext.lib.Ajax.abort=function(o, callback, isTimeout)
    {
        if (this.isCallInProgress(o)) {
            o.conn.abort();
            window.clearInterval(this.poll[o.tId]);
            delete this.poll[o.tId];
            if (isTimeout) {
                delete this.timeout[o.tId];
            }
    
            this.handleTransactionResponse(o, callback, true);
    
            return true;
        }
        else {
            
            // check if the connection is pending and delete it
            for (var i = 0, max_i = this._queue.length; i < max_i; i++) {
                if (this._queue[i].o.tId == o.tId) {
                    this._queue.splice(i, 1);
                    break;
                }
            }
            
            return false;
        }
    };
    
    /**
     * Pushes the XMLHttpRequests into the queue and processes the queue afterwards.
     *
     */
    Ext.lib.Ajax.asyncRequest = function(method, uri, callback, postData)
    {
        var o = this.getConnectionObject();
    
        if (!o) {
            return null;
        }
        else {
            
            this._queue.push({
               o : o,
               method: method,
               uri: uri,
               callback: callback,
               postData : postData 
            });
    
            this._processQueue();
            
            return o;
        }
    };
    
    /**
     * Peeks into the queue and will process the first XMLHttpRequest found, if, and only if
     * there are not more than 2 simultaneously XMLHttpRequests already processing.
     */
    Ext.lib.Ajax._processQueue = function()
    {
        var to = this._queue[0];
        
        if (to && this._activeRequests < 2) {
            to = this._queue.shift();
            this._asyncRequest(to.o, to.method, to.uri, to.callback, to.postData);
        }
        
    };
    
    /**
     * Executes a XMLHttpRequest and updates the _activeRequests property to match the
     * number of concurrent ajax calls.
     */
    Ext.lib.Ajax._asyncRequest = function(o, method, uri, callback, postData)
    {
        this._activeRequests++;
        o.conn.open(method, uri, true);
        
        if (this.useDefaultXhrHeader) {
            if (!this.defaultHeaders['X-Requested-With']) {
                this.initHeader('X-Requested-With', this.defaultXhrHeader, true);
            }
        }
        
        if(postData && this.useDefaultHeader){
            this.initHeader('Content-Type', this.defaultPostHeader);
        }
        
         if (this.hasDefaultHeaders || this.hasHeaders) {
            this.setHeader(o);
        }
        
        this.handleReadyState(o, callback);
        o.conn.send(postData || null);    
        
    };
    
    /**
     * Called after a XMLHttpRequest finishes. Updates the number of ongoing ajax calls
     * and checks afterwards if there are still requests pending.
     */
    Ext.lib.Ajax.releaseObject = function(o)
    {
        o.conn = null;
        o = null;
        
        this._activeRequests--;
        this._processQueue();
    };

  2. #2
    Ext User
    Join Date
    Aug 2007
    Posts
    204
    Vote Rating
    0
    DragonFist is on a distinguished road

      0  

    Default


    Looks, nice. I'll check this out latter and give any feedback needed.

  3. #3
    Ext User
    Join Date
    Jul 2007
    Posts
    3,128
    Vote Rating
    0
    devnull has a little shameless behaviour in the past

      0  

    Default


    hmm, thats interesting about IE. a browser should *never* silently drop connections, but if anyone is gonna do something like that, trust MS. that said, I have an app that definently makes more than 2 concurrent connections and it works fine in ie (6 and 7).

  4. #4
    Ext User
    Join Date
    Aug 2007
    Posts
    204
    Vote Rating
    0
    DragonFist is on a distinguished road

      0  

    Default


    Definitely the MS ajax library does this. I haven't had any trouble since I dropped using it as a way to connect to webservices (use slightly altered Ext.Ajax, Treeloader, etc. for that now).

  5. #5

  6. #6
    Sencha Premium Member dawesi's Avatar
    Join Date
    Mar 2007
    Location
    Melbourne, Australia (aka GMT+10)
    Posts
    1,083
    Vote Rating
    44
    dawesi has a spectacular aura about dawesi has a spectacular aura about

      0  

    Default


    can multiple queues be setup for various reasons?

    ie: you have two components that contstantly talk to the server, so you have three queues, one for component one, one for component two and one for all other components?

    ie: specify a queue as a parameter?

  7. #7
    Ext User
    Join Date
    Feb 2008
    Posts
    6
    Vote Rating
    0
    hakunin is on a distinguished road

      0  

    Thumbs up Thanks

    Thanks


    Its awesome! An absolute must have if you send events fired by user. (chat, online gaming, interactive gui)

    Thank you very much.

  8. #8
    Sencha Premium Member MaximGB's Avatar
    Join Date
    Jun 2007
    Location
    Moscow, Russia
    Posts
    508
    Vote Rating
    5
    MaximGB is on a distinguished road

      0  

    Default


    Hm, this should be proposed as the default behavior I think.
    Use the force - read the source.

  9. #9
    Ext User
    Join Date
    Feb 2008
    Posts
    6
    Vote Rating
    0
    hakunin is on a distinguished road

      0  

    Default


    Quote Originally Posted by dawesi View Post
    can multiple queues be setup for various reasons?

    ie: you have two components that constantly talk to the server, so you have three queues, one for component one, one for component two and one for all other components?

    ie: specify a queue as a parameter?
    You're right. There has to be more queues. For example:

    I have listening thread which waits for events from server and one "slot" for thread that sends requests from user.

    What happens when the user goes crazy and fires 10 events in a second? (my example = 10 messages in a chat room at once)

    1) All 10 requests get into queue. First one triggers an event and listening request recieves that event. Both close.

    2) Closing listening request makes another request for listening. That request is the last one in queue.

    2) Next 8 crazy fired requests are sent as well as other. The response to them comes after all the crazy fired events from user are sent.


    Now in case any of these crazy fired requests from user are slow the response gets ultra slow. We'd certainly get better behaviour if there were two queues.

    If my example isn't clear to anyone please tell me. I'll try to explain it better.

  10. #10
    Sencha - Community Support Team jsakalos's Avatar
    Join Date
    Apr 2007
    Location
    Slovakia
    Posts
    27,552
    Vote Rating
    382
    jsakalos has a brilliant future jsakalos has a brilliant future jsakalos has a brilliant future jsakalos has a brilliant future jsakalos has a brilliant future jsakalos has a brilliant future jsakalos has a brilliant future jsakalos has a brilliant future jsakalos has a brilliant future jsakalos has a brilliant future jsakalos has a brilliant future

      0  

    Default


    Thank you for this override.

    I've been using it for months and it runs w/o any problems so I can only recommend it to others.

    Jozef Sakalos, aka Saki

    Education, extensions and services for developers at new http://extjs.eu
    News: Grid Search Plugin, ExtJS 5 Complex Data Binding using MVVM