/*
* Cettia
* http://cettia.io/projects/cettia-javascript-client/
*
* Copyright 2019 the original author or authors.
* Licensed under the Apache License, Version 2.0
* http://www.apache.org/licenses/LICENSE-2.0
*/
import msgpack from 'msgpack-lite';
import traverse from 'traverse';
if (process.env.NODE_ENV !== "browser") {
window = require("jsdom").jsdom().defaultView;
window.WebSocket = require("ws");
window.EventSource = require("eventsource");
}
// A global identifier
var guid = 1;
// Prototype shortcuts
var slice = Array.prototype.slice;
// Variables for Node
var document = window.document;
var location = window.location;
var navigator = window.navigator;
var XMLHttpRequest = window.XMLHttpRequest;
// Most are inspired by jQuery
var util = {};
util.makeAbsolute = function(url) {
// Assumes the given url is absolute in an environment such as React Native where the document is not available
if (!document) {
return url;
}
var div = document.createElement("div");
// Uses an innerHTML property to obtain an absolute URL
div.innerHTML = '';
// 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));
};
util.on = function(elem, type, fn) {
if (elem.addEventListener) {
elem.addEventListener(type, fn, false);
} else if (elem.attachEvent) {
elem.attachEvent("on" + type, fn);
}
};
util.stringifyURI = function(url, params) {
var name;
var s = [];
params = params || {};
params._ = guid++;
// params is supposed to be one-depth object
for (name in params) {
// null or undefined param value should be excluded
if (params[name] != null) {
s.push(encodeURIComponent(name) + "=" + encodeURIComponent(params[name]));
}
}
return url + (/\?/.test(url) ? "&" : "?") + s.join("&").replace(/%20/g, "+");
};
util.parseURI = function(url) {
// Deal with only query part
var obj = {
query: {}
};
var match = /.*\?([^#]*)/.exec(url);
if (match) {
var array = match[1].split("&");
for (var i = 0; i < array.length; i++) {
var part = array[i].split("=");
obj.query[decodeURIComponent(part[0])] = decodeURIComponent(part[1] || "");
}
}
return obj;
};
// CORS able
util.corsable = "withCredentials" in new XMLHttpRequest();
// Browser sniffing
util.browser = (function() {
// navigator.userAgent is undefined in React Native
var ua = (navigator.userAgent || "").toLowerCase();
var browser = {};
var match =
// IE 9-10
/(msie) ([\w.]+)/.exec(ua) ||
// IE 11+
/(trident)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
// Safari
ua.indexOf("android") < 0 && /version\/(.+) (safari)/.exec(ua) || [];
// Swaps variables
if (match[2] === "safari") {
match[2] = match[1];
match[1] = "safari";
}
browser[match[1] || ""] = true;
browser.version = match[2] || "0";
browser.vmajor = browser.version.split(".")[0];
// Trident is the layout engine of IE
if (browser.trident) {
browser.msie = true;
}
return browser;
})();
util.crossOrigin = function(uri) {
// Returns true in an environment such as React Native where the location is not available
if (!location) {
return true;
}
// Origin parts
var parts = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/.exec(uri.toLowerCase());
return !!(parts && (
// protocol
parts[1] != location.protocol ||
// hostname
parts[2] != location.hostname ||
// port
(parts[3] || (parts[1] === "http:" ? 80 : 443)) !=
(location.port || (location.protocol === "http:" ? 80 : 443))
));
};
// Inspired by jQuery.Callbacks
function createCallbacks(deferred) {
var locked;
var memory;
var firing;
var firingStart;
var firingLength;
var firingIndex;
var list = [];
var fire = function(context, args) {
args = args || [];
memory = !deferred || [context, args];
firing = true;
firingIndex = firingStart || 0;
firingStart = 0;
firingLength = list.length;
for (; firingIndex < firingLength && !locked; firingIndex++) {
list[firingIndex].apply(context, args);
}
firing = false;
};
var 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 object
function createSocket(uris, options) {
// Default socket options
var defaults = {
reconnect: function(lastDelay) {
return 2 * (lastDelay || 250);
},
transports: [createWebSocketTransport, createHttpStreamTransport, createHttpLongpollTransport]
};
// Overrides defaults
if (options) {
for (var i in options) {
defaults[i] = options[i];
}
}
options = defaults;
// Socket
var self = {};
// Events
var events = {};
// Adds event handler
self.on = function(type, fn) {
var event;
// For custom event
event = events[type];
if (!event) {
if (events.message.locked()) {
return this;
}
event = events[type] = createCallbacks();
event.order = events.message.order;
}
event.add(fn);
return this;
};
// Removes event handler
self.off = function(type, fn) {
var event = events[type];
if (event) {
event.remove(fn);
}
return this;
};
// Adds one time event handler
self.once = 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
self.fire = function(type) {
var event = events[type];
if (event) {
event.fire(self, slice.call(arguments, 1));
}
return this;
};
// Networking
// Transport associated with this socket
var transport;
// Reconnection
var reconnectTimer;
var reconnectDelay;
var reconnectTry = 0;
// For internal use only
// Establishes a connection
self.open = function() {
// Resets the transport
transport = null;
// Cancels the scheduled connection
clearTimeout(reconnectTimer);
// Resets event helpers
events.connecting.unlock();
events.open.unlock();
events.close.unlock();
events.waiting.unlock();
// Fires the connecting event and connects to the server
return self.fire("connecting");
};
// Disconnects the connection
self.close = function() {
// Prevents reconnection
options.reconnect = false;
clearTimeout(reconnectTimer);
if (state === "connecting") {
// It will execute the connecting transport's stop function
self.fire("close");
} else if (state === "opened") {
// It will fire the close event to socket
transport.close();
}
return this;
};
// Id
var id;
// If the user sets this option, we should have full control of window.name
// It's obstructive but inevitable
if (options.name) {
if (window.name) {
var names = JSON.parse(window.name);
id = names[options.name];
}
}
// State
var state;
self.state = function() {
return state;
};
// Each event represents a possible state of this socket
// they are considered as special event and works in a different way
for (var i in {
connecting: 1,
open: 1,
close: 1,
waiting: 1
}) {
// This event fires only one time and handlers being added after fire are fired immediately
events[i] = createCallbacks(true);
// State transition order
events[i].order = guid++;
}
// However all the other event including message event work as you expected
// it fires many times and handlers are executed whenever it fires
events.message = createCallbacks(false);
// It shares the same order with the open event because it can be fired when a socket is in the
// opened state
events.message.order = events.open.order;
// State transition
self.on("connecting", function() {
// From null state
state = "connecting";
// Final URIs to work with transport
var candidates = Array.isArray(uris) ? slice.call(uris) : [uris];
for (var i = 0; i < candidates.length; i++) {
// Attaches the id to uri
var uri = candidates[i] = util.stringifyURI(util.makeAbsolute(candidates[i]), {
"cettia-version": "1.0",
"cettia-id": id
});
// Translates an abbreviated uri
if (/^https?:/.test(uri) && !util.parseURI(uri).query["cettia-transport-name"]) {
candidates.splice(i, 1,
uri.replace(/^http/, "ws"),
// util.stringifyURI is used since we don't know if uri has already query
util.stringifyURI(uri, {
"cettia-transport-name": "stream"
}),
util.stringifyURI(uri, {
"cettia-transport-name": "longpoll"
}));
i = i + 2;
}
}
// Finds a working transport
(function find() {
var uri = candidates.shift();
// If every available transport failed
if (!uri) {
self.fire("error", new Error())
// Fires the close event instead of executing close method which destorys the socket
.fire("close");
return;
}
// Deremines a transport from URI through transports option
var testTransport;
for (var i = 0; i < options.transports.length; i++) {
testTransport = options.transports[i](uri, options);
if (testTransport) {
break;
}
}
// It would be null if it can't run on this environment or handle given URI
if (!testTransport) {
find();
return;
}
// This is to stop the whole process to find a working transport
// when socket's close method is called while doing that
function stop() {
testTransport.off("close", find).close();
}
self.once("close", stop);
testTransport.on("close", find).on("close", function() {
self.off("close", stop);
})
.on("text", function handshaker(data) {
// handshaker is one-time event handler
testTransport.off("text", handshaker);
var headers = util.parseURI(data).query;
// An issued id
if (id !== headers["cettia-id"]) {
id = headers["cettia-id"];
self.fire("new");
}
// An heartbeat option can't be set by user
options.heartbeat = +headers["cettia-heartbeat"];
// To speed up heartbeat test
options._heartbeat = +headers["cettia-_heartbeat"] || 5000;
// Now that handshaking is completed, associates the transport with the socket
transport = testTransport.off("close", find);
// Handles an inbound event object
function onevent(event) {
var latch;
var reply = function(success) {
return function(value) {
// The latch prevents double reply.
if (!latch) {
latch = true;
self.send("reply", {
id: event.id,
data: value,
exception: !success
});
}
};
};
var args = [event.type, event.data, !event.reply ? null : {
resolve: reply(true),
reject: reply(false)
}];
self.fire.apply(self, args);
}
var skip;
transport.on("text", function(data) {
// Because this handler is executed on dispatching text event,
// first message for handshaking should be skipped
if (!skip) {
skip = true;
return;
}
onevent(JSON.parse(data));
})
.on("binary", function(data) {
// In browser, data is ArrayBuffer and should be wrapped in Uint8Array
// In Node, data should be Buffer
if (process.env.NODE_ENV === "browser") {
data = new Uint8Array(data);
}
onevent(msgpack.decode(data));
})
.on("error", function(error) {
// If the underlying connection is closed due to this error, accordingly close event
// will be triggered
self.fire("error", error);
})
.on("close", function() {
self.fire("close");
});
// And fires open event to socket
self.off("close", stop).fire("open");
})
.open();
})();
})
.on("new", function() {
if (options.name) {
var names = window.name ? JSON.parse(window.name) : {};
names[options.name] = id;
window.name = JSON.stringify(names);
}
})
.on("open", function() {
// From connecting state
state = "opened";
var heartbeatTimer;
// Sets a heartbeat timer and clears it on close event
(function setHeartbeatTimer() {
// heartbeat event will be sent after options.heartbeat - options._heartbeat ms
heartbeatTimer = setTimeout(function() {
self.send("heartbeat").once("heartbeat", function() {
clearTimeout(heartbeatTimer);
setHeartbeatTimer();
});
// transport will be closed after options._heartbeat ms unless the server responds it
heartbeatTimer = setTimeout(function() {
self.fire("error", new Error("heartbeat"));
// Now that the transport doesn't realize its connection is closed, execute close method
// It will also fire close event to transport and accordingly socket
transport.close();
}, options._heartbeat);
}, options.heartbeat - options._heartbeat);
})();
self.once("close", function() {
clearTimeout(heartbeatTimer);
});
// Locks the connecting event
events.connecting.lock();
// Initializes variables related with reconnection
reconnectTimer = reconnectDelay = null;
reconnectTry = 0;
})
.on("close", function() {
// From connecting or opened state
state = "closed";
// Locks event whose order is lower than close event among reserved events
events.connecting.lock();
events.open.lock();
// Schedules reconnection
if (options.reconnect) {
// By adding a handler by one method in event handling
// it will be the last one of close event handlers having been added
self.once("close", function() {
reconnectDelay = options.reconnect.call(self, reconnectDelay, reconnectTry);
if (reconnectDelay !== false) {
reconnectTry++;
reconnectTimer = setTimeout(function() {
self.open();
}, reconnectDelay);
self.fire("waiting", reconnectDelay, reconnectTry);
}
});
}
})
.on("waiting", function() {
// From closed state
state = "waiting";
});
// Messaging
// A map for reply callback
var callbacks = {};
// Sends an event to the server via the connection
self.send = function(type, data, onResolved, onRejected) {
if (state !== "opened") {
self.fire("cache", [type, data, onResolved, onRejected]);
return this;
}
// Outbound event
var event = {
id: guid++,
type: type,
data: data,
reply: !!(onResolved || onRejected)
};
if (event.reply) {
callbacks[event.id] = [onResolved, onRejected];
}
// Determines if the given data contains binary
var hasBinary = false;
if (process.env.NODE_ENV !== "browser") {
// Applies to Node.js only
hasBinary = traverse(data).reduce(function(hasBuffer, e) {
// 'ArrayBuffer' refers to window.ArrayBuffer not global.ArrayBuffer
return hasBuffer || Buffer.isBuffer(e) || global.ArrayBuffer.isView(e);
}, false);
} else {
// IE 9 doesn't support typed arrays
var ArrayBuffer = window.ArrayBuffer;
if (ArrayBuffer) {
JSON.stringify(data, function(key, value) {
hasBinary = hasBinary || ArrayBuffer.isView(value);
return value;
});
}
}
// Delegates to the transport
if (hasBinary) {
transport.send(msgpack.encode(event));
} else {
transport.send(JSON.stringify(event));
}
return this;
};
self.on("reply", function(reply) {
// callbacks[reply.id] is [onResolved, onRejected]
// FYI +false and +true is 0 and 1, respectively
callbacks[reply.id][+reply.exception].call(self, reply.data);
delete callbacks[reply.id];
});
return self.open();
}
function createBaseTransport(uri, options) {
var timeout = options && options.timeout || 3000;
var self = {};
self.open = function() {
// Establishes a real connection
self.connect();
// Sets a timeout timer and clear it on open or close event
var timeoutTimer = setTimeout(function() {
self.fire("error", new Error("timeout"))
// To abort connection
.close();
}, timeout);
function clearTimeoutTimer() {
clearTimeout(timeoutTimer);
}
self.on("open", clearTimeoutTimer).on("close", clearTimeoutTimer);
return this;
};
// Transport events
var events = {
open: createCallbacks(true),
text: createCallbacks(),
binary: createCallbacks(),
error: createCallbacks(),
close: createCallbacks(true)
};
self.on = function(type, fn) {
events[type].add(fn);
return this;
};
self.off = function(type, fn) {
events[type].remove(fn);
return this;
};
self.fire = function(type) {
events[type].fire(self, slice.call(arguments, 1));
return this;
};
var opened = false;
self.on("open", function() {
opened = true;
});
self.on("close", function() {
opened = false;
// Locks every event except close event
for (var type in events) {
if (type !== "close") {
events[type].lock();
}
}
});
self.send = function(data) {
if (opened) {
self.write(data);
} else {
self.emit("error", new Error("notopened"));
}
return this;
};
return self;
}
function createWebSocketTransport(uri, options) {
var WebSocket = window.WebSocket;
if (!WebSocket || !/^wss?:/.test(uri)) {
return;
}
var ws;
var self = createBaseTransport(uri, options);
self.connect = function() {
ws = new WebSocket(uri);
// Reads binary frame as ArrayBuffer
ws.binaryType = "arraybuffer";
ws.onopen = function() {
self.fire("open");
};
ws.onmessage = function(event) {
if (typeof event.data === "string") {
self.fire("text", event.data);
} else {
if (process.env.NODE_ENV !== "browser") {
// As of ws 1.1+, event.data is ArrayBuffer
self.fire("binary", Buffer.from(event.data));
} else {
self.fire("binary", event.data);
}
}
};
ws.onerror = function() {
// In some browsers, if onerror is called, onclose is not called.
self.fire("error", new Error()).fire("close");
};
ws.onclose = function() {
self.fire("close");
};
};
self.write = function(data) {
ws.send(data);
};
self.close = function() {
ws.close();
return this;
};
return self;
}
function createHttpBaseTransport(uri, options) {
var xdrURL = options && options.xdrURL;
var self = createBaseTransport(uri, options);
// Because id is set on open event
var sendURI;
self.on("open", function() {
sendURI = util.stringifyURI(uri, {
"cettia-transport-id": self.id
});
})
.on("close", function() {
sendURI = null;
});
var sending = false;
var queue = [];
var onload = function() {
if (queue.length) {
send(queue.shift());
} else {
sending = false;
}
};
var onerror = function() {
// Even though it fails to send a message, the connection may turn out to be opened
if (sendURI) {
// However it's likely that the connection was closed but the transport couldn't detect it
// Because if the connection is really alive, then sending a message shouldn't have failed
// To make it clear, closes the connection
self.fire("error", new Error()).close();
}
};
var send = !util.crossOrigin(uri) || util.corsable ?
// By XMLHttpRequest
function(data) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
onload();
} else {
onerror();
}
}
};
xhr.open("POST", sendURI);
xhr.withCredentials = true;
// data is either a string or an ArrayBuffer
if (typeof data === "string") {
// In XMLHttpRequest of jsdom used to provide window in Node.js,
// request headers are case sensitive and it checks content-type header by 'Content-Type'
xhr.setRequestHeader("Content-Type", "text/plain; charset=UTF-8");
xhr.send("data=" + data);
} else {
// ArrayBuffer can be sent by only XMLHttpRequest 2
xhr.setRequestHeader("Content-Type", "application/octet-stream");
if (process.env.NODE_ENV !== "browser") {
// API for Node is supposed to send Buffer but jsdom's XMLHttpRequest doesn't
// support doing that so convert it to ArrayBuffer
data = new Uint8Array(data).buffer;
}
xhr.send(data);
}
return this;
} : window.XDomainRequest && xdrURL ?
// By XDomainRequest
function(data) {
// Only text/plain is supported for the request's Content-Type header from the fourth at
// http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx
var xdr = new window.XDomainRequest();
xdr.onload = onload;
xdr.onerror = onerror;
xdr.open("POST", xdrURL.call(self, sendURI));
xdr.send("data=" + data);
return this;
} :
// By HTMLFormElement
function(data) {
var iframe;
var textarea;
var form = document.createElement("form");
form.action = sendURI;
form.target = "socket-" + (guid++);
form.method = "POST";
form.enctype = "text/plain";
form.acceptCharset = "UTF-8";
form.style.display = "none";
form.innerHTML = '';
textarea = form.firstChild;
textarea.value = data;
iframe = form.lastChild;
util.on(iframe, "error", function() {
onerror();
});
util.on(iframe, "load", function() {
document.body.removeChild(form);
onload();
});
document.body.appendChild(form);
form.submit();
return this;
};
self.write = function(data) {
if (!sending) {
sending = true;
send(data);
} else {
queue.push(data);
}
};
// To notify server only once
var latch;
self.close = function() {
// Aborts the real connection
self.abort();
if (!latch) {
latch = true;
// Skips sending the abort request in an environment like React Native where the document is not available
if (!document) {
return this;
}
// Sends the abort request to the server
// this request is supposed to work even in unloading event so script tag should be used
var script = document.createElement("script");
script.async = false;
script.src = util.stringifyURI(uri, {
"cettia-transport-id": self.id,
"cettia-transport-when": "abort"
});
script.onload = script.onerror = function() {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
// Fires the close event but it may be already fired by transport
self.fire("close");
};
document.head.appendChild(script);
}
return this;
};
return self;
}
function createHttpStreamTransport(uri, options) {
if (/^https?:/.test(uri) && util.parseURI(uri).query["cettia-transport-name"] === "stream") {
return createHttpSseTransport(uri, options) ||
createHttpStreamXhrTransport(uri, options) ||
createHttpStreamXdrTransport(uri, options) ||
createHttpStreamIframeTransport(uri, options);
}
}
function createHttpStreamBaseTransport(uri, options) {
var buffer = "";
var self = createHttpBaseTransport(uri, options);
// The detail about parsing is explained in the reference implementation
self.parse = function(chunk) {
// Strips off the left padding of the chunk that appears in the
// first chunk
chunk = chunk.replace(/^\s+/, "");
// The chunk should be not empty for correct parsing,
if (chunk) {
// String.prototype.split with string separator is reliable cross-browser
var lines = (buffer + chunk).split("\n\n");
for (var i = 0; i < lines.length - 1; i++) {
self.onmessage(lines[i].substring("data: ".length));
}
buffer = lines[lines.length - 1];
}
};
var handshaked;
self.onmessage = function(data) {
// The first message is handshake result
if (!handshaked) {
handshaked = true;
var query = util.parseURI(data).query;
// Assign a newly issued identifier for this transport
self.id = query["cettia-transport-id"];
self.fire("open");
} else {
var code = data.substring(0, 1);
data = data.substring(1);
switch (code) {
case "1":
self.fire("text", data);
break;
case "2":
// The same condition used in UMD
if (process.env.NODE_ENV !== "browser") {
data = Buffer.from(data, "base64");
} else {
// Decodes Base64 encoded string
var decoded = atob(data);
// And converts it to ArrayBuffer
var array = new Uint8Array(data.length);
for (var i = 0; i < decoded.length; i++) {
array[i] = decoded.charCodeAt(i);
}
data = array.buffer;
}
self.fire("binary", data);
break;
}
}
};
return self;
}
function createHttpSseTransport(uri, options) {
var EventSource = window.EventSource;
if (!EventSource || (util.crossOrigin(uri) && util.browser.safari && util.browser.vmajor < 7)) {
return;
}
var es;
var self = createHttpStreamBaseTransport(uri, options);
self.connect = function() {
es = new EventSource(uri + "&cettia-transport-version=1.0&cettia-transport-when=open&cettia-transport-sse=true", {
withCredentials: true
});
es.onmessage = function(event) {
self.onmessage(event.data);
};
es.onerror = function() {
es.close();
// There is no way to find whether there was an error or not
self.fire("close");
};
};
self.abort = function() {
es.close();
};
return self;
}
function createHttpStreamXhrTransport(uri, options) {
if ((util.browser.msie && util.browser.vmajor < 10) || (util.crossOrigin(uri) && !util.corsable)) {
return;
}
var xhr;
var self = createHttpStreamBaseTransport(uri, options);
self.connect = function() {
var index;
xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 3 && xhr.status === 200) {
self.parse(!index ? xhr.responseText : xhr.responseText.substring(index));
index = xhr.responseText.length;
} else if (xhr.readyState === 4) {
if (xhr.status !== 200) {
// Here the connection is already closed
self.fire("error", new Error());
}
self.fire("close");
}
};
xhr.open("GET", uri + "&cettia-transport-version=1.0&cettia-transport-when=open");
xhr.withCredentials = true;
xhr.send();
};
self.abort = function() {
xhr.abort();
};
return self;
}
function createHttpStreamXdrTransport(uri, options) {
var xdrURL = options && options.xdrURL;
var XDomainRequest = window.XDomainRequest;
if (!xdrURL || !XDomainRequest) {
return;
}
var xdr;
var self = createHttpStreamBaseTransport(uri, options);
self.connect = function() {
var index;
xdr = new XDomainRequest();
xdr.onprogress = function() {
self.parse(!index ? xdr.responseText : xdr.responseText.substring(index));
index = xdr.responseText.length;
};
xdr.onerror = function() {
// Here the connection is already closed
// But onload isn't executed if onerror is executed so fires close event
self.fire("error", new Error()).fire("close");
};
xdr.onload = function() {
self.fire("close");
};
xdr.open("GET", xdrURL.call(self, uri + "&cettia-transport-version=1.0&cettia-transport-when=open"));
xdr.send();
};
self.abort = function() {
xdr.abort();
};
return self;
}
function createHttpStreamIframeTransport(uri, options) {
var ActiveXObject = window.ActiveXObject;
if (!ActiveXObject || util.crossOrigin(uri)) {
return;
}
var doc;
var stop;
var self = createHttpStreamBaseTransport(uri, options);
self.connect = function() {
function iterate(fn) {
var timeoutId;
// Though the interval is 1ms for real-time application, there is a delay between
// setTimeout calls For detail, see
// https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting
(function loop() {
timeoutId = setTimeout(function() {
if (fn() === false) {
return;
}
loop();
}, 1);
})();
return function() {
clearTimeout(timeoutId);
};
}
doc = new ActiveXObject("htmlfile");
doc.open();
doc.close();
var iframe = doc.createElement("iframe");
iframe.src = uri + "&cettia-transport-version=1.0&cettia-transport-when=open";
doc.body.appendChild(iframe);
var cdoc = iframe.contentDocument || iframe.contentWindow.document;
stop = iterate(function() {
// Waits the server's container ignorantly
if (!cdoc.firstChild) {
return;
}
// Response container
var container = cdoc.body.lastChild;
// Detects connection failure
if (!container) {
self.fire("error", new Error()).fire("close");
return false;
}
function readDirty() {
var clone = container.cloneNode(true);
// Adds a character not CR and LF to circumvent an IE bug
// If the contents of an element ends with one or more CR or LF, IE ignores them in the
// innerText property
clone.appendChild(cdoc.createTextNode("."));
// But the above idea causes \n chars to be replaced with \r\n or for some reason
// Restores them to its original state
var text = clone.innerText.replace(/\r\n/g, "\n");
return text.substring(0, text.length - 1);
}
self.parse(readDirty());
// The container is resetable so no index or length variable is needed
container.innerText = "";
stop = iterate(function() {
var text = readDirty();
if (text) {
container.innerText = "";
self.parse(text);
}
if (cdoc.readyState === "complete") {
self.fire("close");
return false;
}
});
return false;
});
};
self.abort = function() {
stop();
doc.execCommand("Stop");
};
return self;
}
function createHttpLongpollTransport(uri, options) {
if (/^https?:/.test(uri) && util.parseURI(uri).query["cettia-transport-name"] === "longpoll") {
return createHttpLongpollAjaxTransport(uri, options) ||
createHttpLongpollXdrTransport(uri, options) ||
createHttpLongpollJsonpTransport(uri, options);
}
}
function createHttpLongpollBaseTransport(uri, options) {
var self = createHttpBaseTransport(uri, options);
self.connect = function() {
self.poll(uri + "&cettia-transport-version=1.0&cettia-transport-when=open", function(data) {
var query = util.parseURI(data).query;
// Assign a newly issued identifier for this transport
self.id = query["cettia-transport-id"];
(function poll() {
self.poll(util.stringifyURI(uri, {
"cettia-transport-id": self.id,
"cettia-transport-when": "poll"
}), function(data) {
if (data) {
poll();
if (typeof data === "string") {
self.fire("text", data);
} else {
// Practically this case only happens with XMLHttpRequest 2
if (process.env.NODE_ENV !== "browser") {
// Even in Node, data is ArrayBuffer not Buffer because of jsdom
// According to API for Node, binary event should receive Buffer
data = Buffer.from(new Uint8Array(data));
}
self.fire("binary", data);
}
} else {
self.fire("close");
}
});
})();
self.fire("open");
});
};
return self;
}
function createHttpLongpollAjaxTransport(uri, options) {
if (util.crossOrigin(uri) && !util.corsable) {
return;
}
var xhr;
var self = createHttpLongpollBaseTransport(uri, options);
self.poll = function(url, fn) {
xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
// HEADERS_RECEIVED
// To set xhr.responseType which can't be set on LOADING and DONE state
case 2:
// No need to make the header value lowercase because it is case sensitive unless there
// is a charset
if (xhr.getResponseHeader("content-type") === "application/octet-stream") {
// Reads response body as ArrayBuffer as it's binary
xhr.responseType = "arraybuffer";
}
break;
// DONE
// To avoid c00c023f error on IE 9
case 4:
if (xhr.status === 200) {
// xhr.response follows the type specified by xhr.responseType
fn(xhr.response || xhr.responseText);
} else {
// Here is the end of the connection due to error
self.fire("error", new Error()).fire("close");
}
break;
}
};
xhr.open("GET", url);
xhr.withCredentials = true;
xhr.send(null);
};
self.abort = function() {
xhr.abort();
};
return self;
}
function createHttpLongpollXdrTransport(uri, options) {
var xdrURL = options && options.xdrURL;
var XDomainRequest = window.XDomainRequest;
if (!xdrURL || !XDomainRequest) {
return;
}
var xdr;
var self = createHttpLongpollBaseTransport(uri, options);
self.poll = function(url, fn) {
url = xdrURL.call(self, url);
xdr = new XDomainRequest();
xdr.onload = function() {
fn(xdr.responseText);
};
xdr.onerror = function() {
// Since if onerror is called, onload is not called,
// fn which triggers poll request is also not called and the connection ends here
self.fire("error", new Error()).fire("close");
};
xdr.open("GET", url);
xdr.send();
};
self.abort = function() {
xdr.abort();
};
return self;
}
var jsonpCallbacks = [];
// Only for IE 9
function createHttpLongpollJsonpTransport(uri, options) {
var script;
var self = createHttpLongpollBaseTransport(uri, options);
var callback = jsonpCallbacks.pop() || ("socket_" + (guid++));
self.on("close", function() {
delete window[callback];
jsonpCallbacks.push(callback);
});
self.poll = function(url, fn) {
window[callback] = function(data) {
fn(data);
};
script = document.createElement("script");
script.async = true;
// In fact, jsonp and callback params are only for first request
script.src = util.stringifyURI(url, {
"cettia-transport-jsonp": "true",
"cettia-transport-callback": callback
});
script.onload = script.onerror = function(event) {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
if (event.type === "error") {
self.fire("error", new Error()).fire("close");
}
};
document.head.appendChild(script);
};
self.abort = function() {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
};
return self;
}
// Socket instances
var sockets = [];
// For browser environment
util.on(window, "unload", function() {
var socket;
for (var i = 0; i < sockets.length; i++) {
socket = sockets[i];
// Closes a socket as the document is unloaded
if (socket.state() !== "closed") {
socket.close();
}
}
});
util.on(window, "online", function() {
var socket;
for (var i = 0; i < sockets.length; i++) {
socket = sockets[i];
// Opens a socket because of no reason to wait
if (socket.state() === "waiting") {
socket.open();
}
}
});
util.on(window, "offline", function() {
var socket;
for (var i = 0; i < sockets.length; i++) {
socket = sockets[i];
// Fires a close event immediately
if (socket.state() === "opened") {
// The underlying transport will detect disconnection and fire close event after a few
// seconds
socket.fire("error", new Error()).fire("close");
}
}
});
// Defines the module
const Cettia = {
// Creates a socket and connects to the server
open: function(uris, options) {
// Opens a new socket
var socket = createSocket(uris, options);
sockets.push(socket);
return socket;
},
// Defines the transport module
transport: {
// Creates a WebSocket transport
createWebSocketTransport: createWebSocketTransport,
// Creates a HTTP streaming transport
createHttpStreamTransport: createHttpStreamTransport,
// Creates a HTTP long polling transport
createHttpLongpollTransport: createHttpLongpollTransport
},
// To help debug or apply hotfix only
util: util
};
module.exports = Cettia;