/files/portal/1.0/portal.js
JavaScript | 1786 lines | 1749 code | 16 blank | 21 comment | 23 complexity | ffd035b7d59b80e98325f91786f94e84 MD5 | raw file
- /*
- * Portal v1.0
- * 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 = '<a href="' + url + '"/>';
-
- // 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 (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,
- 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;
- },
-
- // Undocumented
- _heartbeat: 5000,
- longpollTest: true
- // method: null
- // initIframe: null
- };
-
- // 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 = '<iframe name="' + neim + '" />';
- document.body.appendChild(container);
- }
-
- win = container.firstChild.contentWindow;
-
- return {
- init: function() {
- // Callbacks from different windows
- win.callbacks = [listener];
- // In IE 8 and less, only string argument can be safely passed to the function in other window
- win.fire = function(string) {
- var i;
-
- for (i = 0; i < win.callbacks.length; i++) {
- win.callbacks[i](string);
- }
- };
- },
- broadcast: function(obj) {
- if (!win.closed && win.fire) {
- win.fire(portal.support.stringifyJSON(obj));
- }
- },
- get: function(key) {
- return !win.closed ? win[key] : null;
- },
- set: function(key, value) {
- if (!win.closed) {
- win[key] = value;
- }
- }
- };
- }
- };
-
- // Receives send and close command from the children
- function listener(string) {
- var command = portal.support.parseJSON(string), data = command.data;
-
- if (!command.target) {
- if (command.type === "fire") {
- self.fire(data.type, data.data);
- }
- } else if (command.target === "p") {
- switch (command.type) {
- case "send":
- self.send(data.type, data.data, data.doneCallback, data.failCallback);
- break;
- case "close":
- self.close();
- break;
- }
- }
- }
-
- function propagateMessageEvent(args) {
- server.broadcast({target: "c", type: "message", data: args});
- }
-
- function leaveTrace() {
- document.cookie = encodeURIComponent(name) + "=" +
- // Opera 12.00's parseFloat and JSON.stringify causes a strange bug with a number larger than 10 digit
- // JSON.stringify(parseFloat(10000000000) + 1).length === 11;
- // JSON.stringify(parseFloat(10000000000 + 1)).length === 10;
- encodeURIComponent(portal.support.stringifyJSON({ts: portal.support.now() + 1, heir: (server.get("children") || [])[0]}));
- }
-
- // Chooses a server
- server = servers.storage() || servers.windowref();
- server.init();
-
- // For broadcast method
- connection.broadcastable = server;
-
- // List of children sockets
- server.set("children", []);
- // Flag indicating the parent socket is opened
- server.set("opened", false);
-
- // Leaves traces
- leaveTrace();
- traceTimer = setInterval(leaveTrace, 1000);
-
- self.on("_message", propagateMessageEvent)
- .one("open", function() {
- server.set("opened", true);
- server.broadcast({target: "c", type: "open"});
- })
- .one("close", function(reason) {
- // Clears trace timer
- clearInterval(traceTimer);
- // Removes the trace
- document.cookie = encodeURIComponent(name) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
- // The heir is the parent unless unloading
- server.broadcast({target: "c", type: "close", data: {reason: reason, heir: !unloading ? opts.id : (server.get("children") || [])[0]}});
- self.off("_message", propagateMessageEvent);
- });
- }
-
- if (opts.timeout > 0) {
- setTimeoutTimer();
- self.one("open", clearTimeoutTimer).one("close", clearTimeoutTimer);
- }
-
- // Share the socket if possible
- if (opts.sharing && connection.transport !== "session") {
- share();
- }
- },
- open: function() {
- // From connecting state
- state = "opened";
-
- var heartbeatTimer;
-
- // Sets heartbeat timer
- function setHeartbeatTimer() {
- heartbeatTimer = setTimeout(function() {
- self.send("heartbeat").one("heartbeat", function() {
- clearHeartbeatTimer();
- setHeartbeatTimer();
- });
-
- heartbeatTimer = setTimeout(function() {
- transport.close();
- self.fire("close", "error");
- }, opts._heartbeat);
- }, opts.heartbeat - opts._heartbeat);
- }
-
- // Clears heartbeat timer
- function clearHeartbeatTimer() {
- clearTimeout(heartbeatTimer);
- }
-
- if (opts.heartbeat > opts._heartbeat) {
- setHeartbeatTimer();
- self.one("close", clearHeartbeatTimer);
- }
-
- // Locks the connecting event
- events.connecting.lock();
-
- // Initializes variables related with reconnection
- reconnectTimer = reconnectDelay = reconnectTry = null;
-
- // Flushes buffer
- while (buffer.length) {
- self.send.apply(self, buffer.shift());
- }
- },
- close: function() {
- // From preparing, connecting, or opened state
- state = "closed";
-
- var type, event, order = events.close.order;
-
- // Locks event whose order is lower than close event
- for (type in events) {
- event = events[type];
- if (event.order < order) {
- event.lock();
- }
- }
-
- // Schedules reconnection
- if (opts.reconnect) {
- self.one("close", function() {
- reconnectTry = reconnectTry || 1;
- reconnectDelay = opts.reconnect.call(self, reconnectDelay, reconnectTry);
-
- if (reconnectDelay !== false) {
- reconnectTimer = setTimeout(function() {
- self.open();
- }, reconnectDelay);
- self.fire("waiting", reconnectDelay, reconnectTry);
- }
- });
- }
- },
- waiting: function() {
- // From closed state
- state = "waiting";
- },
- reply: function(reply) {
- var fn,
- id = reply.id,
- data = reply.data,
- exception = reply.exception,
- callback = replyCallbacks[id];
-
- if (callback) {
- fn = exception ? callback.fail : callback.done;
- if (fn) {
- if (portal.support.isFunction(fn)) {
- fn.call(self, data);
- } else {
- self.fire(fn, data).fire("_message", [fn, data]);
- }
-
- delete replyCallbacks[id];
- }
- }
- }
- });
-
- return self.open();
- }
-
- // Transports
- portal.transports = {
- // Session socket
- session: function(socket, options) {
- var trace,
- orphan,
- connector,
- name = "socket-" + options.url,
- connectors = {
- storage: function() {
- if (!portal.support.storage) {
- return;
- }
-
- var storage = window.localStorage;
-
- function get(key) {
- return portal.support.parseJSON(storage.getItem(name + "-" + key));
- }
-
- function set(key, value) {
- storage.setItem(name + "-" + key, portal.support.stringifyJSON(value));
- }
-
- return {
- init: function() {
- function onstorage(event) {
- if (event.key === name && event.newValue) {
- listener(event.newValue);
- }
- }
-
- set("children", get("children").concat([options.id]));
- portal.support.on(window, "storage", onstorage);
-
- socket.one("close", function() {
- var children = get("children");
-
- portal.support.off(window, "storage", onstorage);
- if (children) {
- if (removeFromArray(children, options.id)) {
- set("children", children);
- }
- }
- });
-
- return get("opened");
- },
- broadcast: function(obj) {
- var string = portal.support.stringifyJSON(obj);
-
- storage.setItem(name, string);
- setTimeout(function() {
- listener(string);
- }, 50);
- }
- };
- },
- windowref: function() {
- var win = window.open("", name.replace(/\W/g, ""));
-
- if (!win || win.closed || !win.callbacks) {
- return;
- }
-
- return {
- init: function() {
- win.callbacks.push(listener);
- win.children.push(options.id);
-
- socket.one("close", function() {
- // Removes traces only if the parent is alive
- if (!orphan) {
- removeFromArray(win.callbacks, listener);
- removeFromArray(win.children, options.id);
- }
- });
-
- return win.opened;
- },
- broadcast: function(obj) {
- if (!win.closed && win.fire) {
- win.fire(portal.support.stringifyJSON(obj));
- }
- }
- };
- }
- };
-
- function removeFromArray(array, val) {
- var i,
- length = array.length;
-
- for (i = 0; i < length; i++) {
- if (array[i] === val) {
- array.splice(i, 1);
- }
- }
-
- return length !== array.length;
- }
-
- // Receives open, close and message command from the parent
- function listener(string) {
- var command = portal.support.parseJSON(string), data = command.data;
-
- if (!command.target) {
- if (command.type === "fire") {
- socket.fire(data.type, data.data);
- }
- } else if (command.target === "c") {
- switch (command.type) {
- case "open":
- socket.fire("open");
- break;
- case "close":
- if (!orphan) {
- orphan = true;
- if (data.reason === "aborted") {
- socket.close();
- } else {
- // Gives the heir some time to reconnect
- if (data.heir === options.id) {
- socket.fire("close", data.reason);
- } else {
- setTimeout(function() {
- socket.fire("close", data.reason);
- }, 100);
- }
- }
- }
- break;
- case "message":
- // When using the session transport, message events could be sent before the open event
- if (socket.state() === "connecting") {
- socket.one("open", function() {
- socket.fire.apply(socket, data);
- });
- } else {
- socket.fire.apply(socket, data);
- }
- break;
- }
- }
- }
-
- function findTrace() {
- var matcher = new RegExp("(?:^|; )(" + encodeURIComponent(name) + ")=([^;]*)").exec(document.cookie);
- if (matcher) {
- return portal.support.parseJSON(decodeURIComponent(matcher[2]));
- }
- }
-
- // Finds and validates the parent socket's trace from the cookie
- trace = findTrace();
- if (!trace || portal.support.now() - trace.ts > 1000) {
- return;
- }
-
- // Chooses a connector
- connector = connectors.storage() || connectors.windowref();
- if (!connector) {
- return;
- }
-
- // For broadcast method
- socket.data("broadcastable", connector);
-
- return {
- open: function() {
- var traceTimer,
- parentOpened,
- timeout = options.timeout,
- heartbeat = options.heartbeat,
- outbound = options.outbound;
-
- // Prevents side effects
- options.timeout = options.heartbeat = false;
- options.outbound = function(arg) {
- return arg;
- };
-
- // Checks the shared one is alive
- traceTimer = setInterval(function() {
- var oldTrace = trace;
-
- trace = findTrace();
- if (!trace || oldTrace.ts === trace.ts) {
- // Simulates a close signal
- listener(portal.support.stringifyJSON({target: "c", type: "close", data: {reason: "error", heir: oldTrace.heir}}));
- }
- }, 1000);
-
- // Restores options
- socket.one("close", function() {
- clearInterval(traceTimer);
- options.timeout = timeout;
- options.heartbeat = heartbeat;
- options.outbound = outbound;
- });
-
- parentOpened = connector.init();
- if (parentOpened) {
- // Gives the user the opportunity to bind connecting event handlers
- setTimeout(function() {
- socket.fire("open");
- }, 50);
- }
- },
- send: function(event) {
- connector.broadcast({target: "p", type: "send", data: event});
- },
- close: function() {
- // Do not signal the parent if this method is executed by the unload event handler
- if (!unloading) {
- connector.broadcast({target: "p", type: "close"});
- }
- }
- };
- },
- // WebSocket
- ws: function(socket) {
- var ws,
- aborted,
- WebSocket = window.WebSocket || window.MozWebSocket;
-
- if (!WebSocket) {
- return;
- }
-
- return {
- feedback: true,
- open: function() {
- // Makes an absolute url whose scheme is ws or wss
- var url = portal.support.getAbsoluteURL(socket.data("url")).replace(/^http/, "ws");
-
- socket.data("url", url);
-
- ws = new WebSocket(url);
- ws.onopen = function(event) {
- socket.data("event", event).fire("open");
- };
- ws.onmessage = function(event) {
- socket.data("event", event)._fire(event.data);
- };
- ws.onerror = function(event) {
- socket.data("event", event).fire("close", aborted ? "aborted" : "error");
- };
- ws.onclose = function(event) {
- socket.data("event", event).fire("close", aborted ? "aborted" : event.wasClean ? "done" : "error");
- };
- },
- send: function(data) {
- ws.send(data);
- },
- close: function() {
- aborted = true;
- ws.close();
- }
- };
- },
- // HTTP Support
- httpbase: function(socket, options) {
- var send,
- sending,
- queue = [];
-
- function post() {
- if (queue.length) {
- send(options.url, queue.shift());
- } else {
- sending = false;
- }
- }
-
- // The Content-Type is not application/x-www-form-urlencoded but text/plain on account of XDomainRequest
- // See the fourth at http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx
- send = !options.crossDomain || portal.support.corsable ?
- function(url, data) {
- var xhr = portal.support.xhr();
-
- xhr.onreadystatechange = function() {
- if (xhr.readyState === 4) {
- post();
- }
- };
-
- xhr.open("POST", url);
- xhr.setRequestHeader("Content-Type", "text/plain; charset=UTF-8");
- if (portal.support.corsable) {
- xhr.withCredentials = options.credentials;
- }
-
- xhr.send("data=" + data);
- } : window.XDomainRequest && options.xdrURL && options.xdrURL.call(socket, "t") ?
- function(url, data) {
- var xdr = new window.XDomainRequest();
-
- xdr.onload = xdr.onerror = post;
- xdr.open("POST", options.xdrURL.call(socket, url));
- xdr.send("data=" + data);
- } :
- function(url, data) {
- var iframe,
- textarea,
- form = document.createElement("form");
-
- form.action = url;
- form.target = "socket-" + (++guid);
- form.method = "POST";
- // IE 6 needs encoding property
- form.enctype = form.encoding = "text/plain";
- form.acceptCharset = "UTF-8";
- form.style.display = "none";
- form.innerHTML = '<textarea name="data"></textarea><iframe name="' + form.target + '"></iframe>';
-
- 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;
- } else if (options.crossDomain) {
- try {
- if (!portal.support.corsable || !("withCredentials" in new EventSource("about:blank"))) {
- return;
- }
- } catch(e) {
- 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(options.method || "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;
- }
-
- if (options.initIframe) {
- options.initIframe.call(socket, iframe);
- }
-
- 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(options.method || "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(options.method || "GET", url);
- if (portal.support.corsable) {
- xhr.withCredentials = options.credentials;
- }
-
- xhr.send(null);
- }
-
- if (!options.longpollTest) {
- // Skips the test that checks the server's status
- setTimeout(function() {
- socket.fire("open");
- poll();
- }, 50);
- } else {
- 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(options.method || "GET", url);
- xdr.send();
- }
-
- if (!options.longpollTest) {
- setTimeout(function() {
- socket.fire("open")