/sub-projects/jquery-stream-jetty/trunk/src/main/webapp/jquery.stream-1.2.js
http://jquery-stream.googlecode.com/ · JavaScript · 763 lines · 511 code · 122 blank · 130 comment · 89 complexity · 572963f882b5a82fa526094bad48516a MD5 · raw file
- /*
- * 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;
- message.index = text.indexOf(";", stream.id.length + 1) + 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);