Subversion Repositories Applications.papyrus

Rev

Blame | Last modification | View Log | RSS feed

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 <script> element to generate the request
                dojo.io.script.get({
                        load: dojo.hitch(this, function(data){
                                this._cometd._polling = false;
                                this._cometd.deliver(data);
                                this.tunnelCollapse();
                        }),
                        error: function(){
                                this._cometd._polling = false;
                                console.debug("tunnel opening failed");
                        },
                        url: (url||this._cometd.url),
                        content: content,
                        callbackParamName: "jsonp"
                });
                this._cometd._polling = true;
        }

        this.sendMessages = function(/*array*/ messages){
                for(var i=0; i<messages.length; i++){
                        messages[i].clientId = this._cometd.clientId;
                        messages[i].id = ""+this._cometd.messageId++;
                }
                var bindArgs = {
                        url: this._cometd.url||djConfig["cometdRoot"],
                        load: dojo.hitch(this._cometd, "deliver"),
                        callbackParamName: "jsonp",
                        content: { message: dojo.toJson( messages ) }
                };
                return dojo.io.script.get(bindArgs);
        }

        this.startup = function(handshakeData){
                if(this._cometd._connected){ return; }
                this.tunnelInit();
        }

        this.disconnect = dojox.cometd.longPollTransport.disconnect;
        

        this.disconnect = function(){
                dojo.io.script.get({
                        url: this._cometd.url||djConfig["cometdRoot"],
                        callbackParamName: "jsonp",
                        content: {
                                message: dojo.toJson([{
                                        channel:        "/meta/disconnect",
                                        clientId:       this._cometd.clientId,
                                        id:             ""+this._cometd.messageId++
                                }])
                        }
                });
        }
}
dojox.cometd.connectionTypes.register("long-polling", dojox.cometd.longPollTransport.check, dojox.cometd.longPollTransport);
dojox.cometd.connectionTypes.register("callback-polling", dojox.cometd.callbackPollTransport.check, dojox.cometd.callbackPollTransport);

dojo.addOnUnload(dojox.cometd,"_onUnload");


}