/** * Copyright 2012 Jeanfrancois Arcand * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * IE streaming/XDR supports is copied/highly inspired by http://code.google.com/p/jquery-stream/ * * Copyright 2011, Donghwan Kim * Licensed under the Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 * * LocalStorage supports is copied/highly inspired by https://github.com/flowersinthesand/jquery-socket * Copyright 2011, Donghwan Kim * Licensed under the Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 * */ /** * Official documentation of this library: https://github.com/Atmosphere/atmosphere/wiki/jQuery.atmosphere.js-API */ jQuery.atmosphere = function() { jQuery(window).bind("unload.atmosphere", function() { jQuery.atmosphere.unsubscribe(); }); // Prevent ESC to kill the connection from Firefox. jQuery(window).keypress(function(e){ if(e.keyCode == 27){ e.preventDefault(); } }); var parseHeaders = function(headerString) { var match, rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, headers = {}; while (match = rheaders.exec(headerString)) { headers[match[1]] = match[2]; } return headers; }; return { version : "1.0.13", requests : [], callbacks : [], onError : function(response) { }, onClose : function(response) { }, onOpen : function(response) { }, onMessage : function(response) { }, onReconnect : function(request, response) { }, onMessagePublished : function(response) { }, onTransportFailure : function (reason, request) { }, onLocalMessage : function (response) { }, AtmosphereRequest : function(options) { /** * {Object} Request parameters. * @private */ var _request = { timeout: 300000, method: 'GET', headers: {}, contentType : '', callback: null, url : '', data : '', suspend : true, maxRequest : -1, reconnect : true, maxStreamingLength : 10000000, lastIndex : 0, logLevel : 'info', requestCount : 0, fallbackMethod: 'GET', fallbackTransport : 'streaming', transport : 'long-polling', webSocketImpl: null, webSocketUrl: null, webSocketPathDelimiter: "@@", enableXDR : false, rewriteURL : false, attachHeadersAsQueryString : true, executeCallbackBeforeReconnect : false, readyState : 0, lastTimestamp : 0, withCredentials : false, trackMessageLength : false , messageDelimiter : '|', connectTimeout : -1, reconnectInterval : 0, dropAtmosphereHeaders : true, uuid : 0, shared : false, readResponsesHeaders : true, maxReconnectOnClose: 5, enableProtocol: false, onError : function(response) { }, onClose : function(response) { }, onOpen : function(response) { }, onMessage : function(response) { }, onReconnect : function(request, response) { }, onMessagePublished : function(response) { }, onTransportFailure : function (reason, request) { }, onLocalMessage : function (request) { } }; /** * {Object} Request's last response. * @private */ var _response = { status: 200, reasonPhrase : "OK", responseBody : '', messages : [], headers : [], state : "messageReceived", transport : "polling", error: null, request : null, partialMessage : "", errorHandled: false, id : 0 }; /** * {websocket} Opened web socket. * * @private */ var _websocket = null; /** * {SSE} Opened SSE. * * @private */ var _sse = null; /** * {XMLHttpRequest, ActiveXObject} Opened ajax request (in case of * http-streaming or long-polling) * * @private */ var _activeRequest = null; /** * {Object} Object use for streaming with IE. * * @private */ var _ieStream = null; /** * {Object} Object use for jsonp transport. * * @private */ var _jqxhr = null; /** * {boolean} If request has been subscribed or not. * * @private */ var _subscribed = true; /** * {number} Number of test reconnection. * * @private */ var _requestCount = 0; /** * {boolean} If request is currently aborded. * * @private */ var _abordingConnection = false; /** * A local "channel' of communication. * @private */ var _localSocketF = null; /** * The storage used. * @private */ var _storageService; /** * Local communication * @private */ var _localStorageService = null; /** * A Unique ID * @private */ var guid = jQuery.now(); /** Trace time */ var _traceTimer; // Automatic call to subscribe _subscribe(options); /** * Initialize atmosphere request object. * * @private */ function _init() { _subscribed = true; _abordingConnection = false; _requestCount = 0; _websocket = null; _sse = null; _activeRequest = null; _ieStream = null; } /** * Re-initialize atmosphere object. * @private */ function _reinit() { _clearState(); _init(); } /** * Subscribe request using request transport.
* If request is currently opened, this one will be closed. * * @param {Object} * Request parameters. * @private */ function _subscribe(options) { _reinit(); _request = jQuery.extend(_request, options); // Allow at least 1 request _request.mrequest = _request.reconnect; if (!_request.reconnect) { _request.reconnect = true; } } /** * Check if web socket is supported (check for custom implementation * provided by request object or browser implementation). * * @returns {boolean} True if web socket is supported, false * otherwise. * @private */ function _supportWebsocket() { return _request.webSocketImpl != null || window.WebSocket || window.MozWebSocket; } /** * Check if server side events (SSE) is supported (check for custom implementation * provided by request object or browser implementation). * * @returns {boolean} True if web socket is supported, false * otherwise. * @private */ function _supportSSE() { return window.EventSource; } /** * Open request using request transport.
* If request transport is 'websocket' but websocket can't be * opened, request will automatically reconnect using fallback * transport. * * @private */ function _execute() { // Shared across multiple tabs/windows. if (_request.shared) { _localStorageService = _local(_request); if (_localStorageService != null) { if (_request.logLevel == 'debug') { jQuery.atmosphere.debug("Storage service available. All communication will be local"); } if (_localStorageService.open(_request)) { // Local connection. return; } } if (_request.logLevel == 'debug') { jQuery.atmosphere.debug("No Storage service available."); } // Wasn't local or an error occurred _localStorageService = null; } // Protocol _request.firstMessage= true; _request.ctime = jQuery.now(); if (_request.transport != 'websocket' && _request.transport != 'sse') { _executeRequest(); } else if (_request.transport == 'websocket') { if (!_supportWebsocket()) { _reconnectWithFallbackTransport("Websocket is not supported, using request.fallbackTransport (" + _request.fallbackTransport + ")"); } else { _executeWebSocket(false); } } else if (_request.transport == 'sse') { if (!_supportSSE()) { _reconnectWithFallbackTransport("Server Side Events(SSE) is not supported, using request.fallbackTransport (" + _request.fallbackTransport + ")"); } else { _executeSSE(false); } } } function _local(request) { var trace, connector, orphan, name = "atmosphere-" + request.url, connectors = { storage: function() { if (!jQuery.atmosphere.supportStorage()) { return; } var storage = window.localStorage, get = function(key) { return jQuery.parseJSON(storage.getItem(name + "-" + key)); }, set = function(key, value) { storage.setItem(name + "-" + key, jQuery.stringifyJSON(value)); }; return { init: function() { set("children", get("children").concat([guid])); jQuery(window).on("storage.socket", function(event) { event = event.originalEvent; if (event.key === name && event.newValue) { listener(event.newValue); } }); return get("opened"); }, signal: function(type, data) { storage.setItem(name, jQuery.stringifyJSON({target: "p", type: type, data: data})); }, close: function() { var index, children = get("children"); jQuery(window).off("storage.socket"); if (children) { index = jQuery.inArray(request.id, children); if (index > -1) { children.splice(index, 1); set("children", children); } } } }; }, 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(guid); return win.opened; }, signal: function(type, data) { if (!win.closed && win.fire) { win.fire(jQuery.stringifyJSON({target: "p", type: type, data: data})); } }, close : function() { function remove(array, e) { var index = jQuery.inArray(e, array); if (index > -1) { array.splice(index, 1); } } // Removes traces only if the parent is alive if (!orphan) { remove(win.callbacks, listener); remove(win.children, guid); } } }; } }; // Receives open, close and message command from the parent function listener(string) { var command = jQuery.parseJSON(string), data = command.data; if (command.target === "c") { switch (command.type) { case "open": _open("opening", 'local', _request) break; case "close": if (!orphan) { orphan = true; if (data.reason === "aborted") { _close(); } else { // Gives the heir some time to reconnect if (data.heir === guid) { _execute(); } else { setTimeout(function() { _execute(); }, 100); } } } break; case "message": _prepareCallback(data, "messageReceived", 200, request.transport); break; case "localMessage": _localMessage(data); break; } } } function findTrace() { var matcher = new RegExp("(?:^|; )(" + encodeURIComponent(name) + ")=([^;]*)").exec(document.cookie); if (matcher) { return jQuery.parseJSON(decodeURIComponent(matcher[2])); } } // Finds and validates the parent socket's trace from the cookie trace = findTrace(); if (!trace || jQuery.now() - trace.ts > 1000) { return; } // Chooses a connector connector = connectors.storage() || connectors.windowref(); if (!connector) { return; } return { open: function() { var parentOpened; // 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(jQuery.stringifyJSON({target: "c", type: "close", data: {reason: "error", heir: oldTrace.heir}})); } }, 1000); parentOpened = connector.init(); if (parentOpened) { // Firing the open event without delay robs the user of the opportunity to bind connecting event handlers setTimeout(function() { _open("opening", 'local', request) }, 50); } return parentOpened; }, send: function(event) { connector.signal("send", event); }, localSend: function(event) { connector.signal("localSend", jQuery.stringifyJSON({id: guid , event: event})); }, close: function() { // Do not signal the parent if this method is executed by the unload event handler if (!_abordingConnection) { clearInterval(_traceTimer); connector.signal("close"); connector.close(); } } }; }; function share() { var storageService, name = "atmosphere-" + _request.url, servers = { // Powered by the storage event and the localStorage // http://www.w3.org/TR/webstorage/#event-storage storage: function() { if (!jQuery.atmosphere.supportStorage()) { return; } var storage = window.localStorage; return { init: function() { // Handles the storage event jQuery(window).on("storage.socket", function(event) { event = event.originalEvent; // When a deletion, newValue initialized to null if (event.key === name && event.newValue) { listener(event.newValue); } }); }, signal: function(type, data) { storage.setItem(name, jQuery.stringifyJSON({target: "c", type: type, data: data})); }, get: function(key) { return jQuery.parseJSON(storage.getItem(name + "-" + key)); }, set: function(key, value) { storage.setItem(name + "-" + key, jQuery.stringifyJSON(value)); }, close : function() { jQuery(window).off("storage.socket"); storage.removeItem(name); storage.removeItem(name + "-opened"); storage.removeItem(name + "-children"); } }; }, // 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, ""), win = (jQuery('iframe[name="' + neim + '"]')[0] || jQuery('