carina
Version:
A NodeJS and Browser compatible client for Mixer.com's constellation socket.
320 lines • 12.9 kB
JavaScript
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;
};
import { MessageParseError, ConstellationError, CancelledError } from './errors';
import { ExponentialReconnectionPolicy } from './reconnection';
import { EventEmitter } from 'events';
import { Packet, PacketState } from './packets';
import { stringify } from 'querystring';
import { resolveOn } from './util';
import * as pako from 'pako';
import { DEFAULT_MAX_EVENT_LISTENERS } from './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;
}());
export { 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;
}());
export { GzipTransform };
/**
* State is used to record the status of the websocket connection.
*/
export 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 || (State = {}));
function getDefaults() {
return {
url: 'wss://constellation.mixer.com',
userAgent: "Carina " + packageVersion,
replyTimeout: 10000,
isBot: false,
autoReconnect: true,
reconnectionPolicy: new 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 || 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 += "?" + 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 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:
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:
resolveOn(this, 'close', timeout + 1)
.then(function () { throw new CancelledError(); }),
]);
packet.emit('send', promise);
packet.setState(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 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 ? ConstellationError.from(message.error) : null;
this.emit("reply:" + message.id, { err: err, result: message.result });
break;
default:
throw new 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 Packet('ping', null);
var timeout = _this.options.replyTimeout;
setTimeout(function () {
_this.sendPacketInner(packet);
_this.emit('ping');
});
Promise.race([
resolveOn(_this, "reply:" + packet.id(), timeout),
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 = 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;
}(EventEmitter));
export { ConstellationSocket };
//# sourceMappingURL=socket.js.map