UNPKG

a-atmosphere-javascript

Version:
1,747 lines (1,564 loc) 111 kB
/* * Copyright 2011-2022 Async-IO.org * * 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. */ /** * Atmosphere.js * https://github.com/Atmosphere/atmosphere-javascript * * API reference * https://github.com/Atmosphere/atmosphere/wiki/jQuery.atmosphere.js-API * * Highly inspired by * - Portal by Donghwan Kim http://flowersinthesand.github.io/portal/ */ "use strict"; var guid, offline = false, requests = [], callbacks = [], uuid = 0, hasOwn = Object.prototype.hasOwnProperty; export const atmosphere = { version: "3.1.4-javascript", onError: function (response) {}, onClose: function (response) {}, onOpen: function (response) {}, onReopen: function (response) {}, onMessage: function (response) {}, onReconnect: function (request, response) {}, onMessagePublished: function (response) {}, onTransportFailure: function (errorMessage, _request) {}, onLocalMessage: function (response) {}, onFailureToReconnect: function (request, response) {}, onClientTimeout: function (request) {}, onOpenAfterResume: function (request) {}, /** * Creates an object based on an atmosphere subscription that exposes functions defined by the Websocket interface. * * @class WebsocketApiAdapter * @param {Object} request the request object to build the underlying subscription * @constructor */ WebsocketApiAdapter: function (request) { var _socket, _adapter; /** * Overrides the onMessage callback in given request. * * @method onMessage * @param {Object} e the event object */ request.onMessage = function (e) { _adapter.onmessage({ data: e.responseBody }); }; /** * Overrides the onMessagePublished callback in given request. * * @method onMessagePublished * @param {Object} e the event object */ request.onMessagePublished = function (e) { _adapter.onmessage({ data: e.responseBody }); }; /** * Overrides the onOpen callback in given request to proxy the event to the adapter. * * @method onOpen * @param {Object} e the event object */ request.onOpen = function (e) { _adapter.onopen(e); }; _adapter = { close: function () { _socket.close(); }, send: function (data) { _socket.push(data); }, onmessage: function (e) {}, onopen: function (e) {}, onclose: function (e) {}, onerror: function (e) {}, }; _socket = new atmosphere.subscribe(request); return _adapter; }, 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, webSocketBinaryType: null, dispatchUrl: null, webSocketPathDelimiter: "@@", enableXDR: false, rewriteURL: false, attachHeadersAsQueryString: true, executeCallbackBeforeReconnect: false, readyState: 0, withCredentials: false, trackMessageLength: false, messageDelimiter: "|", connectTimeout: -1, reconnectInterval: 0, dropHeaders: true, uuid: 0, shared: false, readResponsesHeaders: false, maxReconnectOnClose: 5, enableProtocol: true, disableDisconnect: false, pollingInterval: 0, heartbeat: { client: null, server: null, }, ackInterval: 0, reconnectOnServerError: true, handleOnlineOffline: true, maxWebsocketErrorRetries: 1, curWebsocketErrorRetries: 0, unloadBackwardCompat: !navigator.sendBeacon, onError: function (response) {}, onClose: function (response) {}, onOpen: function (response) {}, onMessage: function (response) {}, onReopen: function (request, response) {}, onReconnect: function (request, response) {}, onMessagePublished: function (response) {}, onTransportFailure: function (reason, request) {}, onLocalMessage: function (request) {}, onFailureToReconnect: function (request, response) {}, onClientTimeout: function (request) {}, onOpenAfterResume: 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, closedByClientTimeout: false, ffTryingReconnect: false, }; /** * {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; /** * The Heartbeat interval send by the server. * @type {int} * @private */ var _heartbeatInterval = 0; /** * The Heartbeat bytes send by the server. * @type {string} * @private */ var _heartbeatPadding = "X"; /** * {boolean} If request is currently aborted. * * @private */ var _abortingConnection = 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 = atmosphere.util.now(); /** Trace time */ var _traceTimer; /** Key for connection sharing */ var _sharingKey; /** * {boolean} If window beforeUnload event has been called. * Flag will be reset after 5000 ms * * @private */ var _beforeUnloadState = false; /** * {number} Holds the timeout ID for the beforeUnload flag reset. * * @private */ var _beforeUnloadTimeoutId; // Automatic call to subscribe _subscribe(options); /** * Initialize atmosphere request object. * * @private */ function _init() { _subscribed = true; _abortingConnection = false; _requestCount = 0; _websocket = null; _sse = null; _activeRequest = null; _ieStream = null; } /** * Re-initialize atmosphere object. * * @private */ function _reinit() { _clearState(); _init(); } /** * Returns true if the given level is equal or above the configured log level. * * @private */ function _canLog(level) { if (level == "debug") { return _request.logLevel === "debug"; } else if (level == "info") { return _request.logLevel === "info" || _request.logLevel === "debug"; } else if (level == "warn") { return ( _request.logLevel === "warn" || _request.logLevel === "info" || _request.logLevel === "debug" ); } else if (level == "error") { return ( _request.logLevel === "error" || _request.logLevel === "warn" || _request.logLevel === "info" || _request.logLevel === "debug" ); } else { return false; } } function _debug(msg) { if (_canLog("debug")) { atmosphere.util.debug(new Date() + " Atmosphere: " + msg); } } /** * * @private */ function _verifyStreamingLength(ajaxRequest, rq) { // Wait to be sure we have the full message before closing. if ( _response.partialMessage === "" && rq.transport === "streaming" && ajaxRequest.responseText.length > rq.maxStreamingLength ) { return true; } return false; } /** * Disconnect * * @private */ function _disconnect() { if ( _request.enableProtocol && !_request.disableDisconnect && !_request.firstMessage ) { var query = "X-Atmosphere-Transport=close&X-Atmosphere-tracking-id=" + _request.uuid; atmosphere.util.each(_request.headers, function (name, value) { var h = atmosphere.util.isFunction(value) ? value.call(this, _request, _request, _response) : value; if (h != null) { query += "&" + encodeURIComponent(name) + "=" + encodeURIComponent(h); } }); var url = _request.url.replace(/([?&])_=[^&]*/, query); url = url + (url === _request.url ? (/\?/.test(_request.url) ? "&" : "?") + query : ""); var rq = { connected: false, }; var closeR = new atmosphere.AtmosphereRequest(rq); closeR.connectTimeout = _request.connectTimeout; closeR.attachHeadersAsQueryString = false; closeR.dropHeaders = true; closeR.url = url; closeR.contentType = "text/plain"; closeR.transport = "polling"; closeR.method = "GET"; closeR.data = ""; closeR.heartbeat = null; if (_request.enableXDR) { closeR.enableXDR = _request.enableXDR; } _pushOnClose("", closeR); } } /** * Close request. * * @private */ function _close() { _debug("Closing (AtmosphereRequest._close() called)"); _abortingConnection = true; if (_request.reconnectId) { clearTimeout(_request.reconnectId); delete _request.reconnectId; } if (_request.heartbeatTimer) { clearTimeout(_request.heartbeatTimer); } _request.reconnect = false; _response.request = _request; _response.state = "unsubscribe"; _response.responseBody = ""; _response.status = 408; _response.partialMessage = ""; _request.curWebsocketErrorRetries = 0; _invokeCallback(); _disconnect(); _clearState(); } function _clearState() { _response.partialMessage = ""; if (_request.id) { clearTimeout(_request.id); } if (_request.heartbeatTimer) { clearTimeout(_request.heartbeatTimer); } // https://github.com/Atmosphere/atmosphere/issues/1860#issuecomment-74707226 if (_request.reconnectId) { clearTimeout(_request.reconnectId); delete _request.reconnectId; } if (_ieStream != null) { _ieStream.close(); _ieStream = null; } if (_jqxhr != null) { _jqxhr.abort(); _jqxhr = null; } if (_activeRequest != null) { _activeRequest.abort(); _activeRequest = null; } if (_websocket != null) { if (_websocket.canSendMessage) { _debug("invoking .close() on WebSocket object"); _websocket.close(); } _websocket = null; } if (_sse != null) { _sse.close(); _sse = null; } _clearStorage(); } function _clearStorage() { // Stop sharing a connection if (_storageService != null) { // Clears trace timer clearInterval(_traceTimer); // Removes the trace document.cookie = _sharingKey + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"; // The heir is the parent unless unloading _storageService.signal("close", { reason: "", heir: !_abortingConnection ? guid : (_storageService.get("children") || [])[0], }); _storageService.close(); } if (_localStorageService != null) { _localStorageService.close(); } } /** * Subscribe request using request transport. <br> * If request is currently opened, this one will be closed. * * @param {Object} Request parameters. * @private */ function _subscribe(options) { _reinit(); _request = atmosphere.util.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() { // Origin parts var url = atmosphere.util.getAbsoluteURL(_request.url.toLowerCase()); var parts = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/.exec(url); var crossOrigin = !!( parts && // protocol (parts[1] != window.location.protocol || // hostname parts[2] != window.location.hostname || // port (parts[3] || (parts[1] === "http:" ? 80 : 443)) != (window.location.port || (window.location.protocol === "http:" ? 80 : 443))) ); return ( window.EventSource && (!crossOrigin || !atmosphere.util.browser.safari || atmosphere.util.browser.vmajor >= 7) ); } /** * Open request using request transport. <br> * 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 (_canLog("debug")) { atmosphere.util.debug( "Storage service available. All communication will be local" ); } if (_localStorageService.open(_request)) { // Local connection. return; } } if (_canLog("debug")) { atmosphere.util.debug("No Storage service available."); } // Wasn't local or an error occurred _localStorageService = null; } // Protocol _request.firstMessage = uuid == 0 ? true : false; _request.isOpen = false; _request.ctime = atmosphere.util.now(); // We carry any UUID set by the user or from a previous connection. if (_request.uuid === 0) { _request.uuid = uuid; } _response.closedByClientTimeout = false; if (_request.transport !== "websocket" && _request.transport !== "sse") { _executeRequest(_request); } 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 () { function onstorage(event) { if (event.key === name && event.newValue) { listener(event.newValue); } } if (!atmosphere.util.storage) { return; } var storage = window.localStorage, get = function (key) { var item = storage.getItem(name + "-" + key); return item === null ? [] : JSON.parse(item); }, set = function (key, value) { storage.setItem(name + "-" + key, JSON.stringify(value)); }; return { init: function () { set("children", get("children").concat([guid])); atmosphere.util.on(window, "storage", onstorage); return get("opened"); }, signal: function (type, data) { storage.setItem( name, JSON.stringify({ target: "p", type: type, data: data, }) ); }, close: function () { var children = get("children"); atmosphere.util.off(window, "storage", onstorage); if (children) { if (removeFromArray(children, request.id)) { 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( JSON.stringify({ target: "p", type: type, data: data, }) ); } }, close: function () { // Removes traces only if the parent is alive if (!orphan) { removeFromArray(win.callbacks, listener); removeFromArray(win.children, guid); } }, }; }, }; 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 = JSON.parse(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 JSON.parse(decodeURIComponent(matcher[2])); } } // Finds and validates the parent socket's trace from the cookie trace = findTrace(); if (!trace || atmosphere.util.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( JSON.stringify({ 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", JSON.stringify({ id: guid, event: event, }) ); }, close: function () { // Do not signal the parent if this method is executed by the unload event handler if (!_abortingConnection) { 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 () { function onstorage(event) { // When a deletion, newValue initialized to null if (event.key === name && event.newValue) { listener(event.newValue); } } if (!atmosphere.util.storage) { return; } var storage = window.localStorage; return { init: function () { // Handles the storage event atmosphere.util.on(window, "storage", onstorage); }, signal: function (type, data) { storage.setItem( name, JSON.stringify({ target: "c", type: type, data: data, }) ); }, get: function (key) { return JSON.parse(storage.getItem(name + "-" + key)); }, set: function (key, value) { storage.setItem(name + "-" + key, JSON.stringify(value)); }, close: function () { atmosphere.util.off(window, "storage", onstorage); 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, ""), container = document.getElementById(neim), win; if (!container) { container = document.createElement("div"); container.id = neim; container.style.display = "none"; container.innerHTML = '<iframe name="' + neim + '"></iframe>'; 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); } }; }, signal: function (type, data) { if (!win.closed && win.fire) { win.fire( JSON.stringify({ target: "c", type: type, data: data, }) ); } }, get: function (key) { return !win.closed ? win[key] : null; }, set: function (key, value) { if (!win.closed) { win[key] = value; } }, close: function () {}, }; }, }; // Receives send and close command from the children function listener(string) { var command = JSON.parse(string), data = command.data; if (command.target === "p") { switch (command.type) { case "send": _push(data); break; case "localSend": _localMessage(data); break; case "close": _close(); break; } } } _localSocketF = function propagateMessageEvent(context) { storageService.signal("message", context); }; function leaveTrace() { document.cookie = _sharingKey + "=" + // Opera's JSON implementation ignores a number whose a last digit of 0 strangely // but has no problem with a number whose a last digit of 9 + 1 encodeURIComponent( JSON.stringify({ ts: atmosphere.util.now() + 1, heir: (storageService.get("children") || [])[0], }) ) + "; path=/"; } // Chooses a storageService storageService = servers.storage() || servers.windowref(); storageService.init(); if (_canLog("debug")) { atmosphere.util.debug("Installed StorageService " + storageService); } // List of children sockets storageService.set("children", []); if ( storageService.get("opened") != null && !storageService.get("opened") ) { // Flag indicating the parent socket is opened storageService.set("opened", false); } // Leaves traces _sharingKey = encodeURIComponent(name); leaveTrace(); _traceTimer = setInterval(leaveTrace, 1000); _storageService = storageService; } /** * @private */ function _open(state, transport, request) { if (_request.shared && transport !== "local") { share(); } if (_storageService != null) { _storageService.set("opened", true); } request.close = function () { _close(); }; if (_requestCount > 0 && state === "re-connecting") { request.isReopen = true; _tryingToReconnect(_response); } else if (!_response.error) { _response.request = request; var prevState = _response.state; _response.state = state; var prevTransport = _response.transport; _response.transport = transport; var _body = _response.responseBody; _invokeCallback(); _response.responseBody = _body; _response.state = prevState; _response.transport = prevTransport; } } /** * Execute request using jsonp transport. * * @param request {Object} request Request parameters, if undefined _request object will be used. * @private */ function _jsonp(request) { // When CORS is enabled, make sure we force the proper transport. request.transport = "jsonp"; var rq = _request, script; if (request != null && typeof request !== "undefined") { rq = request; } _jqxhr = { open: function () { var callback = "atmosphere" + ++guid; function _reconnectOnFailure() { rq.lastIndex = 0; if (rq.openId) { clearTimeout(rq.openId); } if (rq.heartbeatTimer) { clearTimeout(rq.heartbeatTimer); } if (rq.reconnect && _requestCount++ < rq.maxReconnectOnClose) { _open("re-connecting", rq.transport, rq); _reconnect(_jqxhr, rq, request.reconnectInterval); rq.openId = setTimeout(function () { _triggerOpen(rq); }, rq.reconnectInterval + 1000); } else { _onError(0, "maxReconnectOnClose reached"); } } function poll() { var url = rq.url; if (rq.dispatchUrl != null) { url += rq.dispatchUrl; } var data = rq.data; if (rq.attachHeadersAsQueryString) { url = _attachHeaders(rq); if (data !== "") { url += "&X-Atmosphere-Post-Body=" + encodeURIComponent(data); } data = ""; } var head = document.head || document.getElementsByTagName("head")[0] || document.documentElement; script = document.createElement("script"); script.src = url + "&jsonpTransport=" + callback; script.clean = function () { script.clean = script.onerror = script.onload = script.onreadystatechange = null; if (script.parentNode) { script.parentNode.removeChild(script); } if (++request.scriptCount === 2) { request.scriptCount = 1; _reconnectOnFailure(); } }; script.onload = script.onreadystatechange = function () { _debug("jsonp.onload"); if ( !script.readyState || /loaded|complete/.test(script.readyState) ) { script.clean(); } }; script.onerror = function () { _debug("jsonp.onerror"); request.scriptCount = 1; script.clean(); }; head.insertBefore(script, head.firstChild); } // Attaches callback window[callback] = function (msg) { _debug("jsonp.window"); request.scriptCount = 0; if ( (rq.reconnect && rq.maxRequest === -1) || rq.requestCount++ < rq.maxRequest ) { // _readHeaders(_jqxhr, rq); if (!rq.executeCallbackBeforeReconnect) { _reconnect(_jqxhr, rq, rq.pollingInterval); } if (msg != null && typeof msg !== "string") { try { msg = msg.message; } catch (err) { // The message was partial } } var skipCallbackInvocation = _trackMessageSize( msg, rq, _response ); if (!skipCallbackInvocation) { _prepareCallback( _response.responseBody, "messageReceived", 200, rq.transport ); } if (rq.executeCallbackBeforeReconnect) { _reconnect(_jqxhr, rq, rq.pollingInterval); } _timeout(rq); } else { atmosphere.util.log(_request.logLevel, [ "JSONP reconnect maximum try reached " + _request.requestCount, ]); _onError(0, "maxRequest reached"); } }; setTimeout(function () { poll(); }, 50); }, abort: function () { if (script && script.clean) { script.clean(); } }, }; _jqxhr.open(); } /** * Build websocket object. * * @param location {string} Web socket url. * @returns {websocket} Web socket object. * @private */ function _getWebSocket(location) { if (_request.webSocketImpl != null) { return _request.webSocketImpl; } else { if (window.WebSocket) { return new WebSocket(location); } else { return new MozWebSocket(location); } } } /** * Build web socket url from request url. * * @return {string} Web socket url (start with "ws" or "wss" for secure web socket). * @private */ function _buildWebSocketUrl() { return _attachHeaders( _request, atmosphere.util.getAbsoluteURL(_request.webSocketUrl || _request.url) ).replace(/^http/, "ws"); } /** * Build SSE url from request url. * * @return a url with Atmosphere's headers * @private */ function _buildSSEUrl() { var url = _attachHeaders(_request); return url; } /** * Open SSE. <br> * Automatically use fallback transport if SSE can't be opened. * * @private */ function _executeSSE(sseOpened) { _response.transport = "sse"; var location = _buildSSEUrl(); if (_canLog("debug")) { atmosphere.util.debug("Invoking executeSSE"); atmosphere.util.debug("Using URL: " + location); } if (sseOpened && !_request.reconnect) { if (_sse != null) { _clearState(); } return; } try { _sse = new EventSource(location, { withCredentials: _request.withCredentials, }); } catch (e) { _onError(0, e); _reconnectWithFallbackTransport( "SSE failed. Downgrading to fallback transport and resending" ); return; } if (_request.connectTimeout > 0) { _request.id = setTimeout(function () { if (!sseOpened) { _clearState(); } }, _request.connectTimeout); } _sse.onopen = function () { _debug("sse.onopen"); _timeout(_request); if (_canLog("debug")) { atmosphere.util.debug("SSE successfully opened"); } if (!_request.enableProtocol) { if (!sseOpened) { _open("opening", "sse", _request); } else { _open("re-opening", "sse", _request); } } else if (_request.isReopen) { _request.isReopen = false; _open("re-opening", _request.transport, _request); } sseOpened = true; if (_request.method === "POST") { _response.state = "messageReceived"; _push(_request.data); } }; _sse.onmessage = function (message) { _debug("sse.onmessage"); _timeout(_request); if ( !_request.enableXDR && window.location.host && message.origin && message.origin !== window.location.protocol + "//" + window.location.host ) { atmosphere.util.log(_request.logLevel, [ "Origin was not " + window.location.protocol + "//" + window.location.host, ]); return; } _response.state = "messageReceived"; _response.status = 200; message = message.data; var skipCallbackInvocation = _trackMessageSize( message, _request, _response ); // https://github.com/remy/polyfills/blob/master/EventSource.js // Since we polling. /* if (_sse.URL) { _sse.interval = 100; _sse.URL = _buildSSEUrl(); } */ if (!skipCallbackInvocation) { _invokeCallback(); _response.responseBody = ""; _response.messages = []; } }; _sse.onerror = function () { _debug("sse.onerror"); clearTimeout(_request.id); if (_request.heartbeatTimer) { clearTimeout(_request.heartbeatTimer); } if (_response.closedByClientTimeout) { return; } _invokeClose(sseOpened); _clearState(); if (_abortingConnection) { atmosphere.util.log(_request.logLevel, ["SSE closed normally"]); } else if (!sseOpened) { _reconnectWithFallbackTransport( "SSE failed. Downgrading to fallback transport and resending" ); } else if (_request.reconnect && _response.transport === "sse") { if (_requestCount++ < _request.maxReconnectOnClose) { _open("re-connecting", _request.transport, _request); if (_request.reconnectInterval > 0) { // Prevent the online event to open a second connection while waiting for reconnect var handleOnlineOffline = _request.handleOnlineOffline; _request.handleOnlineOffline = false; _request.reconnectId = setTimeout(function () { _request.handleOnlineOffline = handleOnlineOffline; _executeSSE(true); }, _request.reconnectInterval); } else { _executeSSE(true); } _response.responseBody = ""; _response.messages = []; } else { atmosphere.util.log(_request.logLevel, [ "SSE reconnect maximum try reached " + _requestCount, ]); _onError(0, "maxReconnectOnClose reached"); } } }; } /** * Open web socket. <br> * Automatically use fallback transport if web socket can't be opened. * * @private */ function _executeWebSocket(webSocketOpened) { _response.transport = "websocket"; var location = _buildWebSocketUrl(_request.url); if (_canLog("debug")) { atmosphere.util.debug( "Invoking executeWebSocket, using URL: " + location ); } if (webSocketOpened && !_request.reconnect) { if (_websocket != null) { _clearState(); } return; } _websocket = _getWebSocket(location); if (_request.webSocketBinaryType != null) { _websocket.binaryType = _request.webSocketBinaryType; } if (_request.connectTimeout > 0) { _request.id = setTimeout(function () { if (!webSocketOpened) { var _message = { code: 1002, reason: "Connection timeout after " + _request.connectTimeout + "ms.", wasClean: false, }; var socket = _websocket; // Close it anyway try { _clearState(); } catch (e) {} socket.onclose(_message); } }, _request.connectTimeout); } _websocket.onopen = function () { if (_websocket == null) { this.close(); if (_request.transport == "websocket") _close(); return; } _debug("websocket.onopen"); if (!_request.enableProtocol || _request.connectTimeout <= 0) _timeout(_request); offline = false; if (_canLog("debug")) { atmosphere.util.debug("Websocket successfully opened"); } var reopening = webSocketOpened; _websocket.canSendMessage = true; if (!_request.enableProtocol) { webSocketOpened = true; if (reopening) { _open("re-opening", "websocket", _request); } else { _open("opening", "websocket", _request); } } if (_request.method === "POST") { _response.state = "messageReceived"; _websocket.send(_request.data); } }; _websocket.onmessage = function (message) { if (_websocket == null) { this.close(); if (_request.transport == "websocket") _close(); return; } _debug("websocket.onmessage"); _timeout(_request); // We only consider it opened if we get the handshake data // https://github.com/Atmosphere/atmosphere-javascript/issues/74 if (_request.enableProtocol) { webSocketOpened = true; } _response.state = "messageReceived"; _response.status = 200; message = message.data; var isString = typeof message === "string"; if (isString) { var skipCallbackInvocation = _trackMessageSize( message, _request, _response ); if (!skipCallbackInvocation) { _invokeCallback(); _response.responseBody = ""; _response.messages = []; } } else { message = _handleProtocol(_request, message); if (message === "") return; _response.responseBody = message; _invokeCallback(); _response.responseBody = null; } }; _websocket.onerror = function () { _debug("websocket.onerror"); clearTimeout(_request.id); if (_request.heartbeatTimer) { clearTimeout(_request.heartbeatTimer); } _response.error = true; }; _websocket.onclose = function (message) { _debug("websocket.onclose"); if (_response.transport !== "websocket") return; clearTimeout(_request.id); if (_response.state === "closed") return; var reason = message.reason; if (reason === "") { switch (message.code) { case 1000: reason = "Normal closure; the connection successfully completed whatever purpose for which it was created."; break; case 1001: reason = "The endpoint is going away, either because of a server failure or because the " + "browser is navigating away from the page that opened the connection."; break; case 1002: reason = "The endpoint is terminating the connection due to a protocol error."; break; case 1003: reason = "The connection is being terminated because the endpoint received data of a type it " + "cannot accept (for example, a text-only endpoint received binary data)."; break; case 1004: reason = "The endpoint is terminating the connection because a data frame was received that is too large."; break; case 1005: reason = "Unknown: no status code was provided even though one was expected."; break; case 1006: reason = "Connection was closed abnormally (that is, with no close frame being sent)."; break; } } if (_canLog("warn")) { atmosphere.util.warn( "Websocket closed, reason: " + reason + " - wasClean: " + message.wasClean ); } if ( _response.closedByClientTimeout || (_request.handleOnlineOffline && offline) ) { // IFF online/offline events are handled and we happen to be offline, we stop all reconnect attempts and // resume them in the "online" event (if we get here in that case, something else went wrong as the // offline handler should stop any reconnect attempt). // // On the other hand, if we DO NOT handle online/offline events, we continue as before with reconnecting // even if we are offline. Failing to do so would stop all reconnect attemps forever. if (_request.reconnectId) { clearTimeout(_request.reconnectId); delete _request.reconnectId; } return; } _invokeClose(webSocketOpened); _response.state = "closed"; if (_abortingConnection) { atmosphere.util.log(_request.logLevel, ["Websocket closed normally"]); } else if ( _response.error && _request.curWebsocketErrorRetries < _request.maxWebsocketErrorRetries && _requestCount + 1 < _request.maxReconnectOnClose ) { _response.error = false; _request.curWebsocketErrorRetries++; _reconnectWebSocket(); } else if ( (_response.error || !webSocketOpened || _request.maxWebsocketErrorRetries === 0) && _request.fallbackTransport !== "websocket" ) { _response.error = false; _reconnectWithFallbackTransport( "Websocket failed on first connection attempt. Downgrading to " + _request.fallbackTransport + " and resending" ); } else if (_request.reconnect) { _reconnectWebSocket(); } }; var ua = navigator.userAgent.toLowerCase(); var isAndroid = ua.indexOf("android") > -1; if (isAndroid && _websocket.url === undefined) { // Android 4.1 does not really support websockets and fails silently _websocket.onclose({ reason: "Android 4.1 does not support websockets.", wasClean: false, }); } } function _handleProtocol(request, message) { var nMessage = message; if (request.transport === "polling") return nMessage; if ( request.enableProtocol && request.firstMessage && atmosphere.util.trim(message).length !== 0 ) { var pos = request.trackMessageLength ? 1 : 0; var messages = message.split(request.messageDelimiter); if (messages.length <= pos + 1) { // Something went wrong, normally with IE or when a message is written before the // handshake has been received. return nMessage; } request.firstMessage = false; request.uuid = atmosphere.util.trim(messages[pos]); if (messages.length <= pos + 2) { atmosphere.util.log("error", [ "Protocol data not sent by the server. " + "If you enable protocol on client side, be sure to install JavascriptProtocol interceptor on server side." + "Also note that atmosphere-runtime 2.2+ should be used.", ]); } _heartbeatInterval = parseInt( atmosphere.util.trim(messages[pos + 1]), 10 ); _heartbeatPadding = messages[pos + 2]; if (request.transport !== "long-polling") { _triggerOpen(request); } uuid = request.uuid; nMessage = ""; // We have trailing messages pos = request.trackMessageLength ? 4 : 3; if (messages.length > pos + 1) { for (var i = pos; i < messages.length; i++) { nMessage += messages[i]; if (i + 1 !== messages.length) { nMessage +=