if (!dojo._hasResource["dojox._cometd.cometd"]) { // _hasResource checks added // by build. Do not use // _hasResource directly in // your code. dojo._hasResource["dojox._cometd.cometd"] = true; dojo.provide("dojox._cometd.cometd"); dojo.require("dojo.AdapterRegistry"); dojo.require("dojo.io.script"); // FIXME: need to add local topic support to advise about: // successful handshake // network failure of channel // graceful disconnect /* * this file defines Comet protocol client. Actual message transport is * deferred to one of several connection type implementations. The default * is a long-polling implementation. A single global object named * "dojox.cometd" is used to mediate for these connection types in order to * provide a stable interface. */ dojox.cometd = new function() { /* * cometd states: DISCONNECTED: _initialized==false && _connected==false * CONNECTING: _initialized==true && _connected==false (handshake sent) * CONNECTED: _initialized==true && _connected==true (first successful * connect) DISCONNECTING: _initialized==false && _connected==true * (disconnect sent) */ this._initialized = false; this._connected = false; this._polling = false; this.connectionTypes = new dojo.AdapterRegistry(true); this.version = "1.0"; this.minimumVersion = "0.9"; this.clientId = null; this.messageId = 0; this.batch = 0; this._isXD = false; this.handshakeReturn = null; this.currentTransport = null; this.url = null; this.lastMessage = null; this.topics = {}; this._messageQ = []; this.handleAs = "json-comment-optional"; this.advice; this.pendingSubscriptions = {} this.pendingUnsubscriptions = {} this._subscriptions = []; this.tunnelInit = function(childLocation, childDomain) { // placeholder } this.tunnelCollapse = function() { console.debug("tunnel collapsed!"); // placeholder } this.init = function(root, props, bargs) { // FIXME: if the root isn't from the same host, we should // automatically // try to select an XD-capable transport props = props || {}; // go ask the short bus server what we can support props.version = this.version; props.minimumVersion = this.minimumVersion; props.channel = "/meta/handshake"; props.id = "" + this.messageId++; this.url = root || djConfig["cometdRoot"]; if (!this.url) { console .debug("no cometd root specified in djConfig and no root passed"); return; } // Are we x-domain? borrowed from dojo.uri.Uri in lieu of fixed host // and port properties var regexp = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$"; var r = ("" + window.location).match(new RegExp(regexp)); if (r[4]) { var tmp = r[4].split(":"); var thisHost = tmp[0]; var thisPort = tmp[1] || "80"; // FIXME: match 443 r = this.url.match(new RegExp(regexp)); if (r[4]) { tmp = r[4].split(":"); var urlHost = tmp[0]; var urlPort = tmp[1] || "80"; this._isXD = ((urlHost != thisHost) || (urlPort != thisPort)); } } if (!this._isXD) { if (props.ext) { if (props.ext["json-comment-filtered"] !== true && props.ext["json-comment-filtered"] !== false) { props.ext["json-comment-filtered"] = true; } } else { props.ext = { "json-comment-filtered" : true }; } } var bindArgs = { url : this.url, handleAs : this.handleAs, content : { "message" : dojo.toJson([props]) }, load : dojo.hitch(this, "finishInit"), error : function(e) { console.debug("handshake error!:", e); } }; if (bargs) { dojo.mixin(bindArgs, bargs); } this._props = props; this._initialized = true; this.batch = 0; this.startBatch(); // if xdomain, then we assume jsonp for handshake if (this._isXD) { bindArgs.callbackParamName = "jsonp"; return dojo.io.script.get(bindArgs); } return dojo.xhrPost(bindArgs); } this.finishInit = function(data) { data = data[0]; this.handshakeReturn = data; // pick a transport if (data["advice"]) { this.advice = data.advice; } if (!data.successful) { console.debug("cometd init failed"); if (this.advice && this.advice["reconnect"] == "none") { return; } if (this.advice && this.advice["interval"] && this.advice.interval > 0) { var cometd = this; setTimeout(function() { cometd.init(cometd.url, cometd._props); }, this.advice.interval); } else { this.init(this.url, this._props); } return; } if (data.version < this.minimumVersion) { console.debug("cometd protocol version mismatch. We wanted", this.minimumVersion, "but got", data.version); return; } this.currentTransport = this.connectionTypes.match( data.supportedConnectionTypes, data.version, this._isXD); this.currentTransport._cometd = this; this.currentTransport.version = data.version; this.clientId = data.clientId; this.tunnelInit = dojo.hitch(this.currentTransport, "tunnelInit"); this.tunnelCollapse = dojo.hitch(this.currentTransport, "tunnelCollapse"); this.currentTransport.startup(data); } // public API functions called by cometd or by the transport classes this.deliver = function(messages) { // console.debug(messages); dojo.forEach(messages, this._deliver, this); return messages; } this._deliver = function(message) { // dipatch events along the specified path if (!message["channel"]) { if (message["success"] !== true) { console.debug("cometd error: no channel for message!", message); return; } } this.lastMessage = message; if (message.advice) { this.advice = message.advice; // TODO maybe merge? } // check to see if we got a /meta channel message that we care about if ((message["channel"]) && (message.channel.length > 5) && (message.channel.substr(0, 5) == "/meta")) { // check for various meta topic actions that we need to respond // to switch (message.channel) { case "/meta/connect" : if (message.successful && !this._connected) { this._connected = this._initialized; this.endBatch(); } else if (!this._initialized) { this._connected = false; // finish disconnect } break; case "/meta/subscribe" : var pendingDef = this.pendingSubscriptions[message.subscription]; if (!message.successful) { if (pendingDef) { pendingDef.errback(new Error(message.error)); delete this.pendingSubscriptions[message.subscription]; } return; } dojox.cometd.subscribed(message.subscription, message); if (pendingDef) { pendingDef.callback(true); delete this.pendingSubscriptions[message.subscription]; } break; case "/meta/unsubscribe" : var pendingDef = this.pendingUnsubscriptions[message.subscription]; if (!message.successful) { if (pendingDef) { pendingDef.errback(new Error(message.error)); delete this.pendingUnsubscriptions[message.subscription]; } return; } this.unsubscribed(message.subscription, message); if (pendingDef) { pendingDef.callback(true); delete this.pendingUnsubscriptions[message.subscription]; } break; } } // send the message down for processing by the transport this.currentTransport.deliver(message); if (message.data) { // dispatch the message to any locally subscribed listeners var tname = "/cometd" + message.channel; dojo.publish(tname, [message]); } } this.disconnect = function() { dojo.forEach(this._subscriptions, dojo.unsubscribe); this._subscriptions = []; this._messageQ = []; if (this._initialized && this.currentTransport) { this._initialized = false; this.currentTransport.disconnect(); } this._initialized = false; if (!this._polling) this._connected = false; } // public API functions called by end users this.publish = function(/* string */channel, /* object */data, /* object */ properties) { // summary: // publishes the passed message to the cometd server for delivery // on the specified topic // channel: // the destination channel for the message // data: // a JSON object containing the message "payload" // properties: // Optional. Other meta-data to be mixed into the top-level of the // message var message = { data : data, channel : channel }; if (properties) { dojo.mixin(message, properties); } this._sendMessage(message); } this._sendMessage = function(/* object */message) { if (this.currentTransport && this._connected && this.batch == 0) { return this.currentTransport.sendMessages([message]); } else { this._messageQ.push(message); } } this.subscribe = function( /* string */channel, /* object, optional */objOrFunc, /* string, optional */funcName) { // return: boolean // summary: // inform the server of this client's interest in channel // channel: // name of the cometd channel to subscribe to // objOrFunc: // an object scope for funcName or the name or reference to a // function to be called when messages are delivered to the // channel // funcName: // the second half of the objOrFunc/funcName pair for identifying // a callback function to notifiy upon channel message delivery if (this.pendingSubscriptions[channel]) { // We already asked to subscribe to this channel, and // haven't heard back yet. Fail the previous attempt. var oldDef = this.pendingSubscriptions[channel]; oldDef.cancel(); delete this.pendingSubscriptions[channel]; } var pendingDef = new dojo.Deferred(); this.pendingSubscriptions[channel] = pendingDef; if (objOrFunc) { var tname = "/cometd" + channel; if (this.topics[tname]) { dojo.unsubscribe(this.topics[tname]); } var topic = dojo.subscribe(tname, objOrFunc, funcName); this.topics[tname] = topic; } this._sendMessage({ channel : "/meta/subscribe", subscription : channel }); return pendingDef; } this.subscribed = function( /* string */channel, /* obj */message) { } this.unsubscribe = function(/* string */channel) { // return: boolean // summary: // inform the server of this client's disinterest in channel // channel: // name of the cometd channel to unsubscribe from if (this.pendingUnsubscriptions[channel]) { // We already asked to unsubscribe from this channel, and // haven't heard back yet. Fail the previous attempt. var oldDef = this.pendingUnsubscriptions[channel]; oldDef.cancel(); delete this.pendingUnsubscriptions[channel]; } var pendingDef = new dojo.Deferred(); this.pendingUnsubscriptions[channel] = pendingDef; var tname = "/cometd" + channel; if (this.topics[tname]) { dojo.unsubscribe(this.topics[tname]); } this._sendMessage({ channel : "/meta/unsubscribe", subscription : channel }); return pendingDef; } this.unsubscribed = function( /* string */channel, /* obj */message) { } this.startBatch = function() { this.batch++; } this.endBatch = function() { if (--this.batch <= 0 && this.currentTransport && this._connected) { this.batch = 0; var messages = this._messageQ; this._messageQ = []; if (messages.length > 0) { this.currentTransport.sendMessages(messages); } } } this._onUnload = function() { // make this the last of the onUnload method dojo.addOnUnload(dojox.cometd, "disconnect"); } } /* * transport objects MUST expose the following methods: - check - startup - * sendMessages - deliver - disconnect optional, standard but transport * dependent methods are: - tunnelCollapse - tunnelInit * * Transports SHOULD be namespaced under the cometd object and transports * MUST register themselves with cometd.connectionTypes * * here's a stub transport defintion: * * cometd.blahTransport = new function(){ this._connectionType="my-polling"; * this._cometd=null; this.lastTimestamp = null; * * this.check = function(types, version, xdomain){ // summary: // determines * whether or not this transport is suitable given a // list of transport * types that the server supports return dojo.lang.inArray(types, "blah"); } * * this.startup = function(){ if(dojox.cometd._polling){ return; } // FIXME: * fill in startup routine here dojox.cometd._polling = true; } * * this.sendMessages = function(message){ // FIXME: fill in message array * sending logic } * * this.deliver = function(message){ if(message["timestamp"]){ * this.lastTimestamp = message.timestamp; } if( (message.channel.length > * 5)&& (message.channel.substr(0, 5) == "/meta")){ // check for various * meta topic actions that we need to respond to // switch(message.channel){ // * case "/meta/connect": // // FIXME: fill in logic here // break; // // * case ...: ... // } } } * * this.disconnect = function(){ } } cometd.connectionTypes.register("blah", * cometd.blahTransport.check, cometd.blahTransport); */ dojox.cometd.longPollTransport = new function() { this._connectionType = "long-polling"; this._cometd = null; this.lastTimestamp = null; this.check = function(types, version, xdomain) { return ((!xdomain) && (dojo.indexOf(types, "long-polling") >= 0)); } this.tunnelInit = function() { if (this._cometd._polling) { return; } this.openTunnelWith({ message : dojo.toJson([{ channel : "/meta/connect", clientId : this._cometd.clientId, connectionType : this._connectionType, id : "" + this._cometd.messageId++ }]) }); } this.tunnelCollapse = function() { if (!this._cometd._polling) { // try to restart the tunnel this._cometd._polling = false; // TODO handle transport specific advice if (this._cometd["advice"]) { if (this._cometd.advice["reconnect"] == "none") { return; } if ((this._cometd.advice["interval"]) && (this._cometd.advice.interval > 0)) { var transport = this; setTimeout(function() { transport._connect(); }, this._cometd.advice.interval); } else { this._connect(); } } else { this._connect(); } } } this._connect = function() { if ((this._cometd["advice"]) && (this._cometd.advice["reconnect"] == "handshake")) { this._cometd.init(this._cometd.url, this._cometd._props); } else if (this._cometd._connected) { this.openTunnelWith({ message : dojo.toJson([{ channel : "/meta/connect", connectionType : this._connectionType, clientId : this._cometd.clientId, timestamp : this.lastTimestamp, id : "" + this._cometd.messageId++ }]) }); } } this.deliver = function(message) { // console.debug(message); if (message["timestamp"]) { this.lastTimestamp = message.timestamp; } } this.openTunnelWith = function(content, url) { // console.debug("openTunnelWith:", content, (url||cometd.url)); var d = dojo.xhrPost({ url : (url || this._cometd.url), content : content, handleAs : this._cometd.handleAs, load : dojo.hitch(this, function(data) { // console.debug(evt.responseText); // console.debug(data); this._cometd._polling = false; this._cometd.deliver(data); this.tunnelCollapse(); }), error : function(err) { console.debug("tunnel opening failed:", err); dojo.cometd._polling = false; // TODO - follow advice to reconnect or rehandshake? } }); this._cometd._polling = true; } this.sendMessages = function(messages) { for (var i = 0; i < messages.length; i++) { messages[i].clientId = this._cometd.clientId; messages[i].id = "" + this._cometd.messageId++; } return dojo.xhrPost({ url : this._cometd.url || djConfig["cometdRoot"], handleAs : this._cometd.handleAs, load : dojo.hitch(this._cometd, "deliver"), content : { message : dojo.toJson(messages) } }); } this.startup = function(handshakeData) { if (this._cometd._connected) { return; } this.tunnelInit(); } this.disconnect = function() { dojo.xhrPost({ url : this._cometd.url || djConfig["cometdRoot"], handleAs : this._cometd.handleAs, content : { message : dojo.toJson([{ channel : "/meta/disconnect", clientId : this._cometd.clientId, id : "" + this._cometd.messageId++ }]) } }); } } dojox.cometd.callbackPollTransport = new function() { this._connectionType = "callback-polling"; this._cometd = null; this.lastTimestamp = null; this.check = function(types, version, xdomain) { // we handle x-domain! return (dojo.indexOf(types, "callback-polling") >= 0); } this.tunnelInit = function() { if (this._cometd._polling) { return; } this.openTunnelWith({ message : dojo.toJson([{ channel : "/meta/connect", clientId : this._cometd.clientId, connectionType : this._connectionType, id : "" + this._cometd.messageId++ }]) }); } this.tunnelCollapse = dojox.cometd.longPollTransport.tunnelCollapse; this._connect = dojox.cometd.longPollTransport._connect; this.deliver = dojox.cometd.longPollTransport.deliver; this.openTunnelWith = function(content, url) { // create a