Ticket #6131: channels.diff

File channels.diff, 18.3 kB (added by kriszyp, 8 months ago)

HTTP Channels

  • BaseChannels.js

     
     1dojo.provide("dojox.cometd.BaseChannels"); 
     2dojo.require("dojox.rpc.Client"); 
     3if(dojox.data && dojox.data.JsonRestStore){ 
     4        dojo.require("dojox.data.restListener"); 
     5} 
     6// Note that cometd _base is _not_ required, this can run standalone, but ifyou want  
     7// cometd functionality, you must explicitly load/require it elsewhere, and cometd._base 
     8// MUST be loaded prior to Channels ifyou use it. 
     9 
     10/** 
     11        * BaseChannels is used by JsonChannels and HttpChannels 
     12        *  
     13        */ 
     14         
     15 
     16dojox.cometd.BaseChannels = function(options){ 
     17        var isOpera = navigator.userAgent.indexOf("Opera") >= 0; 
     18         
     19        var Channels = { 
     20                absoluteUrl : function(relativeUrl){ 
     21                        return new dojo._Url(location.href,relativeUrl)+''; 
     22                }, 
     23                subscriptions : {}, 
     24                subCallbacks : {}, 
     25                autoReconnectTime : 30000, 
     26                url : '/channels', 
     27                open : function(){ 
     28                        // summary: 
     29                        //              Startup the transport (connect to the "channels" resource to receive updates from the server). 
     30                        // 
     31                        // description: 
     32                        //              Note that if there is no connection open, this is automatically called when you do a subscription, 
     33                        //              it is often not necessary to call this 
     34                        // 
     35                        if(!this.connected){ 
     36                                var xdr = dojox.cometd.useXDR && window.XDomainRequest; 
     37                                // we are currently directly using the XHR or XDR object, because Dojo's XHR wrapper is not architected for progress events. If that changes, we can use Dojo's XHR here. 
     38                                this.xhr = (window.XMLHttpRequest && new (xdr || XMLHttpRequest)) || new ActiveXObject("Microsoft.XMLHTTP"); 
     39                                var xhr = this.xhr; 
     40                                this.connectionId = dojox._clientId; 
     41                                var clientIdHeader = this.started ? 'Client-Id' : 'Create-Client-Id'; 
     42                                // post to our channel url 
     43                                if(xdr){ 
     44                                        xhr.open("POST",this.absoluteUrl(this.url + "?Accept=" + this.acceptType + "&" + clientIdHeader + "=" + this.connectionId));  
     45                                } 
     46                                else {// xdr doesn't have setRequestHeader, and it must have an absolute url 
     47                                        xhr.open("POST",this.url,true); 
     48                                        xhr.setRequestHeader('X-' + clientIdHeader,this.connectionId);  
     49                                        // let the server know what type of response we can accept 
     50                                        xhr.setRequestHeader('Accept',this.acceptType); 
     51                                } 
     52                                var self = this; 
     53                                this.lastIndex = 0;  
     54                                var onstatechange = function(){ // get all the possible event handlers 
     55                                        var error,loaded,data; 
     56                                try{ 
     57                                                if(!xhr.readyState || xhr.readyState > 2){// only if"OK" (xdr doesn't have a readyState) 
     58                                                        self.readyState = xhr.readyState; 
     59                                                        error = xhr.status > 300;                
     60                                                        data = xhr.responseText.substring(self.lastIndex); 
     61                                                loaded = !error && typeof data=='string';//firefox can throw an exception right here 
     62                                                } 
     63                                } catch(e){ 
     64                                        error = xhr.readyState == 4; // an error in ready state 3 is common with IE's old API. But in ready state 4, it is an indication of a real error in firefox  
     65                                } 
     66                                if(loaded){ 
     67                                        var contentType = xhr.contentType || xhr.getResponseHeader("Content-Type"); 
     68                                        self.started = true; 
     69                                                try{                                     
     70                                                error = self.onprogress(xhr,xdr,data,contentType); 
     71                                                } 
     72                                        finally { 
     73                                                        if(xhr.readyState==4){ 
     74                                                        xhr = null; 
     75                                                        if(self.connected){ 
     76                                                                self.connected = false; 
     77                                                                self.open(); 
     78                                                        } 
     79                                                        } 
     80                                        } 
     81                                } 
     82                                if(error){ // an error has occurred 
     83                                 
     84                                        if(self.started){ // this means we need to reconnect 
     85                                                self.started = false; 
     86                                                        self.connected = false; 
     87                                                var subscriptions = self.subscriptions; 
     88                                                self.subscriptions = {}; 
     89                                                        for(var i in subscriptions){ 
     90                                                                self.subscribe(i,{since:subscriptions[i]}); 
     91                                                        } 
     92                                        } 
     93                                        else { 
     94                                                self.disconnected(); 
     95                                        } 
     96                                } 
     97                                }; 
     98                                xhr.onreadystatechange = onstatechange; 
     99                                if(xdr){ 
     100                                        xhr.onerror = function(){ 
     101                                                xhr.readyState = 4; 
     102                                                xhr.status = 404; // this is so that we can restartup ifnecessary 
     103                                                onstatechange(); 
     104                                        }; 
     105                                        xhr.onload = function(){ 
     106                                                xhr.readyState = 4; 
     107                                                onstatechange(); 
     108                                        }; 
     109                                        xhr.onprogress = onstatechange; 
     110                                } 
     111                                  
     112                                xhr.send(null); 
     113                                if(window.attachEvent){// IE needs a little help with cleanup 
     114                                        attachEvent("onunload",function(){ 
     115                                                self.connected= false; 
     116                                                if(xhr){ 
     117                                                        xhr.abort(); 
     118                                                } 
     119                                        }); 
     120                                } 
     121                                 
     122                                this.connected = true; 
     123                        } 
     124                }, 
     125                subscribe : function(channel,args){ 
     126                        // summary: 
     127                        //              subscribe to a channel/uri 
     128                        // 
     129                        // channel:  
     130                        //              the uri for the resource you want to monitor 
     131                        //  
     132                        // args:  
     133                        //              The follow properties can be defined in args: 
     134                        // since:  
     135                        //              The time after which you want to receive modification notices/events for this uri/channel 
     136                        //  
     137                        // headers: 
     138                        //              These are the headers to be applied to the channel subscription request 
     139                        // 
     140                        // callback: 
     141                        //              This will be called when a event occurs for the channel 
     142                        //              The callback will be called with a single argument: 
     143                        //      |       callback(message) 
     144                        //              where message is an object that follows the XHR API: 
     145                        //              status : Http status 
     146                        //              statusText : Http status text 
     147                        //              getAllResponseHeaders() : The response headers 
     148                        //              getResponseHeaders(headerName) : Retrieve a header by name 
     149                        //              responseText : The response body as text 
     150                        //                      with the following additional Bayeux properties  
     151                        //              data : The response body as JSON 
     152                        //              channel : The channel/url of the response 
     153                        args = args || {}; 
     154                        args.url = this.absoluteUrl(channel); 
     155                        var oldSince = this.subscriptions[channel]; 
     156                        var method = args.method || "HEAD"; // HEAD is the default for a subscription 
     157                        var since = args.since; 
     158                        var callback = args.callback; 
     159                        var headers = args.headers || (args.headers = {}); 
     160                        this.subscriptions[channel] = since || oldSince || 0; 
     161                        var oldCallback = this.subCallbacks[channel]; 
     162                        if(callback){ 
     163                                this.subCallbacks[channel] = oldCallback ? function(m){ 
     164                                        oldCallback(m); 
     165                                        callback(m); 
     166                                } : callback; 
     167                        }  
     168                        if(!this.connected){ 
     169                                this.open(); 
     170                        } 
     171                        if(oldSince === undefined || oldSince != since){ 
     172                                headers["Cache-Control"] = "max-age=0"; 
     173                                since = typeof since == 'number' ? new Date(since).toUTCString() : since; 
     174                                if(since){ 
     175                                        headers["X-Subscribe-Since"] = since; 
     176                                } 
     177                                headers["X-Subscribe"] = args.unsubscribe ? 'none' : '*'; 
     178/*                              var xhr = (window.XMLHttpRequest && new XMLHttpRequest) || new ActiveXObject("Microsoft.XMLHTTP");                               
     179                                xhr.open("GET",url,true); 
     180                                for(var i in headers) 
     181                                        xhr.setRequestHeader(i,headers[i]);*/ 
     182                                var dfd = dojo.xhr(method,args); 
     183                                 
     184                                var self = this; 
     185/*                              xhr.onreadystatechange = function(){ 
     186                                        if(xhr.readyState == 4){*/ 
     187                                dfd.addBoth(function(){ 
     188                                        var xhr = dfd.ioArgs.xhr; 
     189                                        if(xhr.status < 400){ 
     190                                                if(args.confirmation){ 
     191                                                        args.confirmation(); 
     192                                                } 
     193                                        } 
     194                                        if(xhr.getResponseHeader("X-Subscribed")  == "OK"){ 
     195                                                var lastMod = xhr.getResponseHeader('Last-Modified'); 
     196//                                                      log("lastMod " + lastMod); 
     197                                                 
     198                                                if(xhr.responseText){  
     199                                                        self.subscriptions[channel] = lastMod || new Date().toUTCString(); 
     200                                                } 
     201                                                else { 
     202                                                        return; // don't process the response, the response will be received in the main channels response 
     203                                                } 
     204                                        } 
     205                                        else { // ifit is not a 202 response, that means it is did not accept the subscription 
     206                                                delete self.subscriptions[channel]; 
     207                                        } 
     208                                        if(xhr.status < 300){ 
     209                                                if('channel' in xhr){ 
     210                                                        // firefox uses this property as internal property (and throws an exception on usage), 
     211                                                        xhr = {channel:channel,__proto__:xhr}; // so we create an instance to shadow this property 
     212                                                } 
     213                                                xhr.channel = channel;  
     214                                                 
     215                                                try{ 
     216                                                        xhr.data = dojo.fromJson(xhr.responseText); 
     217                                                } 
     218                                                catch (e){} 
     219                                                if(self.subCallbacks[channel]){ 
     220                                                        self.subCallbacks[channel](xhr); // call with the actual xhr object 
     221                                                } 
     222                                        } 
     223                                }); 
     224                                return dfd; 
     225                                //xhr.send(null); 
     226                        } 
     227                }, 
     228                get : function(channel,args){ 
     229                        // summary: 
     230                        //              get the initial value of the resource and subscribe to it   
     231                        //              See subscribe for parameter values 
     232                        (args = args || {}).method = "GET";  
     233                        return this.subscribe(channel,args); 
     234                }, 
     235                disconnected : function(){ 
     236                        // summary: 
     237                        //              called when our channel gets disconnected 
     238                        var self = this; 
     239                        if(this.connected){ // ifwe are connected, we shall tryto reconnect  
     240                                setTimeout(function(){ // auto reconnect 
     241                                        self.open(); 
     242                                },this.autoReconnectTime); 
     243                        } 
     244                        this.connected = false; 
     245                }, 
     246                unsubscribe : function(channel,args){ 
     247                        // summary: 
     248                        //              unsubscribes from the resource   
     249                        //              See subscribe for parameter values  
     250                         
     251                        args = args || {}; 
     252                        args.unsubscribe = true; 
     253                        this.subscribe(channel,args); // change the time frame to after 5000AD  
     254                }, 
     255                deliver : function(message){ // nothing to do 
     256                }, 
     257                disconnect : function(){ 
     258                        // summary: 
     259                        //              disconnect from the server   
     260                        this.connected = false; 
     261                        this.xhr.abort(); 
     262                } 
     263        }; 
     264        Channels = dojo.mixin(Channels,options); 
     265        if(dojox.cometd.connectionTypes){ // register as a dojox.cometd transport and wire everything for cometd handling 
     266                // below are the necessary adaptions for cometd 
     267                Channels.startup = function(data){ // must be able to handle objects or strings 
     268                        Channels.open(); 
     269                        this.deliver({channel:"/meta/connect",successful:true}); // tell cometd we are connected so it can proceed to send subscriptions, even though we aren't yet  
     270 
     271                }; 
     272                Channels.check = function(types, version, xdomain){ 
     273                        for(var i = 0; i< types.length; i++){ 
     274                                if(types[i] == Channels._connectionType){ 
     275                                        return !xdomain; 
     276                                } 
     277                        } 
     278                }; 
     279                Channels.sendMessages = function(messages){ 
     280                        for(var i = 0; i < messages.length; i++){ 
     281                                var message = messages[i]; 
     282                                var channel = message.channel; 
     283                                var args = {confirmation: function(){ // send a confirmation back to cometd 
     284                                                Channels.deliver({channel:channel,successful:true}); 
     285                                        }}; 
     286                                if(channel == '/meta/subscribe'){ 
     287                                        this.subscribe(message.subscription,args); 
     288                                } 
     289                                else if(channel == '/meta/unsubscribe'){ 
     290                                        this.unsubscribe(message.subscription,args); 
     291                                } 
     292                                else if(channel == '/meta/connect'){ 
     293                                        args.confirmation(); 
     294                                } 
     295                                else if(channel == '/meta/disconnect'){ 
     296                                        Channels.disconnect(); 
     297                                        args.confirmation(); 
     298                                } 
     299                                else if(channel.substring(0,6) != '/meta/'){ 
     300                                        this.publish(channel,message.data); 
     301                                } 
     302                        } 
     303                }; 
     304                dojox.cometd.connectionTypes.register(Channels._connectionType, Channels.check, Channels,false,true); 
     305        } 
     306        var handleContent = function(xhr){ 
     307                // automatically choose the right handler based on the returned content type 
     308                var handlers = dojo._contentHandlers; 
     309                var retContentType = xhr.getResponseHeader("Content-Type"); 
     310                results = retContentType.match(/\/json/) ? handlers.json(xhr) :  
     311                        retContentType.match(/\/javascript/) ? handlers.javascript(xhr) : 
     312                        retContentType.match(/\/xml/) ? handlers.xml(xhr) : handlers.text(xhr);  
     313                return results;                                                                  
     314        }; 
     315        dojo.xhrGet = function(r){ 
     316                var dfd = new dojo.Deferred(); 
     317                r.callback = function(message){ 
     318                        if(dfd.fired ==-1){ 
     319                                dojox._newId = message.channel; 
     320                                dfd.callback(handleContent(message)); 
     321                        } 
     322                }; 
     323                dfd.ioArgs = Channels.get(r.url,r).ioArgs; // copy the ioArgs over, so others can access it 
     324                return dfd;                      
     325        }; 
     326        if(dojox.data && dojox.data.restListener){ 
     327                dojo.connect(Channels,"deliver",null,dojox.data.restListener); 
     328        } 
     329        return Channels; 
     330}; 
  • HttpChannels.js

     
     1dojo.provide("dojox.cometd.HttpChannels"); 
     2dojo.require("dojox.cometd.BaseChannels"); 
     3// Note that cometd _base is _not_ required, this can run standalone, but ifyou want  
     4// cometd functionality, you must explicitly load/require it elsewhere, and cometd._base 
     5// MUST be loaded prior to HttpChannels ifyou use it. 
     6 
     7/** 
     8        * HTTP Channels - An HTTP Based approach to Comet transport with full HTTP messaging  
     9        * semantics including REST 
     10        * HTTP Channels is a efficient, reliable duplex transport for Comet 
     11        * by Kris Zyp - www.sitepen.com 
     12 
     13        * This can be used: 
     14        * 1. As a cometd transport 
     15        * 2. As an enhancement for the REST RPC service, to enable "live" data (real-time updates directly alter the data in indexes) 
     16        * 2a. With the JsonRestStore (which is driven by the REST RPC service), so this dojo.data has real-time data. Updates can be heard through the dojo.data notification API. 
     17        * 3. As a standalone transport. To use it as a standalone transport looks like this: 
     18        * dojox.cometd.HttpChannels.open(); 
     19        * dojox.cometd.HttpChannels.get("/myResource",{callback:function(){ 
     20        *               // this is called when the resource is first retrieved and any time the  
     21        *               // resource is changed in the future. This provides a means for retrieving a 
     22        *               // resource and subscribing to it in a single request 
     23        * }); 
     24        * dojox.cometd.HttpChannels.subscribe("/anotherResource",{callback:function(){ 
     25        *               // this is called when the resource is changed in the future 
     26        * }); 
     27        * Channels HTTP can be configured to a different delays: 
     28        * dojox.cometd.HttpChannels.autoReconnectTime = 60000; // reconnect after one minute 
     29        */ 
     30         
     31dojox.cometd.HttpChannels = dojox.cometd.BaseChannels({ 
     32        /* 
     33        absoluteUrl : function(baseUrl, relativeUrl){ 
     34                // summary: 
     35                // This takes a base url and a relative url and resolves the target url. 
     36                // For example: 
     37                // resolveUrl("http://www.domain.com/path1/path2","../path3") ->"http://www.domain.com/path1/path3"   
     38                // 
     39                if(relativeUrl.match(/\w+:\/\//)) 
     40                        return relativeUrl; 
     41                if(relativeUrl.charAt(0)=='/'){ 
     42                        baseUrl = baseUrl.match(/.*\/\/[^\/]+/) 
     43                        return (baseUrl ? baseUrl[0] : '') + relativeUrl; 
     44                }        
     45                        //TODO: handle protocol relative urls:  ://www.domain.com 
     46                baseUrl = baseUrl.substring(0,baseUrl.length - baseUrl.match(/[^\/]*$/)[0].length);// clean off the trailing path 
     47                if(relativeUrl == '.') 
     48                        return baseUrl;  
     49                while (relativeUrl.substring(0,3) == '../'){ 
     50                        baseUrl = baseUrl.substring(0,baseUrl.length - baseUrl.match(/[^\/]*\/$/)[0].length); 
     51                        relativeUrl = relativeUrl.substring(3); 
     52                } 
     53                return baseUrl + relativeUrl;    
     54        }*/ 
     55        _connectionType : "http-channels", 
     56        acceptType : "application/http,*/*;q=0.8", 
     57        publish : function(channel,data){ 
     58                // summary: 
     59                //              Publish an event. 
     60                // description: 
     61                //              This does a simple POST operation to the provided URL, 
     62                //              POST is the semantic equivalent of publishing a message within REST/Channels 
     63                // channel: 
     64                //              Channel/resource path to publish to 
     65                // data: 
     66                //              data to publish 
     67                headers={}; 
     68                dojo.rawXhrPost({url:channel,postData:dojo.toJson(data),headers:headers,        contentType : 'application/json'}); 
     69                 
     70/*              var xhr = new XMLHttpRequest(); 
     71                xhr.open("POST",channel,true); 
     72                xhr.send(dojo.toJson(data)); // fire and forget*/ 
     73        }, 
     74        _processMessage: function(message) { 
     75                message.event = message.getResponseHeader('X-Event'); 
     76                if(message.event=="connection-conflict"){ 
     77                        return "conflict"; // indicate an error 
     78                } 
     79                try { 
     80                        message.data = dojo.fromJson(message.responseText); 
     81                } 
     82                catch (e){} 
     83                var self = this;         
     84                message.channel = message.getResponseHeader('Content-Location');//for cometd 
     85                var loc = new dojo._Url(location.href,message.channel); // TODO: more robust URL matching 
     86                if(loc in this.subscriptions){ 
     87                        this.subscriptions[loc] = message.getResponseHeader('Last-Modified');  
     88                } 
     89                if(this.subCallbacks[loc]){ 
     90                        setTimeout(function(){ //give it it's own stack  
     91                                self.subCallbacks[loc](message); 
     92                        },0); 
     93                } 
     94                this.deliver(message); 
     95                 
     96        }, 
     97        onprogress : function(xhr,xdr,data,contentType){ 
     98                // internal XHR progress handler 
     99                if (contentType.match(/application\/http/)) { 
     100                        // do tunneling 
     101                        var topHeaders = ''; 
     102                        if(!xdr){ 
     103                        topHeaders = xhr.getAllResponseHeaders(); 
     104                        var topHeaderParts = topHeaders.match(/[^:\n]+:[^\n]+\n/g); 
     105                        } 
     106                        while(data){ 
     107                                var headers = {}; 
     108                                var httpParts = data.match(/(\n*[^\n]+)/); 
     109                                if(!httpParts){  
     110                                        return; 
     111                                } 
     112                                data = data.substring(httpParts[0].length+1); 
     113                                httpParts = httpParts[1]; 
     114                                var headerParts = data.match(/([^\n]+\n)*/)[0]; 
     115                                data = data.substring(headerParts.length+1); 
     116                                headerParts = topHeaders + headerParts; 
     117                                var headerStr = headerParts; 
     118                                headerParts = headerParts.match(/[^:\n]+:[^\n]+\n/g); // parse the containing and contained response headers with the contained taking precedence (by going last) 
     119                                for(var j = 0; j < headerParts.length; j++){ 
     120                                        var colonIndex = headerParts[j].indexOf(':'); 
     121                                        headers[headerParts[j].substring(0,colonIndex)] = headerParts[j].substring(colonIndex+1).replace(/(^[ \r\n]*)|([ \r\n]*)$/g,''); // trim 
     122                                }                                                                                
     123 
     124                                httpParts = httpParts.split(' '); 
     125                                var message = { // make it look like an xhr object, at least for the response part of the API 
     126                                        status : parseInt(httpParts[1],10), 
     127                                        statusText : httpParts[2], 
     128                                        readyState : 4, 
     129                                        getAllResponseHeaders : function(){ return headerStr;}, 
     130                                        getResponseHeader : function(name){ return headers[name];}};  
     131                                 
     132                                var contentLength = headers['Content-Length'];  
     133                                if(contentLength){ 
     134                                        if(contentLength <= data.length){ 
     135                                                message.responseText = data.substring(0,contentLength); 
     136                                                data = data.substring(contentLength); 
     137                                                this.lastIndex = xhr.responseText.length - data.length; // need to pick up from where we left on streaming connections 
     138                                        } 
     139                                        else { 
     140                                                return; // the response not finished 
     141                                        } 
     142                                } 
     143                                else {// TODO: Need to check if the headers are complete before incrementing the last index 
     144                                        this.lastIndex = xhr.responseText.length - data.length; // need to pick up from where we left on streaming connections 
     145                                } 
     146                                if (this._processMessage(message)) 
     147                                        return "conflict"; 
     148                        } 
     149                        return; 
     150                } 
     151                if(xhr.readyState != 4){ // we only want finished responses here if we are not streaming  
     152                        return; 
     153                } 
     154                 
     155                if(xhr.__proto__){// firefox uses this property, so we create an instance to shadow this property 
     156                        xhr = {channel:"channel",__proto__:xhr}; 
     157                } 
     158                return this._processMessage(xhr); 
     159         
     160        } 
     161});