// Copyright (c) 2004 Friendster Inc., Licensed under the Academic Free
// License version 2.0 or later
dojo.require("dojo.event.*");
dojo.require("dojo.io.BrowserIO");
dojo.provide("dojo.io.RepubsubIO");
dojo.io.repubsubTranport = new function(){
var rps = dojo.io.repubsub;
this.canHandle = function(/*dojo.io.Request*/kwArgs){
//summary: Tells dojo.io.bind() if this is a good transport to
//use for the particular type of request. This is a legacy transport
//and should not be used unless you are dealing with repubsub.
//Consider a comet transport instead.
if((kwArgs["mimetype"] == "text/javascript")&&(kwArgs["method"] == "repubsub")){
return true;
}
return false;
}
this.bind = function(/*dojo.io.Request*/kwArgs){
//summary: This is a legacy transport and should not be used unless you are dealing with repubsub.
//Consider a comet transport instead.
if(!rps.isInitialized){
// open up our tunnel, queue up requests anyway
rps.init();
}
// FIXME: we need to turn this into a topic subscription
// var tgtURL = kwArgs.url+"?"+dojo.io.argsFromMap(kwArgs.content);
// sampleTransport.sendRequest(tgtURL, hdlrFunc);
// a normal "bind()" call in a request-response transport layer is
// something that (usually) encodes most of it's payload with the
// request. Multi-event systems like repubsub are a bit more complex,
// and repubsub in particular distinguishes the publish and subscribe
// portions of thep rocess with different method calls to handle each.
// Therefore, a "bind" in the sense of repubsub must first determine if
// we have an open subscription to a channel provided by the server,
// and then "publish" the request payload if there is any. We therefore
// must take care not to incorrectly or too agressively register or
// file event handlers which are provided with the kwArgs method.
// NOTE: we ONLY pay attention to those event handlers that are
// registered with the bind request that subscribes to the channel. If
// event handlers are provided with subsequent requests, we might in
// the future support some additive or replacement syntax, but for now
// they get dropped on the floor.
// NOTE: in this case, url MUST be the "topic" to which we
// subscribe/publish for this channel
if(!rps.topics[kwArgs.url]){
kwArgs.rpsLoad = function(evt){
kwArgs.load("load", evt);
}
rps.subscribe(kwArgs.url, kwArgs, "rpsLoad");
}
if(kwArgs["content"]){
// what we wanted to send
var cEvt = dojo.io.repubsubEvent.initFromProperties(kwArgs.content);
rps.publish(kwArgs.url, cEvt);
}
}
dojo.io.transports.addTransport("repubsubTranport");
}
dojo.io.repubsub = new function(){
this.initDoc = "init.html";
this.isInitialized = false;
this.subscriptionBacklog = [];
this.debug = true;
this.rcvNodeName = null;
this.sndNodeName = null;
this.rcvNode = null;
this.sndNode = null;
this.canRcv = false;
this.canSnd = false;
this.canLog = false;
this.sndTimer = null;
this.windowRef = window;
this.backlog = [];
this.tunnelInitCount = 0;
this.tunnelFrameKey = "tunnel_frame";
this.serverBaseURL = location.protocol+"//"+location.host+location.pathname;
this.logBacklog = [];
this.getRandStr = function(){
return Math.random().toString().substring(2, 10);
}
this.userid = "guest";
this.tunnelID = this.getRandStr();
this.attachPathList = [];
this.topics = []; // list of topics we have listeners to
// actually, now that I think about it a little bit more, it would sure be
// useful to parse out the <script> src attributes. We're looking for
// something with a "do_method=lib", since that's what would have included
// us in the first place (in the common case).
this.parseGetStr = function(){
var baseUrl = document.location.toString();
var params = baseUrl.split("?", 2);
if(params.length > 1){
var paramStr = params[1];
var pairs = paramStr.split("&");
var opts = [];
for(var x in pairs){
var sp = pairs[x].split("=");
// FIXME: is this eval dangerous?
try{
opts[sp[0]]=eval(sp[1]);
}catch(e){
opts[sp[0]]=sp[1];
}
}
return opts;
}else{
return [];
}
}
// parse URL params and use them as default vals
var getOpts = this.parseGetStr();
for(var x in getOpts){
// FIXME: should I be checking for undefined here before setting? Does
// that buy me anything?
this[x] = getOpts[x];
}
if(!this["tunnelURI"]){
this.tunnelURI = [ "/who/", escape(this.userid), "/s/",
this.getRandStr(), "/kn_journal"].join("");
// this.tunnelURI = this.absoluteTopicURI(this.tunnelURI);
}
/*
if (self.kn_tunnelID) kn.tunnelID = self.kn_tunnelID; // the server says
if (kn._argv.kn_tunnelID) kn.tunnelID = kn._argv.kn_tunnelID; // the url says
*/
// check the options object if it exists and use its properties as an
// over-ride
if(window["repubsubOpts"]||window["rpsOpts"]){
var optObj = window["repubsubOpts"]||window["rpsOpts"];
for(var x in optObj){
this[x] = optObj[x]; // copy the option object properties
}
}
// things that get called directly from our iframe to inform us of events
this.tunnelCloseCallback = function(){
// when we get this callback, we should immediately attempt to re-start
// our tunnel connection
dojo.io.setIFrameSrc(this.rcvNode, this.initDoc+"?callback=repubsub.rcvNodeReady&domain="+document.domain);
}
this.receiveEventFromTunnel = function(evt, srcWindow){
// we should never be getting events from windows we didn't create
// NOTE: events sourced from the local window are also supported for
// debugging purposes
// any event object MUST have a an "elements" property
if(!evt["elements"]){
this.log("bailing! event received without elements!", "error");
return;
}
// if the event passes some minimal sanity tests, we need to attempt to
// dispatch it!
// first, it seems we have to munge the event object a bit
var e = {};
for(var i=0; i<evt.elements.length; i++){
var ee = evt.elements[i];
e[ee.name||ee.nameU] = (ee.value||ee.valueU);
// FIXME: need to enable this only in some extreme debugging mode!
this.log("[event]: "+(ee.name||ee.nameU)+": "+e[ee.name||ee.nameU]);
}
// NOTE: the previous version of this library put a bunch of code here
// to manage state that tried to make sure that we never, ever, lost
// any info about an event. If we unload RIGHT HERE, I don't think it's
// going to make a huge difference one way or another. Time will tell.
// and with THAT out of the way, dispatch it!
this.dispatch(e);
// TODO: remove the script block that created the event obj to save
// memory, etc.
}
this.widenDomain = function(domainStr){
// the purpose of this is to set the most liberal domain policy
// available
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(".");
}
// FIXME: parseCookie and setCookie should be methods that are more broadly
// available. Perhaps in htmlUtils?
this.parseCookie = function(){
var cs = document.cookie;
var keypairs = cs.split(";");
for(var x=0; x<keypairs.length; x++){
keypairs[x] = keypairs[x].split("=");
if(x!=keypairs.length-1){ cs+=";"; }
}
return keypairs;
}
this.setCookie = function(keypairs, clobber){
// NOTE: we want to only ever set session cookies, so never provide an
// expires date
if((clobber)&&(clobber==true)){ document.cookie = ""; }
var cs = "";
for(var x=0; x<keypairs.length; x++){
cs += keypairs[x][0]+"="+keypairs[x][1];
if(x!=keypairs.length-1){ cs+=";"; }
}
document.cookie = cs;
}
// FIXME: need to replace w/ dojo.log.*
this.log = function(str, lvl){
if(!this.debug){ return; } // we of course only care if we're in debug mode
while(this.logBacklog.length>0){
if(!this.canLog){ break; }
var blo = this.logBacklog.shift();
this.writeLog("["+blo[0]+"]: "+blo[1], blo[2]);
}
this.writeLog(str, lvl);
}
this.writeLog = function(str, lvl){
dojo.debug(((new Date()).toLocaleTimeString())+": "+str);
}
this.init = function(){
this.widenDomain();
// this.findPeers();
this.openTunnel();
this.isInitialized = true;
// FIXME: this seems like entirely the wrong place to replay the backlog
while(this.subscriptionBacklog.length){
this.subscribe.apply(this, this.subscriptionBacklog.shift());
}
}
this.clobber = function(){
if(this.rcvNode){
this.setCookie( [
[this.tunnelFrameKey,"closed"],
["path","/"]
], false
);
}
}
this.openTunnel = function(){
// We create two iframes here:
// one for getting data
this.rcvNodeName = "rcvIFrame_"+this.getRandStr();
// set cookie that can be used to find the receiving iframe
this.setCookie( [
[this.tunnelFrameKey,this.rcvNodeName],
["path","/"]
], false
);
this.rcvNode = dojo.io.createIFrame(this.rcvNodeName);
// FIXME: set the src attribute here to the initialization URL
dojo.io.setIFrameSrc(this.rcvNode, this.initDoc+"?callback=repubsub.rcvNodeReady&domain="+document.domain);
// the other for posting data in reply
this.sndNodeName = "sndIFrame_"+this.getRandStr();
this.sndNode = dojo.io.createIFrame(this.sndNodeName);
// FIXME: set the src attribute here to the initialization URL
dojo.io.setIFrameSrc(this.sndNode, this.initDoc+"?callback=repubsub.sndNodeReady&domain="+document.domain);
}
this.rcvNodeReady = function(){
// FIXME: why is this sequence number needed? Why isn't the UID gen
// function enough?
var statusURI = [this.tunnelURI, '/kn_status/index.html', this.getRandStr(), '_',
String(this.tunnelInitCount++)].join("");
// (kn._seqNum++); // FIXME: !!!!
// this.canRcv = true;
this.log("rcvNodeReady");
// FIXME: initialize receiver and request the base topic
// dojo.io.setIFrameSrc(this.rcvNode, this.serverBaseURL+"/kn?do_method=blank");
var initURIArr = [ this.serverBaseURL, "/kn?kn_from=", escape(this.tunnelURI),
"&kn_id=", escape(this.tunnelID), "&kn_status_from=",
escape(statusURI)];
// FIXME: does the above really need a kn_response_flush? won't the
// server already know? If not, what good is it anyway?
dojo.io.setIFrameSrc(this.rcvNode, initURIArr.join(""));
// setup a status path listener, but don't tell the server about it,
// since it already knows we're itnerested in our own tunnel status
this.subscribe(statusURI, this, "statusListener", true);
this.log(initURIArr.join(""));
}
this.sndNodeReady = function(){
this.canSnd = true;
this.log("sndNodeReady");
this.log(this.backlog.length);
// FIXME: handle any pent-up send commands
if(this.backlog.length > 0){
this.dequeueEvent();
}
}
this.statusListener = function(evt){
this.log("status listener called");
this.log(evt.status, "info");
}
// this handles local event propigation
this.dispatch = function(evt){
// figure out what topic it came from
if(evt["to"]||evt["kn_routed_from"]){
var rf = evt["to"]||evt["kn_routed_from"];
// split off the base server URL
var topic = rf.split(this.serverBaseURL, 2)[1];
if(!topic){
// FIXME: how do we recover when we don't get a sane "from"? Do
// we try to route to it anyway?
topic = rf;
}
this.log("[topic] "+topic);
if(topic.length>3){
if(topic.slice(0, 3)=="/kn"){
topic = topic.slice(3);
}
}
if(this.attachPathList[topic]){
this.attachPathList[topic](evt);
}
}
}
this.subscribe = function( topic /* kn_from in the old terminilogy */,
toObj, toFunc, dontTellServer){
if(!this.isInitialized){
this.subscriptionBacklog.push([topic, toObj, toFunc, dontTellServer]);
return;
}
if(!this.attachPathList[topic]){
this.attachPathList[topic] = function(){ return true; }
this.log("subscribing to: "+topic);
this.topics.push(topic);
}
var revt = new dojo.io.repubsubEvent(this.tunnelURI, topic, "route");
var rstr = [this.serverBaseURL+"/kn", revt.toGetString()].join("");
dojo.event.kwConnect({
once: true,
srcObj: this.attachPathList,
srcFunc: topic,
adviceObj: toObj,
adviceFunc: toFunc
});
// NOTE: the above is a local mapping, if we're not the leader, we
// should connect our mapping to the topic handler of the peer
// leader, this ensures that not matter what happens to the
// leader, we don't really loose our heads if/when the leader
// goes away.
if(!this.rcvNode){ /* this should be an error! */ }
if(dontTellServer){
return;
}
this.log("sending subscription to: "+topic);
// create a subscription event object and give it all the props we need
// to updates on the specified topic
// FIXME: we should only enqueue if this is our first subscription!
this.sendTopicSubToServer(topic, rstr);
}
this.sendTopicSubToServer = function(topic, str){
if(!this.attachPathList[topic]["subscriptions"]){
this.enqueueEventStr(str);
this.attachPathList[topic].subscriptions = 0;
}
this.attachPathList[topic].subscriptions++;
}
this.unSubscribe = function(topic, toObj, toFunc){
// first, locally disconnect
dojo.event.kwDisconnect({
srcObj: this.attachPathList,
srcFunc: topic,
adviceObj: toObj,
adviceFunc: toFunc
});
// FIXME: figure out if there are any remaining listeners to the topic,
// and if not, inform the server of our desire not to be
// notified of updates to the topic
}
// the "publish" method is really a misnomer, since it really means "take
// this event and send it to the server". Note that the "dispatch" method
// handles local event promigulation, and therefore we emulate both sides
// of a real event router without having to swallow all of the complexity.
this.publish = function(topic, event){
var evt = dojo.io.repubsubEvent.initFromProperties(event);
// FIXME: need to make sure we have from and to set correctly
// before we serialize and send off to the great blue
// younder.
evt.to = topic;
// evt.from = this.tunnelURI;
var evtURLParts = [];
evtURLParts.push(this.serverBaseURL+"/kn");
// serialize the event to a string and then post it to the correct
// topic
evtURLParts.push(evt.toGetString());
this.enqueueEventStr(evtURLParts.join(""));
}
this.enqueueEventStr = function(evtStr){
this.log("enqueueEventStr");
this.backlog.push(evtStr);
this.dequeueEvent();
}
this.dequeueEvent = function(force){
this.log("dequeueEvent");
if(this.backlog.length <= 0){ return; }
if((this.canSnd)||(force)){
dojo.io.setIFrameSrc(this.sndNode, this.backlog.shift()+"&callback=repubsub.sndNodeReady");
this.canSnd = false;
}else{
this.log("sndNode not available yet!", "debug");
}
}
}
dojo.io.repubsubEvent = function(to, from, method, id, routeURI, payload, dispname, uid){
this.to = to;
this.from = from;
this.method = method||"route";
this.id = id||repubsub.getRandStr();
this.uri = routeURI;
this.displayname = dispname||repubsub.displayname;
this.userid = uid||repubsub.userid;
this.payload = payload||"";
this.flushChars = 4096;
this.initFromProperties = function(evt){
if(evt.constructor = dojo.io.repubsubEvent){
for(var x in evt){
this[x] = evt[x];
}
}else{
// we want to copy all the properties of the evt object, and transform
// those that are "stock" properties of dojo.io.repubsubEvent. All others should
// be copied as-is
for(var x in evt){
if(typeof this.forwardPropertiesMap[x] == "string"){
this[this.forwardPropertiesMap[x]] = evt[x];
}else{
this[x] = evt[x];
}
}
}
}
this.toGetString = function(noQmark){
var qs = [ ((noQmark) ? "" : "?") ];
for(var x=0; x<this.properties.length; x++){
var tp = this.properties[x];
if(this[tp[0]]){
qs.push(tp[1]+"="+encodeURIComponent(String(this[tp[0]])));
}
// FIXME: we need to be able to serialize non-stock properties!!!
}
return qs.join("&");
}
}
dojo.io.repubsubEvent.prototype.properties = [["from", "kn_from"], ["to", "kn_to"],
["method", "do_method"], ["id", "kn_id"],
["uri", "kn_uri"],
["displayname", "kn_displayname"],
["userid", "kn_userid"],
["payload", "kn_payload"],
["flushChars", "kn_response_flush"],
["responseFormat", "kn_response_format"] ];
// maps properties from their old names to their new names...
dojo.io.repubsubEvent.prototype.forwardPropertiesMap = {};
// ...and vice versa...
dojo.io.repubsubEvent.prototype.reversePropertiesMap = {};
// and we then populate them both from the properties list
for(var x=0; x<dojo.io.repubsubEvent.prototype.properties.length; x++){
var tp = dojo.io.repubsubEvent.prototype.properties[x];
dojo.io.repubsubEvent.prototype.reversePropertiesMap[tp[0]] = tp[1];
dojo.io.repubsubEvent.prototype.forwardPropertiesMap[tp[1]] = tp[0];
}
// static version of initFromProperties, creates new event and object and
// returns it after init
dojo.io.repubsubEvent.initFromProperties = function(evt){
var eventObj = new dojo.io.repubsubEvent();
eventObj.initFromProperties(evt);
return eventObj;
}