@donation-alerts/events
Version:
Listen to various Donation Alerts events.
309 lines (308 loc) • 13.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserEventsClient = void 0;
const tslib_1 = require("tslib");
const isomorphic_ws_1 = require("@d-fischer/isomorphic-ws");
const typed_event_emitter_1 = require("@d-fischer/typed-event-emitter");
const common_1 = require("@donation-alerts/common");
const logger_1 = require("@stimulcross/logger");
const shared_utils_1 = require("@stimulcross/shared-utils");
const centrifuge_1 = tslib_1.__importDefault(require("centrifuge"));
const donation_alerts_donation_event_1 = require("./events/donations/donation-alerts-donation-event");
const donation_alerts_goal_update_event_1 = require("./events/goals/donation-alerts-goal-update-event");
const donation_alerts_poll_update_event_1 = require("./events/polls/donation-alerts-poll-update-event");
const events_listener_1 = require("./events-listener");
const transform_channel_1 = require("./helpers/transform-channel");
/**
* Client for managing and subscribing to Donation Alerts events.
*
* This class provides a WebSocket-based interface for real-time interaction with the Donation Alerts platform.
* It connects to its Centrifugo WebSocket server, enabling receiving notifications
* about donations, goal updates, and poll updates.
*
* Designed for single-user use cases, it supports managing connection states (connect, disconnect, reconnect)
* and subscribing to or unsubscribing from specific event types.
*/
let UserEventsClient = class UserEventsClient extends typed_event_emitter_1.EventEmitter {
/**
* Initializes a client for listening to various Donation Alerts events.
*
* @remarks This client is designed for single-user scenarios, with event subscriptions managed
* through Centrifugo WebSocket connections.
*
* @param config Configuration required for setting up the client, including user information,
* an API client for server communication, and optional logger options.
*
*/
constructor(config) {
super();
this._listeners = new Map();
this._client = null;
this._subscriptionListeners = {
subscribe: (ctx) => {
this._logger.debug(`[USER:${this._userId}] [SUBSCRIBE]`, ctx);
this._logger.info(`[USER:${this._userId}] ${ctx.isResubscribe ? 'Resubscribed' : 'Subscribed'} to ${ctx.channel}`);
},
error: (ctx) => {
this._logger.debug(`[USER:${this._userId}] [SUBSCRIBE ERROR]`, ctx);
},
unsubscribe: (ctx) => {
this._logger.debug(`[UNSUBSCRIBE] [USER:${this._userId}]`, ctx);
this._logger.info(`[USER:${this._userId}] Unsubscribed from ${ctx.channel}`);
},
join: (ctx) => {
this._logger.debug(`[USER:${this._userId}] [JOIN]`, ctx);
},
leave: (ctx) => {
this._logger.debug(`[USER:${this._userId}] [LEAVE]`, ctx);
},
};
/**
* Fires when the client establishes a connection with Centrifugo server.
*/
this.onConnect = this.registerEvent();
/**
* Fires when the client disconnects from the Centrifugo server.
*/
this.onDisconnect = this.registerEvent();
this._logger = (0, logger_1.createLogger)({ context: 'da:events', minLevel: logger_1.LogLevel.SUCCESS, ...config.logger });
this._userId = (0, common_1.extractUserId)(config.user);
this._apiClient = config.apiClient;
this._centrifuge = new centrifuge_1.default('wss://centrifugo.donationalerts.com/connection/websocket', {
websocket: isomorphic_ws_1.WebSocket,
pingInterval: 15000,
ping: true,
minRetry: 0,
maxRetry: 30000,
onPrivateSubscribe: (ctx, callback) => {
this._apiClient.centrifugo
.subscribeUserToPrivateChannels(this._userId, ctx.data.client, ctx.data.channels, { transformChannel: false })
.then(channels => {
callback({
status: 200,
data: {
channels: channels.map(channel => ({ channel: channel.channel, token: channel.token })),
},
});
})
.catch(e => this._logger.error(e));
},
});
this._centrifuge.on('connect', (ctx) => {
this._client = ctx.client;
this._logger.debug(`[USER:${this._userId}] [CONNECT]`, ctx);
this._logger.info(`[USER:${this._userId}] Connection established to Centrifugo server`);
this.emit(this.onConnect);
});
this._centrifuge.on('disconnect', (ctx) => {
this._logger.debug(`[USER:${this._userId}] [DISCONNECT]`, ctx);
this.emit(this.onDisconnect, ctx.reason, ctx.reconnect);
});
}
/**
* Unique identifier of the user associated with this client.
*/
get userId() {
return this._userId;
}
/**
* Client ID assigned by the Centrifugo server.
*
* Returns `null` if the client is not connected.
*/
get clientId() {
return this._client;
}
/**
* Indicates whether the client is connected to the Centrifugo server.
*
* @returns `true` if the client is connected; otherwise `false`.
*/
get isConnected() {
return this._centrifuge.isConnected();
}
/**
* Establishes a connection to the Centrifugo server.
*
* @param restoreExistingListeners Specifies whether existing listeners should be restored after connection.
* Defaults to `true`.
*/
async connect(restoreExistingListeners = true) {
for (const [, listener] of this._listeners) {
if (restoreExistingListeners) {
this._logger.info(`[USER:${this._userId}] Restoring previously registered listeners...`);
listener._subscription.subscribe();
}
else {
this._logger.info(`[USER:${this._userId}] Removing previously registered listeners...`);
await listener.remove();
}
}
await this._connect();
}
/**
* Closes the connection to the Centrifugo server.
*
* @param removeListeners Indicates whether all active listeners should be removed on disconnect.
* If set to `false`, the listeners will be restored on the next connection.
* Default to `false`.
*/
async disconnect(removeListeners = false) {
if (removeListeners) {
this._logger.info(`[USER:${this._userId}] Removing listeners...`);
for (const [, listener] of this._listeners) {
await this.removeEventsListener(listener);
}
}
await this._disconnect();
}
/**
* Re-establishes the connection to the Centrifugo server.
*
* @param removeListeners Indicates whether all listeners should be removed during reconnection.
* If `false`, all listeners will be restored automatically after reconnection.
* Defaults to `false`.
*/
async reconnect(removeListeners = false) {
await this.disconnect(removeListeners);
await this.connect(!removeListeners);
}
/**
* Subscribes to donation events from Donation Alerts.
*
* @param callback A function invoked whenever a donation event is received.
* The callback receives an instance of {@link DonationAlertsDonationEvent}.
* @returns An {@link EventsListener} instance that manages the subscription.
*/
async onDonation(callback) {
return await this._createListener('$alerts:donation', donation_alerts_donation_event_1.DonationAlertsDonationEvent, callback);
}
/**
* Subscribes to goal update events from Donation Alerts.
*
* @param callback A function invoked whenever a goal update event is received.
* The callback receives an instance of {@link DonationAlertsGoalUpdateEvent}.
* @returns An {@link EventsListener} instance that manages the subscription.
*/
async onGoalUpdate(callback) {
return await this._createListener('$goals:goal', donation_alerts_goal_update_event_1.DonationAlertsGoalUpdateEvent, callback);
}
/**
* Subscribes to poll update events from Donation Alerts.
*
* @param callback A function invoked whenever a poll update event is received.
* The callback receives an instance of {@link DonationAlertsPollUpdateEvent}.
* @returns An {@link EventsListener} instance that manages the subscription.
*/
async onPollUpdate(callback) {
return await this._createListener('$polls:poll', donation_alerts_poll_update_event_1.DonationAlertsPollUpdateEvent, callback);
}
/**
* Unsubscribes and removes a listener for a specific channel.
*
* @remarks
* If this is the last listener, the WebSocket connection is also closed.
*
* @param listener The {@link EventsListener} instance to be removed.
*/
async removeEventsListener(listener) {
if (this._listeners.has(listener.channelName)) {
const existingListener = this._listeners.get(listener.channelName);
this._unsubscribe(existingListener._subscription);
this._listeners.delete(listener.channelName);
if (this._listeners.size === 0) {
await this._disconnect();
}
}
}
async _connect() {
if (!this.isConnected) {
const token = await this._apiClient.users.getSocketConnectionToken(this._userId);
return await new Promise((resolve, reject) => {
try {
this._centrifuge.setToken(token);
const rejectTimer = setTimeout(() => reject(new Error(`[USER:${this._userId}] Could not connect to Centrifugo server`)), 10000);
this._centrifuge.once('connect', (ctx) => {
clearTimeout(rejectTimer);
this._client = ctx.client;
return resolve();
});
this._centrifuge.connect();
}
catch (e) {
return reject(e);
}
});
}
}
async _disconnect() {
return await new Promise((resolve, reject) => {
if (this.isConnected) {
try {
this._logger.debug(`[USER:${this._userId}] Disconnecting...`);
const rejectTimer = setTimeout(() => {
this._logger.warn(`[USER:${this._userId}] Disconnect timeout. But the connection should be already closed anyway.`);
resolve();
}, 10000);
this._centrifuge.once('disconnect', () => {
clearTimeout(rejectTimer);
this._client = null;
return resolve();
});
this._centrifuge.disconnect();
}
catch (e) {
return reject(e);
}
}
else {
return resolve();
}
});
}
async _createListener(channel, evt, callback) {
await this._connect();
const subscription = this._subscribe((0, transform_channel_1.transformChannel)(channel, this._userId));
subscription.on('publish', (ctx) => {
this._logger.debug(`[USER:${this._userId}] [PUBLISH] (${channel}_${this._userId})`, ctx);
try {
return callback(new evt(ctx.data));
}
catch (e) {
this._logger.error(e);
}
});
const listener = new events_listener_1.EventsListener(channel, this._userId, subscription, this);
this._listeners.set(channel, listener);
return listener;
}
_subscribe(channel) {
return this._centrifuge.subscribe(channel, this._subscriptionListeners);
}
_unsubscribe(subscription) {
subscription.unsubscribe();
subscription.removeAllListeners();
}
};
exports.UserEventsClient = UserEventsClient;
tslib_1.__decorate([
shared_utils_1.nonenumerable
], UserEventsClient.prototype, "_logger", void 0);
tslib_1.__decorate([
shared_utils_1.nonenumerable
], UserEventsClient.prototype, "_userId", void 0);
tslib_1.__decorate([
shared_utils_1.nonenumerable
], UserEventsClient.prototype, "_apiClient", void 0);
tslib_1.__decorate([
shared_utils_1.nonenumerable
], UserEventsClient.prototype, "_listeners", void 0);
tslib_1.__decorate([
shared_utils_1.nonenumerable
], UserEventsClient.prototype, "_centrifuge", void 0);
tslib_1.__decorate([
shared_utils_1.nonenumerable
], UserEventsClient.prototype, "_client", void 0);
exports.UserEventsClient = UserEventsClient = tslib_1.__decorate([
(0, common_1.ReadDocumentation)('events')
], UserEventsClient);