/* * Portal v1.1.1 * http://flowersinthesand.github.io/portal/ * * Copyright 2011-2014, Donghwan Kim * Licensed under the Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 */ // Implement the Universal Module Definition (UMD) pattern // see https://github.com/umdjs/umd/blob/master/returnExports.js (function(root, factory) { if (typeof define === "function" && define.amd) { // AMD define(function() { return factory(root); }); } else if (typeof exports === "object") { // Node module.exports = factory((function() { // Prepare the window powered by jsdom var window = require("jsdom").jsdom().createWindow(); window.WebSocket = require("ws"); window.EventSource = require("eventsource"); return window; })()); // node-XMLHttpRequest 1.x conforms XMLHttpRequest Level 1 but can perform a cross-domain request module.exports.support.corsable = true; } else { // Browser globals, Window root.portal = factory(root); } }(this, function(window) { // Enables ECMAScript 5′s strict mode "use strict"; var // A global identifier guid, // Is the unload event being processed? unloading, // Portal portal, // Convenience utilities support, // Default options defaults, // Transports transports, // Socket instances sockets = {}, // Callback names for JSONP jsonpCallbacks = [], // Core prototypes toString = Object.prototype.toString, hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice, // Regard for Node since these are not defined document = window.document, location = window.location; // Callback function function callbacks(deferred) { var locked, memory, firing, firingStart, firingLength, firingIndex, list = [], fire = function(context, args) { args = args || []; memory = !deferred || [context, args]; firing = true; firingIndex = firingStart || 0; firingStart = 0; firingLength = list.length; for (; firingIndex < firingLength && !locked; 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, // Reconnection reconnectTimer, reconnectDelay, reconnectTry, // Event helpers events = {}, eventId = 0, // Reply callbacks replyCallbacks = {}, // Buffer buffer = [], // 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) { return opts[key]; }, // 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 = 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 = 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(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 (support.isBinary(data)) { array = [{type: "message", data: data}]; } else { array = opts.inbound.call(self, data); array = array == null ? [] : !support.isArray(array) ? [array] : array; } connection.lastEventIds = []; 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 } : {}; 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 = support.extend({}, 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)))); 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 (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() { // The storage event of Internet Explorer works strangely // TODO test Internet Explorer 11 if (support.browser.msie) { 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 support.on(window, "storage", onstorage); self.one("close", function() { 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 = support.stringifyJSON(obj); storage.setItem(name, string); setTimeout(function() { listener(string); }, 50); }, get: function(key) { return support.parseJSON(storage.getItem(name + "-" + key)); }, set: function(key, value) { storage.setItem(name + "-" + key, 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; 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 support.extend(transports.httpbase(socket, options), { open: function() { var url = socket.data("url"); es = 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 ((support.browser.msie && +support.browser.version.split(".")[0] < 10) || (options.crossDomain && !support.corsable)) { return; } return support.extend(transports.httpbase(socket, options), { open: function() { xhr = 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) { onprogress(); } else if (xhr.readyState === 4) { socket.fire("close", xhr.status === 200 ? "done" : "error"); } }; xhr.open("GET", socket.data("url")); if (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 { // Internet Explorer 10 Metro doesn't support ActiveXObject try { new ActiveXObject("htmlfile"); } catch (e) { return; } } return support.extend(transports.httpbase(socket, options), { open: function() { var iframe, cdoc; function iterate(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); }; } 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 = iterate(function() { // Response container var container; function readDirty() { var text, clone = container.cloneNode(true); // 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 = 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 support.extend(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 && !support.corsable) { return; } return support.extend(transports.httpbase(socket, options), { open: function() { function poll() { var url = socket.buildURL(!count ? "open" : "poll", {count: ++count}); socket.data("url", url); xhr = 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); } // Do not poll again if the connection has been aborted in open event if (!aborted) { poll(); } } else { socket.fire("close", "done"); } } else { socket.fire("close", "error"); } } }; xhr.open("GET", url); if (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, aborted, // deprecated count = 0, XDomainRequest = window.XDomainRequest; if (!XDomainRequest || !options.xdrURL || !options.xdrURL.call(socket, "t")) { return; } return support.extend(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); } // Do not poll again if the connection has been aborted in open event if (!aborted) { poll(); } } else { socket.fire("close", "done"); } }; xdr.onerror = function() { socket.fire("close", "error"); }; xdr.open("GET", url); xdr.send(); } poll(); }, close: function() { aborted = true; xdr.abort(); } }); }, // Long polling - JSONP longpolljsonp: function(socket, options) { var script, called, aborted, // deprecated count = 0, callback = jsonpCallbacks.pop() || ("socket_" + (++guid)); return support.extend(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() { // Assigning null to src attribute works in IE 6 and 7 script.clean = script.src = 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; if (!aborted) { poll(); } } else if (count === 1) { socket.fire("open"); // Do not poll again if the connection has been aborted in open event if (!aborted) { 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() { aborted = true; if (script.clean) { script.clean(); } } }); } }; return portal; }));