UNPKG

@mixer/interactive-node

Version:

A NodeJS and Browser compatible client for mixer.com's interactive 2 Protocol

310 lines 12.6 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); var events_1 = require("events"); var Url = require("url"); var errors_1 = require("../errors"); var util_1 = require("../util"); var packets_1 = require("./packets"); var reconnection_1 = require("./reconnection"); /** * SocketState is used to record the status of the websocket connection. */ var SocketState; (function (SocketState) { /** * A connection attempt has not been made yet. */ SocketState[SocketState["Idle"] = 1] = "Idle"; /** * A connection attempt is currently being made. */ SocketState[SocketState["Connecting"] = 2] = "Connecting"; /** * The socket is connection and data may be sent. */ SocketState[SocketState["Connected"] = 3] = "Connected"; /** * The socket is gracefully closing; after this it will become Idle. */ SocketState[SocketState["Closing"] = 4] = "Closing"; /** * The socket is reconnecting after closing unexpectedly. */ SocketState[SocketState["Reconnecting"] = 5] = "Reconnecting"; /** * Connect was called whilst the old socket was still open. */ SocketState[SocketState["Refreshing"] = 6] = "Refreshing"; })(SocketState = exports.SocketState || (exports.SocketState = {})); function getDefaults() { return { urls: [], replyTimeout: 10000, compressionScheme: 'none', autoReconnect: true, reconnectionPolicy: new reconnection_1.ExponentialReconnectionPolicy(), pingInterval: 10 * 1000, extraHeaders: {}, queryParams: {}, reconnectChecker: function () { return Promise.resolve(); }, }; } var InteractiveSocket = /** @class */ (function (_super) { __extends(InteractiveSocket, _super); function InteractiveSocket(options) { if (options === void 0) { options = {}; } var _this = _super.call(this) || this; _this.state = SocketState.Idle; _this.queue = new Set(); _this.lastSequenceNumber = 0; _this.endpointIndex = 0; _this.setMaxListeners(Infinity); _this.setOptions(options); if (InteractiveSocket.WebSocket === undefined) { throw new Error('Cannot find a websocket implementation; please provide one by ' + 'running InteractiveSocket.WebSocket = myWebSocketModule;'); } _this.on('message', function (msg) { _this.extractMessage(msg); }); _this.on('open', function () { _this.options.reconnectionPolicy.reset(); _this.state = SocketState.Connected; _this.queue.forEach(function (data) { return _this.send(data); }); }); _this.on('close', function (evt) { // If this close event's code is an application error (e.g. bad authentication) // or invalid status code (for Edge), we raise it as an error and refuse to // reconnect. if (evt.code < 1000 || evt.code > 1999 || evt.code === 1005 || evt.code === 4027) { var err = errors_1.InteractiveError.fromSocketMessage({ code: evt.code, message: evt.reason, }); _this.state = SocketState.Closing; _this.emit('error', err); // Refuse to continue, these errors usually mean something is very wrong with our connection. return; } if (_this.state === SocketState.Refreshing) { _this.state = SocketState.Idle; _this.options.reconnectChecker().then(function () { return _this.connect(); }); return; } if (_this.state === SocketState.Closing || !_this.options.autoReconnect) { _this.state = SocketState.Idle; return; } _this.state = SocketState.Reconnecting; _this.reconnectTimeout = setTimeout(function () { _this.options.reconnectChecker().then(function () { return _this.connect(); }); }, _this.options.reconnectionPolicy.next()); }); return _this; } /** * Get the options the socket is using. */ InteractiveSocket.prototype.getOptions = function () { return this.options; }; /** * Set the given options. * Defaults and previous option values will be used if not supplied. */ InteractiveSocket.prototype.setOptions = function (options) { this.options = Object.assign({}, this.options || getDefaults(), options); }; /** * Open a new socket connection. By default, the socket will auto * connect when creating a new instance. */ InteractiveSocket.prototype.connect = function () { var _this = this; if (this.state === SocketState.Closing) { this.state = SocketState.Refreshing; return this; } var defaultHeaders = { 'X-Protocol-Version': '2.0', }; var headers = Object.assign({}, defaultHeaders, this.options.extraHeaders); var extras = { headers: headers, }; var url = Url.parse(this.getURL(), true); // Clear out search so it populates query using the query // https://nodejs.org/api/url.html#url_url_format_urlobject url.search = null; if (this.options.authToken) { extras.headers['Authorization'] = "Bearer " + this.options.authToken; } if (typeof WebSocket === 'function' && WebSocket === InteractiveSocket.WebSocket) { url.query = Object.assign({}, url.query, this.options.queryParams, extras.headers); this.socket = new InteractiveSocket.WebSocket(Url.format(url)); } else { url.query = Object.assign({}, url.query, this.options.queryParams); this.socket = new InteractiveSocket.WebSocket(Url.format(url), [], extras); } this.state = SocketState.Connecting; this.socket.addEventListener('close', function (evt) { return _this.emit('close', evt); }); this.socket.addEventListener('open', function () { return _this.emit('open'); }); this.socket.addEventListener('message', function (evt) { return _this.emit('message', evt.data); }); this.socket.addEventListener('error', function (err) { if (_this.state === SocketState.Closing) { // Ignore errors on a closing socket. return; } _this.emit('error', err); }); return this; }; /** * Returns the current state of the socket. * @return {State} */ InteractiveSocket.prototype.getState = function () { return this.state; }; /** * Close gracefully shuts down the websocket. */ InteractiveSocket.prototype.close = function () { if (this.state === SocketState.Reconnecting) { clearTimeout(this.reconnectTimeout); this.state = SocketState.Idle; return; } if (this.state !== SocketState.Idle) { this.state = SocketState.Closing; this.socket.close(1000, 'Closed normally.'); this.queue.forEach(function (packet) { return packet.cancel(); }); this.queue.clear(); } }; /** * Executes an RPC method on the server. Returns a promise which resolves * after it completes, or after a timeout occurs. */ InteractiveSocket.prototype.execute = function (method, params, discard) { if (params === void 0) { params = {}; } if (discard === void 0) { discard = false; } var methodObj = new packets_1.Method(method, params, discard); return this.send(new packets_1.Packet(methodObj)); }; /** * Send emits a Method over the websocket, wrapped in a Packet to provide queueing and * cancellation. It returns a promise which resolves with the reply payload from the Server. */ InteractiveSocket.prototype.send = function (packet) { var _this = this; if (packet.getState() === packets_1.PacketState.Cancelled) { return Promise.reject(new errors_1.CancelledError()); } this.queue.add(packet); // If the socket has not said hello, queue the request and return // the promise eventually emitted when it is sent. if (this.state !== SocketState.Connected) { return Promise.race([ util_1.resolveOn(packet, 'send'), util_1.resolveOn(packet, 'cancel').then(function () { throw new errors_1.CancelledError(); }), ]); } var timeout = packet.getTimeout(this.options.replyTimeout); var promise = Promise.race([ // Wait for replies to that packet ID: util_1.resolveOn(this, "reply:" + packet.id(), timeout) .then(function (result) { _this.queue.delete(packet); if (result.error) { throw result.error; } return result.result; }) .catch(function (err) { _this.queue.delete(packet); throw err; }), // Never resolve if the consumer cancels the packets: util_1.resolveOn(packet, 'cancel', timeout + 1).then(function () { throw new errors_1.CancelledError(); }), // Re-queue packets if the socket closes: util_1.resolveOn(this, 'close', timeout + 1).then(function () { if (!_this.queue.has(packet)) { // skip if we already resolved return null; } packet.setState(packets_1.PacketState.Pending); return _this.send(packet); }), ]); packet.emit('send', promise); packet.setState(packets_1.PacketState.Sending); this.sendPacketInner(packet); return promise; }; InteractiveSocket.prototype.reply = function (reply) { this.sendRaw(reply); }; InteractiveSocket.prototype.sendPacketInner = function (packet) { this.sendRaw(packet.setSequenceNumber(this.lastSequenceNumber)); }; InteractiveSocket.prototype.sendRaw = function (packet) { var data = JSON.stringify(packet); var payload = data; this.emit('send', payload); this.socket.send(payload); }; InteractiveSocket.prototype.getURL = function () { var addresses = this.options.urls; return this.options.urls[this.endpointIndex++ % addresses.length]; }; InteractiveSocket.prototype.extractMessage = function (packet) { var messageString; messageString = packet; var message; try { message = JSON.parse(messageString); } catch (err) { throw new errors_1.MessageParseError('Message returned was not valid JSON'); } if (message.hasOwnProperty('seq')) { this.lastSequenceNumber = message.seq; } switch (message.type) { case 'method': this.emit('method', packets_1.Method.fromSocket(message)); break; case 'reply': this.emit("reply:" + message.id, packets_1.Reply.fromSocket(message)); break; default: throw new errors_1.MessageParseError("Unknown message type \"" + message.type + "\""); } }; InteractiveSocket.prototype.getQueueSize = function () { return this.queue.size; }; // WebSocket constructor, may be overridden if the environment // does not natively support it. //tslint:disable-next-line:variable-name InteractiveSocket.WebSocket = typeof WebSocket === 'undefined' ? null : WebSocket; return InteractiveSocket; }(events_1.EventEmitter)); exports.InteractiveSocket = InteractiveSocket; //# sourceMappingURL=Socket.js.map