UNPKG

@twurple/eventsub-ws

Version:

Listen to events on Twitch via their EventSub API using WebSockets.

176 lines (175 loc) 7.28 kB
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);