UNPKG

@omneedia/socketcluster

Version:

SocketCluster - A Highly parallelized WebSocket server cluster to make the most of multi-core machines/instances.

445 lines (370 loc) 12.1 kB
var Emitter = require('component-emitter'); var Response = require('./response').Response; var querystring = require('querystring'); var WebSocket; var createWebSocket; if (global.WebSocket) { WebSocket = global.WebSocket; createWebSocket = function (uri, options) { return new WebSocket(uri); }; } else { WebSocket = require('ws'); createWebSocket = function (uri, options) { return new WebSocket(uri, null, options); }; } var scErrors = require('sc-errors'); var TimeoutError = scErrors.TimeoutError; var BadConnectionError = scErrors.BadConnectionError; var SCTransport = function (authEngine, codecEngine, options) { var self = this; this.state = this.CLOSED; this.auth = authEngine; this.codec = codecEngine; this.options = options; this.connectTimeout = options.connectTimeout; this.pingTimeout = options.ackTimeout; this.pingTimeoutDisabled = !!options.pingTimeoutDisabled; this.callIdGenerator = options.callIdGenerator; this.authTokenName = options.authTokenName; this._pingTimeoutTicker = null; this._callbackMap = {}; this._batchSendList = []; // Open the connection. this.state = this.CONNECTING; var uri = this.uri(); var wsSocket = createWebSocket(uri, this.options); wsSocket.binaryType = this.options.binaryType; this.socket = wsSocket; wsSocket.onopen = function () { self._onOpen(); }; wsSocket.onclose = function (event) { var code; if (event.code == null) { // This is to handle an edge case in React Native whereby // event.code is undefined when the mobile device is locked. // TODO: This is not perfect since this condition could also apply to // an abnormal close (no close control frame) which would be a 1006. code = 1005; } else { code = event.code; } self._onClose(code, event.reason); }; wsSocket.onmessage = function (message, flags) { self._onMessage(message.data); }; wsSocket.onerror = function (error) { // The onclose event will be called automatically after the onerror event // if the socket is connected - Otherwise, if it's in the middle of // connecting, we want to close it manually with a 1006 - This is necessary // to prevent inconsistent behavior when running the client in Node.js // vs in a browser. if (self.state === self.CONNECTING) { self._onClose(1006); } }; this._connectTimeoutRef = setTimeout(function () { self._onClose(4007); self.socket.close(4007); }, this.connectTimeout); }; SCTransport.prototype = Object.create(Emitter.prototype); SCTransport.CONNECTING = SCTransport.prototype.CONNECTING = 'connecting'; SCTransport.OPEN = SCTransport.prototype.OPEN = 'open'; SCTransport.CLOSED = SCTransport.prototype.CLOSED = 'closed'; SCTransport.prototype.uri = function () { var query = this.options.query || {}; var schema = this.options.secure ? 'wss' : 'ws'; if (this.options.timestampRequests) { query[this.options.timestampParam] = (new Date()).getTime(); } query = querystring.encode(query); if (query.length) { query = '?' + query; } var host; if (this.options.host) { host = this.options.host; } else { var port = ''; if (this.options.port && ((schema === 'wss' && this.options.port !== 443) || (schema === 'ws' && this.options.port !== 80))) { port = ':' + this.options.port; } host = this.options.hostname + port; } return schema + '://' + host + this.options.path + query; }; SCTransport.prototype._onOpen = function () { var self = this; clearTimeout(this._connectTimeoutRef); this._resetPingTimeout(); this._handshake(function (err, status) { if (err) { var statusCode; if (status && status.code) { statusCode = status.code; } else { statusCode = 4003; } self._onError(err); self._onClose(statusCode, err.toString()); self.socket.close(statusCode); } else { self.state = self.OPEN; Emitter.prototype.emit.call(self, 'open', status); self._resetPingTimeout(); } }); }; SCTransport.prototype._handshake = function (callback) { var self = this; this.auth.loadToken(this.authTokenName, function (err, token) { if (err) { callback(err); } else { // Don't wait for this.state to be 'open'. // The underlying WebSocket (this.socket) is already open. var options = { force: true }; self.emit('#handshake', { authToken: token }, options, function (err, status) { if (status) { // Add the token which was used as part of authentication attempt // to the status object. status.authToken = token; if (status.authError) { status.authError = scErrors.hydrateError(status.authError); } } callback(err, status); }); } }); }; SCTransport.prototype._abortAllPendingEventsDueToBadConnection = function (failureType) { for (var i in this._callbackMap) { if (this._callbackMap.hasOwnProperty(i)) { var eventObject = this._callbackMap[i]; delete this._callbackMap[i]; clearTimeout(eventObject.timeout); delete eventObject.timeout; var errorMessage = "Event '" + eventObject.event + "' was aborted due to a bad connection"; var badConnectionError = new BadConnectionError(errorMessage, failureType); var callback = eventObject.callback; delete eventObject.callback; callback.call(eventObject, badConnectionError, eventObject); } } }; SCTransport.prototype._onClose = function (code, data) { delete this.socket.onopen; delete this.socket.onclose; delete this.socket.onmessage; delete this.socket.onerror; clearTimeout(this._connectTimeoutRef); clearTimeout(this._pingTimeoutTicker); clearTimeout(this._batchTimeout); if (this.state === this.OPEN) { this.state = this.CLOSED; Emitter.prototype.emit.call(this, 'close', code, data); this._abortAllPendingEventsDueToBadConnection('disconnect'); } else if (this.state === this.CONNECTING) { this.state = this.CLOSED; Emitter.prototype.emit.call(this, 'openAbort', code, data); this._abortAllPendingEventsDueToBadConnection('connectAbort'); } }; SCTransport.prototype._handleEventObject = function (obj, message) { if (obj && obj.event != null) { var response = new Response(this, obj.cid); Emitter.prototype.emit.call(this, 'event', obj.event, obj.data, response); } else if (obj && obj.rid != null) { var eventObject = this._callbackMap[obj.rid]; if (eventObject) { clearTimeout(eventObject.timeout); delete eventObject.timeout; delete this._callbackMap[obj.rid]; if (eventObject.callback) { var rehydratedError = scErrors.hydrateError(obj.error); eventObject.callback(rehydratedError, obj.data); } } } else { Emitter.prototype.emit.call(this, 'event', 'raw', message); } }; SCTransport.prototype._onMessage = function (message) { Emitter.prototype.emit.call(this, 'event', 'message', message); var obj = this.decode(message); // If ping if (obj === '#1') { this._resetPingTimeout(); if (this.socket.readyState === this.socket.OPEN) { this.sendObject('#2'); } } else { if (Array.isArray(obj)) { var len = obj.length; for (var i = 0; i < len; i++) { this._handleEventObject(obj[i], message); } } else { this._handleEventObject(obj, message); } } }; SCTransport.prototype._onError = function (err) { Emitter.prototype.emit.call(this, 'error', err); }; SCTransport.prototype._resetPingTimeout = function () { if (this.pingTimeoutDisabled) { return; } var self = this; var now = (new Date()).getTime(); clearTimeout(this._pingTimeoutTicker); this._pingTimeoutTicker = setTimeout(function () { self._onClose(4000); self.socket.close(4000); }, this.pingTimeout); }; SCTransport.prototype.getBytesReceived = function () { return this.socket.bytesReceived; }; SCTransport.prototype.close = function (code, data) { code = code || 1000; if (this.state === this.OPEN) { var packet = { code: code, data: data }; this.emit('#disconnect', packet); this._onClose(code, data); this.socket.close(code); } else if (this.state === this.CONNECTING) { this._onClose(code, data); this.socket.close(code); } }; SCTransport.prototype.emitObject = function (eventObject, options) { var simpleEventObject = { event: eventObject.event, data: eventObject.data }; if (eventObject.callback) { simpleEventObject.cid = eventObject.cid = this.callIdGenerator(); this._callbackMap[eventObject.cid] = eventObject; } this.sendObject(simpleEventObject, options); return eventObject.cid || null; }; SCTransport.prototype._handleEventAckTimeout = function (eventObject) { if (eventObject.cid) { delete this._callbackMap[eventObject.cid]; } delete eventObject.timeout; var callback = eventObject.callback; if (callback) { delete eventObject.callback; var error = new TimeoutError("Event response for '" + eventObject.event + "' timed out"); callback.call(eventObject, error, eventObject); } }; // The last two optional arguments (a and b) can be options and/or callback SCTransport.prototype.emit = function (event, data, a, b) { var self = this; var callback, options; if (b) { options = a; callback = b; } else { if (a instanceof Function) { options = {}; callback = a; } else { options = a; } } var eventObject = { event: event, data: data, callback: callback }; if (callback && !options.noTimeout) { eventObject.timeout = setTimeout(function () { self._handleEventAckTimeout(eventObject); }, this.options.ackTimeout); } var cid = null; if (this.state === this.OPEN || options.force) { cid = this.emitObject(eventObject, options); } return cid; }; SCTransport.prototype.cancelPendingResponse = function (cid) { delete this._callbackMap[cid]; }; SCTransport.prototype.decode = function (message) { return this.codec.decode(message); }; SCTransport.prototype.encode = function (object) { return this.codec.encode(object); }; SCTransport.prototype.send = function (data) { if (this.socket.readyState !== this.socket.OPEN) { this._onClose(1005); } else { this.socket.send(data); } }; SCTransport.prototype.serializeObject = function (object) { var str, formatError; try { str = this.encode(object); } catch (err) { formatError = err; this._onError(formatError); } if (!formatError) { return str; } return null; }; SCTransport.prototype.sendObjectBatch = function (object) { var self = this; this._batchSendList.push(object); if (this._batchTimeout) { return; } this._batchTimeout = setTimeout(function () { delete self._batchTimeout; if (self._batchSendList.length) { var str = self.serializeObject(self._batchSendList); if (str != null) { self.send(str); } self._batchSendList = []; } }, this.options.pubSubBatchDuration || 0); }; SCTransport.prototype.sendObjectSingle = function (object) { var str = this.serializeObject(object); if (str != null) { this.send(str); } }; SCTransport.prototype.sendObject = function (object, options) { if (options && options.batch) { this.sendObjectBatch(object); } else { this.sendObjectSingle(object); } }; module.exports.SCTransport = SCTransport;