| | 1 | dojo.provide("dojox.cometd.BaseChannels"); |
| | 2 | dojo.require("dojox.rpc.Client"); |
| | 3 | if(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 | |
| | 16 | dojox.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 | }; |