/* * Pushlet client using AJAX XMLHttpRequest. * * DESCRIPTION * This file provides self-contained support for using the * Pushlet protocol through AJAX-technology. The XMLHttpRequest is used * to exchange the Pushlet protocol XML messages (may use JSON in later versions). * Currently only HTTP GET is used in asynchronous mode. * * The Pushlet protocol provides a Publish/Subscribe service for * simple messages. The Pushlet server provides session management (join/leave), * subscription management (subscribe/unsubscribe), server originated push * and publication (publish). * * For subscriptions server-push is emulated using a single * long-lived XMLHttpRequests (Pushlet pull mode) where the server holds the * request until events arrive for which a session has subscriptions. * This is thus different from polling. In future versions XML streaming * may be used since this is currently only supported in Moz-family browsers. * * Users should supply global callback functions for the events they are interested in. * For now see _onEvent() for the specific functions that are called. * The most important one is onData(). If onEvent() is available that catches * all events. All callback functions have a single * argument with a PushletEvent object. * A future version should provide a more OO (Observer) approach. * * EXAMPLES * PL.join(); * PL.listen(); * PL.subscribe('/temperature'); * // or shorter * PL.joinListen('/temperature'); * // You provide as callback: * onData(pushletEvent); * See examples in the Pushlet distribution (e.g. webapps/pushlet/examples/ajax) * * WHY * IMO using XMLHttpRequest has many advantages over the original JS streaming: * more stability, no browser busy-bees, better integration with other AJAX frameworks, * more debugable, more understandable, ... * * $Id: ajax-pushlet-client.js,v 1.7 2007/07/27 11:45:08 justb Exp $ */ /** Namespaced Pushlet functions. */ var PL = { NV_P_FORMAT: 'p_format=xml-strict', NV_P_MODE: 'p_mode=pull', pushletURL: null, webRoot: null, sessionId: null, STATE_ERROR: -2, STATE_ABORT: -1, STATE_NULL: 1, STATE_READY: 2, STATE_JOINED: 3, STATE_LISTENING: 3, state: 1, /************** START PUBLIC FUNCTIONS **************/ /** Send heartbeat. */ heartbeat: function() { PL._doRequest('hb'); }, /** Join. */ join: function() { PL.sessionId = null; // Streaming is only supported in Mozilla. E.g. IE does not allow access to responseText on readyState == 3 PL._doRequest('join', PL.NV_P_FORMAT + '&' + PL.NV_P_MODE); }, /** Join, listen and subscribe. */ joinListen: function(aSubject) { PL._setStatus('join-listen ' + aSubject); // PL.join(); // PL.listen(aSubject); PL.sessionId = null; // Create event URI for listen var query = PL.NV_P_FORMAT + '&' + PL.NV_P_MODE; // Optional subject to subscribe to if (aSubject) { query = query + '&p_subject=' + aSubject; } PL._doRequest('join-listen', query); }, /** Close pushlet session. */ leave: function() { PL._doRequest('leave'); }, /** Listen on event channel. */ listen: function(aSubject) { // Create event URI for listen var query = PL.NV_P_MODE; // Optional subject to subscribe to if (aSubject) { query = query + '&p_subject=' + aSubject; } PL._doRequest('listen', query); }, /** Publish to subject. */ publish: function(aSubject, theQueryArgs) { var query = 'p_subject=' + aSubject; if (theQueryArgs) { query = query + '&' + theQueryArgs; } PL._doRequest('publish', query); }, /** Subscribe to (comma separated) subject(s). */ subscribe: function(aSubject, aLabel) { var query = 'p_subject=' + aSubject; if (aLabel) { query = query + '&p_label=' + aLabel; } PL._doRequest('subscribe', query); }, /** Unsubscribe from (all) subject(s). */ unsubscribe: function(aSubscriptionId) { var query; // If no sid we unsubscribe from all subscriptions if (aSubscriptionId) { query = 'p_sid=' + aSubscriptionId; } PL._doRequest('unsubscribe', query); }, setDebug: function(bool) { PL.debugOn = bool; }, /************** END PUBLIC FUNCTIONS **************/ // Cross-browser add event listener to element _addEvent: function (elm, evType, callback, useCapture) { var obj = PL._getObject(elm); if (obj.addEventListener) { obj.addEventListener(evType, callback, useCapture); return true; } else if (obj.attachEvent) { var r = obj.attachEvent('on' + evType, callback); return r; } else { obj['on' + evType] = callback; } }, _doCallback: function(event, cbFunction) { // Do specific callback function if provided by client if (cbFunction) { // Do specific callback like onData(), onJoinAck() etc. cbFunction(event); } else if (window.onEvent) { // general callback onEvent() provided to catch all events onEvent(event); } }, // Do XML HTTP request _doRequest: function(anEvent, aQuery) { // Check if we are not in any error state if (PL.state < 0) { PL._setStatus('died (' + PL.state + ')'); return; } // We may have (async) requests outstanding and thus // may have to wait for them to complete and change state. var waitForState = false; if (anEvent == 'join' || anEvent == 'join-listen') { // We can only join after initialization waitForState = (PL.state < PL.STATE_READY); } else if (anEvent == 'leave') { PL.state = PL.STATE_READY; } else if (anEvent == 'refresh') { // We must be in the listening state if (PL.state != PL.STATE_LISTENING) { return; } } else if (anEvent == 'listen') { // We must have joined before we can listen waitForState = (PL.state < PL.STATE_JOINED); } else if (anEvent == 'subscribe' || anEvent == 'unsubscribe') { // We must be listeing for subscription mgmnt waitForState = (PL.state < PL.STATE_LISTENING); } else { // All other requests require that we have at least joined waitForState = (PL.state < PL.STATE_JOINED); } // May have to wait for right state to issue request if (waitForState == true) { PL._setStatus(anEvent + ' , waiting... state=' + PL.state); setTimeout(function() { PL._doRequest(anEvent, aQuery); }, 100); return; } // ASSERTION: PL.state is OK for this request // Construct base URL for GET var url = PL.pushletURL + '?p_event=' + anEvent; // Optionally attach query string if (aQuery) { url = url + '&' + aQuery; } // Optionally attach session id if (PL.sessionId != null) { url = url + '&p_id=' + PL.sessionId; if (anEvent == 'p_leave') { PL.sessionId = null; } } PL.debug('_doRequest', url); PL._getXML(url, PL._onResponse); // uncomment to use synchronous XmlHttpRequest //var rsp = PL._getXML(url); //PL._onResponse(rsp); */ }, // Get object reference _getObject: function(obj) { if (typeof obj == "string") { return document.getElementById(obj); } else { // pass through object reference return obj; } }, _getWebRoot: function() { /* if (PL.webRoot != null) { return PL.webRoot; } //derive the baseDir value by looking for the script tag that loaded this file var head = document.getElementsByTagName('head')[0]; var nodes = head.childNodes; for (var i = 0; i < nodes.length; ++i) { var src = nodes.item(i).src; if (src) { var index = src.indexOf("ajax-pushlet-client.js"); if (index >= 0) { index = src.indexOf("lib"); PL.webRoot = src.substring(0, index); break; } } } return PL.webRoot; */ var curWwwPath=window.document.location.href; var pathName=window.document.location.pathname; var pos=curWwwPath.indexOf(pathName); var localhostPaht=curWwwPath.substring(0,pos); var projectName=pathName.substring(0,pathName.substr(1).indexOf('/')+1); return(localhostPaht+projectName+"/"); }, // Get XML doc from server // On response optional callback fun is called with optional user data. _getXML: function(url, callback) { // Obtain XMLHttpRequest object var xmlhttp = new XMLHttpRequest(); if (!xmlhttp || xmlhttp == null) { alert('No browser XMLHttpRequest (AJAX) support'); return; } // Setup optional async response handling via callback var cb = callback; var async = false; if (cb) { // Async mode async = true; xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4) { if (xmlhttp.status == 200) { // Processing statements go here... cb(xmlhttp.responseXML); // Avoid memory leaks in IE // 12.may.2007 thanks to Julio Santa Cruz xmlhttp = null; } else { var event = new PushletEvent(); event.put('p_event', 'error') event.put('p_reason', '[pushlet] problem retrieving XML data:\n' + xmlhttp.statusText); PL._onEvent(event); } } }; } // Open URL xmlhttp.open('GET', url, async); // Send XML to KW server xmlhttp.send(null); if (!cb) { if (xmlhttp.status != 200) { var event = new PushletEvent(); event.put('p_event', 'error') event.put('p_reason', '[pushlet] problem retrieving XML data:\n' + xmlhttp.statusText); PL._onEvent(event) return null; } // Sync mode (no callback) // alert(xmlhttp.responseText); return xmlhttp.responseXML; } }, _init: function () { PL._showStatus(); PL._setStatus('initializing...'); /* Setup Cross-Browser XMLHttpRequest v1.2 Emulate Gecko 'XMLHttpRequest()' functionality in IE and Opera. Opera requires the Sun Java Runtime Environment . by Andrew Gregory http://www.scss.com.au/family/andrew/webdesign/xmlhttprequest/ This work is licensed under the Creative Commons Attribution License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/2.5/ or send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA. */ // IE support if (window.ActiveXObject && !window.XMLHttpRequest) { window.XMLHttpRequest = function() { var msxmls = new Array( 'Msxml2.XMLHTTP.5.0', 'Msxml2.XMLHTTP.4.0', 'Msxml2.XMLHTTP.3.0', 'Msxml2.XMLHTTP', 'Microsoft.XMLHTTP'); for (var i = 0; i < msxmls.length; i++) { try { return new ActiveXObject(msxmls[i]); } catch (e) { } } return null; }; } // ActiveXObject emulation if (!window.ActiveXObject && window.XMLHttpRequest) { window.ActiveXObject = function(type) { switch (type.toLowerCase()) { case 'microsoft.xmlhttp': case 'msxml2.xmlhttp': case 'msxml2.xmlhttp.3.0': case 'msxml2.xmlhttp.4.0': case 'msxml2.xmlhttp.5.0': return new XMLHttpRequest(); } return null; }; } PL.pushletURL = PL._getWebRoot() + 'pushlet.srv'; PL._setStatus('initialized'); PL.state = PL.STATE_READY; }, /** Handle incoming events from server. */ _onEvent: function (event) { // Create a PushletEvent object from the arguments passed in // push.arguments is event data coming from the Server PL.debug('_onEvent()', event.toString()); // Do action based on event type var eventType = event.getEvent(); if (eventType == 'data') { PL._setStatus('data'); PL._doCallback(event, window.onData); } else if (eventType == 'refresh') { if (PL.state < PL.STATE_LISTENING) { PL._setStatus('not refreshing state=' + PL.STATE_LISTENING); } var timeout = event.get('p_wait'); setTimeout(function () { PL._doRequest('refresh'); }, timeout); return; } else if (eventType == 'error') { PL.state = PL.STATE_ERROR; PL._setStatus('server error: ' + event.get('p_reason')); PL._doCallback(event, window.onError); } else if (eventType == 'join-ack') { PL.state = PL.STATE_JOINED; PL.sessionId = event.get('p_id'); PL._setStatus('connected'); PL._doCallback(event, window.onJoinAck); } else if (eventType == 'join-listen-ack') { PL.state = PL.STATE_LISTENING; PL.sessionId = event.get('p_id'); PL._setStatus('join-listen-ack'); PL._doCallback(event, window.onJoinListenAck); } else if (eventType == 'listen-ack') { PL.state = PL.STATE_LISTENING; PL._setStatus('listening'); PL._doCallback(event, window.onListenAck); } else if (eventType == 'hb') { PL._setStatus('heartbeat'); PL._doCallback(event, window.onHeartbeat); } else if (eventType == 'hb-ack') { PL._doCallback(event, window.onHeartbeatAck); } else if (eventType == 'leave-ack') { PL._setStatus('disconnected'); PL._doCallback(event, window.onLeaveAck); } else if (eventType == 'refresh-ack') { PL._doCallback(event, window.onRefreshAck); } else if (eventType == 'subscribe-ack') { PL._setStatus('subscribed to ' + event.get('p_subject')); PL._doCallback(event, window.onSubscribeAck); } else if (eventType == 'unsubscribe-ack') { PL._setStatus('unsubscribed'); PL._doCallback(event, window.onUnsubscribeAck); } else if (eventType == 'abort') { PL.state = PL.STATE_ERROR; PL._setStatus('abort'); PL._doCallback(event, window.onAbort); } else if (eventType.match(/nack$/)) { PL._setStatus('error response: ' + event.get('p_reason')); PL._doCallback(event, window.onNack); } }, /** Handle XMLHttpRequest response XML. */ _onResponse: function(xml) { PL.debug('_onResponse', xml); var events = PL._rsp2Events(xml); if (events == null) { PL._setStatus('null events') return; } delete xml; PL.debug('_onResponse eventCnt=', events.length); // Go through all elements for (i = 0; i < events.length; i++) { PL._onEvent(events[i]); } }, /** Convert XML response to PushletEvent objects. */ _rsp2Events: function(xml) { // check empty response or xml document if (!xml || !xml.documentElement) { return null; } // Convert xml doc to array of PushletEvent objects var eventElements = xml.documentElement.getElementsByTagName('event'); var events = new Array(eventElements.length); for (i = 0; i < eventElements.length; i++) { events[i] = new PushletEvent(eventElements[i]); } return events; }, statusMsg: 'null', statusChanged: false, statusChar: '|', _showStatus: function() { // To show progress if (PL.statusChanged == true) { if (PL.statusChar == '|') PL.statusChar = '/'; else if (PL.statusChar == '/') PL.statusChar = '--'; else if (PL.statusChar == '--') PL.statusChar = '\\'; else PL.statusChar = '|'; PL.statusChanged = false; } window.defaultStatus = PL.statusMsg; window.status = PL.statusMsg + ' ' + PL.statusChar; timeout = window.setTimeout('PL._showStatus()', 400); }, _setStatus: function(status) { PL.statusMsg = "pushlet - " + status; PL.statusChanged = true; }, /*************** Debug utility *******************************/ timestamp: 0, debugWindow: null, messages: new Array(), messagesIndex: 0, debugOn: false, /** Send debug messages to a (D)HTML window. */ debug: function(label, value) { if (PL.debugOn == false) { return; } var funcName = "none"; // Fetch JS function name if any if (PL.debug.caller) { funcName = PL.debug.caller.toString() funcName = funcName.substring(9, funcName.indexOf(")") + 1) } // Create message var msg = "-" + funcName + ": " + label + "=" + value // Add optional timestamp var now = new Date() var elapsed = now - PL.timestamp if (elapsed < 10000) { msg += " (" + elapsed + " msec)" } PL.timestamp = now; // Show. if ((PL.debugWindow == null) || PL.debugWindow.closed) { PL.debugWindow = window.open("", "p_debugWin", "toolbar=no,scrollbars=yes,resizable=yes,width=600,height=400"); } // Add message to current list PL.messages[PL.messagesIndex++] = msg // Write doc header PL.debugWindow.document.writeln('Pushlet Debug Window'); // Write the messages for (var i = 0; i < PL.messagesIndex; i++) { PL.debugWindow.document.writeln('
' + i + ': ' + PL.messages[i] + '
'); } // Write doc footer and close PL.debugWindow.document.writeln(''); PL.debugWindow.document.close(); PL.debugWindow.focus(); } } /* Represents nl.justobjects.pushlet.Event in JS. */ function PushletEvent(xml) { // Member variable setup; the assoc array stores the N/V pairs this.arr = new Array(); this.getSubject = function() { return this.get('p_subject'); } this.getEvent = function() { return this.get('p_event'); } this.put = function(name, value) { return this.arr[name] = value; } this.get = function(name) { return this.arr[name]; } this.toString = function() { var res = ''; for (var i in this.arr) { res = res + i + '=' + this.arr[i] + '\n'; } return res; } this.toTable = function() { var res = ''; var styleDiv = '
' for (var i in this.arr) { res = res + '
'; } res += '
' + styleDiv + i + '' + styleDiv + this.arr[i] + '
' return res; } // Optional XML element if (xml) { // Put the attributes in Map for (var i = 0; i < xml.attributes.length; i++) { this.put(xml.attributes[i].name, xml.attributes[i].value); } } } /********************************************************************** START - OLD application functions (LEFT HERE FOR FORWARD COMPAT) ***********************************************************************/ // Debug util function p_debug(aBool, aLabel, aMsg) { if (aBool == false) { return; } PL.setDebug(true); PL.debug(aLabel, aMsg); PL.setDebug(false); } // Embed pushlet frame in page (OBSOLETE) function p_embed(thePushletWebRoot) { alert('Pushlet: p_embed() is no longer required for AJAX client') } // Join the pushlet server function p_join() { PL.join(); } // Create data event channel with the server function p_listen(aSubject, aMode) { // Note: mode is fixed to 'pull' PL.listen(aSubject); } // Shorthand: Join the pushlet server and start listening immediately function p_join_listen(aSubject) { PL.joinListen(aSubject); } // Leave the pushlet server function p_leave() { PL.leave(); } // Send heartbeat event; callback is onHeartbeatAck() function p_heartbeat() { PL.heartbeat(); } // Publish to a subject function p_publish(aSubject, nvPairs) { var args = p_publish.arguments; // Put the arguments' name/value pairs in the URI var query = ''; var amp = ''; for (var i = 1; i < args.length; i++) { if (i > 1) { amp = '&'; } query = query + amp + args[i] + '=' + args[++i]; } PL.publish(aSubject, query); } // Subscribe to a subject with optional label function p_subscribe(aSubject, aLabel) { PL.subscribe(aSubject, aLabel); } // Unsubscribe from a subject function p_unsubscribe(aSid) { PL.unsubscribe(aSid); } /********************************************************************** END - Public application functions (LEFT HERE FOR FORWARD COMPAT) ***********************************************************************/ // Initialize when page completely loaded PL._addEvent(window, 'load', PL._init, false);