首页 > 代码库 > pushlet

pushlet

自己准备做一个小游戏,租个云服务,然后挂在网上,可以跟同学一起玩,不过首先布置的是,这个游戏是否能实现,多人在线网页游戏,考虑到是否能够实时查询,在网上借鉴了下聊天原理,http长连接,搜索到pushlet可以满足要求,特来学习

 

首先网上下载了jar包,根据http://blog.csdn.net/lupeng0527/article/details/19019585提到,可以自制maven jar,所以学着达成jar包了

 

借鉴这个人写的文章:http://www.cnblogs.com/linjiqin/archive/2011/12/30/2307788.html

一、comet基本概念
1.comet是一个用于描述客户端和服务器之间交互的术语,即使用长期保持的http连接来在连接保持畅通的情况下支持客户端和服务器间的事件驱动的通信。
2.传统的web系统的工作流程是客户端发出请求,服务器端进行响应,而comet则是在现有技术的基础上,实现服务器数据、事件等快速push到客户端,所以会出现一个术语”服务器推“技术。

            

二、push实现方式
1.原理:
利用jsp/servel技术,在不关闭http流的情况下push数据到客户端浏览器;
2.实现:
基于ajax的长轮询(long-polling)方式

               

ajax的出现使得javascript可以调用xmlhttprequest对象发出http请求,javascript响应处理函数根据服务器返回的信息对html页面的显示进行更新。使用ajax实现“服务器推”与传统的ajax应用不同之处在于:

1)、服务器端会阻塞请求直到有数据传递或超时才返回。 
2)、客户端 javascript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。 
3)、当客户端处理接收的数据、重新建立连接时,服务器端可能有新的数据到达;这些信息会被服务器端保存直到客户端重 新建立连接,客户端会一次把当前服务器端所有的信息取回。

             

按照上述的博客的确能做到http长连,等有新进展成果重新编辑

 

只有博客后面有人说道的js或者pushlet.properties找不到的,麻烦看下pushlet的source,里面是有的 

 pushlet.properties

## Pushlet configuration.# Place this file in the CLASSPATH (e.g. WEB-INF/classes) or directly under WEB-INF.## $Id: pushlet.properties,v 1.13 2007/12/07 12:57:40 justb Exp $####config.version=1.0.2## CLASS FACTORY SPECIFICATION## Change these if you want to override any of the core classes# within the Pushlet framework with your own custom classes.## Examples:# - custom SessionManager for authorisation# - maintain lists of active subjects (topics)# - send events on subscription# - plug in custom logging like log4j# Note that you must maintain the semantics of each class !# Below are the default properties for the core classes.controller.class=nl.justobjects.pushlet.core.Controllerdispatcher.class=nl.justobjects.pushlet.core.Dispatcherlogger.class=nl.justobjects.pushlet.util.Log4jLogger# logger.class=nl.justobjects.pushlet.util.DefaultLoggersessionmanager.class=nl.justobjects.pushlet.core.SessionManagersession.class=nl.justobjects.pushlet.core.Sessionsubscriber.class=nl.justobjects.pushlet.core.Subscribersubscription.class=nl.justobjects.pushlet.core.Subscription # sessionmanager.maxsessions=200## DISPATCHER## TODO: allow properties to be maintained in# a user dir# config.redirect=/etc/pushlet.properties## LOGGING## log level (trace(6) debug(5) info (4), warn(3), error(2), fatal(1))# default is info(4)log.level=4## LOCAL EVENT SOURCES## should local sources be loaded ?sources.activate=true## SESSION## algoritm to generate session key:# values: "randomstring" (default) or "uuid".# session.id.generation=uuidsession.id.generation=randomstring# length of generated session key when using "randomstring" generationsession.id.size=10# Overall session lease time in minutes# Mainly used for clients that do not perform# listening, e.g. when publishing only.session.timeout.mins=5## EVENT QUEUE## Properties for per-client data event queue# Size forqueue.size=24queue.read.timeout.millis=20000queue.write.timeout.millis=20## LISTENING MODE## You may force all clients to use pull mode# for scalabilitylisten.force.pull.all=false## Comma-separated list of User Agent substrings.# Force these browsers to use pull mode, since they# don‘t support JS streaming, matching is done using# String.indexOf() with lowercased agent strings# use multiple criteria with &.#listen.force.pull.agents=safari## PULL MODE## time server should wait on refresing pull clientpull.refresh.timeout.millis=45000# minimum/maximum wait time client should wait before refreshing# server provides a random time between these valuespull.refresh.wait.min.millis=2000pull.refresh.wait.max.millis=6000## POLL MODE## time server should wait on refresing poll clientpoll.refresh.timeout.millis=60000# minimum/maximum wait time client should wait before refreshing# server provides a random time between these valuespoll.refresh.wait.min.millis=6000poll.refresh.wait.max.millis=10000

  

 

/* * 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() {		/** Return directory of this relative to document URL. */		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 = http://www.mamicode.com/nodes.item(i).src;"ajax-pushlet-client.js");				if (index >= 0) {					index = src.indexOf("lib");					PL.webRoot = src.substring(0, index);					break;				}			}		}		return PL.webRoot;	},// 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 <http://www.java.com/>.		   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 <event/> 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(‘<html><head><title>Pushlet Debug Window</title></head><body bgcolor=#DDDDDD>‘);		// Write the messages		for (var i = 0; i < PL.messagesIndex; i++) {			PL.debugWindow.document.writeln(‘<pre>‘ + i + ‘: ‘ + PL.messages[i] + ‘</pre>‘);		}		// Write doc footer and close		PL.debugWindow.document.writeln(‘</body></html>‘);		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 = ‘<table border="1" cellpadding="3">‘;		var styleDiv = ‘<div style="color:black; font-family:monospace; font-size:10pt; white-space:pre;">‘		for (var i in this.arr) {			res = res + ‘<tr><td bgColor=white>‘ + styleDiv + i + ‘</div></td><td bgColor=white>‘ + styleDiv + this.arr[i] + ‘</div></td></tr>‘;		}		res += ‘</table>‘		return res;	}	// Optional XML element <event name="value" ... />	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 utilfunction 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 serverfunction p_join() {	PL.join();}// Create data event channel with the serverfunction p_listen(aSubject, aMode) {	// Note: mode is fixed to ‘pull‘	PL.listen(aSubject);}// Shorthand: Join the pushlet server and start listening immediatelyfunction p_join_listen(aSubject) {	PL.joinListen(aSubject);}// Leave the pushlet serverfunction p_leave() {	PL.leave();}// Send heartbeat event; callback is onHeartbeatAck()function p_heartbeat() {	PL.heartbeat();}// Publish to a subjectfunction 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 labelfunction p_subscribe(aSubject, aLabel) {	PL.subscribe(aSubject, aLabel);}// Unsubscribe from a subjectfunction p_unsubscribe(aSid) {	PL.unsubscribe(aSid);}/********************************************************************** END - Public application functions (LEFT HERE FOR FORWARD COMPAT) ***********************************************************************/// Initialize when page completely loadedPL._addEvent(window, ‘load‘, PL._init, false);

  

pushlet