ajax-pushlet-client.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. /*
  2. * Pushlet client using AJAX XMLHttpRequest.
  3. *
  4. * DESCRIPTION
  5. * This file provides self-contained support for using the
  6. * Pushlet protocol through AJAX-technology. The XMLHttpRequest is used
  7. * to exchange the Pushlet protocol XML messages (may use JSON in later versions).
  8. * Currently only HTTP GET is used in asynchronous mode.
  9. *
  10. * The Pushlet protocol provides a Publish/Subscribe service for
  11. * simple messages. The Pushlet server provides session management (join/leave),
  12. * subscription management (subscribe/unsubscribe), server originated push
  13. * and publication (publish).
  14. *
  15. * For subscriptions server-push is emulated using a single
  16. * long-lived XMLHttpRequests (Pushlet pull mode) where the server holds the
  17. * request until events arrive for which a session has subscriptions.
  18. * This is thus different from polling. In future versions XML streaming
  19. * may be used since this is currently only supported in Moz-family browsers.
  20. *
  21. * Users should supply global callback functions for the events they are interested in.
  22. * For now see _onEvent() for the specific functions that are called.
  23. * The most important one is onData(). If onEvent() is available that catches
  24. * all events. All callback functions have a single
  25. * argument with a PushletEvent object.
  26. * A future version should provide a more OO (Observer) approach.
  27. *
  28. * EXAMPLES
  29. * PL.join();
  30. * PL.listen();
  31. * PL.subscribe('/temperature');
  32. * // or shorter
  33. * PL.joinListen('/temperature');
  34. * // You provide as callback:
  35. * onData(pushletEvent);
  36. * See examples in the Pushlet distribution (e.g. webapps/pushlet/examples/ajax)
  37. *
  38. * WHY
  39. * IMO using XMLHttpRequest has many advantages over the original JS streaming:
  40. * more stability, no browser busy-bees, better integration with other AJAX frameworks,
  41. * more debugable, more understandable, ...
  42. *
  43. * $Id: ajax-pushlet-client.js,v 1.7 2007/07/27 11:45:08 justb Exp $
  44. */
  45. /** Namespaced Pushlet functions. */
  46. var PL = {
  47. NV_P_FORMAT: 'p_format=xml-strict',
  48. NV_P_MODE: 'p_mode=pull',
  49. pushletURL: null,
  50. webRoot: null,
  51. sessionId: null,
  52. STATE_ERROR: -2,
  53. STATE_ABORT: -1,
  54. STATE_NULL: 1,
  55. STATE_READY: 2,
  56. STATE_JOINED: 3,
  57. STATE_LISTENING: 3,
  58. state: 1,
  59. /************** START PUBLIC FUNCTIONS **************/
  60. /** Send heartbeat. */
  61. heartbeat: function() {
  62. PL._doRequest('hb');
  63. },
  64. /** Join. */
  65. join: function() {
  66. PL.sessionId = null;
  67. // Streaming is only supported in Mozilla. E.g. IE does not allow access to responseText on readyState == 3
  68. PL._doRequest('join', PL.NV_P_FORMAT + '&' + PL.NV_P_MODE);
  69. },
  70. /** Join, listen and subscribe. */
  71. joinListen: function(aSubject) {
  72. PL._setStatus('join-listen ' + aSubject);
  73. // PL.join();
  74. // PL.listen(aSubject);
  75. PL.sessionId = null;
  76. // Create event URI for listen
  77. var query = PL.NV_P_FORMAT + '&' + PL.NV_P_MODE;
  78. // Optional subject to subscribe to
  79. if (aSubject) {
  80. query = query + '&p_subject=' + aSubject;
  81. }
  82. PL._doRequest('join-listen', query);
  83. },
  84. /** Close pushlet session. */
  85. leave: function() {
  86. PL._doRequest('leave');
  87. },
  88. /** Listen on event channel. */
  89. listen: function(aSubject) {
  90. // Create event URI for listen
  91. var query = PL.NV_P_MODE;
  92. // Optional subject to subscribe to
  93. if (aSubject) {
  94. query = query + '&p_subject=' + aSubject;
  95. }
  96. PL._doRequest('listen', query);
  97. },
  98. /** Publish to subject. */
  99. publish: function(aSubject, theQueryArgs) {
  100. var query = 'p_subject=' + aSubject;
  101. if (theQueryArgs) {
  102. query = query + '&' + theQueryArgs;
  103. }
  104. PL._doRequest('publish', query);
  105. },
  106. /** Subscribe to (comma separated) subject(s). */
  107. subscribe: function(aSubject, aLabel) {
  108. var query = 'p_subject=' + aSubject;
  109. if (aLabel) {
  110. query = query + '&p_label=' + aLabel;
  111. }
  112. PL._doRequest('subscribe', query);
  113. },
  114. /** Unsubscribe from (all) subject(s). */
  115. unsubscribe: function(aSubscriptionId) {
  116. var query;
  117. // If no sid we unsubscribe from all subscriptions
  118. if (aSubscriptionId) {
  119. query = 'p_sid=' + aSubscriptionId;
  120. }
  121. PL._doRequest('unsubscribe', query);
  122. },
  123. setDebug: function(bool) {
  124. PL.debugOn = bool;
  125. },
  126. /************** END PUBLIC FUNCTIONS **************/
  127. // Cross-browser add event listener to element
  128. _addEvent: function (elm, evType, callback, useCapture) {
  129. var obj = PL._getObject(elm);
  130. if (obj.addEventListener) {
  131. obj.addEventListener(evType, callback, useCapture);
  132. return true;
  133. } else if (obj.attachEvent) {
  134. var r = obj.attachEvent('on' + evType, callback);
  135. return r;
  136. } else {
  137. obj['on' + evType] = callback;
  138. }
  139. },
  140. _doCallback: function(event, cbFunction) {
  141. // Do specific callback function if provided by client
  142. if (cbFunction) {
  143. // Do specific callback like onData(), onJoinAck() etc.
  144. cbFunction(event);
  145. } else if (window.onEvent) {
  146. // general callback onEvent() provided to catch all events
  147. onEvent(event);
  148. }
  149. },
  150. // Do XML HTTP request
  151. _doRequest: function(anEvent, aQuery) {
  152. // Check if we are not in any error state
  153. if (PL.state < 0) {
  154. PL._setStatus('died (' + PL.state + ')');
  155. return;
  156. }
  157. // We may have (async) requests outstanding and thus
  158. // may have to wait for them to complete and change state.
  159. var waitForState = false;
  160. if (anEvent == 'join' || anEvent == 'join-listen') {
  161. // We can only join after initialization
  162. waitForState = (PL.state < PL.STATE_READY);
  163. } else if (anEvent == 'leave') {
  164. PL.state = PL.STATE_READY;
  165. } else if (anEvent == 'refresh') {
  166. // We must be in the listening state
  167. if (PL.state != PL.STATE_LISTENING) {
  168. return;
  169. }
  170. } else if (anEvent == 'listen') {
  171. // We must have joined before we can listen
  172. waitForState = (PL.state < PL.STATE_JOINED);
  173. } else if (anEvent == 'subscribe' || anEvent == 'unsubscribe') {
  174. // We must be listeing for subscription mgmnt
  175. waitForState = (PL.state < PL.STATE_LISTENING);
  176. } else {
  177. // All other requests require that we have at least joined
  178. waitForState = (PL.state < PL.STATE_JOINED);
  179. }
  180. // May have to wait for right state to issue request
  181. if (waitForState == true) {
  182. PL._setStatus(anEvent + ' , waiting... state=' + PL.state);
  183. setTimeout(function() {
  184. PL._doRequest(anEvent, aQuery);
  185. }, 100);
  186. return;
  187. }
  188. // ASSERTION: PL.state is OK for this request
  189. // Construct base URL for GET
  190. var url = PL.pushletURL + '?p_event=' + anEvent;
  191. // Optionally attach query string
  192. if (aQuery) {
  193. url = url + '&' + aQuery;
  194. }
  195. // Optionally attach session id
  196. if (PL.sessionId != null) {
  197. url = url + '&p_id=' + PL.sessionId;
  198. if (anEvent == 'p_leave') {
  199. PL.sessionId = null;
  200. }
  201. }
  202. PL.debug('_doRequest', url);
  203. PL._getXML(url, PL._onResponse);
  204. // uncomment to use synchronous XmlHttpRequest
  205. //var rsp = PL._getXML(url);
  206. //PL._onResponse(rsp); */
  207. },
  208. // Get object reference
  209. _getObject: function(obj) {
  210. if (typeof obj == "string") {
  211. return document.getElementById(obj);
  212. } else {
  213. // pass through object reference
  214. return obj;
  215. }
  216. },
  217. _getWebRoot: function() {
  218. /*
  219. if (PL.webRoot != null) {
  220. return PL.webRoot;
  221. }
  222. //derive the baseDir value by looking for the script tag that loaded this file
  223. var head = document.getElementsByTagName('head')[0];
  224. var nodes = head.childNodes;
  225. for (var i = 0; i < nodes.length; ++i) {
  226. var src = nodes.item(i).src;
  227. if (src) {
  228. var index = src.indexOf("ajax-pushlet-client.js");
  229. if (index >= 0) {
  230. index = src.indexOf("lib");
  231. PL.webRoot = src.substring(0, index);
  232. break;
  233. }
  234. }
  235. }
  236. return PL.webRoot;
  237. */
  238. var curWwwPath=window.document.location.href;
  239. var pathName=window.document.location.pathname;
  240. var pos=curWwwPath.indexOf(pathName);
  241. var localhostPaht=curWwwPath.substring(0,pos);
  242. var projectName=pathName.substring(0,pathName.substr(1).indexOf('/')+1);
  243. return(localhostPaht+projectName+"/");
  244. },
  245. // Get XML doc from server
  246. // On response optional callback fun is called with optional user data.
  247. _getXML: function(url, callback) {
  248. // Obtain XMLHttpRequest object
  249. var xmlhttp = new XMLHttpRequest();
  250. if (!xmlhttp || xmlhttp == null) {
  251. alert('No browser XMLHttpRequest (AJAX) support');
  252. return;
  253. }
  254. // Setup optional async response handling via callback
  255. var cb = callback;
  256. var async = false;
  257. if (cb) {
  258. // Async mode
  259. async = true;
  260. xmlhttp.onreadystatechange = function() {
  261. if (xmlhttp.readyState == 4) {
  262. if (xmlhttp.status == 200) {
  263. // Processing statements go here...
  264. cb(xmlhttp.responseXML);
  265. // Avoid memory leaks in IE
  266. // 12.may.2007 thanks to Julio Santa Cruz
  267. xmlhttp = null;
  268. } else {
  269. var event = new PushletEvent();
  270. event.put('p_event', 'error')
  271. event.put('p_reason', '[pushlet] problem retrieving XML data:\n' + xmlhttp.statusText);
  272. PL._onEvent(event);
  273. }
  274. }
  275. };
  276. }
  277. // Open URL
  278. xmlhttp.open('GET', url, async);
  279. // Send XML to KW server
  280. xmlhttp.send(null);
  281. if (!cb) {
  282. if (xmlhttp.status != 200) {
  283. var event = new PushletEvent();
  284. event.put('p_event', 'error')
  285. event.put('p_reason', '[pushlet] problem retrieving XML data:\n' + xmlhttp.statusText);
  286. PL._onEvent(event)
  287. return null;
  288. }
  289. // Sync mode (no callback)
  290. // alert(xmlhttp.responseText);
  291. return xmlhttp.responseXML;
  292. }
  293. },
  294. _init: function () {
  295. PL._showStatus();
  296. PL._setStatus('initializing...');
  297. /*
  298. Setup Cross-Browser XMLHttpRequest v1.2
  299. Emulate Gecko 'XMLHttpRequest()' functionality in IE and Opera. Opera requires
  300. the Sun Java Runtime Environment <http://www.java.com/>.
  301. by Andrew Gregory
  302. http://www.scss.com.au/family/andrew/webdesign/xmlhttprequest/
  303. This work is licensed under the Creative Commons Attribution License. To view a
  304. copy of this license, visit http://creativecommons.org/licenses/by-sa/2.5/ or
  305. send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California
  306. 94305, USA.
  307. */
  308. // IE support
  309. if (window.ActiveXObject && !window.XMLHttpRequest) {
  310. window.XMLHttpRequest = function() {
  311. var msxmls = new Array(
  312. 'Msxml2.XMLHTTP.5.0',
  313. 'Msxml2.XMLHTTP.4.0',
  314. 'Msxml2.XMLHTTP.3.0',
  315. 'Msxml2.XMLHTTP',
  316. 'Microsoft.XMLHTTP');
  317. for (var i = 0; i < msxmls.length; i++) {
  318. try {
  319. return new ActiveXObject(msxmls[i]);
  320. } catch (e) {
  321. }
  322. }
  323. return null;
  324. };
  325. }
  326. // ActiveXObject emulation
  327. if (!window.ActiveXObject && window.XMLHttpRequest) {
  328. window.ActiveXObject = function(type) {
  329. switch (type.toLowerCase()) {
  330. case 'microsoft.xmlhttp':
  331. case 'msxml2.xmlhttp':
  332. case 'msxml2.xmlhttp.3.0':
  333. case 'msxml2.xmlhttp.4.0':
  334. case 'msxml2.xmlhttp.5.0':
  335. return new XMLHttpRequest();
  336. }
  337. return null;
  338. };
  339. }
  340. PL.pushletURL = PL._getWebRoot() + 'pushlet.srv';
  341. PL._setStatus('initialized');
  342. PL.state = PL.STATE_READY;
  343. },
  344. /** Handle incoming events from server. */
  345. _onEvent: function (event) {
  346. // Create a PushletEvent object from the arguments passed in
  347. // push.arguments is event data coming from the Server
  348. PL.debug('_onEvent()', event.toString());
  349. // Do action based on event type
  350. var eventType = event.getEvent();
  351. if (eventType == 'data') {
  352. PL._setStatus('data');
  353. PL._doCallback(event, window.onData);
  354. } else if (eventType == 'refresh') {
  355. if (PL.state < PL.STATE_LISTENING) {
  356. PL._setStatus('not refreshing state=' + PL.STATE_LISTENING);
  357. }
  358. var timeout = event.get('p_wait');
  359. setTimeout(function () {
  360. PL._doRequest('refresh');
  361. }, timeout);
  362. return;
  363. } else if (eventType == 'error') {
  364. PL.state = PL.STATE_ERROR;
  365. PL._setStatus('server error: ' + event.get('p_reason'));
  366. PL._doCallback(event, window.onError);
  367. } else if (eventType == 'join-ack') {
  368. PL.state = PL.STATE_JOINED;
  369. PL.sessionId = event.get('p_id');
  370. PL._setStatus('connected');
  371. PL._doCallback(event, window.onJoinAck);
  372. } else if (eventType == 'join-listen-ack') {
  373. PL.state = PL.STATE_LISTENING;
  374. PL.sessionId = event.get('p_id');
  375. PL._setStatus('join-listen-ack');
  376. PL._doCallback(event, window.onJoinListenAck);
  377. } else if (eventType == 'listen-ack') {
  378. PL.state = PL.STATE_LISTENING;
  379. PL._setStatus('listening');
  380. PL._doCallback(event, window.onListenAck);
  381. } else if (eventType == 'hb') {
  382. PL._setStatus('heartbeat');
  383. PL._doCallback(event, window.onHeartbeat);
  384. } else if (eventType == 'hb-ack') {
  385. PL._doCallback(event, window.onHeartbeatAck);
  386. } else if (eventType == 'leave-ack') {
  387. PL._setStatus('disconnected');
  388. PL._doCallback(event, window.onLeaveAck);
  389. } else if (eventType == 'refresh-ack') {
  390. PL._doCallback(event, window.onRefreshAck);
  391. } else if (eventType == 'subscribe-ack') {
  392. PL._setStatus('subscribed to ' + event.get('p_subject'));
  393. PL._doCallback(event, window.onSubscribeAck);
  394. } else if (eventType == 'unsubscribe-ack') {
  395. PL._setStatus('unsubscribed');
  396. PL._doCallback(event, window.onUnsubscribeAck);
  397. } else if (eventType == 'abort') {
  398. PL.state = PL.STATE_ERROR;
  399. PL._setStatus('abort');
  400. PL._doCallback(event, window.onAbort);
  401. } else if (eventType.match(/nack$/)) {
  402. PL._setStatus('error response: ' + event.get('p_reason'));
  403. PL._doCallback(event, window.onNack);
  404. }
  405. },
  406. /** Handle XMLHttpRequest response XML. */
  407. _onResponse: function(xml) {
  408. PL.debug('_onResponse', xml);
  409. var events = PL._rsp2Events(xml);
  410. if (events == null) {
  411. PL._setStatus('null events')
  412. return;
  413. }
  414. delete xml;
  415. PL.debug('_onResponse eventCnt=', events.length);
  416. // Go through all <event/> elements
  417. for (i = 0; i < events.length; i++) {
  418. PL._onEvent(events[i]);
  419. }
  420. },
  421. /** Convert XML response to PushletEvent objects. */
  422. _rsp2Events: function(xml) {
  423. // check empty response or xml document
  424. if (!xml || !xml.documentElement) {
  425. return null;
  426. }
  427. // Convert xml doc to array of PushletEvent objects
  428. var eventElements = xml.documentElement.getElementsByTagName('event');
  429. var events = new Array(eventElements.length);
  430. for (i = 0; i < eventElements.length; i++) {
  431. events[i] = new PushletEvent(eventElements[i]);
  432. }
  433. return events;
  434. },
  435. statusMsg: 'null',
  436. statusChanged: false,
  437. statusChar: '|',
  438. _showStatus: function() {
  439. // To show progress
  440. if (PL.statusChanged == true) {
  441. if (PL.statusChar == '|') PL.statusChar = '/';
  442. else if (PL.statusChar == '/') PL.statusChar = '--';
  443. else if (PL.statusChar == '--') PL.statusChar = '\\';
  444. else PL.statusChar = '|';
  445. PL.statusChanged = false;
  446. }
  447. window.defaultStatus = PL.statusMsg;
  448. window.status = PL.statusMsg + ' ' + PL.statusChar;
  449. timeout = window.setTimeout('PL._showStatus()', 400);
  450. },
  451. _setStatus: function(status) {
  452. PL.statusMsg = "pushlet - " + status;
  453. PL.statusChanged = true;
  454. },
  455. /*************** Debug utility *******************************/
  456. timestamp: 0,
  457. debugWindow: null,
  458. messages: new Array(),
  459. messagesIndex: 0,
  460. debugOn: false,
  461. /** Send debug messages to a (D)HTML window. */
  462. debug: function(label, value) {
  463. if (PL.debugOn == false) {
  464. return;
  465. }
  466. var funcName = "none";
  467. // Fetch JS function name if any
  468. if (PL.debug.caller) {
  469. funcName = PL.debug.caller.toString()
  470. funcName = funcName.substring(9, funcName.indexOf(")") + 1)
  471. }
  472. // Create message
  473. var msg = "-" + funcName + ": " + label + "=" + value
  474. // Add optional timestamp
  475. var now = new Date()
  476. var elapsed = now - PL.timestamp
  477. if (elapsed < 10000) {
  478. msg += " (" + elapsed + " msec)"
  479. }
  480. PL.timestamp = now;
  481. // Show.
  482. if ((PL.debugWindow == null) || PL.debugWindow.closed) {
  483. PL.debugWindow = window.open("", "p_debugWin", "toolbar=no,scrollbars=yes,resizable=yes,width=600,height=400");
  484. }
  485. // Add message to current list
  486. PL.messages[PL.messagesIndex++] = msg
  487. // Write doc header
  488. PL.debugWindow.document.writeln('<html><head><title>Pushlet Debug Window</title></head><body bgcolor=#DDDDDD>');
  489. // Write the messages
  490. for (var i = 0; i < PL.messagesIndex; i++) {
  491. PL.debugWindow.document.writeln('<pre>' + i + ': ' + PL.messages[i] + '</pre>');
  492. }
  493. // Write doc footer and close
  494. PL.debugWindow.document.writeln('</body></html>');
  495. PL.debugWindow.document.close();
  496. PL.debugWindow.focus();
  497. }
  498. }
  499. /* Represents nl.justobjects.pushlet.Event in JS. */
  500. function PushletEvent(xml) {
  501. // Member variable setup; the assoc array stores the N/V pairs
  502. this.arr = new Array();
  503. this.getSubject = function() {
  504. return this.get('p_subject');
  505. }
  506. this.getEvent = function() {
  507. return this.get('p_event');
  508. }
  509. this.put = function(name, value) {
  510. return this.arr[name] = value;
  511. }
  512. this.get = function(name) {
  513. return this.arr[name];
  514. }
  515. this.toString = function() {
  516. var res = '';
  517. for (var i in this.arr) {
  518. res = res + i + '=' + this.arr[i] + '\n';
  519. }
  520. return res;
  521. }
  522. this.toTable = function() {
  523. var res = '<table border="1" cellpadding="3">';
  524. var styleDiv = '<div style="color:black; font-family:monospace; font-size:10pt; white-space:pre;">'
  525. for (var i in this.arr) {
  526. res = res + '<tr><td bgColor=white>' + styleDiv + i + '</div></td><td bgColor=white>' + styleDiv + this.arr[i] + '</div></td></tr>';
  527. }
  528. res += '</table>'
  529. return res;
  530. }
  531. // Optional XML element <event name="value" ... />
  532. if (xml) {
  533. // Put the attributes in Map
  534. for (var i = 0; i < xml.attributes.length; i++) {
  535. this.put(xml.attributes[i].name, xml.attributes[i].value);
  536. }
  537. }
  538. }
  539. /**********************************************************************
  540. START - OLD application functions (LEFT HERE FOR FORWARD COMPAT)
  541. ***********************************************************************/
  542. // Debug util
  543. function p_debug(aBool, aLabel, aMsg) {
  544. if (aBool == false) {
  545. return;
  546. }
  547. PL.setDebug(true);
  548. PL.debug(aLabel, aMsg);
  549. PL.setDebug(false);
  550. }
  551. // Embed pushlet frame in page (OBSOLETE)
  552. function p_embed(thePushletWebRoot) {
  553. alert('Pushlet: p_embed() is no longer required for AJAX client')
  554. }
  555. // Join the pushlet server
  556. function p_join() {
  557. PL.join();
  558. }
  559. // Create data event channel with the server
  560. function p_listen(aSubject, aMode) {
  561. // Note: mode is fixed to 'pull'
  562. PL.listen(aSubject);
  563. }
  564. // Shorthand: Join the pushlet server and start listening immediately
  565. function p_join_listen(aSubject) {
  566. PL.joinListen(aSubject);
  567. }
  568. // Leave the pushlet server
  569. function p_leave() {
  570. PL.leave();
  571. }
  572. // Send heartbeat event; callback is onHeartbeatAck()
  573. function p_heartbeat() {
  574. PL.heartbeat();
  575. }
  576. // Publish to a subject
  577. function p_publish(aSubject, nvPairs) {
  578. var args = p_publish.arguments;
  579. // Put the arguments' name/value pairs in the URI
  580. var query = '';
  581. var amp = '';
  582. for (var i = 1; i < args.length; i++) {
  583. if (i > 1) {
  584. amp = '&';
  585. }
  586. query = query + amp + args[i] + '=' + args[++i];
  587. }
  588. PL.publish(aSubject, query);
  589. }
  590. // Subscribe to a subject with optional label
  591. function p_subscribe(aSubject, aLabel) {
  592. PL.subscribe(aSubject, aLabel);
  593. }
  594. // Unsubscribe from a subject
  595. function p_unsubscribe(aSid) {
  596. PL.unsubscribe(aSid);
  597. }
  598. /**********************************************************************
  599. END - Public application functions (LEFT HERE FOR FORWARD COMPAT)
  600. ***********************************************************************/
  601. // Initialize when page completely loaded
  602. PL._addEvent(window, 'load', PL._init, false);