UNPKG

carina

Version:

A NodeJS and Browser compatible client for Mixer.com's constellation socket.

322 lines 13.1 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 __()); }; })(); var __assign = (this && this.__assign) || Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); var errors_1 = require("./errors"); var reconnection_1 = require("./reconnection"); var events_1 = require("events"); var packets_1 = require("./packets"); var querystring_1 = require("querystring"); var util_1 = require("./util"); var pako = require("pako"); var carina_1 = require("./carina"); // DO NOT EDIT, THIS IS UPDATE BY THE BUILD SCRIPT var packageVersion = '0.11.2'; // package version /** * SizeThresholdGzipDetector is a GzipDetector which zips all packets longer * than a certain number of bytes. */ var SizeThresholdGzipDetector = /** @class */ (function () { function SizeThresholdGzipDetector(threshold) { this.threshold = threshold; } SizeThresholdGzipDetector.prototype.shouldZip = function (packet) { return packet.length > this.threshold; }; return SizeThresholdGzipDetector; }()); exports.SizeThresholdGzipDetector = SizeThresholdGzipDetector; /** * GzipTransform zips incoming and outgoing messages. */ var GzipTransform = /** @class */ (function () { function GzipTransform(detector) { this.detector = detector; } GzipTransform.prototype.outgoing = function (data, raw) { if (this.detector.shouldZip(data, raw)) { return pako.gzip(data); } return data; }; GzipTransform.prototype.incoming = function (data) { if (typeof data !== 'string') { return pako.ungzip(data, { to: 'string' }); } return data; }; return GzipTransform; }()); exports.GzipTransform = GzipTransform; /** * State is used to record the status of the websocket connection. */ var State; (function (State) { // a connection attempt has not been made yet State[State["Idle"] = 1] = "Idle"; // a connection attempt is currently being made State[State["Connecting"] = 2] = "Connecting"; // the socket is connection and data may be sent State[State["Connected"] = 3] = "Connected"; // the socket is gracefully closing; after this it will become Idle State[State["Closing"] = 4] = "Closing"; // the socket is reconnecting after closing unexpectedly State[State["Reconnecting"] = 5] = "Reconnecting"; // connect was called whilst the old socket was still open State[State["Refreshing"] = 6] = "Refreshing"; })(State = exports.State || (exports.State = {})); function getDefaults() { return { url: 'wss://constellation.mixer.com', userAgent: "Carina " + packageVersion, replyTimeout: 10000, isBot: false, autoReconnect: true, reconnectionPolicy: new reconnection_1.ExponentialReconnectionPolicy(), pingInterval: 10 * 1000, maxEventListeners: 30, }; } var jwtValidator = /^[\w_-]+?\.[\w_-]+?\.([\w_-]+)?$/i; /** * The ConstellationSocket provides a somewhat low-level RPC framework for * interacting with Constellation over a websocket. It also provides * reconnection logic. */ var ConstellationSocket = /** @class */ (function (_super) { __extends(ConstellationSocket, _super); function ConstellationSocket(options) { if (options === void 0) { options = {}; } var _this = _super.call(this) || this; _this.state = State.Idle; _this.setOptions(options); if (ConstellationSocket.WebSocket === undefined) { throw new Error('Cannot find a websocket implementation; please provide one by ' + 'running ConstellationSocket.WebSocket = myWebSocketModule;'); } _this.on('message', function (msg) { return _this.extractMessage(msg.data); }); _this.on('open', function () { return _this.schedulePing(); }); _this.on('event:hello', function () { _this.options.reconnectionPolicy.reset(); _this.setState(State.Connected); }); _this.on('close', function (err) { return _this.handleSocketClose(err); }); return _this; } /** * Set the given options. * Defaults and previous option values will be used if not supplied. */ ConstellationSocket.prototype.setOptions = function (options) { this.options = __assign({}, getDefaults(), { transform: new GzipTransform(options.gzip || new SizeThresholdGzipDetector(1024)) }, this.options, options); if (this.options.jwt && !jwtValidator.test(this.options.jwt)) { throw new Error('Invalid jwt'); } if (this.options.jwt && this.options.authToken) { throw new Error('Cannot connect to Constellation with both JWT and OAuth token.'); } this.setMaxListeners(options.maxEventListeners || carina_1.DEFAULT_MAX_EVENT_LISTENERS); }; /** * Open a new socket connection. By default, the socket will auto * connect when creating a new instance. */ ConstellationSocket.prototype.connect = function () { var _this = this; var options = this.options; if (this.state === State.Closing) { this.setState(State.Refreshing); return this; } var protocol = options.gzip ? 'cnstl-gzip' : 'cnstl'; var extras = { headers: { 'User-Agent': options.userAgent, 'X-Is-Bot': options.isBot, }, }; var url = options.url, queryString = options.queryString; if (options.authToken) { extras.headers['Authorization'] = "Bearer " + options.authToken; } else if (options.jwt) { queryString = __assign({}, queryString, { jwt: options.jwt }); } url += "?" + querystring_1.stringify(queryString); var socket = new ConstellationSocket.WebSocket(url, protocol, extras); this.socket = socket; this.socket.binaryType = 'arraybuffer'; this.setState(State.Connecting); this.rebroadcastEvent('open'); this.rebroadcastEvent('close'); this.rebroadcastEvent('message'); this.socket.addEventListener('error', function (err) { if (_this.state === State.Closing) { // Ignore errors on a closing socket. return; } _this.emit('error', err); }); return this; }; /** * Returns the current state of the socket. * @return {State} */ ConstellationSocket.prototype.getState = function () { return this.state; }; /** * Close gracefully shuts down the websocket. */ ConstellationSocket.prototype.close = function () { if (this.state === State.Reconnecting) { clearTimeout(this.reconnectTimeout); this.setState(State.Idle); return; } if (this.state !== State.Idle) { this.setState(State.Closing); this.socket.close(); clearTimeout(this.pingTimeout); } }; /** * Executes an RPC method on the server. Returns a promise which resolves * after it completes, or after a timeout occurs. */ ConstellationSocket.prototype.execute = function (method, params) { if (params === void 0) { params = {}; } return this.send(new packets_1.Packet(method, params)); }; /** * Send emits a packet over the websocket. */ ConstellationSocket.prototype.send = function (packet) { 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) { if (result.err) { throw result.err; } return result.result; }), // Reject the packet if the socket closes before we get a reply: util_1.resolveOn(this, 'close', timeout + 1) .then(function () { throw new errors_1.CancelledError(); }), ]); packet.emit('send', promise); packet.setState(packets_1.PacketState.Sending); this.sendPacketInner(packet); return promise; }; ConstellationSocket.prototype.setState = function (state) { if (this.state === state) { return; } this.state = state; this.emit('state', state); }; ConstellationSocket.prototype.sendPacketInner = function (packet) { var data = JSON.stringify(packet); var payload = this.options.transform.outgoing(data, packet.toJSON()); this.emit('send', payload); this.socket.send(payload); }; ConstellationSocket.prototype.extractMessage = function (packet) { var message; try { message = JSON.parse(this.options.transform.incoming(packet)); } catch (err) { throw new errors_1.MessageParseError("Message returned was not valid JSON: " + err.stack); } // Bump the ping timeout whenever we get a message reply. this.schedulePing(); switch (message.type) { case 'event': this.emit("event:" + message.event, message.data); break; case 'reply': var err = message.error ? errors_1.ConstellationError.from(message.error) : null; this.emit("reply:" + message.id, { err: err, result: message.result }); break; default: throw new errors_1.MessageParseError("Unknown message type \"" + message.type + "\""); } }; ConstellationSocket.prototype.rebroadcastEvent = function (name) { var _this = this; this.socket.addEventListener(name, function (evt) { return _this.emit(name, evt); }); }; ConstellationSocket.prototype.schedulePing = function () { var _this = this; clearTimeout(this.pingTimeout); this.pingTimeout = setTimeout(function () { if (_this.state !== State.Connected) { return; } var packet = new packets_1.Packet('ping', null); var timeout = _this.options.replyTimeout; setTimeout(function () { _this.sendPacketInner(packet); _this.emit('ping'); }); Promise.race([ util_1.resolveOn(_this, "reply:" + packet.id(), timeout), util_1.resolveOn(_this, 'close', timeout + 1), ]) .then(function () { return _this.emit('pong'); }) .catch(function (err) { _this.socket.close(); _this.emit('warning', err); }); }, this.options.pingInterval); }; ConstellationSocket.prototype.handleSocketClose = function (cause) { var _this = this; var _a = this.options, autoReconnect = _a.autoReconnect, reconnectionPolicy = _a.reconnectionPolicy; if (this.state === State.Refreshing) { this.setState(State.Idle); this.connect(); return; } if (this.state === State.Closing || !autoReconnect) { this.setState(State.Idle); return; } var err = errors_1.ConstellationError.from({ code: cause.code, message: cause.reason }); if (!err.shouldReconnect()) { this.setState(State.Idle); this.emit('error', err); return; } this.setState(State.Reconnecting); this.reconnectTimeout = setTimeout(function () { return _this.connect(); }, reconnectionPolicy.next()); }; // WebSocket constructor, may be overridden if the environment // does not natively support it. ConstellationSocket.WebSocket = typeof WebSocket === 'undefined' ? null : WebSocket; return ConstellationSocket; }(events_1.EventEmitter)); exports.ConstellationSocket = ConstellationSocket; //# sourceMappingURL=socket.js.map