@twurple/eventsub-ws
Version:
Listen to events on Twitch via their EventSub API using WebSockets.
176 lines (175 loc) • 7.28 kB
JavaScript
import { __decorate } from "tslib";
import { PersistentConnection, WebSocketConnection } from '@d-fischer/connection';
import { createLogger } from '@d-fischer/logger';
import { Enumerable } from '@d-fischer/shared-utils';
/** @internal */
export class EventSubWsSocket {
_listener;
_userId;
_connection;
_sessionId;
_initialUrl;
_reconnectInProgress = false;
_reconnectUrl;
_keepaliveTimeout;
_keepaliveTimer;
_logger;
_readyToSubscribe = false;
constructor(_listener, _userId, initialUrl, loggerOptions) {
this._listener = _listener;
this._userId = _userId;
this._logger = createLogger({
name: `twurple:eventsub:ws:${_userId}`,
...loggerOptions,
});
this._initialUrl = initialUrl;
this._keepaliveTimeout = null;
this._keepaliveTimer = null;
this._connection = new PersistentConnection(WebSocketConnection, () => ({
url: this._reconnectUrl ?? this._initialUrl,
}), {
overlapManualReconnect: true,
});
this._connection.onConnect(() => {
if (!this._reconnectInProgress) {
this._listener._notifySocketConnect(this);
}
});
this._connection.onDisconnect((_, e) => {
if (this._reconnectInProgress) {
this._reconnectInProgress = false;
}
else {
this._listener._notifySocketDisconnect(this, e);
this._readyToSubscribe = false;
this._clearKeepaliveTimer();
this._keepaliveTimeout = null;
for (const sub of this._listener._getSubscriptionsForUser(this._userId)) {
sub._droppedByTwitch();
}
}
});
this._connection.onReceive(data => {
this._logger.debug(`Received data: ${data.trim()}`);
const { metadata, payload } = JSON.parse(data);
switch (metadata.message_type) {
case 'session_welcome': {
this._logger.info(this._reconnectInProgress ? 'Reconnect: new connection established' : 'Connection established');
this._sessionId = payload.session.id;
this._readyToSubscribe = true;
if (!this._reconnectInProgress) {
const subs = this._listener._getSubscriptionsForUser(this._userId);
if (!subs.length) {
this._logger.debug(`Stopping socket for user ${this._userId} because no subscriptions are active`);
this.stop();
break;
}
for (const sub of subs) {
sub.start();
}
}
this._initializeKeepaliveTimeout(payload.session.keepalive_timeout_seconds);
this._reconnectUrl = undefined;
this._connection.acknowledgeSuccessfulReconnect();
break;
}
case 'session_keepalive': {
this._restartKeepaliveTimer();
break;
}
case 'session_reconnect': {
this._logger.info('Reconnect message received; initiating new connection');
this._reconnectInProgress = true;
this._reconnectUrl = payload.session.reconnect_url;
this._connection.reconnect();
break;
}
case 'notification': {
this._restartKeepaliveTimer();
const notificationPayload = payload;
const { id } = notificationPayload.subscription;
const subscription = this._listener._getCorrectSubscriptionByTwitchId(id);
if (!subscription) {
this._logger.error(`Notification from unknown event received: ${id}`);
break;
}
if (new Date(metadata.message_timestamp).getTime() < Date.now() - 10 * 60 * 1000) {
this._logger.debug(`Old notification(s) prevented for event: ${id}`);
break;
}
if ('events' in notificationPayload) {
for (const event of notificationPayload.events) {
this._listener._handleSingleEventPayload(subscription, event.data, event.id);
}
}
else {
this._listener._handleSingleEventPayload(subscription, notificationPayload.event, metadata.message_id);
}
break;
}
case 'revocation': {
const { id, status } = payload.subscription;
const subscription = this._listener._getCorrectSubscriptionByTwitchId(id);
if (!subscription) {
this._logger.error(`Revocation from unknown event received: ${id}`);
break;
}
this._listener._dropSubscription(subscription.id);
this._listener._dropTwitchSubscription(subscription.id);
this._listener._handleSubscriptionRevoke(subscription, status);
this._logger.debug(`Subscription revoked by Twitch for event: ${id}`);
break;
}
default: {
this._logger.warn(`Unknown message type encountered: ${metadata.message_type}`);
}
}
});
}
start() {
if (!this._connection.isConnected && !this._connection.isConnecting) {
this._connection.connect();
}
}
stop() {
if (this._connection.isConnected || this._connection.isConnecting) {
this._connection.disconnect();
}
}
get readyToSubscribe() {
return this._readyToSubscribe;
}
get sessionId() {
return this._sessionId;
}
get userId() {
return this._userId;
}
_initializeKeepaliveTimeout(timeoutInSeconds) {
this._keepaliveTimeout = timeoutInSeconds;
this._restartKeepaliveTimer();
}
_clearKeepaliveTimer() {
if (this._keepaliveTimer) {
clearTimeout(this._keepaliveTimer);
this._keepaliveTimer = null;
}
}
_restartKeepaliveTimer() {
this._clearKeepaliveTimer();
if (this._keepaliveTimeout) {
// 1200 instead of 1000 to allow for a little more leeway than Twitch wants to give us
this._keepaliveTimer = setTimeout(() => this._handleKeepaliveTimeout(), this._keepaliveTimeout * 1200);
}
}
_handleKeepaliveTimeout() {
this._keepaliveTimer = null;
this._connection.assumeExternalDisconnect();
}
}
__decorate([
Enumerable(false)
], EventSubWsSocket.prototype, "_connection", void 0);
__decorate([
Enumerable(false)
], EventSubWsSocket.prototype, "_sessionId", void 0);