a-atmosphere-javascript
Version:
Atmosphere browser libraries
1,747 lines (1,564 loc) • 111 kB
JavaScript
/*
* 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 +=