/* * Portal v1.0.1 * http://github.com/flowersinthesand/portal * * Copyright 2011-2013, Donghwan Kim * Licensed under the Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 */ (function() { "use strict"; var // A global identifier guid, // Is the unload event being processed? unloading, // Portal portal = {}, // Socket instances sockets = {}, // Callback names for JSONP jsonpCallbacks = [], // Core prototypes toString = Object.prototype.toString, hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice; // Convenience utilities // Most utility functions are borrowed from jQuery portal.support = { now: function() { return new Date().getTime(); }, isArray: function(array) { return toString.call(array) === "[object Array]"; }, isBinary: function(data) { var string = toString.call(data); return string === "[object Blob]" || string === "[object ArrayBuffer]"; }, isFunction: function(fn) { return toString.call(fn) === "[object Function]"; }, getAbsoluteURL: function(url) { var div = document.createElement("div"); // Uses an innerHTML property to obtain an absolute URL div.innerHTML = ''; // encodeURI and decodeURI are needed to normalize URL between IE and non-IE, // since IE doesn't encode the href property value and return it - http://jsfiddle.net/Yq9M8/1/ return encodeURI(decodeURI(div.firstChild.href)); }, iterate: function(fn) { var timeoutId; // Though the interval is 1ms 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 (function loop() { timeoutId = setTimeout(function() { if (fn() === false) { return; } loop(); }, 1); })(); return function() { clearTimeout(timeoutId); }; }, each: function(array, callback) { var i; for (i = 0; i < array.length; i++) { callback(i, array[i]); } }, extend: function(target) { var i, options, name; for (i = 1; i < arguments.length; i++) { if ((options = arguments[i]) != null) { for (name in options) { target[name] = options[name]; } } } return target; }, on: function(elem, type, fn) { if (elem.addEventListener) { elem.addEventListener(type, fn, false); } else if (elem.attachEvent) { elem.attachEvent("on" + type, fn); } }, off: function(elem, type, fn) { if (elem.removeEventListener) { elem.removeEventListener(type, fn, false); } else if (elem.detachEvent) { elem.detachEvent("on" + type, fn); } }, param: function(params) { var prefix, s = []; function add(key, value) { value = portal.support.isFunction(value) ? value() : (value == null ? "" : value); s.push(encodeURIComponent(key) + "=" + encodeURIComponent(value)); } function buildParams(prefix, obj) { var name; if (portal.support.isArray(obj)) { portal.support.each(obj, function(i, v) { if (/\[\]$/.test(prefix)) { add(prefix, v); } else { buildParams(prefix + "[" + (typeof v === "object" ? i : "") + "]", v); } }); } else if (obj != null && toString.call(obj) === "[object Object]") { for (name in obj) { buildParams(prefix + "[" + name + "]", obj[name]); } } else { add(prefix, obj); } } for (prefix in params) { buildParams(prefix, params[prefix]); } return s.join("&").replace(/%20/g, "+"); }, xhr: function() { try { return new window.XMLHttpRequest(); } catch (e1) { try { return new window.ActiveXObject("Microsoft.XMLHTTP"); } catch (e2) {} } }, parseJSON: function(data) { return !data ? null : window.JSON && window.JSON.parse ? window.JSON.parse(data) : new Function("return " + data)(); }, // http://github.com/flowersinthesand/stringifyJSON stringifyJSON: function(value) { var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, meta = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"': '\\"', '\\': '\\\\' }; function quote(string) { return '"' + string.replace(escapable, function(a) { var c = meta[a]; return typeof c === "string" ? c : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); }) + '"'; } function f(n) { return n < 10 ? "0" + n : n; } return window.JSON && window.JSON.stringify ? window.JSON.stringify(value) : (function str(key, holder) { var i, v, len, partial, value = holder[key], type = typeof value; if (value && typeof value === "object" && typeof value.toJSON === "function") { value = value.toJSON(key); type = typeof value; } switch (type) { case "string": return quote(value); case "number": return isFinite(value) ? String(value) : "null"; case "boolean": return String(value); case "object": if (!value) { return "null"; } switch (toString.call(value)) { case "[object Date]": return isFinite(value.valueOf()) ? '"' + value.getUTCFullYear() + "-" + f(value.getUTCMonth() + 1) + "-" + f(value.getUTCDate()) + "T" + f(value.getUTCHours()) + ":" + f(value.getUTCMinutes()) + ":" + f(value.getUTCSeconds()) + "Z" + '"' : "null"; case "[object Array]": len = value.length; partial = []; for (i = 0; i < len; i++) { partial.push(str(i, value) || "null"); } return "[" + partial.join(",") + "]"; default: partial = []; for (i in value) { if (hasOwn.call(value, i)) { v = str(i, value); if (v) { partial.push(quote(i) + ":" + v); } } } return "{" + partial.join(",") + "}"; } } })("", {"": value}); }, browser: {}, storage: !!(window.localStorage && window.StorageEvent) }; portal.support.corsable = "withCredentials" in portal.support.xhr(); guid = portal.support.now(); // Browser sniffing (function() { var ua = navigator.userAgent.toLowerCase(), match = /(chrome)[ \/]([\w.]+)/.exec(ua) || /(webkit)[ \/]([\w.]+)/.exec(ua) || /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || /(msie) ([\w.]+)/.exec(ua) || ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || []; portal.support.browser[match[1] || ""] = true; portal.support.browser.version = match[2] || "0"; // The storage event of Internet Explorer and Firefox 3 works strangely if (portal.support.browser.msie || (portal.support.browser.mozilla && portal.support.browser.version.split(".")[0] === "1")) { portal.support.storage = false; } })(); // Finds the socket object which is mapped to the given url portal.find = function(url) { var i; // Returns the first socket in the document if (!arguments.length) { for (i in sockets) { if (sockets[i]) { return sockets[i]; } } return null; } // The url is a identifier of this socket within the document return sockets[portal.support.getAbsoluteURL(url)] || null; }; // Creates a new socket and connects to the given url portal.open = function(url, options) { // Makes url absolute to normalize URL url = portal.support.getAbsoluteURL(url); sockets[url] = socket(url, options); return portal.find(url); }; // Default options portal.defaults = { // Socket options transports: ["ws", "sse", "stream", "longpoll"], timeout: false, heartbeat: false, _heartbeat: 5000, lastEventId: 0, sharing: false, prepare: function(connect) { connect(); }, reconnect: function(lastDelay) { return 2 * (lastDelay || 250); }, idGenerator: function() { // Generates a random UUID // Logic borrowed from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c === "x" ? r : (r & 0x3 | 0x8); return v.toString(16); }); }, urlBuilder: function(url, params, when) { return url + (/\?/.test(url) ? "&" : "?") + "when=" + when + "&" + portal.support.param(params); }, inbound: portal.support.parseJSON, outbound: portal.support.stringifyJSON, // Transport options credentials: false, notifyAbort: false, xdrURL: function(url) { // Maintaining session by rewriting URL // http://stackoverflow.com/questions/6453779/maintaining-session-by-rewriting-url var match = /(?:^|; )(JSESSIONID|PHPSESSID)=([^;]*)/.exec(document.cookie); switch (match && match[1]) { case "JSESSIONID": return url.replace(/;jsessionid=[^\?]*|(\?)|$/, ";jsessionid=" + match[2] + "$1"); case "PHPSESSID": return url.replace(/\?PHPSESSID=[^&]*&?|\?|$/, "?PHPSESSID=" + match[2] + "&").replace(/&$/, ""); default: return false; } }, streamParser: function(chunk) { // Chunks are formatted according to the event stream format // http://www.w3.org/TR/eventsource/#event-stream-interpretation var reol = /\r\n|[\r\n]/g, lines = [], data = this.data("data"), array = [], i = 0, match, line; // Strips off the left padding of the chunk // the first chunk of some streaming transports and every chunk for Android browser 2 and 3 has padding chunk = chunk.replace(/^\s+/g, ""); // String.prototype.split is not reliable cross-browser while (match = reol.exec(chunk)) { lines.push(chunk.substring(i, match.index)); i = match.index + match[0].length; } lines.push(chunk.length === i ? "" : chunk.substring(i)); if (!data) { data = []; this.data("data", data); } // Processes the data field only for (i = 0; i < lines.length; i++) { line = lines[i]; if (!line) { // Finish array.push(data.join("\n")); data = []; this.data("data", data); } else if (/^data:\s/.test(line)) { // A single data field data.push(line.substring("data: ".length)); } else { // A fragment of a data field data[data.length - 1] += line; } } return array; } }; // Callback function function callbacks(deferred) { var list = [], locked, memory, firing, firingStart, firingLength, firingIndex, fire = function(context, args) { args = args || []; memory = !deferred || [context, args]; firing = true; firingIndex = firingStart || 0; firingStart = 0; firingLength = list.length; for (; firingIndex < firingLength; firingIndex++) { list[firingIndex].apply(context, args); } firing = false; }, self = { add: function(fn) { var length = list.length; list.push(fn); if (firing) { firingLength = list.length; } else if (!locked && memory && memory !== true) { firingStart = length; fire(memory[0], memory[1]); } }, remove: function(fn) { var i; for (i = 0; i < list.length; i++) { if (fn === list[i] || (fn.guid && fn.guid === list[i].guid)) { if (firing) { if (i <= firingLength) { firingLength--; if (i <= firingIndex) { firingIndex--; } } } list.splice(i--, 1); } } }, fire: function(context, args) { if (!locked && !firing && !(deferred && memory)) { fire(context, args); } }, lock: function() { locked = true; }, locked: function() { return !!locked; }, unlock: function() { locked = memory = firing = firingStart = firingLength = firingIndex = undefined; } }; return self; } // Socket function function socket(url, options) { var // Final options opts, // Transport transport, // The state of the connection state, // Event helpers events = {}, eventId = 0, // Reply callbacks replyCallbacks = {}, // Buffer buffer = [], // Reconnection reconnectTimer, reconnectDelay, reconnectTry, // Map of the connection-scoped values connection = {}, parts = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/.exec(url.toLowerCase()), // Socket object self = { // Finds the value of an option option: function(key, /* undocumented */ value) { if (value === undefined) { return opts[key]; } opts[key] = value; return this; }, // Gets or sets a connection-scoped value data: function(key, value) { if (value === undefined) { return connection[key]; } connection[key] = value; return this; }, // Returns the state state: function() { return state; }, // Adds event handler on: function(type, fn) { var event; // Handles a map of type and handler if (typeof type === "object") { for (event in type) { self.on(event, type[event]); } return this; } // For custom event event = events[type]; if (!event) { if (events.message.locked()) { return this; } event = events[type] = callbacks(); event.order = events.message.order; } event.add(fn); return this; }, // Removes event handler off: function(type, fn) { var event = events[type]; if (event) { event.remove(fn); } return this; }, // Adds one time event handler one: function(type, fn) { function proxy() { self.off(type, proxy); fn.apply(self, arguments); } fn.guid = fn.guid || guid++; proxy.guid = fn.guid; return self.on(type, proxy); }, // Fires event handlers fire: function(type) { var event = events[type]; if (event) { event.fire(self, slice.call(arguments, 1)); } return this; }, // Establishes a connection open: function() { var type, latch, connect = function() { var candidates, type; if (!latch) { latch = true; candidates = connection.candidates = slice.call(opts.transports); while (!transport && candidates.length) { type = candidates.shift(); connection.transport = type; connection.url = self.buildURL("open"); transport = portal.transports[type](self, opts); } // Increases the number of reconnection attempts if (reconnectTry) { reconnectTry++; } // Fires the connecting event and connects if (transport) { self.fire("connecting"); transport.open(); } else { self.fire("close", "notransport"); } } }, cancel = function() { if (!latch) { latch = true; self.fire("close", "canceled"); } }; // Cancels the scheduled connection if (reconnectTimer) { clearTimeout(reconnectTimer); } // Resets the connection scope and event helpers connection = {}; for (type in events) { events[type].unlock(); } // Chooses transport transport = undefined; // From null or waiting state state = "preparing"; // Check if possible to make use of a shared socket if (opts.sharing) { connection.transport = "session"; transport = portal.transports.session(self, opts); } // Executes the prepare handler if a physical connection is needed if (transport) { connect(); } else { opts.prepare.call(self, connect, cancel, opts); } return this; }, // Sends an event to the server via the connection send: function(type, data, doneCallback, failCallback) { var event; // Defers sending an event until the state become opened if (state !== "opened") { buffer.push(arguments); return this; } // Outbound event event = { id: ++eventId, socket: opts.id, type: type, data: data, reply: !!(doneCallback || failCallback) }; if (event.reply) { // Shared socket needs to know the callback event name // because it fires the callback event directly instead of using reply event if (connection.transport === "session") { event.doneCallback = doneCallback; event.failCallback = failCallback; } else { replyCallbacks[eventId] = {done: doneCallback, fail: failCallback}; } } // Delegates to the transport transport.send(portal.support.isBinary(data) ? data : opts.outbound.call(self, event)); return this; }, // Disconnects the connection close: function() { var script, head; // Prevents reconnection opts.reconnect = false; if (reconnectTimer) { clearTimeout(reconnectTimer); } // Fires the close event immediately for transport which doesn't give feedback on disconnection if (unloading || !transport || !transport.feedback) { self.fire("close", unloading ? "error" : "aborted"); if (opts.notifyAbort && connection.transport !== "session") { head = document.head || document.getElementsByTagName("head")[0] || document.documentElement; script = document.createElement("script"); script.async = false; script.src = self.buildURL("abort"); script.onload = script.onreadystatechange = function() { if (!script.readyState || /loaded|complete/.test(script.readyState)) { script.onload = script.onreadystatechange = null; if (script.parentNode) { script.parentNode.removeChild(script); } } }; head.insertBefore(script, head.firstChild); } } // Delegates to the transport if (transport) { transport.close(); } return this; }, // Broadcasts event to session sockets broadcast: function(type, data) { // TODO rename var broadcastable = connection.broadcastable; if (broadcastable) { broadcastable.broadcast({type: "fire", data: {type: type, data: data}}); } return this; }, // For internal use only // fires events from the server _fire: function(data, isChunk) { var array; if (isChunk) { data = opts.streamParser.call(self, data); while (data.length) { self._fire(data.shift()); } return this; } if (portal.support.isBinary(data)) { array = [{type: "message", data: data}]; } else { array = opts.inbound.call(self, data); array = array == null ? [] : !portal.support.isArray(array) ? [array] : array; } connection.lastEventIds = []; portal.support.each(array, function(i, event) { var latch, args = [event.type, event.data]; opts.lastEventId = event.id; connection.lastEventIds.push(event.id); if (event.reply) { args.push(function(result) { if (!latch) { latch = true; self.send("reply", {id: event.id, data: result}); } }); } self.fire.apply(self, args).fire("_message", args); }); return this; }, // For internal use only // builds an effective URL buildURL: function(when, params) { var p = when === "open" ? { transport: connection.transport, heartbeat: opts.heartbeat, lastEventId: opts.lastEventId } : when === "poll" ? { transport: connection.transport, lastEventIds: connection.lastEventIds && connection.lastEventIds.join(","), /* deprecated */lastEventId: opts.lastEventId } : {}; portal.support.extend(p, {id: opts.id, _: guid++}, opts.params && opts.params[when], params); return opts.urlBuilder.call(self, url, p, when); } }; // Create the final options opts = portal.support.extend({}, portal.defaults, options); if (options) { // Array should not be deep extended if (options.transports) { opts.transports = slice.call(options.transports); } } // Saves original URL opts.url = url; // Generates socket id, opts.id = opts.idGenerator.call(self); opts.crossDomain = !!(parts && // protocol and hostname (parts[1] != location.protocol || parts[2] != location.hostname || // port (parts[3] || (parts[1] === "http:" ? 80 : 443)) != (location.port || (location.protocol === "http:" ? 80 : 443)))); portal.support.each(["connecting", "open", "message", "close", "waiting"], function(i, type) { // Creates event helper events[type] = callbacks(type !== "message"); events[type].order = i; // Shortcuts for on method var old = self[type], on = function(fn) { return self.on(type, fn); }; self[type] = !old ? on : function(fn) { return (portal.support.isFunction(fn) ? on : old).apply(this, arguments); }; }); // Initializes self.on({ connecting: function() { // From preparing state state = "connecting"; var timeoutTimer; // Sets timeout timer function setTimeoutTimer() { timeoutTimer = setTimeout(function() { transport.close(); self.fire("close", "timeout"); }, opts.timeout); } // Clears timeout timer function clearTimeoutTimer() { clearTimeout(timeoutTimer); } // Makes the socket sharable function share() { var traceTimer, server, name = "socket-" + url, servers = { // Powered by the storage event and the localStorage // http://www.w3.org/TR/webstorage/#event-storage storage: function() { if (!portal.support.storage) { return; } var storage = window.localStorage; return { init: function() { function onstorage(event) { // When a deletion, newValue initialized to null if (event.key === name && event.newValue) { listener(event.newValue); } } // Handles the storage event portal.support.on(window, "storage", onstorage); self.one("close", function() { portal.support.off(window, "storage", onstorage); // Defers again to clean the storage self.one("close", function() { storage.removeItem(name); storage.removeItem(name + "-opened"); storage.removeItem(name + "-children"); }); }); }, broadcast: function(obj) { var string = portal.support.stringifyJSON(obj); storage.setItem(name, string); setTimeout(function() { listener(string); }, 50); }, get: function(key) { return portal.support.parseJSON(storage.getItem(name + "-" + key)); }, set: function(key, value) { storage.setItem(name + "-" + key, portal.support.stringifyJSON(value)); } }; }, // Powered by the window.open method // https://developer.mozilla.org/en/DOM/window.open windowref: function() { // Internet Explorer raises an invalid argument error // when calling the window.open method with the name containing non-word characters var neim = name.replace(/\W/g, ""), container = document.getElementById(neim), win; if (!container) { container = document.createElement("div"); container.id = neim; container.style.display = "none"; container.innerHTML = ''; textarea = form.firstChild; textarea.value = data; iframe = form.lastChild; portal.support.on(iframe, "load", function() { document.body.removeChild(form); post(); }); document.body.appendChild(form); form.submit(); }; return { send: function(data) { queue.push(data); if (!sending) { sending = true; post(); } } }; }, // Server-Sent Events sse: function(socket, options) { var es, EventSource = window.EventSource; if (!EventSource) { return; } return portal.support.extend(portal.transports.httpbase(socket, options), { open: function() { var url = socket.data("url"); // Uses proper constructor for Chrome 10-15 es = !options.crossDomain ? new EventSource(url) : new EventSource(url, {withCredentials: options.credentials}); es.onopen = function(event) { socket.data("event", event).fire("open"); }; es.onmessage = function(event) { socket.data("event", event)._fire(event.data); }; es.onerror = function(event) { es.close(); // There is no way to find whether this connection closed normally or not socket.data("event", event).fire("close", "done"); }; }, close: function() { es.close(); } }); }, // Streaming facade stream: function(socket) { socket.data("candidates").unshift("streamxhr", "streamxdr", "streamiframe"); }, // Streaming - XMLHttpRequest streamxhr: function(socket, options) { var xhr; if ((portal.support.browser.msie && +portal.support.browser.version < 10) || (options.crossDomain && !portal.support.corsable)) { return; } return portal.support.extend(portal.transports.httpbase(socket, options), { open: function() { var stop; xhr = portal.support.xhr(); xhr.onreadystatechange = function() { function onprogress() { var index = socket.data("index"), length = xhr.responseText.length; if (!index) { socket.fire("open")._fire(xhr.responseText, true); } else if (length > index) { socket._fire(xhr.responseText.substring(index, length), true); } socket.data("index", length); } if (xhr.readyState === 3 && xhr.status === 200) { // Despite the change in response, Opera doesn't fire the readystatechange event if (portal.support.browser.opera && !stop) { stop = portal.support.iterate(onprogress); } else { onprogress(); } } else if (xhr.readyState === 4) { if (stop) { stop(); } socket.fire("close", xhr.status === 200 ? "done" : "error"); } }; xhr.open("GET", socket.data("url")); if (portal.support.corsable) { xhr.withCredentials = options.credentials; } xhr.send(null); }, close: function() { xhr.abort(); } }); }, // Streaming - Iframe streamiframe: function(socket, options) { var doc, stop, ActiveXObject = window.ActiveXObject; if (!ActiveXObject || options.crossDomain) { return; } else { // IE 10 Metro doesn't support ActiveXObject try { new ActiveXObject("htmlfile"); } catch (e) { return; } } return portal.support.extend(portal.transports.httpbase(socket, options), { open: function() { var iframe, cdoc; doc = new ActiveXObject("htmlfile"); doc.open(); doc.close(); iframe = doc.createElement("iframe"); iframe.src = socket.data("url"); doc.body.appendChild(iframe); cdoc = iframe.contentDocument || iframe.contentWindow.document; stop = portal.support.iterate(function() { // Response container var container; function readDirty() { var clone = container.cloneNode(true), text; // Adds a character not CR and LF to circumvent an Internet Explorer bug // If the contents of an element ends with one or more CR or LF, Internet Explorer ignores them in the innerText property clone.appendChild(cdoc.createTextNode(".")); text = clone.innerText; return text.substring(0, text.length - 1); } // Waits the server's container ignorantly if (!cdoc.firstChild) { return; } container = cdoc.body.lastChild; // Detects connection failure if (!container) { socket.fire("close", "error"); return false; } socket.fire("open")._fire(readDirty(), true); container.innerText = ""; stop = portal.support.iterate(function() { var text = readDirty(); if (text) { container.innerText = ""; socket._fire(text, true); } if (cdoc.readyState === "complete") { socket.fire("close", "done"); return false; } }); return false; }); }, close: function() { stop(); doc.execCommand("Stop"); } }); }, // Streaming - XDomainRequest streamxdr: function(socket, options) { var xdr, XDomainRequest = window.XDomainRequest; if (!XDomainRequest || !options.xdrURL || !options.xdrURL.call(socket, "t")) { return; } return portal.support.extend(portal.transports.httpbase(socket, options), { open: function() { var url = options.xdrURL.call(socket, socket.data("url")); socket.data("url", url); xdr = new XDomainRequest(); xdr.onprogress = function() { var index = socket.data("index"), length = xdr.responseText.length; if (!index) { socket.fire("open")._fire(xdr.responseText, true); } else { socket._fire(xdr.responseText.substring(index, length), true); } socket.data("index", length); }; xdr.onerror = function() { socket.fire("close", "error"); }; xdr.onload = function() { socket.fire("close", "done"); }; xdr.open("GET", url); xdr.send(); }, close: function() { xdr.abort(); } }); }, // Long polling facade longpoll: function(socket) { socket.data("candidates").unshift("longpollajax", "longpollxdr", "longpolljsonp"); }, // Long polling - AJAX longpollajax: function(socket, options) { var xhr, aborted, // deprecated count = 0; if (options.crossDomain && !portal.support.corsable) { return; } return portal.support.extend(portal.transports.httpbase(socket, options), { open: function() { function poll() { var url = socket.buildURL(!count ? "open" : "poll", {count: ++count}); socket.data("url", url); xhr = portal.support.xhr(); xhr.onreadystatechange = function() { var data; // Avoids c00c023f error on Internet Explorer 9 if (!aborted && xhr.readyState === 4) { if (xhr.status === 200) { data = xhr.responseText; if (data || count === 1) { if (count === 1) { socket.fire("open"); } if (data) { socket._fire(data); } poll(); } else { socket.fire("close", "done"); } } else { socket.fire("close", "error"); } } }; xhr.open("GET", url); if (portal.support.corsable) { xhr.withCredentials = options.credentials; } xhr.send(null); } poll(); }, close: function() { aborted = true; xhr.abort(); } }); }, // Long polling - XDomainRequest longpollxdr: function(socket, options) { var xdr, // deprecated count = 0, XDomainRequest = window.XDomainRequest; if (!XDomainRequest || !options.xdrURL || !options.xdrURL.call(socket, "t")) { return; } return portal.support.extend(portal.transports.httpbase(socket, options), { open: function() { function poll() { var url = options.xdrURL.call(socket, socket.buildURL(!count ? "open" : "poll", {count: ++count})); socket.data("url", url); xdr = new XDomainRequest(); xdr.onload = function() { var data = xdr.responseText; if (data || count === 1) { if (count === 1) { socket.fire("open"); } if (data) { socket._fire(data); } poll(); } else { socket.fire("close", "done"); } }; xdr.onerror = function() { socket.fire("close", "error"); }; xdr.open("GET", url); xdr.send(); } poll(); }, close: function() { xdr.abort(); } }); }, // Long polling - JSONP longpolljsonp: function(socket, options) { var script, called, // deprecated count = 0, callback = jsonpCallbacks.pop() || ("socket_" + (++guid)); return portal.support.extend(portal.transports.httpbase(socket, options), { open: function() { function poll() { var url = socket.buildURL(!count ? "open" : "poll", {callback: callback, count: ++count}), head = document.head || document.getElementsByTagName("head")[0] || document.documentElement; socket.data("url", url); script = document.createElement("script"); script.async = true; script.src = url; script.clean = function() { script.clean = script.onerror = script.onload = script.onreadystatechange = null; if (script.parentNode) { script.parentNode.removeChild(script); } }; script.onload = script.onreadystatechange = function() { if (!script.readyState || /loaded|complete/.test(script.readyState)) { script.clean(); if (called) { called = false; poll(); } else if (count === 1) { socket.fire("open"); poll(); } else { socket.fire("close", "done"); } } }; script.onerror = function() { script.clean(); socket.fire("close", "error"); }; head.insertBefore(script, head.firstChild); } // Attaches callback window[callback] = function(data) { called = true; if (count === 1) { socket.fire("open"); } socket._fire(data); }; socket.one("close", function() { // Assings an empty function for browsers which are not able to cancel a request made from script tag window[callback] = function() {}; jsonpCallbacks.push(callback); }); poll(); }, close: function() { if (script.clean) { script.clean(); } } }); } }; // Closes all sockets portal.finalize = function() { var url, socket; for (url in sockets) { socket = sockets[url]; if (socket.state() !== "closed") { socket.close(); } // To run the test suite delete sockets[url]; } }; portal.support.on(window, "unload", function() { // Check the unload event is fired by the browser unloading = true; // Closes all sockets when the document is unloaded portal.finalize(); }); portal.support.on(window, "online", function() { var url, socket; for (url in sockets) { socket = sockets[url]; // There is no reason to wait if (socket.state() === "waiting") { socket.open(); } } }); portal.support.on(window, "offline", function() { var url, socket; for (url in sockets) { socket = sockets[url]; // Closes sockets which cannot detect disconnection manually if (socket.state() === "opened") { socket.fire("close", "error"); } } }); // Exposes portal to the global object window.portal = portal; })(); /* jshint noarg:true, noempty:true, eqeqeq:true, evil:true, laxbreak:true, undef:true, browser:true, jquery:true, indent:4, maxerr:50 */