atom-nuclide
Version:
A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.
416 lines (363 loc) • 13.7 kB
JavaScript
Object.defineProperty(exports, '__esModule', {
value: true
});
/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { var callNext = step.bind(null, 'next'); var callThrow = step.bind(null, 'throw'); function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(callNext, callThrow); } } callNext(); }); }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _url2;
function _url() {
return _url2 = _interopRequireDefault(require('url'));
}
var _ws2;
function _ws() {
return _ws2 = _interopRequireDefault(require('ws'));
}
var _uuid2;
function _uuid() {
return _uuid2 = _interopRequireDefault(require('uuid'));
}
var _eventKit2;
function _eventKit() {
return _eventKit2 = require('event-kit');
}
var _WebSocketTransport2;
function _WebSocketTransport() {
return _WebSocketTransport2 = require('./WebSocketTransport');
}
var _QueuedTransport2;
function _QueuedTransport() {
return _QueuedTransport2 = require('./QueuedTransport');
}
var _XhrConnectionHeartbeat2;
function _XhrConnectionHeartbeat() {
return _XhrConnectionHeartbeat2 = require('./XhrConnectionHeartbeat');
}
var _assert2;
function _assert() {
return _assert2 = _interopRequireDefault(require('assert'));
}
var _commonsNodeEvent2;
function _commonsNodeEvent() {
return _commonsNodeEvent2 = require('../../commons-node/event');
}
var _commonsNodeString2;
function _commonsNodeString() {
return _commonsNodeString2 = require('../../commons-node/string');
}
var _nuclideLogging2;
function _nuclideLogging() {
return _nuclideLogging2 = require('../../nuclide-logging');
}
var logger = (0, (_nuclideLogging2 || _nuclideLogging()).getLogger)();
var PING_SEND_INTERVAL = 5000;
var PING_WAIT_INTERVAL = 5000;
var INITIAL_RECONNECT_TIME_MS = 10;
var MAX_RECONNECT_TIME_MS = 5000;
// The Nuclide Socket class does several things:
// - Provides a transport mechanism for sending/receiving JSON messages
// - Provides a transport layer for xhr requests
// - monitors connection with a heartbeat (over xhr) and automatically attempts to reconnect
// - caches JSON messages when the connection is down and retries on reconnect
//
// Can be in one of the following states:
// - Connected - everything healthy
// - Disconnected - Was connected, but connection died. Will attempt to reconnect.
// - Closed - No longer connected. May not send/recieve messages. Cannot be resurected.
//
// Publishes the following events:
// - status(boolean): on connect/disconnect
// - connect: on first Connection
// - reconnect: on reestablishing connection after a disconnect
// - message(message: Object): on receipt fo JSON message
// - heartbeat: On receipt of successful heartbeat
// - heartbeat.error({code, originalCode, message}): On failure of heartbeat
var NuclideSocket = (function () {
function NuclideSocket(serverUri, options) {
var _this = this;
_classCallCheck(this, NuclideSocket);
this._emitter = new (_eventKit2 || _eventKit()).Emitter();
this._serverUri = serverUri;
this._options = options;
this.id = (_uuid2 || _uuid()).default.v4();
this._pingTimer = null;
this._reconnectTime = INITIAL_RECONNECT_TIME_MS;
this._reconnectTimer = null;
this._previouslyConnected = false;
var transport = new (_QueuedTransport2 || _QueuedTransport()).QueuedTransport(this.id);
this._transport = transport;
transport.onDisconnect(function () {
if (_this.isDisconnected()) {
_this._emitter.emit('status', false);
_this._emitter.emit('disconnect');
_this._scheduleReconnect();
}
});
var _default$parse = (_url2 || _url()).default.parse(serverUri);
var protocol = _default$parse.protocol;
var host = _default$parse.host;
// TODO verify that `host` is non-null rather than using maybeToString
this._websocketUri = 'ws' + (protocol === 'https:' ? 's' : '') + '://' + (0, (_commonsNodeString2 || _commonsNodeString()).maybeToString)(host);
this._heartbeat = new (_XhrConnectionHeartbeat2 || _XhrConnectionHeartbeat()).XhrConnectionHeartbeat(serverUri, options);
this._heartbeat.onConnectionRestored(function () {
if (_this.isDisconnected()) {
_this._scheduleReconnect();
}
});
this._reconnect();
}
_createClass(NuclideSocket, [{
key: 'isConnected',
value: function isConnected() {
return this._transport != null && this._transport.getState() === 'open';
}
}, {
key: 'isDisconnected',
value: function isDisconnected() {
return this._transport != null && this._transport.getState() === 'disconnected';
}
}, {
key: 'waitForConnect',
value: function waitForConnect() {
var _this2 = this;
return new Promise(function (resolve, reject) {
if (_this2.isConnected()) {
return resolve();
} else {
_this2.onConnect(resolve);
_this2.onReconnect(resolve);
}
});
}
}, {
key: '_reconnect',
value: function _reconnect() {
var _this3 = this;
(0, (_assert2 || _assert()).default)(this.isDisconnected());
var websocket = new (_ws2 || _ws()).default(this._websocketUri, this._options);
// Need to add this otherwise unhandled errors during startup will result
// in uncaught exceptions. This is due to EventEmitter treating 'error'
// events specially.
var onSocketError = function onSocketError(error) {
logger.error('WebSocket Error - attempting connection... ' + error.message);
};
websocket.on('error', onSocketError);
var onSocketOpen = _asyncToGenerator(function* () {
var sendIdResult = yield sendOneMessage(websocket, _this3.id);
switch (sendIdResult.kind) {
case 'close':
websocket.close();
logger.info('WebSocket closed before sending id handshake');
if (_this3.isDisconnected()) {
logger.info('WebSocket reconnecting after closed.');
_this3._scheduleReconnect();
}
break;
case 'error':
websocket.close();
logger.error('WebSocket Error before sending id handshake', sendIdResult.message);
if (_this3.isDisconnected()) {
logger.info('WebSocket reconnecting after error.');
_this3._scheduleReconnect();
}
break;
case 'success':
if (_this3.isDisconnected()) {
(function () {
var ws = new (_WebSocketTransport2 || _WebSocketTransport()).WebSocketTransport(_this3.id, websocket);
var pingId = (_uuid2 || _uuid()).default.v4();
ws.onClose(function () {
_this3._clearPingTimer();
});
ws.onError(function (error) {
ws.close();
});
ws.onPong(function (data) {
if (pingId === data) {
_this3._schedulePing(pingId, ws);
} else {
logger.error('pingId mismatch');
}
});
ws.onMessage().subscribe(function () {
_this3._schedulePing(pingId, ws);
});
_this3._schedulePing(pingId, ws);
(0, (_assert2 || _assert()).default)(_this3._transport != null);
_this3._transport.reconnect(ws);
websocket.removeListener('error', onSocketError);
_this3._emitter.emit('status', true);
if (_this3._previouslyConnected) {
logger.info('WebSocket reconnected');
_this3._emitter.emit('reconnect');
} else {
logger.info('WebSocket connected');
_this3._emitter.emit('connect');
}
_this3._previouslyConnected = true;
_this3._reconnectTime = INITIAL_RECONNECT_TIME_MS;
})();
}
break;
}
});
websocket.on('open', onSocketOpen);
}
}, {
key: '_schedulePing',
value: function _schedulePing(data, ws) {
var _this4 = this;
this._clearPingTimer();
this._pingTimer = setTimeout(function () {
ws.ping(data);
_this4._pingTimer = setTimeout(function () {
logger.error('Failed to receive pong in response to ping');
ws.close();
}, PING_WAIT_INTERVAL);
}, PING_SEND_INTERVAL);
}
}, {
key: '_clearPingTimer',
value: function _clearPingTimer() {
if (this._pingTimer != null) {
clearTimeout(this._pingTimer);
this._pingTimer = null;
}
}
}, {
key: '_scheduleReconnect',
value: function _scheduleReconnect() {
var _this5 = this;
if (this._reconnectTimer) {
return;
}
// Exponential reconnect time trials.
this._reconnectTimer = setTimeout(function () {
_this5._reconnectTimer = null;
if (_this5.isDisconnected()) {
_this5._reconnect();
}
}, this._reconnectTime);
this._reconnectTime = this._reconnectTime * 2;
if (this._reconnectTime > MAX_RECONNECT_TIME_MS) {
this._reconnectTime = MAX_RECONNECT_TIME_MS;
}
}
}, {
key: '_clearReconnectTimer',
value: function _clearReconnectTimer() {
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer);
this._reconnectTimer = null;
}
}
}, {
key: 'send',
value: function send(message) {
(0, (_assert2 || _assert()).default)(this._transport != null);
this._transport.send(message);
}
// Resolves if the connection looks healthy.
// Will reject quickly if the connection looks unhealthy.
}, {
key: 'testConnection',
value: function testConnection() {
return this._heartbeat.sendHeartBeat();
}
}, {
key: 'getServerUri',
value: function getServerUri() {
return this._serverUri;
}
}, {
key: 'getServerPort',
value: function getServerPort() {
var _default$parse2 = (_url2 || _url()).default.parse(this.getServerUri());
var port = _default$parse2.port;
if (port == null) {
return null;
}
return Number(port);
}
}, {
key: 'close',
value: function close() {
var transport = this._transport;
if (transport != null) {
this._transport = null;
transport.close();
}
this._clearReconnectTimer();
this._reconnectTime = INITIAL_RECONNECT_TIME_MS;
this._heartbeat.close();
}
}, {
key: 'onHeartbeat',
value: function onHeartbeat(callback) {
return this._heartbeat.onHeartbeat(callback);
}
}, {
key: 'onHeartbeatError',
value: function onHeartbeatError(callback) {
return this._heartbeat.onHeartbeatError(callback);
}
}, {
key: 'onMessage',
value: function onMessage() {
(0, (_assert2 || _assert()).default)(this._transport != null);
return this._transport.onMessage();
}
}, {
key: 'onStatus',
value: function onStatus(callback) {
return this._emitter.on('status', callback);
}
}, {
key: 'onConnect',
value: function onConnect(callback) {
return this._emitter.on('connect', callback);
}
}, {
key: 'onReconnect',
value: function onReconnect(callback) {
return this._emitter.on('reconnect', callback);
}
}, {
key: 'onDisconnect',
value: function onDisconnect(callback) {
return this._emitter.on('disconnect', callback);
}
}]);
return NuclideSocket;
})();
exports.NuclideSocket = NuclideSocket;
function sendOneMessage(socket, message) {
return new Promise(function (resolve, reject) {
function finish(result) {
onError.dispose();
onClose.dispose();
resolve(result);
}
var onError = (0, (_commonsNodeEvent2 || _commonsNodeEvent()).attachEvent)(socket, 'event', function (err) {
return finish({ kind: 'error', message: err });
});
var onClose = (0, (_commonsNodeEvent2 || _commonsNodeEvent()).attachEvent)(socket, 'close', function () {
return finish({ kind: 'close' });
});
socket.send(message, function (error) {
if (error == null) {
finish({ kind: 'success' });
} else {
finish({ kind: 'error', message: error.toString() });
}
});
});
}
// ID from a setTimeout() call.