John Cappiello - Dojo.common-0.4.1

Documentation | Source
dojo.require("dojo.io.common"); // io/common.js provides setIFrameSrc and the IO module
dojo.provide("dojo.io.cometd");
dojo.require("dojo.AdapterRegistry");
dojo.require("dojo.json");
dojo.require("dojo.io.BrowserIO"); // we need XHR for the handshake, etc.
// FIXME: determine if we can use XMLHTTP to make x-domain posts despite not
//        being able to hear back about the result
dojo.require("dojo.io.IframeIO");
dojo.require("dojo.io.ScriptSrcIO"); // for x-domain long polling
dojo.require("dojo.io.cookie"); // for peering
dojo.require("dojo.event.*");
dojo.require("dojo.lang.common");
dojo.require("dojo.lang.func");

/*
 * this file defines Comet protocol client. Actual message transport is
 * deferred to one of several connection type implementations. The default is a
 * forever-frame implementation. A single global object named "cometd" is
 * used to mediate for these connection types in order to provide a stable
 * interface.
 */

// TODO: the auth handling in this file is a *mess*. It should probably live in
// the cometd object with the ability to mix in or call down to an auth-handler
// object, the prototypical variant of which is a no-op

cometd = new function(){

	this.initialized = false;
	this.connected = false;

	this.connectionTypes = new dojo.AdapterRegistry(true);

	this.version = 0.1;
	this.minimumVersion = 0.1;
	this.clientId = null;

	this._isXD = false;
	this.handshakeReturn = null;
	this.currentTransport = null;
	this.url = null;
	this.lastMessage = null;
	this.globalTopicChannels = {};
	this.backlog = [];

	this.tunnelInit = function(childLocation, childDomain){
		// placeholder
	}

	this.tunnelCollapse = function(){
		dojo.debug("tunnel collapsed!");
		// placeholder
	}

	this.init = function(props, root, 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";
		// FIXME: do we just assume that the props knows
		// everything we care about WRT to auth? Should we be trying to
		// call back into it for subsequent auth actions? Should we fire
		// local auth functions to ask for/get auth data?

		// FIXME: what about ScriptSrcIO for x-domain comet?
		this.url = root||djConfig["cometdRoot"];
		if(!this.url){
			dojo.debug("no cometd root specified in djConfig and no root passed");
			return;
		}
		
		// FIXME: we need to select a way to handle JSONP-style stuff
		// generically here. We already know if the server is gonna be on
		// another domain (or can know it), so we should select appropriate
		// negotiation methods here as well as in final transport type
		// selection.
		var bindArgs = {
			url: this.url,
			method: "POST",
			mimetype: "text/json",
			load: dojo.lang.hitch(this, "finishInit"),
			content: { "message": dojo.json.serialize([props]) }
		};

		// 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";
				if(	(urlHost != thisHost)||
					(urlPort != thisPort) ){
					dojo.debug(thisHost, urlHost);
					dojo.debug(thisPort, urlPort);

					this._isXD = true;
					bindArgs.transport = "ScriptSrcTransport";
					bindArgs.jsonParamName = "jsonp";
					bindArgs.method = "GET";
				}
			}
		}
		if(bargs){
			dojo.lang.mixin(bindArgs, bargs);
		}
		return dojo.io.bind(bindArgs);
	}

	this.finishInit = function(type, data, evt, request){
		data = data[0];
		this.handshakeReturn = data;
		// pick a transport
		if(data["authSuccessful"] == false){
			dojo.debug("cometd authentication failed");
			return;
		}
		if(data.version < this.minimumVersion){
			dojo.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.version = data.version;
		this.clientId = data.clientId;
		this.tunnelInit = dojo.lang.hitch(this.currentTransport, "tunnelInit");
		this.tunnelCollapse = dojo.lang.hitch(this.currentTransport, "tunnelCollapse");
		this.initialized = true;
		this.currentTransport.startup(data);
		while(this.backlog.length != 0){
			var cur = this.backlog.shift();
			var fn = cur.shift();
			this[fn].apply(this, cur);
		}
	}

	this._getRandStr = function(){
		return Math.random().toString().substring(2, 10);
	}

	// public API functions called by cometd or by the transport classes
	this.deliver = function(messages){
		dojo.lang.forEach(messages, this._deliver, this);
	}

	this._deliver = function(message){
		// dipatch events along the specified path
		if(!message["channel"]){
			dojo.debug("cometd error: no channel for message!");
			return;
		}
		if(!this.currentTransport){
			this.backlog.push(["deliver", message]);
			return;
		}
		this.lastMessage = message;
		// check to see if we got a /meta channel message that we care about
		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/subscribe":
					if(!message.successful){
						dojo.debug("cometd subscription error for channel", message.channel, ":", message.error);
						return;
					}
					this.subscribed(message.subscription, message);
					break;
				case "/meta/unsubscribe":
					if(!message.successful){
						dojo.debug("cometd unsubscription error for channel", message.channel, ":", message.error);
						return;
					}
					this.unsubscribed(message.subscription, message);
					break;
			}
		}
		// send the message down for processing by the transport
		this.currentTransport.deliver(message);

		// dispatch the message to any locally subscribed listeners
		var tname = (this.globalTopicChannels[message.channel]) ? message.channel : "/cometd"+message.channel;
		dojo.event.topic.publish(tname, message);
	}

	this.disconnect = function(){
		if(!this.currentTransport){
			dojo.debug("no current transport to disconnect from");
			return;
		}
		this.currentTransport.disconnect();
	}

	// 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
		if(!this.currentTransport){
			this.backlog.push(["publish", channel, data, properties]);
			return;
		}
		var message = {
			data: data,
			channel: channel
		};
		if(properties){
			dojo.lang.mixin(message, properties);
		}
		return this.currentTransport.sendMessage(message);
	}

	this.subscribe = function(	/*string*/				channel, 
								/*boolean, optional*/	useLocalTopics, 
								/*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
		// useLocalTopics:
		//		Determines if up a local event topic subscription to the passed
		//		function using the channel name that was passed is constructed,
		//		or if the topic name will be prefixed with some other
		//		identifier for local message distribution. Setting this to
		//		"true" is a good way to hook up server-sent message delivery to
		//		pre-existing local topics.
		// 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.currentTransport){
			this.backlog.push(["subscribe", channel, useLocalTopics, objOrFunc, funcName]);
			return;
		}
		if(objOrFunc){
			var tname = (useLocalTopics) ? channel : "/cometd"+channel;
			if(useLocalTopics){
				this.globalTopicChannels[channel] = true;
			}
			dojo.event.topic.subscribe(tname, objOrFunc, funcName);
		}
		// FIXME: would we handle queuing of the subscription if not connected?
		// Or should the transport object?
		return this.currentTransport.sendMessage({
			channel: "/meta/subscribe",
			subscription: channel
		});
	}

	this.subscribed = function(	/*string*/				channel, 
								/*obj*/					message){
		dojo.debug(channel);
		dojo.debugShallow(message);
	}

	this.unsubscribe = function(/*string*/				channel, 
								/*boolean, optional*/	useLocalTopics, 
								/*object, optional*/	objOrFunc, 
								/*string, optional*/	funcName){ // return: boolean
		// summary:
		//		inform the server of this client's disinterest in channel
		// channel:
		//		name of the cometd channel to subscribe to
		// useLocalTopics:
		//		Determines if up a local event topic subscription to the passed
		//		function using the channel name that was passed is destroyed,
		//		or if the topic name will be prefixed with some other
		//		identifier for stopping message distribution.
		// 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
		if(!this.currentTransport){
			this.backlog.push(["unsubscribe", channel, useLocalTopics, objOrFunc, funcName]);
			return;
		}
		//		a callback function to notifiy upon channel message delivery
		if(objOrFunc){
			// FIXME: should actual local topic unsubscription be delayed for
			// successful unsubcribe notices from the other end? (guessing "no")
			// FIXME: if useLocalTopics is false, should we go ahead and
			// destroy the local topic?
			var tname = (useLocalTopics) ? channel : "/cometd"+channel;
			dojo.event.topic.unsubscribe(tname, objOrFunc, funcName);
		}
		return this.currentTransport.sendMessage({
			channel: "/meta/unsubscribe",
			subscription: channel
		});
	}

	this.unsubscribed = function(/*string*/				channel, 
								/*obj*/					message){
		dojo.debug(channel);
		dojo.debugShallow(message);
	}

	// FIXME: add an "addPublisher" function

}

/*
transport objects MUST expose the following methods:
	- check
	- startup
	- sendMessage
	- 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.connected = false;
	this.connectionId = null;
	this.authToken = null;
	this.lastTimestamp = null;
	this.lastId = 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(this.connected){ return; }
		// FIXME: fill in startup routine here
		this.connected = true;
	}

	this.sendMessage = function(message){
		// FIXME: fill in message sending logic
	}

	this.deliver = function(message){
		if(message["timestamp"]){
			this.lastTimestamp = message.timestamp;
		}
		if(message["id"]){
			this.lastId = message.id;
		}
		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(){
		if(!this.connected){ return; }
		// FIXME: fill in shutdown routine here
		this.connected = false;
	}
}
cometd.connectionTypes.register("blah", cometd.blahTransport.check, cometd.blahTransport);
*/

cometd.iframeTransport = new function(){
	this.connected = false;
	this.connectionId = null;

	this.rcvNode = null;
	this.rcvNodeName = "";
	this.phonyForm = null;
	this.authToken = null;
	this.lastTimestamp = null;
	this.lastId = null;
	this.backlog = [];

	this.check = function(types, version, xdomain){
		return ((!xdomain)&&
				(!dojo.render.html.safari)&&
				(dojo.lang.inArray(types, "iframe")));
	}

	this.tunnelInit = function(){
		// we've gotten our initialization document back in the iframe, so
		// now open up a connection and start passing data!
		this.postToIframe({
			message: dojo.json.serialize([
				{
					channel:	"/meta/connect",
					clientId:	cometd.clientId,
					connectionType: "iframe"
					// FIXME: auth not passed here!
					// "authToken": this.authToken
				}
			])
		});
	}

	this.tunnelCollapse = function(){
		if(this.connected){
			// try to restart the tunnel
			this.connected = false;

			this.postToIframe({
				message: dojo.json.serialize([
					{
						channel:	"/meta/reconnect",
						clientId:	cometd.clientId,
						connectionId:	this.connectionId,
						timestamp:	this.lastTimestamp,
						id:			this.lastId
						// FIXME: no authToken provision!
					}
				])
			});
		}
	}

	this.deliver = function(message){
		// handle delivery details that this transport particularly cares
		// about. Most functions of should be handled by the main cometd object
		// with only transport-specific details and state being tracked here.
		if(message["timestamp"]){
			this.lastTimestamp = message.timestamp;
		}
		if(message["id"]){
			this.lastId = message.id;
		}
		// check to see if we got a /meta channel message that we care about
		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":
					if(!message.successful){
						dojo.debug("cometd connection error:", message.error);
						return;
					}
					this.connectionId = message.connectionId;
					this.connected = true;
					this.processBacklog();
					break;
				case "/meta/reconnect":
					if(!message.successful){
						dojo.debug("cometd reconnection error:", message.error);
						return;
					}
					this.connected = true;
					break;
				case "/meta/subscribe":
					if(!message.successful){
						dojo.debug("cometd subscription error for channel", message.channel, ":", message.error);
						return;
					}
					// this.subscribed(message.channel);
					dojo.debug(message.channel);
					break;
			}
		}
	}

	this.widenDomain = function(domainStr){
		// allow us to make reqests to the TLD
		var cd = domainStr||document.domain;
		if(cd.indexOf(".")==-1){ return; } // probably file:/// or localhost
		var dps = cd.split(".");
		if(dps.length<=2){ return; } // probably file:/// or an RFC 1918 address
		dps = dps.slice(dps.length-2);
		document.domain = dps.join(".");
		return document.domain;
	}

	this.postToIframe = function(content, url){
		if(!this.phonyForm){
			if(dojo.render.html.ie){
				this.phonyForm = document.createElement("<form enctype='application/x-www-form-urlencoded' method='POST' style='display: none;'>");
				dojo.body().appendChild(this.phonyForm);
			}else{
				this.phonyForm = document.createElement("form");
				this.phonyForm.style.display = "none"; // FIXME: will this still work?
				dojo.body().appendChild(this.phonyForm);
				this.phonyForm.enctype = "application/x-www-form-urlencoded";
				this.phonyForm.method = "POST";
			}
		}

		this.phonyForm.action = url||cometd.url;
		this.phonyForm.target = this.rcvNodeName;
		this.phonyForm.setAttribute("target", this.rcvNodeName);

		while(this.phonyForm.firstChild){
			this.phonyForm.removeChild(this.phonyForm.firstChild);
		}

		for(var x in content){
			var tn;
			if(dojo.render.html.ie){
				tn = document.createElement("<input type='hidden' name='"+x+"' value='"+content[x]+"'>");
				this.phonyForm.appendChild(tn);
			}else{
				tn = document.createElement("input");
				this.phonyForm.appendChild(tn);
				tn.type = "hidden";
				tn.name = x;
				tn.value = content[x];
			}
		}
		this.phonyForm.submit();
	}

	this.processBacklog = function(){
		while(this.backlog.length > 0){
			this.sendMessage(this.backlog.shift(), true);
		}
	}

	this.sendMessage = function(message, bypassBacklog){
		// FIXME: what about auth fields?
		if((bypassBacklog)||(this.connected)){
			message.connectionId = this.connectionId;
			message.clientId = cometd.clientId;
			var bindArgs = {
				url: cometd.url||djConfig["cometdRoot"],
				method: "POST",
				mimetype: "text/json",
				// FIXME: we should be able to do better than this given that we're sending an array!
				content: { message: dojo.json.serialize([ message ]) }
			};
			return dojo.io.bind(bindArgs);
		}else{
			this.backlog.push(message);
		}
	}

	this.startup = function(handshakeData){
		dojo.debug("startup!");
		dojo.debug(dojo.json.serialize(handshakeData));

		if(this.connected){ return; }

		// this.widenDomain();

		// NOTE: we require the server to cooperate by hosting
		// cometdInit.html at the designated endpoint
		this.rcvNodeName = "cometdRcv_"+cometd._getRandStr();
		// the "forever frame" approach

		var initUrl = cometd.url+"/?tunnelInit=iframe"; // &domain="+document.domain;
		if(false && dojo.render.html.ie){ // FIXME: DISALBED FOR NOW
			// use the "htmlfile hack" to prevent the background click junk
			this.rcvNode = new ActiveXObject("htmlfile");
			this.rcvNode.open();
			this.rcvNode.write("<html>");
			this.rcvNode.write("<script>document.domain = '"+document.domain+"'");
			this.rcvNode.write("</html>");
			this.rcvNode.close();

			var ifrDiv = this.rcvNode.createElement("div");
			this.rcvNode.appendChild(ifrDiv);
			this.rcvNode.parentWindow.dojo = dojo;
			ifrDiv.innerHTML = "<iframe src='"+initUrl+"'></iframe>"
		}else{
			this.rcvNode = dojo.io.createIFrame(this.rcvNodeName, "", initUrl);
			// dojo.io.setIFrameSrc(this.rcvNode, initUrl);
			// we're still waiting on the iframe to call back up to use and
			// advertise that it's been initialized via tunnelInit
		}
	}
}

cometd.mimeReplaceTransport = new function(){
	this.connected = false;
	this.connectionId = null;
	this.xhr = null;

	this.authToken = null;
	this.lastTimestamp = null;
	this.lastId = null;
	this.backlog = [];

	this.check = function(types, version, xdomain){
		return ((!xdomain)&&
				(dojo.render.html.mozilla)&& // seems only Moz really supports this right now = (
				(dojo.lang.inArray(types, "mime-message-block")));
	}

	this.tunnelInit = function(){
		if(this.connected){ return; }
		// FIXME: open up the connection here
		this.openTunnelWith({
			message: dojo.json.serialize([
				{
					channel:	"/meta/connect",
					clientId:	cometd.clientId,
					connectionType: "mime-message-block"
					// FIXME: auth not passed here!
					// "authToken": this.authToken
				}
			])
		});
		this.connected = true;
	}

	this.tunnelCollapse = function(){
		if(this.connected){
			// try to restart the tunnel
			this.connected = false;
			this.openTunnelWith({
				message: dojo.json.serialize([
					{
						channel:	"/meta/reconnect",
						clientId:	cometd.clientId,
						connectionId:	this.connectionId,
						timestamp:	this.lastTimestamp,
						id:			this.lastId
						// FIXME: no authToken provision!
					}
				])
			});
		}
	}

	this.deliver = cometd.iframeTransport.deliver;
	// the logic appears to be the same

	this.handleOnLoad = function(resp){
		cometd.deliver(dojo.json.evalJson(this.xhr.responseText));
	}

	this.openTunnelWith = function(content, url){
		// set up the XHR object and register the multipart callbacks
		this.xhr = dojo.hostenv.getXmlhttpObject();
		this.xhr.multipart = true; // FIXME: do Opera and Safari support this flag?
		if(dojo.render.html.mozilla){
			this.xhr.addEventListener("load", dojo.lang.hitch(this, "handleOnLoad"), false);
		}else if(dojo.render.html.safari){
			// Blah. WebKit doesn't actually populate responseText and/or responseXML. Useless.
			dojo.debug("Webkit is broken with multipart responses over XHR = (");
			this.xhr.onreadystatechange = dojo.lang.hitch(this, "handleOnLoad");
		}else{
			this.xhr.onload = dojo.lang.hitch(this, "handleOnLoad");
		}
		this.xhr.open("POST", (url||cometd.url), true); // async post
		this.xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
		dojo.debug(dojo.json.serialize(content));
		this.xhr.send(dojo.io.argsFromMap(content, "utf8"));
	}

	this.processBacklog = function(){
		while(this.backlog.length > 0){
			this.sendMessage(this.backlog.shift(), true);
		}
	}

	this.sendMessage = function(message, bypassBacklog){
		// FIXME: what about auth fields?
		if((bypassBacklog)||(this.connected)){
			message.connectionId = this.connectionId;
			message.clientId = cometd.clientId;
			var bindArgs = {
				url: cometd.url||djConfig["cometdRoot"],
				method: "POST",
				mimetype: "text/json",
				content: { message: dojo.json.serialize([ message ]) }
			};
			return dojo.io.bind(bindArgs);
		}else{
			this.backlog.push(message);
		}
	}

	this.startup = function(handshakeData){
		dojo.debugShallow(handshakeData);
		if(this.connected){ return; }
		this.tunnelInit();
	}
}

cometd.longPollTransport = new function(){
	this.connected = false;
	this.connectionId = null;

	this.authToken = null;
	this.lastTimestamp = null;
	this.lastId = null;
	this.backlog = [];

	this.check = function(types, version, xdomain){
		return ((!xdomain)&&(dojo.lang.inArray(types, "long-polling")));
	}

	this.tunnelInit = function(){
		if(this.connected){ return; }
		// FIXME: open up the connection here
		this.openTunnelWith({
			message: dojo.json.serialize([
				{
					channel:	"/meta/connect",
					clientId:	cometd.clientId,
					connectionType: "long-polling"
					// FIXME: auth not passed here!
					// "authToken": this.authToken
				}
			])
		});
		this.connected = true;
	}

	this.tunnelCollapse = function(){
		if(!this.connected){
			// try to restart the tunnel
			this.connected = false;
			dojo.debug("clientId:", cometd.clientId);
			this.openTunnelWith({
				message: dojo.json.serialize([
					{
						channel:	"/meta/reconnect",
						connectionType: "long-polling",
						clientId:	cometd.clientId,
						connectionId:	this.connectionId,
						timestamp:	this.lastTimestamp,
						id:			this.lastId
						// FIXME: no authToken provision!
					}
				])
			});
		}
	}

	this.deliver = cometd.iframeTransport.deliver;
	// the logic appears to be the same

	this.openTunnelWith = function(content, url){
		dojo.io.bind({
			url: (url||cometd.url),
			method: "post",
			content: content,
			mimetype: "text/json",
			load: dojo.lang.hitch(this, function(type, data, evt, args){
				// dojo.debug(evt.responseText);
				cometd.deliver(data);
				this.connected = false;
				this.tunnelCollapse();
			}),
			error: function(){ dojo.debug("tunnel opening failed"); }
		});
		this.connected = true;
	}

	this.processBacklog = function(){
		while(this.backlog.length > 0){
			this.sendMessage(this.backlog.shift(), true);
		}
	}

	this.sendMessage = function(message, bypassBacklog){
		// FIXME: what about auth fields?
		if((bypassBacklog)||(this.connected)){
			message.connectionId = this.connectionId;
			message.clientId = cometd.clientId;
			var bindArgs = {
				url: cometd.url||djConfig["cometdRoot"],
				method: "post",
				mimetype: "text/json",
				content: { message: dojo.json.serialize([ message ]) }
			};
			return dojo.io.bind(bindArgs);
		}else{
			this.backlog.push(message);
		}
	}

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

cometd.callbackPollTransport = new function(){
	this.connected = false;
	this.connectionId = null;

	this.authToken = null;
	this.lastTimestamp = null;
	this.lastId = null;
	this.backlog = [];

	this.check = function(types, version, xdomain){
		// we handle x-domain!
		return dojo.lang.inArray(types, "callback-polling");
	}

	this.tunnelInit = function(){
		if(this.connected){ return; }
		// FIXME: open up the connection here
		this.openTunnelWith({
			message: dojo.json.serialize([
				{
					channel:	"/meta/connect",
					clientId:	cometd.clientId,
					connectionType: "callback-polling"
					// FIXME: auth not passed here!
					// "authToken": this.authToken
				}
			])
		});
		this.connected = true;
	}

	this.tunnelCollapse = function(){
		if(!this.connected){
			// try to restart the tunnel
			this.connected = false;
			this.openTunnelWith({
				message: dojo.json.serialize([
					{
						channel:	"/meta/reconnect",
						connectionType: "long-polling",
						clientId:	cometd.clientId,
						connectionId:	this.connectionId,
						timestamp:	this.lastTimestamp,
						id:			this.lastId
						// FIXME: no authToken provision!
					}
				])
			});
		}
	}

	this.deliver = cometd.iframeTransport.deliver;
	// the logic appears to be the same

	this.openTunnelWith = function(content, url){
		// create a <script> element to generate the request
		var req = dojo.io.bind({
			url: (url||cometd.url),
			content: content,
			mimetype: "text/json",
			transport: "ScriptSrcTransport",
			jsonParamName: "jsonp",
			load: dojo.lang.hitch(this, function(type, data, evt, args){
				dojo.debug(dojo.json.serialize(data));
				cometd.deliver(data);
				this.connected = false;
				this.tunnelCollapse();
			}),
			error: function(){ dojo.debug("tunnel opening failed"); }
		});
		this.connected = true;
	}

	this.processBacklog = function(){
		while(this.backlog.length > 0){
			this.sendMessage(this.backlog.shift(), true);
		}
	}

	this.sendMessage = function(message, bypassBacklog){
		// FIXME: what about auth fields?
		if((bypassBacklog)||(this.connected)){
			message.connectionId = this.connectionId;
			message.clientId = cometd.clientId;
			var bindArgs = {
				url: cometd.url||djConfig["cometdRoot"],
				mimetype: "text/json",
				transport: "ScriptSrcTransport",
				jsonParamName: "jsonp",
				content: { message: dojo.json.serialize([ message ]) }
			};
			return dojo.io.bind(bindArgs);
		}else{
			this.backlog.push(message);
		}
	}

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

cometd.connectionTypes.register("mime-message-block", cometd.mimeReplaceTransport.check, cometd.mimeReplaceTransport);
cometd.connectionTypes.register("long-polling", cometd.longPollTransport.check, cometd.longPollTransport);
cometd.connectionTypes.register("callback-polling", cometd.callbackPollTransport.check, cometd.callbackPollTransport);
cometd.connectionTypes.register("iframe", cometd.iframeTransport.check, cometd.iframeTransport);

// FIXME: need to implement fallback-polling, IE XML block

dojo.io.cometd = cometd;