Commit 21f2c6a7 authored by Wandenberg Peixoto's avatar Wandenberg Peixoto

refactoring PushStream javascript client

parent 48c039f1
...@@ -7,6 +7,10 @@ ...@@ -7,6 +7,10 @@
</head> </head>
<body> <body>
<form action="/pub" method="POST"> <form action="/pub" method="POST">
<p>
<span style="display: block; float: left; width: 55px;">mode:</span>
<span id="mode" style="display:none;"></span>
</p>
<p> <p>
<span style="display: block; float: left; width: 55px;">satus:</span> <span style="display: block; float: left; width: 55px;">satus:</span>
<span class="online" style="display:none; color:green">online</span> <span class="online" style="display:none; color:green">online</span>
...@@ -30,71 +34,81 @@ ...@@ -30,71 +34,81 @@
</p> </p>
<p><input type="submit" value="Send" id="sendButton"/></p> <p><input type="submit" value="Send" id="sendButton"/></p>
</form> </form>
<p><input type="button" value="Show Log" id="showLog"/></p>
<div id="Log4jsLogOutput" style="width:800px;height:200px;overflow:scroll;display:none;"></div>
<script src="js/jquery-1.4.2.min.js" type="text/javascript" language="javascript" charset="utf-8"></script> <script src="/js/jquery.min.js" type="text/javascript" language="javascript" charset="utf-8"></script>
<script src="js/log4js.js" type="text/javascript" language="javascript" charset="utf-8"></script> <script src="/js/pushstream.js" type="text/javascript" language="javascript" charset="utf-8"></script>
<script src="js/pushstream.js" type="text/javascript" language="javascript" charset="utf-8"></script>
<script type="text/javascript" language="javascript" charset="utf-8"> <script type="text/javascript" language="javascript" charset="utf-8">
// <![CDATA[ // <![CDATA[
$(function() { PushStream.LOG_LEVEL = 'debug';
function onSendText() { var pushstream = new PushStream({
$("#message").val(''); host: window.location.hostname,
}; port: window.location.port,
modes: "eventsource|stream"
});
pushstream.onmessage = _manageEvent;
pushstream.onstatuschange = _statuschanged;
function onSendText() {
$("#message").val('');
};
function _manageEvent(eventMessage) { function _manageEvent(eventMessage) {
var chat = $("#chat"); var chat = $("#chat");
if (eventMessage != '') { if (eventMessage != '') {
var values = $.parseJSON(eventMessage); var values = $.parseJSON(eventMessage);
var line = values.nick + ': ' + values.text; var line = values.nick + ': ' + values.text;
if (chat.val() == '') { if (chat.val() == '') {
chat.val(line); chat.val(line);
} else { } else {
chat.val(chat.val() + '\n' + line); chat.val(chat.val() + '\n' + line);
} }
} }
chat.scrollTop(chat[0].scrollHeight - chat.height()); chat.scrollTop(chat[0].scrollHeight - chat.height());
}; };
function _statuschanged() { function _statuschanged(state) {
if ((PushStream.status == 4) || (PushStream.status == 5)) { if (state == PushStream.OPEN) {
$(".offline").hide(); $(".offline").hide();
$(".online").show(); $(".online").show();
} else { $("#mode").html(pushstream.wrapper.type).show();
$(".online").hide(); } else {
$(".offline").show(); $(".offline").show();
} $(".online").hide();
}; $("#mode").html("").hide();
}
};
$("#room").change(function(){ function _connect(channel) {
if (PushStream.status == 5) { pushstream.removeAllChannels();
PushStream.disconnect(); pushstream.addChannel(channel);
} try {
$("#chat").val(''); pushstream.connect();
PushStream.joinChannel($("#room").val(), 0 ); } catch(e) {alert(e)};
PushStream.connect();
});
$("#sendButton").click(function(){ $("#chat").val('');
if (($("#nick").val() != "") && ($("#message").val() != "") && ($("#room").val() != "")) { }
$.post( '/pub?id=' + $("#room").val(), '{"nick":"' + $("#nick").val() + '", "text":"' + $("#message").val() + '"}', onSendText);
} else {
alert("nick, room and text are required");
}
return false; $("#sendButton").click(function(){
}); if (($("#nick").val() != "") && ($("#message").val() != "") && ($("#room").val() != "")) {
$.post( '/pub?id=' + $("#room").val(), '{"nick":"' + $("#nick").val() + '", "text":"' + $("#message").val() + '"}', onSendText);
} else {
alert("nick, room and text are required");
}
var now = new Date(); return false;
var _hostId = (now.getTime() + "" + (Math.random() * 10000)).replace('.',''); });
$("#room").change(function(){
_connect($("#room").val());
});
PushStream.host = window.location.hostname; $("#showLog").click(function(){
PushStream.port = window.location.port; $("#Log4jsLogOutput").show();
PushStream.hostid = _hostId;
PushStream.registerEventCallback("process", _manageEvent);
PushStream.registerEventCallback("statuschanged", _statuschanged);
PushStream.joinChannel($("#room").val(), 0 );
PushStream.connect();
}); });
_connect($("#room").val());
// ]]> // ]]>
</script> </script>
</body> </body>
......
Log4js = {
logLevel: 'error', /* debug, info, error */
logOutputId: 'Log4jsLogOutput',
debug : function(logstr) {
Log4js._log(logstr, 'debug');
},
info : function(logstr) {
Log4js._log(logstr, 'info');
},
error : function(logstr) {
Log4js._log(logstr, 'error');
},
_log: function(logstr, level) {
if ((Log4js.logLevel === level) || ('error' === level) || (Log4js.logLevel === 'debug') ) {
if (window.console) {
window.console.log(logstr);
} else if (document.getElementById(Log4js.logOutputId)) {
document.getElementById(Log4js.logOutputId).innerHTML += logstr+"<br/>";
}
}
}
};
PushStream = {
callbacks: {
process: function() {},
reset: function() {},
eof: function() {},
statuschanged: function() {}
},
host: null,
port: 80,
hostid: null,
status: 0,
channelcount: 0,
channels: {},
lastrequest: 0,
frameref: null,
frameloadtimer: null,
frameloadingtimeout: 15000,
pingtimer: null,
pingingtimeout: 30000,
reconnecttimer: null,
reconnecttimeout: 3000,
checkChannelAvailabilityInterval: 60000,
backtrackDefault: 10,
mode: 'iframe',
connect: function() {
Log4js.debug('entering connect');
if (!PushStream.host) throw "PushStream host not specified";
if (isNaN(PushStream.port)) throw "PushStream port not specified";
if (!PushStream.channelcount) throw "No channels specified";
if (!PushStream.hostid) PushStream.hostid = t+""+Math.floor(Math.random()*1000000);
document.domain = PushStream.extract_xss_domain(document.domain);
if (PushStream.status) PushStream.disconnect();
PushStream.setStatus(1);
var now = new Date();
var t = now.getTime();
PushStream.loadFrame(PushStream.getSubsUrl());
PushStream.lastrequest = t;
Log4js.debug('leaving connect');
},
reconnect: function(interval) {
if (PushStream.status != 6) {
PushStream.reconnecttimer = setTimeout(PushStream.connect, interval || PushStream.reconnecttimeout);
}
},
disconnect: function() {
Log4js.debug('entering disconnect');
if (PushStream.status) {
PushStream.clearPingtimer();
PushStream.clearFrameloadtimer();
PushStream.clearReconnecttimer();
if (typeof CollectGarbage == 'function') CollectGarbage();
if (PushStream.status != 6) PushStream.setStatus(0);
Log4js.info("Disconnected");
}
Log4js.debug('leaving disconnect');
},
joinChannel: function(channelname, backtrack) {
Log4js.debug('entering joinChannel');
if (typeof(PushStream.channels[channelname]) != "undefined") throw "Cannot join channel "+channelname+": already subscribed";
PushStream.channels[channelname] = {backtrack:backtrack, lastmsgreceived:-1};
Log4js.info("Joined channel " + channelname);
PushStream.channelcount++;
if (PushStream.status != 0) PushStream.connect();
Log4js.debug('leaving joinChannel');
},
loadFrame: function(url) {
try {
var transferDoc = (!PushStream.frameref) ? new ActiveXObject("htmlfile") : PushStream.frameref;
transferDoc.open();
transferDoc.write("<html><script>document.domain=\""+(document.domain)+"\";</script></html>");
transferDoc.parentWindow.PushStream = PushStream;
transferDoc.close();
var ifrDiv = transferDoc.createElement("div");
transferDoc.appendChild(ifrDiv);
ifrDiv.innerHTML = "<iframe src=\""+url+"\" onload=\"PushStream.frameload();\"></iframe>";
PushStream.frameref = transferDoc;
} catch (e) {
if (!PushStream.frameref) {
var ifr = document.createElement("IFRAME");
ifr.style.width = "10px";
ifr.style.height = "10px";
ifr.style.border = "none";
ifr.style.position = "absolute";
ifr.style.top = "-10px";
ifr.style.marginTop = "-10px";
ifr.style.zIndex = "-20";
ifr.PushStream = PushStream;
ifr.onload = PushStream.frameload;
document.body.appendChild(ifr);
PushStream.frameref = ifr;
}
PushStream.frameref.setAttribute("src", url);
}
Log4js.info("Loading URL '" + url + "' into frame...");
PushStream.frameloadtimer = setTimeout(PushStream.frameloadtimeout, PushStream.frameloadingtimeout);
},
frameload: function() {
Log4js.info("Frame loaded whitout streaming");
PushStream.clearFrameloadtimer();
PushStream.setStatus(8);
PushStream.reconnect(PushStream.checkChannelAvailabilityInterval);
},
frameloadtimeout: function() {
Log4js.info("Frame load timeout");
PushStream.clearFrameloadtimer();
PushStream.setStatus(3);
PushStream.reconnect(PushStream.frameloadingtimeout);
},
register: function(ifr) {
PushStream.clearFrameloadtimer();
ifr.p = PushStream.process;
ifr.r = PushStream.reset;
ifr.eof = PushStream.eof;
PushStream.setStatus(4);
PushStream.setPingtimer();
Log4js.info("Frame registered");
},
pingtimeout: function() {
Log4js.info("Ping timeout");
PushStream.setStatus(7);
PushStream.clearPingtimer();
PushStream.reconnect();
},
process: function(id, channel, data) {
Log4js.info("Message received");
PushStream.setStatus(5);
PushStream.clearPingtimer();
if (id == -1) {
Log4js.debug("Ping");
} else if (typeof(PushStream.channels[channel]) != "undefined") {
Log4js.debug("Message " + id + " received on channel " + channel + " (last id on channel: " + PushStream.channels[channel].lastmsgreceived + ")\n" + data);
PushStream.callbacks["process"](data);
PushStream.channels[channel].lastmsgreceived = id;
}
PushStream.setPingtimer();
},
reset: function() {
if (PushStream.status != 6) {
Log4js.info("Stream reset");
PushStream.callbacks["reset"]();
PushStream.reconnect();
}
},
eof: function() {
Log4js.info("Received end of stream, will not reconnect");
PushStream.callbacks["eof"]();
PushStream.setStatus(6);
PushStream.disconnect();
},
setStatus: function(newstatus) {
// Statuses: 0 = Uninitialised,
// 1 = Loading stream,
// 2 = Loading controller frame,
// 3 = Controller frame timeout, retrying.
// 4 = Controller frame loaded and ready
// 5 = Receiving data
// 6 = End of stream, will not reconnect
// 7 = Ping Timeout
// 8 = Frame loaded whitout streaming, channel problably empty or not exists
if (PushStream.status != newstatus) {
Log4js.info('PushStream.status ' + newstatus);
PushStream.status = newstatus;
PushStream.callbacks["statuschanged"](newstatus);
}
},
registerEventCallback: function(evt, funcRef) {
Function.prototype.andThen=function(g) {
var f=this;
var a=PushStream.arguments
return function(args) {
f(a);g(args);
}
};
if (typeof PushStream.callbacks[evt] == "function") {
PushStream.callbacks[evt] = (PushStream.callbacks[evt]).andThen(funcRef);
} else {
PushStream.callbacks[evt] = funcRef;
}
},
extract_xss_domain: function(old_domain) {
if (old_domain.match(/^(\d{1,3}\.){3}\d{1,3}$/)) return old_domain;
domain_pieces = old_domain.split('.');
return domain_pieces.slice(-2, domain_pieces.length).join(".");
},
getSubsUrl: function() {
var surl = "http://" + PushStream.host + ((PushStream.port==80)?"":":"+PushStream.port) + "/sub";
for (var c in PushStream.channels) {
var channelinfo = "/" + c + PushStream.getBacktrack(c);
surl += channelinfo;
}
var now = new Date();
surl += "?nc="+now.getTime();
return surl;
},
getBacktrack: function(channelName) {
var channel = PushStream.channels[channelName];
if (channel.backtrack != 0) {
var backtrack = ".b"
if (channel.backtrack > 0) {
backtrack += channel.backtrack
} else {
backtrack += PushStream.backtrackDefault;
}
return backtrack;
} else return "";
},
clearPingtimer: function() {
if (PushStream.pingtimer) {
clearTimeout(PushStream.pingtimer);
PushStream.pingtimer = null;
}
},
setPingtimer: function() {
PushStream.clearPingtimer();
PushStream.pingtimer = setTimeout(PushStream.pingtimeout, PushStream.pingingtimeout);
},
clearFrameloadtimer: function() {
if (PushStream.frameloadtimer) {
if (PushStream.frameloadtimer) clearTimeout(PushStream.frameloadtimer);
PushStream.frameloadtimer = null;
}
},
clearReconnecttimer: function() {
if (PushStream.reconnecttimer) {
if (PushStream.reconnecttimer) clearTimeout(PushStream.reconnecttimer);
PushStream.reconnecttimer = null;
}
}
};
/*global PushStream */
/**
* Copyright (C) 2010-2011 Wandenberg Peixoto <wandenberg@gmail.com>, Rogério Carvalho Schneider <stockrt@gmail.com>
*
* This file is part of Nginx Push Stream Module.
*
* Nginx Push Stream Module is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Nginx Push Stream Module is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Nginx Push Stream Module. If not, see <http://www.gnu.org/licenses/>.
*
*
* pushstream.js
*
* Created: Nov 01, 2011
* Authors: Wandenberg Peixoto <wandenberg@gmail.com>, Rogério Carvalho Schneider <stockrt@gmail.com>
*/
(function (window, undefined) {
/* prevent duplicate declaration */
if (window.PushStream) { return; }
var PATTERN_MESSAGE = /\{\"id\":(\d*),\"channel\":\"(.*)\",\"text\":\"(.*)\"\}/;
var PATTERN_MESSAGE_WITH_EVENT_ID = /\{\"id\":(\d*),\"channel\":\"(.*)\",\"text\":\"(.*)\",\"eventid\":\"(.*)\"\}/;
var streamWrappersCount = 0;
var Log4js = {
debug : function() { if (PushStream.LOG_LEVEL === 'debug') Log4js._log.apply(Log4js._log, arguments); },
info : function() { if ((PushStream.LOG_LEVEL === 'info') || (PushStream.LOG_LEVEL === 'debug')) Log4js._log.apply(Log4js._log, arguments); },
error : function() { Log4js._log.apply(Log4js._log, arguments); },
_log : function() {
if (window.console && window.console.log && window.console.log.apply) {
window.console.log.apply(window.console, arguments);
}
var logElement = document.getElementById(PushStream.LOG_OUTPUT_ELEMENT_ID);
if (logElement) {
var str = '';
for (var i = 0; i < arguments.length; i++) {
str += arguments[i] + " ";
}
logElement.innerHTML += str + "<br/>";
}
}
};
var Ajax = {
getXHRObject : function() {
var xhr = false;
try { xhr = new window.ActiveXObject("Msxml2.XMLHTTP"); }
catch (e1) {
try { xhr = new window.ActiveXObject("Microsoft.XMLHTTP"); }
catch (e2) {
try { xhr = new window.XMLHttpRequest(); }
catch (e3) {
xhr = false;
}
}
}
return xhr;
},
load : function (settings) {
settings = settings || {};
var cache = settings.cache || true;
var xhr = Ajax.getXHRObject();
if (!xhr||!settings.url) return;
var url = settings.url + ((cache) ? "" : settings.url + ((settings.url.indexOf("?")+1) ? "&" : "?") + "_=" + new Date().getTime());
xhr.open("GET", url, true);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (settings.afterReceive) settings.afterReceive(xhr);
if(xhr.status == 200) {
if (settings.success) settings.success(xhr.responseText);
} else {
if (settings.error) settings.error(xhr.status);
}
}
}
if (settings.beforeSend) settings.beforeSend(xhr);
xhr.send(null);
return xhr;
}
};
var getBacktrack = function(options) {
return (options.backtrack) ? ".b" + Number(options.backtrack) : "";
};
var getChannelsPath = function(channels) {
var path = '';
for (var channelName in channels) {
if (!channels.hasOwnProperty || channels.hasOwnProperty(channelName)) {
path += "/" + channelName + getBacktrack(channels[channelName]);
}
}
return path;
};
var getSubscriberUrl = function(pushstream, prefix) {
var url = (pushstream.useSSL) ? "https://" : "http://";
url += pushstream.host;
url += ((pushstream.port != 80) && (pushstream.port != 443)) ? (":" + pushstream.port) : "";
url += prefix;
url += getChannelsPath(pushstream.channels);
url += "?_=" + (new Date()).getTime();
return url;
};
var extract_xss_domain = function(domain) {
// if domain is a ip address return it, else return the last two parts of it
return (domain.match(/^(\d{1,3}\.){3}\d{1,3}$/)) ? domain : domain.split('.').slice(-2).join('.');
};
var linker = function(method, instance) {
return function() {
return method.apply(instance, arguments);
};
};
var clearTimer = function(timer) {
if (timer) {
clearTimeout(timer);
}
return null;
}
/* wrappers */
var EventSourceWrapper = function(pushstream) {
if (!window.EventSource) throw "EventSource not supported";
this.type = "eventsource";
this.pushstream = pushstream;
this.connection = null;
};
EventSourceWrapper.prototype = {
connect: function() {
this.disconnect();
var url = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixEventsource);
this.connection = new window.EventSource(url);
this.connection.onerror = linker(this.onerror, this);
this.connection.onopen = linker(this.onopen, this);
this.connection.onmessage = linker(this.onmessage, this);
Log4js.debug("[EventSource] connecting to:", url);
},
disconnect: function() {
if (this.connection) {
Log4js.debug("[EventSource] closing connection to:", this.connection.URL);
try { this.connection.close(); } catch (e) { /* ignore error on closing */ }
this.connection = null;
this.pushstream._onclose();
}
},
onerror: function(event) {
Log4js.info("[EventSource] error (disconnected by server):", event);
this.disconnect();
this.pushstream._onerror({type: "timeout"});
},
onopen: function() {
this.pushstream._onopen();
Log4js.info("[EventSource] connection opened");
},
onmessage: function(event) {
Log4js.info("[EventSource] message received", arguments);
var match = event.data.match((event.data.indexOf('"eventid":"') > 0) ? PATTERN_MESSAGE_WITH_EVENT_ID : PATTERN_MESSAGE);
this.pushstream._onmessage(match[3], match[1], match[2], match[4]);
}
};
var StreamWrapper = function(pushstream) {
this.type = "stream";
this.pushstream = pushstream;
this.connection = null;
this.url = null;
this.frameloadtimer = null;
this.pingtimer = null;
this.streamId = "streamWrapper_" + streamWrappersCount++;
window[this.streamId] = this;
};
StreamWrapper.prototype = {
connect: function() {
this.disconnect();
var domain = extract_xss_domain(this.pushstream.host);
try {
document.domain = domain;
} catch(e) {
Log4js.error("[Stream] (warning) problem setting document.domain = " + domain + " (OBS: IE8 does not support set IP numbers as domain)");
}
this.url = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixStream);
this.url += "&streamid=" + this.streamId;
Log4js.debug("[Stream] connecting to:", this.url);
this.loadFrame(this.url);
},
disconnect: function() {
if (this.connection) {
Log4js.debug("[Stream] closing connection to:", this.url);
try { this.connection.onload = null; this.connection.setAttribute("src", ""); } catch (e) { /* ignore error on closing */ }
this.pingtimer = clearTimer(this.pingtimer);
this.frameloadtimer = clearTimer(this.frameloadtimer);
this.connection = null;
this.transferDoc = null;
if (typeof window.CollectGarbage === 'function') window.CollectGarbage();
this.pushstream._onclose();
}
},
loadFrame: function(url) {
try {
var transferDoc = new window.ActiveXObject("htmlfile");
transferDoc.open();
transferDoc.write("<html><script>document.domain=\""+(document.domain)+"\";</script></html>");
transferDoc.parentWindow.PushStream = PushStream;
transferDoc.close();
var ifrDiv = transferDoc.createElement("div");
transferDoc.appendChild(ifrDiv);
ifrDiv.innerHTML = "<iframe src=\""+url+"\"></iframe>";
this.connection = ifrDiv.getElementsByTagName("IFRAME")[0];
this.connection.onload = linker(this.frameerror, this);
this.transferDoc = transferDoc;
} catch (e) {
var ifr = document.createElement("IFRAME");
ifr.style.width = "1px";
ifr.style.height = "1px";
ifr.style.border = "none";
ifr.style.position = "absolute";
ifr.style.top = "-10px";
ifr.style.marginTop = "-10px";
ifr.style.zIndex = "-20";
ifr.PushStream = PushStream;
document.body.appendChild(ifr);
ifr.setAttribute("src", url);
ifr.onload = linker(this.frameerror, this);
this.connection = ifr;
}
this.frameloadtimer = setTimeout(linker(this.frameerror, this), this.pushstream.timeout);
},
register: function(iframeWindow) {
this.frameloadtimer = clearTimer(this.frameloadtimer);
iframeWindow.p = linker(this.process, this);
this.connection.onload = linker(this._onframeloaded, this);
this.pushstream._onopen();
this.setPingTimer();
Log4js.info("[Stream] frame registered");
},
process: function(id, channel, data, eventid) {
this.pingtimer = clearTimer(this.pingtimer);
Log4js.info("[Stream] message received", arguments);
this.pushstream._onmessage(data, id, channel, eventid);
this.setPingTimer();
},
_onframeloaded: function() {
Log4js.info("[Stream] frame loaded (disconnected by server)");
this.connection.onload = null;
this.disconnect();
},
frameerror: function(event) {
var error = {};
error.type = (event && (event.type === "load")) ? "load" : "timeout";
Log4js.info("[Stream] " + (error.type === "load") ? "frame loaded whitout streaming" : "frame load timeout");
this.disconnect();
this.pushstream._onerror(error);
},
pingerror: function() {
Log4js.info("[Stream] ping timeout");
this.disconnect();
this.pushstream._onerror({type: "timeout"});
},
setPingTimer: function() {
if (this.pingtimer) clearTimer(this.pingtimer);
this.pingtimer = setTimeout(linker(this.pingerror, this), this.pushstream.pingtimeout);
}
};
var LongPollingWrapper = function(pushstream) {
this.type = "longpolling";
this.pushstream = pushstream;
this.connection = null;
this.lastModified = null;
this.etag = 0;
this.connectionEnabled = false;
this.xhrSettings = {
url: null,
success: linker(this.onmessage, this),
error: linker(this.onerror, this),
beforeSend: linker(this.beforeSend, this),
afterReceive: linker(this.afterReceive, this)
}
};
LongPollingWrapper.prototype = {
connect: function() {
this.disconnect();
this.connectionEnabled = true;
this._listen();
this.onopen();
Log4js.debug("[LongPolling] connecting to:", this.xhrSettings.url);
},
_listen: function() {
if (this.connectionEnabled) {
this.xhrSettings.url = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixLongpolling);
this.connection = Ajax.load(this.xhrSettings);
}
},
disconnect: function() {
this.connectionEnabled = false;
if (this.connection) {
Log4js.debug("[LongPolling] closing connection to:", this.xhrSettings.url);
try { this.connection.abort(); } catch (e) { /* ignore error on closing */ }
this.connection = null;
this.xhrSettings.url = null;
this.pushstream._onclose();
}
},
beforeSend: function(xhr) {
if (this.lastModified == null) { this.lastModified = new Date().toUTCString(); }
xhr.setRequestHeader("If-None-Match", this.etag);
xhr.setRequestHeader("If-Modified-Since", this.lastModified);
},
afterReceive: function(xhr) {
this.etag = xhr.getResponseHeader('Etag');
this.lastModified = xhr.getResponseHeader('Last-Modified');
},
onerror: function(status) {
if (this.connectionEnabled) { /* abort(), called by disconnect(), call this callback, but should be ignored */
if (status === 304) {
this._listen();
} else {
Log4js.info("[LongPolling] error (disconnected by server):", status);
this.disconnect();
this.pushstream._onerror({type: "timeout"});
}
}
},
onopen: function() {
this.pushstream._onopen();
Log4js.info("[LongPolling] connection opened");
},
onmessage: function(responseText) {
Log4js.info("[LongPolling] message received", arguments);
var match = responseText.match((responseText.indexOf('"eventid":"') > 0) ? PATTERN_MESSAGE_WITH_EVENT_ID : PATTERN_MESSAGE);
this._listen();
this.pushstream._onmessage(match[3], match[1], match[2], match[4]);
}
};
/* mains class */
var PushStream = function(settings) {
settings = settings || {};
this.useSSL = settings.useSSL || false;
this.host = settings.host || window.location.hostname;
this.port = settings.port || (this.useSSL ? 443 : 80);
this.timeout = settings.timeout || 15000;
this.pingtimeout = settings.pingtimeout || 30000;
this.reconnecttimeout = settings.reconnecttimeout || 3000;
this.checkChannelAvailabilityInterval = settings.checkChannelAvailabilityInterval || 60000;
this.reconnecttimer = null;
this.urlPrefixStream = settings.urlPrefixStream || '/sub';
this.urlPrefixEventsource = settings.urlPrefixEventsource || '/ev';
this.urlPrefixLongpolling = settings.urlPrefixLongpolling || '/lp';
//this.urlPrefixWebsocket = settings.urlPrefixWebsocket || '/ws';
this.modes = (settings.modes || 'eventsource|stream|longpolling').split('|');
//this.modes = (settings.modes || 'eventsource|websocket|stream|longpolling').split('|');
this.wrappers = [];
for ( var i = 0; i < this.modes.length; i++) {
try {
var wrapper = null;
switch (this.modes[i]) {
case "eventsource": wrapper = new EventSourceWrapper(this); break;
case "longpolling": wrapper = new LongPollingWrapper(this); break;
default: wrapper = new StreamWrapper(this); break;
}
this.wrappers[this.wrappers.length] = wrapper;
} catch(e) { Log4js.info(e); }
}
this.wrapper = null; //TODO test
this.onopen = null;
this.onmessage = null;
this.onerror = null;
this.onstatuschange = null;
this.channels = {};
this.channelsCount = 0;
this._setState(0);
}
/* constants */
PushStream.LOG_LEVEL = 'error'; /* debug, info, error */
PushStream.LOG_OUTPUT_ELEMENT_ID = 'Log4jsLogOutput';
/* status codes */
PushStream.CLOSED = 0; //TODO test
PushStream.CONNECTING = 1;
PushStream.OPEN = 2; //TODO test
/* main code */
PushStream.prototype = {
addChannel: function(channel, options) {
Log4js.debug("entering addChannel");
if (typeof(this.channels[channel]) !== "undefined") throw "Cannot add channel " + channel + ": already subscribed";
options = options || {};
Log4js.info("adding channel", channel, options);
this.channels[channel] = options;
this.channelsCount++;
if (this.readyState != PushStream.CLOSED) this.connect();
Log4js.debug("leaving addChannel");
},
removeChannel: function(channel) {
if (this.channels[channel]) {
Log4js.info("removing channel", channel);
delete this.channels[channel];
this.channelsCount--;
}
},
removeAllChannels: function() {
Log4js.info("removing all channels");
this.channels = {};
this.channelsCount = 0;
},
_setState: function(state) { //TODO test
if (this.readyState != state) {
Log4js.info("status changed", state);
this.readyState = state;
if (this.onstatuschange) {
this.onstatuschange(this.readyState);
}
}
},
connect: function() { //TODO test
Log4js.debug("entering connect");
if (!this.host) throw "PushStream host not specified";
if (isNaN(this.port)) throw "PushStream port not specified";
if (!this.channelsCount) throw "No channels specified";
if (this.wrappers.length === 0) throw "No available support for this browser";
this._keepConnected = true;
this._lastUsedMode = 0;
this._connect();
Log4js.debug("leaving connect");
},
disconnect: function() { //TODO test
Log4js.debug("entering disconnect");
this._keepConnected = false;
this._disconnect();
this._setState(PushStream.CLOSED);
Log4js.info("disconnected");
Log4js.debug("leaving disconnect");
},
_connect: function() { //TODO test
this._disconnect();
this._setState(PushStream.CONNECTING);
this.wrapper = this.wrappers[this._lastUsedMode++ % this.wrappers.length];
try {
this.wrapper.connect();
} catch (e) {
//each wrapper has a cleanup routine at disconnect method
this.wrapper.disconnect();
}
},
_disconnect: function() {
this.reconnecttimer = clearTimer(this.reconnecttimer);
if (this.wrapper) {
this.wrapper.disconnect();
}
},
_onopen: function() {
this._setState(PushStream.OPEN);
this._lastUsedMode--; //use same mode on next connection
},
_onclose: function() {
this._setState(PushStream.CLOSED);
this._reconnect(this.reconnecttimeout);
},
_onmessage: function(data, id, channel, eventid) {
Log4js.debug("message", data, id, channel, eventid);
if (id == -2) {
if (this.onchanneldeleted) { this.onchanneldeleted(channel); }
} else if (typeof(this.channels[channel]) !== "undefined") {
if (this.onmessage) { this.onmessage(data, id, channel, eventid); }
}
},
_onerror: function(error) {
this._setState(PushStream.CLOSED);
this._reconnect((error.type == "timeout") ? this.reconnecttimeout : this.checkChannelAvailabilityInterval);
if (this.onerror) { this.onerror(error); }
},
_reconnect: function(timeout) {
if (this._keepConnected && !this.reconnecttimer && (this.readyState != PushStream.CONNECTING)) {
Log4js.debug("trying to reconnect in", timeout);
this.reconnecttimer = setTimeout(linker(this._connect, this), timeout);
}
}
};
// to make server header template more clear, it calls register and
// by a url parameter we find the stream wrapper instance
PushStream.register = function(iframe) {
var matcher = iframe.window.location.href.match(/streamid=(.*)&?$/);
if (matcher[1] && window[matcher[1]]) {
window[matcher[1]].register(iframe);
}
};
/* make class public */
window.PushStream = PushStream;
})(window);
...@@ -3,8 +3,145 @@ describe("PushStream", function() { ...@@ -3,8 +3,145 @@ describe("PushStream", function() {
beforeEach(function() { beforeEach(function() {
}); });
it("should use default port", function() { describe("when defining library external interface", function() {
expect(PushStream.port).toEqual(80); it("should has a class named PushStream", function() {
expect(new PushStream()).toBeDefined();
});
it("should has a log level constant", function() {
expect(PushStream.LOG_LEVEL).toBeDefined();
});
it("should has a log output element id constant", function() {
expect(PushStream.LOG_OUTPUT_ELEMENT_ID).toBeDefined();
});
it("should define status code constants", function() {
expect(PushStream.CLOSED).toBeDefined();
expect(PushStream.CONNECTING).toBeDefined();
});
});
describe("when using default values", function() {
var pushstream = null;
beforeEach(function() {
pushstream = new PushStream();
});
it("should use current hostname", function() {
expect(pushstream.host).toBe(window.location.hostname);
});
it("should use port 80", function() {
expect(pushstream.port).toBe(80);
});
it("should not use ssl", function() {
expect(pushstream.useSSL).toBeFalsy();
});
it("should set state as uninitialised", function() {
expect(pushstream.readyState).toBe(PushStream.CLOSED);
});
it("should use '/sub' as url prefix for stream", function() {
expect(pushstream.urlPrefixStream).toBe('/sub');
});
it("should use '/ev' as url prefix for event source", function() {
expect(pushstream.urlPrefixEventsource).toBe('/ev');
});
it("should use '/lp' as url prefix for long-polling", function() {
expect(pushstream.urlPrefixLongpolling).toBe('/lp');
});
it("should has all modes availables", function() {
expect(pushstream.modes).toEqual(['eventsource', 'stream', 'longpolling']);
});
it("should define callbacks attributes", function() {
expect(pushstream.onopen).toBeDefined();
expect(pushstream.onmessage).toBeDefined();
expect(pushstream.onerror).toBeDefined();
expect(pushstream.onstatuschange).toBeDefined();
});
it("should has an empty channels list", function() {
expect(pushstream.channels).toEqual({});
expect(pushstream.channelsCount).toBe(0);
});
});
describe("when manipulating channels", function() {
var pushstream = null;
beforeEach(function() {
pushstream = new PushStream();
});
describe("and is not connected", function() {
describe("and is adding a channel", function() {
it("should keep channel name", function() {
pushstream.addChannel("ch1");
expect(pushstream.channels.ch1).toBeDefined();
});
it("should keep channel options", function() {
var options = {key:"value"};
pushstream.addChannel("ch2", options);
expect(pushstream.channels.ch2).toBe(options);
});
it("should increment channels counter", function() {
var count = pushstream.channelsCount;
pushstream.addChannel("ch3");
expect(pushstream.channelsCount).toBe(count + 1);
});
});
describe("and is removing a channel", function() {
beforeEach(function() {
pushstream.addChannel("ch1", {key:"value1"});
pushstream.addChannel("ch2", {key:"value2"});
pushstream.addChannel("ch3");
});
it("should remove channel name and options", function() {
pushstream.removeChannel("ch2");
expect(pushstream.channels.ch1).toEqual({key:"value1"});
expect(pushstream.channels.ch2).not.toBeDefined();
expect(pushstream.channels.ch3).toBeDefined();
});
it("should decrement channels counter", function() {
var count = pushstream.channelsCount;
pushstream.removeChannel("ch2");
expect(pushstream.channelsCount).toBe(count - 1);
});
});
describe("and is removing all channels", function() {
beforeEach(function() {
pushstream.addChannel("ch1", {key:"value1"});
pushstream.addChannel("ch2", {key:"value2"});
pushstream.addChannel("ch3");
});
it("should remove channels names and options", function() {
pushstream.removeAllChannels();
expect(pushstream.channels.ch1).not.toBeDefined();
expect(pushstream.channels.ch2).not.toBeDefined();
expect(pushstream.channels.ch3).not.toBeDefined();
});
it("should reset channels counter", function() {
pushstream.removeAllChannels();
expect(pushstream.channelsCount).toBe(0);
});
});
});
}); });
}); });
...@@ -11,9 +11,7 @@ ...@@ -11,9 +11,7 @@
# - dist/**/*.js # - dist/**/*.js
# #
src_files: src_files:
- misc/examples/js/jquery-1.4.2.min.js - misc/js/pushstream.js
- misc/examples/js/log4js.js
- misc/examples/js/pushstream.js
# stylesheets # stylesheets
# #
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment