hop-2012/server/web/jquery.stream-1.2.js

769 lines
20 KiB
JavaScript

/*
* jQuery Stream 1.2
* Comet Streaming JavaScript Library
* http://code.google.com/p/jquery-stream/
*
* Copyright 2011, Donghwan Kim
* Licensed under the Apache License, Version 2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Compatible with jQuery 1.5+
*/
(function($, undefined) {
var // Stream object instances
instances = {},
// Streaming agents
agents = {},
// HTTP Streaming transports
transports = {},
// Does the throbber of doom exist?
throbber = $.browser.webkit && !$.isReady;
// Once the window is fully loaded, the throbber of doom will not be appearing
if (throbber) {
$(window).load(function() {
throbber = false;
});
}
// Stream is based on The WebSocket API
// W3C Working Draft 19 April 2011 - http://www.w3.org/TR/2011/WD-websockets-20110419/
$.stream = function(url, options) {
// Returns the first Stream in the document
if (!arguments.length) {
for (var i in instances) {
return instances[i];
}
return null;
}
// Stream to which the specified url or alias is mapped
var instance = instances[url];
if (!options) {
return instance || null;
} else if (instance && instance.readyState < 3) {
return instance;
}
var // Stream object
stream = {
// URL to which to connect
url: url,
// Merges options
options: $.stream.setup({}, options),
// The state of stream
// 0: CONNECTING, 1: OPEN, 2: CLOSING, 3: CLOSED
readyState: 0,
// Fake send
send: function() {},
// Fake close
close: function() {}
},
match = /^(http|ws)s?:/.exec(stream.url),
open = function() {
// Delegates open process
agents[stream.options.type](stream);
};
// Stream type
if (match) {
stream.options.type = match[1];
}
// Makes arrays of event handlers
for (var i in {open: 1, message: 1, error: 1, close: 1}) {
stream.options[i] = $.makeArray(stream.options[i]);
}
// The url and alias are a identifier of this instance within the document
instances[stream.url] = stream;
if (stream.options.alias) {
instances[stream.options.alias] = stream;
}
// Deals with the throbber of doom
if (stream.options.type === "ws" || !throbber) {
open();
} else {
switch (stream.options.throbber.type || stream.options.throbber) {
case "lazy":
$(window).load(function() {
setTimeout(open, stream.options.throbber.delay || 50);
});
break;
case "reconnect":
open();
$(window).load(function() {
if (stream.readyState === 0) {
stream.options.open.push(function() {
stream.options.open.pop();
setTimeout(reconnect, 10);
});
} else {
reconnect();
}
function reconnect() {
stream.options.close.push(function() {
stream.options.close.pop();
setTimeout(function() {
$.stream(stream.url, stream.options);
}, stream.options.throbber.delay || 50);
});
var reconn = stream.options.reconnect;
stream.close();
stream.options.reconnect = reconn;
}
});
break;
}
}
return stream;
};
$.extend($.stream, {
version: "1.2",
// Logic borrowed from jQuery.ajaxSetup
setup: function(target, options) {
if (!options) {
options = target;
target = $.extend(true, $.stream.options, options);
} else {
$.extend(true, target, $.stream.options, options);
}
for (var field in {context: 1, url: 1}) {
if (field in options) {
target[field] = options[field];
} else if (field in $.stream.options) {
target[field] = $.stream.options[field];
}
}
return target;
},
options: {
// Stream type
type: window.WebSocket ? "ws" : "http",
// Whether to automatically reconnect when stream closed
reconnect: true,
// Whether to trigger global stream event handlers
global: true,
// Only for WebKit
throbber: "lazy",
// Message data type
dataType: "text",
// Message data converters
converters: {
text: window.String,
json: $.parseJSON,
xml: $.parseXML
}
// Additional parameters for GET request
// openData: null,
// WebSocket constructor argument
// protocols: null,
// XDomainRequest transport
// enableXDR: false,
// rewriteURL: null
// Polling interval
// operaInterval: 0
// iframeInterval: 0
}
});
$.extend(agents, {
// WebSocket wrapper
ws: function(stream) {
if (!window.WebSocket) {
return;
}
var // Absolute WebSocket URL
url = prepareURL(getAbsoluteURL(stream.url).replace(/^http/, "ws"), stream.options.openData),
// WebSocket instance
ws = stream.options.protocols ? new window.WebSocket(url, stream.options.protocols) : new window.WebSocket(url);
// WebSocket event handlers
$.extend(ws, {
onopen: function(event) {
stream.readyState = 1;
trigger(stream, event);
},
onmessage: function(event) {
trigger(stream, $.extend({}, event, {data: stream.options.converters[stream.options.dataType](event.data)}));
},
onerror: function(event) {
stream.options.reconnect = false;
trigger(stream, event);
},
onclose: function(event) {
var readyState = stream.readyState;
stream.readyState = 3;
trigger(stream, event);
// Reconnect?
if (stream.options.reconnect && readyState !== 0) {
$.stream(stream.url, stream.options);
}
}
});
// Overrides send and close
$.extend(stream, {
send: function(data) {
if (stream.readyState === 0) {
$.error("INVALID_STATE_ERR: Stream not open");
}
ws.send(typeof data === "string" ? data : param(data));
},
close: function() {
if (stream.readyState < 2) {
stream.readyState = 2;
stream.options.reconnect = false;
ws.close();
}
}
});
},
// HTTP Streaming
http: function(stream) {
var // Transport
transportFn,
transport,
// Low-level request and response handler
handleOpen,
handleMessage,
handleSend,
// Latch for AJAX
sending,
// Data queue
dataQueue = [],
// Helper object for parsing response
message = {
// The index from which to start parsing
index: 0,
// The temporary data
data: ""
};
// Chooses a proper transport
transportFn = transports[
// xdr
stream.options.enableXDR && window.XDomainRequest ? "xdr" :
// iframe
window.ActiveXObject ? "iframe" :
// xhr
window.XMLHttpRequest ? "xhr" : null];
if (!transportFn) {
return;
}
// Default response handler
handleOpen = stream.options.handleOpen || function(text, message, stream) {
// The top of the response is made up of the id and padding
// optional identifier within the server
stream.id = text.substring(0, text.indexOf(";"));
// message.index = text.indexOf(";", stream.id.length + ";".length) + ";".length;
// tonyg 20120525: If the current chunk doesn't include the end-of-padding marker,
// don't accept the header until it does.
var semiPos = text.indexOf(";", stream.id.length + 1);
if (semiPos < 0) {
return false;
}
message.index = semiPos + 1;
};
handleMessage = stream.options.handleMessage || function(text, message) {
// Response could contain a single message, multiple messages or a fragment of a message
// default message format is message-size ; message-data ;
if (message.size == null) {
// Checks a semicolon of size part
var sizeEnd = text.indexOf(";", message.index);
if (sizeEnd < 0) {
return false;
}
message.size = +text.substring(message.index, sizeEnd);
// index: sizeEnd + ";".length,
message.index = sizeEnd + 1;
}
var data = text.substr(message.index, message.size - message.data.length);
message.data += data;
message.index += data.length;
// Has stream message been completed?
if (message.size !== message.data.length) {
return false;
}
// Checks a semicolon of data part
var dataEnd = text.indexOf(";", message.index);
if (dataEnd < 0) {
return false;
}
// message.index = dataEnd + ";".length;
message.index = dataEnd + 1;
// Completes parsing
delete message.size;
};
// Default request handler
handleSend = stream.options.handleSend || function(type, options, stream) {
var metadata = {"metadata.id": stream.id, "metadata.type": type};
options.data =
// Close
type === "close" ? param(metadata) :
// Send
// converts data if not already a string
((typeof options.data === "string" ? options.data : param(options.data)) + "&" + param(metadata));
};
transport = transportFn(stream, {
response: function(text) {
if (stream.readyState === 0) {
if (handleOpen(text, message, stream) === false) {
return;
}
stream.readyState = 1;
trigger(stream, "open");
}
for (;;) {
if (handleMessage(text, message, stream) === false) {
return;
}
if (stream.readyState < 3) {
// Pseudo MessageEvent
trigger(stream, "message", {
// Converts the data type
data: stream.options.converters[stream.options.dataType](message.data),
origin: "",
lastEventId: "",
source: null,
ports: null
});
}
// Resets the data
message.data = "";
}
},
close: function(isError) {
var readyState = stream.readyState;
stream.readyState = 3;
if (isError) {
// Prevents reconnecting
stream.options.reconnect = false;
// If establishing a connection fails, fires the close event instead of the error event
if (readyState === 0) {
// Pseudo CloseEvent
trigger(stream, "close", {
wasClean: false,
code: null,
reason: ""
});
} else {
trigger(stream, "error");
}
} else {
// Pseudo CloseEvent
trigger(stream, "close", {
// Presumes that the stream closed cleanly
wasClean: true,
code: null,
reason: ""
});
// Reconnect?
if (stream.options.reconnect) {
$.stream(stream.url, stream.options);
}
}
}
}, message);
transport.open();
// Overrides send and close
$.extend(stream, {
send: function(data) {
if (stream.readyState === 0) {
$.error("INVALID_STATE_ERR: Stream not open");
}
// Pushes the data into the queue
dataQueue.push(data);
if (!sending) {
sending = true;
// Performs an Ajax iterating through the data queue
(function post() {
if (stream.readyState === 1 && dataQueue.length) {
var options = {url: stream.url, type: "POST", data: dataQueue.shift()};
if (handleSend("send", options, stream) !== false) {
$.ajax(options).complete(post);
} else {
post();
}
} else {
sending = false;
}
})();
}
},
close: function() {
// Do nothing if the readyState is in the CLOSING or CLOSED
if (stream.readyState < 2) {
stream.readyState = 2;
var options = {url: stream.url, type: "POST"};
if (handleSend("close", options, stream) !== false) {
// Notifies the server
$.ajax(options);
}
// Prevents reconnecting
stream.options.reconnect = false;
transport.close();
}
}
});
}
});
$.extend(transports, {
// XMLHttpRequest: Modern browsers except Internet Explorer
xhr: function(stream, handler, message) {
var stop,
polling,
preStatus,
xhr = new window.XMLHttpRequest();
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
// Handles open and message event
case 3:
if (xhr.status !== 200) {
return;
}
handler.response(xhr.responseText);
// For Opera
if ($.browser.opera && !polling) {
polling = true;
stop = iterate(function() {
if (xhr.readyState === 4) {
return false;
}
if (xhr.responseText.length > message.index) {
handler.response(xhr.responseText);
}
}, stream.options.operaInterval);
}
break;
// Handles error or close event
case 4:
// HTTP status 0 could mean that the request is terminated by abort method
// but it's not error in Stream object
handler.close(xhr.status !== 200 && preStatus !== 200);
break;
}
};
return {
open: function() {
xhr.open("GET", prepareURL(stream.url, stream.options.openData));
xhr.send();
},
close: function() {
if (stop) {
stop();
}
// Saves status
try {
preStatus = xhr.status;
} catch (e) {}
xhr.abort();
}
};
},
// Hidden iframe: Internet Explorer
iframe: function(stream, handler, message) {
var stop,
closed,
onload = function() {
if (!closed) {
closed = true;
handler.close();
}
},
doc = new window.ActiveXObject("htmlfile");
doc.open();
doc.close();
return {
open: function() {
var iframe = doc.createElement("iframe");
iframe.src = prepareURL(stream.url, stream.options.openData);
doc.body.appendChild(iframe);
// For the server to respond in a consistent format regardless of user agent, we polls response text
var cdoc = iframe.contentDocument || iframe.contentWindow.document;
stop = iterate(function() {
if (!cdoc.documentElement) {
return;
}
// Detects connection failure
if (cdoc.readyState === "complete") {
try {
$.noop(cdoc.fileSize);
} catch(e) {
handler.close(true);
return false;
}
}
var response = cdoc.body.lastChild,
readResponse = function() {
// Clones the element not to disturb the original one
var clone = response.cloneNode(true);
// If the last character is a carriage return or a line feed, IE ignores it in the innerText property
// therefore, we add another non-newline character to preserve it
clone.appendChild(cdoc.createTextNode("."));
var text = clone.innerText;
return text.substring(0, text.length - 1);
};
// To support text/html content type
if (!$.nodeName(response, "pre")) {
// Injects a plaintext element which renders text without interpreting the HTML and cannot be stopped
// it is deprecated in HTML5, but still works
var head = cdoc.head || cdoc.getElementsByTagName("head")[0] || cdoc.documentElement,
script = cdoc.createElement("script");
script.text = "document.write('<plaintext>')";
head.insertBefore(script, head.firstChild);
head.removeChild(script);
// The plaintext element will be the response container
response = cdoc.body.lastChild;
}
// Handles open event
handler.response(readResponse());
// Handles message and close event
stop = iterate(function() {
var text = readResponse();
if (text.length > message.index) {
handler.response(text);
// Empties response every time that it is handled
response.innerText = "";
message.index = 0;
}
if (cdoc.readyState === "complete") {
onload();
return false;
}
}, stream.options.iframeInterval);
return false;
});
},
close: function() {
if (stop) {
stop();
}
doc.execCommand("Stop");
onload();
}
};
},
// XDomainRequest: Optionally Internet Explorer 8+
xdr: function(stream, handler) {
var xdr = new window.XDomainRequest(),
rewriteURL = stream.options.rewriteURL || function(url) {
// Maintaining session by rewriting URL
// http://stackoverflow.com/questions/6453779/maintaining-session-by-rewriting-url
var rewriters = {
JSESSIONID: function(sid) {
return url.replace(/;jsessionid=[^\?]*|(\?)|$/, ";jsessionid=" + sid + "$1");
},
PHPSESSID: function(sid) {
return url.replace(/\?PHPSESSID=[^&]*&?|\?|$/, "?PHPSESSID=" + sid + "&").replace(/&$/, "");
}
};
for (var name in rewriters) {
// Finds session id from cookie
var matcher = new RegExp("(?:^|;\\s*)" + encodeURIComponent(name) + "=([^;]*)").exec(document.cookie);
if (matcher) {
return rewriters[name](matcher[1]);
}
}
return url;
};
// Handles open and message event
xdr.onprogress = function() {
handler.response(xdr.responseText);
};
// Handles error event
xdr.onerror = function() {
handler.close(true);
};
// Handles close event
var onload = xdr.onload = function() {
handler.close();
};
return {
open: function() {
xdr.open("GET", prepareURL(rewriteURL(stream.url), stream.options.openData));
xdr.send();
},
close: function() {
xdr.abort();
onload();
}
};
}
});
// Closes all stream when the document is unloaded
// this works right only in IE
$(window).bind("unload.stream", function() {
for (var url in instances) {
instances[url].close();
delete instances[url];
}
});
$.each("streamOpen streamMessage streamError streamClose".split(" "), function(i, o) {
$.fn[o] = function(f) {
return this.bind(o, f);
};
});
// Works even in IE6
function getAbsoluteURL(url) {
var div = document.createElement("div");
div.innerHTML = "<a href='" + url + "'/>";
return div.firstChild.href;
}
function trigger(stream, event, props) {
event = event.type ?
event :
$.extend($.Event(event), {bubbles: false, cancelable: false}, props);
var handlers = stream.options[event.type],
applyArgs = [event, stream];
// Triggers local event handlers
for (var i = 0, length = handlers.length; i < length; i++) {
handlers[i].apply(stream.options.context, applyArgs);
}
if (stream.options.global) {
// Triggers global event handlers
$.event.trigger("stream" + event.type.substring(0, 1).toUpperCase() + event.type.substring(1), applyArgs);
}
}
function prepareURL(url, data) {
// Converts data into a query string
if (data && typeof data !== "string") {
data = param(data);
}
// Attaches a time stamp to prevent caching
var ts = $.now(),
ret = url.replace(/([?&])_=[^&]*/, "$1_=" + ts);
return ret + (ret === url ? (/\?/.test(url) ? "&" : "?") + "_=" + ts : "") + (data ? ("&" + data) : "");
}
function param(data) {
return $.param(data, $.ajaxSettings.traditional);
}
function iterate(fn, interval) {
var timeoutId;
// Though the interval is 0 for real-time application, there is a delay between setTimeout calls
// For detail, see https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting
interval = interval || 0;
(function loop() {
timeoutId = setTimeout(function() {
if (fn() === false) {
return;
}
loop();
}, interval);
})();
return function() {
clearTimeout(timeoutId);
};
}
})(jQuery);