@metacell/geppetto-meta-client
Version:
Geppetto web frontend. Geppetto is an open-source platform to build web-based tools to visualize and simulate neuroscience data and models.
359 lines (343 loc) • 13 kB
JavaScript
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/*
*
* WebSocket class use for communication between client and server
*
* @author Jesus R. Martinez (jesus@metacell.us)
*/
import Resources from '@metacell/geppetto-meta-core/Resources';
import EventManager from '@metacell/geppetto-meta-client/common/EventManager';
var connectionInterval = 300;
import pako from 'pako';
import FileSaver from 'file-saver';
var callbackHandler = {};
var messageTypes = {
CLIENT_ID: "client_id",
RECONNECTION_ERROR: "reconnection_error"
};
/**
* Web socket creation and communication
*/
export var MessageSocket = /*#__PURE__*/function () {
function MessageSocket() {
_classCallCheck(this, MessageSocket);
_defineProperty(this, "socket", null);
_defineProperty(this, "clientID", null);
_defineProperty(this, "nextID", 0);
// sets protocol to use for connection
_defineProperty(this, "protocol", window.location.protocol === 'https:' ? "wss://" : "ws://");
// flag used to connect using ws protocol if wss failed
_defineProperty(this, "failsafe", false);
// vars used for reconnection
_defineProperty(this, "attempts", 0);
_defineProperty(this, "host", undefined);
_defineProperty(this, "projectId", undefined);
_defineProperty(this, "lostConnectionId", undefined);
_defineProperty(this, "reconnectionLimit", 10);
_defineProperty(this, "autoReconnectInterval", 5 * 1000);
_defineProperty(this, "socketStatus", Resources.SocketStatus.CLOSE);
this.connect = this.connect.bind(this);
this.reconnect = this.reconnect.bind(this);
this.send = this.send.bind(this);
}
return _createClass(MessageSocket, [{
key: "connect",
value: function connect(host) {
var _this = this;
if (this.socket !== null) {
delete this.socket;
}
if ('WebSocket' in window) {
this.socket = new WebSocket(host);
this.host = host;
this.socket.binaryType = "arraybuffer";
} else if ('MozWebSocket' in window) {
this.socket = new MozWebSocket(host);
} else {
console.log(Resources.WEBSOCKET_NOT_SUPPORTED, true);
return;
}
this.socket.onopen = function (e) {
console.log(Resources.WEBSOCKET_OPENED, true);
/*
* attach the handlers once socket is opened on the first connection
* differently handle the reconnection scenario
*/
if (_this.lostConnectionId) {
var parameters = {};
parameters["connectionID"] = _this.lostConnectionId;
parameters["projectId"] = _this.projectId;
_this.send("reconnect", parameters);
_this.lostConnectionId = undefined;
}
// Reset the counter for reconnection
_this.attempts = 0;
_this.socketStatus = Resources.SocketStatus.OPEN;
console.log("%c WebSocket Status - Opened ", 'background: #444; color: #bada55');
};
this.socket.onclose = function (e) {
switch (e.code) {
case 1000:
_this.socketStatus = Resources.SocketStatus.CLOSE;
console.log(Resources.WEBSOCKET_CLOSED, true);
break;
default:
if (_this.lostConnectionId === undefined) {
_this.lostConnectionId = _this.getClientID();
}
_this.reconnect(e);
}
};
this.socket.onmessage = function (msg) {
var messageData = msg.data;
if (messageData == "ping") {
return;
}
// if it's a binary (possibly compressed) then determine its type and process it
if (messageData instanceof ArrayBuffer) {
_this.processBinaryMessage(messageData);
// otherwise, for a text message, parse it and notify listeners
} else {
// a non compressed message
_this.parseAndNotify(messageData);
}
};
// Detects problems when connecting to Geppetto server
this.socket.onerror = function (e) {
var message = Resources.SERVER_CONNECTION_ERROR;
/*
* Attempt to connect using ws first time wss fails,
* if ws fails too then don't try again and display info error window
*/
if (_this.failsafe) {
_this.protocol = "ws://";
_this.failsafe = true;
_this.connect(_this.protocol + window.location.host + '/' + GEPPETTO_CONFIGURATION.contextPath + '/GeppettoServlet');
} else {
switch (e.code) {
case 'ECONNREFUSED':
console.log("%c WebSocket Status - Open connection error ", 'background: #000; color: red');
console.log(Resources.WEBSOCKET_CONNECTION_ERROR, true);
break;
case undefined:
console.log("%c WebSocket Status - Open connection error ", 'background: #000; color: red');
console.log(Resources.WEBSOCKET_RECONNECTION, true);
break;
default:
console.log("%c WebSocket Status - Closed ", 'background: #000; color: red');
_this.socketStatus = Resources.SocketStatus.CLOSE;
break;
}
}
};
}
/**
* Attempt to reconnect to the backend
*/
}, {
key: "reconnect",
value: function reconnect(e) {
var _this2 = this;
if (this.attempts < this.reconnectionLimit) {
this.attempts++;
this.socketStatus = Resources.SocketStatus.RECONNECTING;
console.log("WebSocket Status - retry in ".concat(this.autoReconnectInterval, "ms"), e);
setTimeout(function () {
console.log("%c WebSocket Status - reconnecting... ", 'background: #444; color: #bada55');
_this2.connect(_this2.host);
}, this.autoReconnectInterval);
} else {
this.socketStatus = Resources.SocketStatus.CLOSE;
console.log(Resources.WEBSOCKET_CLOSED, true);
EventManager.actionsHandler[EventManager.clientActions.WEBSOCKET_DISCONNECTED]();
}
}
/**
* Sends messages to the server
*/
}, {
key: "send",
value: function send(command, parameter, callback) {
if (this.socketStatus === Resources.SocketStatus.RECONNECTING && command !== "reconnect") {
EventManager.actionsHandler[EventManager.clientActions.STOP_LOGO]();
return;
}
var requestID = this.createRequestID();
this.waitForConnection(messageTemplate(requestID, command, parameter), connectionInterval);
// add callback with request id if any
if (callback != undefined) {
callbackHandler[requestID] = callback;
}
return requestID;
}
}, {
key: "waitForConnection",
value: function waitForConnection(messageTemplate, interval) {
if (this.isReady() === 1) {
this.socket.send(messageTemplate);
} else if (this.isReady() > 1) {
// connection is either closing (2) or already closed (3).
EventManager.actionsHandler[EventManager.clientActions.WEBSOCKET_DISCONNECTED]();
} else {
// must be in connecting (0) state
var that = this;
setTimeout(function () {
that.waitForConnection(messageTemplate);
}, interval);
}
}
}, {
key: "isReady",
value: function isReady() {
if (this.socket !== null) {
return this.socket.readyState;
} else {
return 0;
}
}
}, {
key: "close",
value: function close() {
this.socket.close();
EventManager.actionsHandler[EventManager.clientActions.WEBSOCKET_DISCONNECTED]();
}
/**
* Sets the id of the client
*/
}, {
key: "setClientID",
value: function setClientID(id) {
this.clientID = id;
}
/**
* Sets the id of the client
*/
}, {
key: "getClientID",
value: function getClientID() {
return this.clientID;
}
/**
* Creates a request id to send with the message to the server
*/
}, {
key: "createRequestID",
value: function createRequestID() {
return this.clientID + "-" + this.nextID++;
}
}, {
key: "loadProjectFromId",
value: function loadProjectFromId(projectId) {
this.send("load_project_from_id", {
projectId: projectId
});
}
}, {
key: "loadProjectFromUrl",
value: function loadProjectFromUrl(projectURL) {
this.send("load_project_from_url", projectURL);
}
}, {
key: "loadProjectFromContent",
value: function loadProjectFromContent(content) {
this.send("load_project_from_content", content);
}
}, {
key: "gzipUncompress",
value: function gzipUncompress(compressedMessage) {
var messageBytes = new Uint8Array(compressedMessage);
var message = pako.ungzip(messageBytes, {
to: "string"
});
return message;
}
/**
* Dispatches through Redux actions all messages received from the socket
* @param {*} messageData
*/
}, {
key: "parseAndNotify",
value: function parseAndNotify(messageData) {
var parsedServerMessage = JSON.parse(messageData);
console.debug("Received websocket message", parsedServerMessage);
var payload = JSON.parse(parsedServerMessage.data);
if (payload[parsedServerMessage.type]) {
try {
payload = JSON.parse(payload[parsedServerMessage.type]);
} catch (e) {
payload = payload[parsedServerMessage.type];
}
}
switch (parsedServerMessage.type) {
case messageTypes.CLIENT_ID:
{
this.setClientID(payload.clientID);
break;
}
case messageTypes.RECONNECTION_ERROR:
{
this.socketStatus = Resources.SocketStatus.CLOSE;
break;
}
default:
break;
}
EventManager.store.dispatch({
type: parsedServerMessage.type,
data: payload
});
// run callback if any
if (parsedServerMessage.requestID != undefined) {
if (callbackHandler[parsedServerMessage.requestID] != undefined) {
callbackHandler[parsedServerMessage.requestID](parsedServerMessage.data);
delete callbackHandler[parsedServerMessage.requestID];
}
}
}
}, {
key: "processBinaryMessage",
value: function processBinaryMessage(message) {
var messageBytes = new Uint8Array(message);
/*
* if it's a binary message and first byte it's zero then assume it's a compressed json string
* otherwise is a file and a 'save as' dialog is opened
*/
if (messageBytes[0] == 0) {
var message = pako.ungzip(messageBytes.subarray(1), {
to: "string"
});
this.parseAndNotify(message);
} else {
var fileNameLength = messageBytes[1];
var fileName = String.fromCharCode.apply(null, messageBytes.subarray(2, 2 + fileNameLength));
var blob = new Blob([message]);
FileSaver.saveAs(blob.slice(2 + fileNameLength), fileName);
}
}
}]);
}();
/**
* Template for Geppetto message
*
* @param msgtype - message type
* @param payload - message payload, can be anything
* @returns JSON stringified object
*/
function messageTemplate(id, msgtype, payload) {
if (!(typeof payload == 'string' || payload instanceof String)) {
payload = JSON.stringify(payload);
}
var object = {
requestID: id,
type: msgtype,
data: payload
};
return JSON.stringify(object);
}
export default new MessageSocket();