@twurple/eventsub-ws
Version:
Listen to events on Twitch via their EventSub API using WebSockets.
170 lines (168 loc) • 6.06 kB
JavaScript
import { __decorate } from "tslib";
import { HellFreezesOverError, } from '@twurple/api';
import { getMockApiPort, rtfm } from '@twurple/common';
import { EventSubBase, } from '@twurple/eventsub-base';
import { EventSubWsSocket } from './EventSubWsSocket.js';
/**
* A WebSocket listener for the Twitch EventSub event distribution mechanism.
*
* @beta
* @hideProtected
* @inheritDoc
*
* @meta category main
*/
let EventSubWsListener = class EventSubWsListener extends EventSubBase {
_sockets = new Map();
_initialUrl;
_accepting = false;
_loggerOptions;
_mockApiPort;
/**
* Fires when a user socket has established a connection with the EventSub server.
*
* @param userId The ID of the user.
*/
onUserSocketConnect = this.registerEvent();
/**
* Fires when a user socket has disconnected from the EventSub server.
*
* @param userId The ID of the user.
* @param error The error that caused the disconnection, or `undefined` for a clean disconnect.
*/
onUserSocketDisconnect = this.registerEvent();
/**
* Creates a new EventSub WebSocket listener.
*
* @param config
*
* @expandParams
*/
constructor(config) {
super(config);
this._mockApiPort = getMockApiPort();
this._initialUrl = this._mockApiPort
? `ws://127.0.0.1:${this._mockApiPort}/ws`
: config.url ?? 'wss://eventsub.wss.twitch.tv/ws';
this._loggerOptions = config.logger;
}
/**
* Starts the WebSocket listener.
*/
start() {
this._accepting = true;
const userSocketsToCreate = new Set([...this._subscriptions.values()].map(sub => sub.authUserId));
for (const userId of userSocketsToCreate) {
this._createSocketForUser(userId);
}
}
/**
* Stops the WebSocket listener.
*/
stop() {
this._accepting = false;
for (const socket of this._sockets.values()) {
socket.stop();
}
this._sockets.clear();
}
/**
* Whether the WebSocket listener is active.
*/
get isActive() {
return this._accepting;
}
/** @private */
async _getCliTestCommandForSubscription(subscription) {
if (!this._mockApiPort) {
throw new Error(`You must use the mock server from the Twitch CLI to be able to test WebSocket events.
To do so, set the \`TWURPLE_MOCK_API_PORT\` environment variable to the port the mock server runs on (usually 8080).`);
}
const { authUserId } = subscription;
if (!authUserId) {
throw new Error('Can not test a WebSocket subscription for a topic without user authentication');
}
if (!subscription._twitchId) {
throw new Error('Subscription must be registered with the mock server before being able to use this method');
}
const socket = this._sockets.get(authUserId);
if (!socket) {
throw new HellFreezesOverError(`Can not get appropriate socket for user ${authUserId}`);
}
if (!socket.sessionId) {
throw new HellFreezesOverError(`Socket for user ${authUserId} does not have a session ID yet`);
}
return `twitch event trigger ${subscription._cliName} -T websocket --session ${socket.sessionId} -u ${subscription._twitchId}`;
}
/** @private */
_isReadyToSubscribe(subscription) {
const { authUserId } = subscription;
if (!authUserId) {
throw new Error('Can not create a WebSocket subscription for a topic without user authentication');
}
const socket = this._sockets.get(authUserId);
return socket?.readyToSubscribe ?? false;
}
/** @private */
async _getTransportOptionsForSubscription(subscription) {
const { authUserId } = subscription;
if (!authUserId) {
throw new Error('Can not create a WebSocket subscription for a topic without user authentication');
}
const socket = this._sockets.get(authUserId);
if (!socket?.sessionId) {
throw new HellFreezesOverError(`Socket for user ${authUserId} is not connected or does not have a session ID yet`);
}
return {
method: 'websocket',
// eslint-disable-next-line @typescript-eslint/naming-convention
session_id: socket.sessionId,
};
}
/** @private */
_getSubscriptionsForUser(userId) {
return [...this._subscriptions.values()].filter(sub => sub.authUserId === userId);
}
/** @private */
_handleSubscriptionRevoke(subscription, status) {
this.emit(this.onRevoke, subscription, status);
}
/** @internal */
_notifySocketConnect(socket) {
this.emit(this.onUserSocketConnect, socket.userId);
}
/** @internal */
_notifySocketDisconnect(socket, error) {
this.emit(this.onUserSocketDisconnect, socket.userId, error);
}
_genericSubscribe(clazz, handler, client, ...params) {
const subscription = super._genericSubscribe(clazz, handler, client, ...params);
const { authUserId } = subscription;
if (!authUserId) {
throw new HellFreezesOverError('WS subscription created without user ID');
}
if (!this._accepting) {
return subscription;
}
if (this._sockets.has(authUserId)) {
this._sockets.get(authUserId).start();
}
else {
this._createSocketForUser(authUserId);
}
return subscription;
}
_findTwitchSubscriptionToContinue() {
return undefined;
}
/** @internal */
_createSocketForUser(authUserId) {
const socket = new EventSubWsSocket(this, authUserId, this._initialUrl, this._loggerOptions);
this._sockets.set(authUserId, socket);
socket.start();
}
};
EventSubWsListener = __decorate([
rtfm('eventsub-ws', 'EventSubWsListener')
], EventSubWsListener);
export { EventSubWsListener };