UNPKG

@slack/rtm-api

Version:

Official library for using the Slack Platform's Real Time Messaging API

176 lines 7.74 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.KeepAlive = void 0; const eventemitter3_1 = require("eventemitter3"); const errors_1 = require("./errors"); const logger_1 = require("./logger"); /** * An object that monitors activity in an RTMClient and generates ping events in an effort to keep its websocket * connection alive. In cases where the websocket connection seems unresponsive, this object emits a * `recommend_reconnect` event. That event should be handled by tearing down the websocket connection and * opening a new one. */ class KeepAlive extends eventemitter3_1.EventEmitter { constructor({ clientPingTimeout = 6000, serverPongTimeout = 4000, logger = undefined, logLevel = logger_1.LogLevel.INFO, } = {}) { super(); this.clientPingTimeout = clientPingTimeout; this.serverPongTimeout = serverPongTimeout; if (this.serverPongTimeout >= this.clientPingTimeout) { const error = new Error('Client ping timeout must be less than server pong timeout'); error.code = errors_1.ErrorCode.KeepAliveConfigError; throw error; } this.isMonitoring = false; this.recommendReconnect = false; // Logging this.logger = (0, logger_1.getLogger)(KeepAlive.loggerName, logLevel, logger); } /** * Start monitoring the RTMClient. This method should only be called after the client's websocket is already open. */ start(client) { this.logger.debug('start monitoring'); if (!client.connected) { const error = new Error(''); error.code = errors_1.ErrorCode.KeepAliveClientNotConnected; throw error; } this.client = client; this.isMonitoring = true; this.client.on('outgoing_message', this.setPingTimer, this); this.setPingTimer(); } /** * Stop monitoring the RTMClient. This method should be called after the `recommend_reconnect` event is emitted and * the client's weboscket is closed. In order to start monitoring the client again, start() needs to be called again * after that. */ stop() { this.logger.debug('stop monitoring'); this.clearPreviousPingTimer(); this.clearPreviousPongTimer(); if (this.client !== undefined) { this.client.off('outgoing_message', this.setPingTimer); this.client.off('slack_event', this.attemptAcknowledgePong); } this.lastPing = undefined; this.client = undefined; this.recommendReconnect = false; this.isMonitoring = false; } /** * Clears the ping timer if its set, otherwise this is a noop. */ clearPreviousPingTimer() { if (this.pingTimer !== undefined) { clearTimeout(this.pingTimer); this.pingTimer = undefined; } } /** * Sets the ping timer (including clearing any previous one). */ setPingTimer() { // if there's already an unacknowledged ping, we don't need to set up a timer for another to be sent if (this.lastPing !== undefined) { return; } this.logger.debug('setting ping timer'); this.clearPreviousPingTimer(); this.pingTimer = setTimeout(this.sendPing.bind(this), this.clientPingTimeout); } /** * Sends a ping and manages the timer to wait for a pong. */ sendPing() { try { if (this.client === undefined) { if (!this.isMonitoring) { // if monitoring stopped before the ping timer fires, its safe to return this.logger.debug('stopped monitoring before ping timer fired'); return; } const error = new Error('no client found'); error.code = errors_1.ErrorCode.KeepAliveInconsistentState; throw error; } this.logger.debug('ping timer expired, sending ping'); this.client .send('ping') .then((messageId) => { if (this.client === undefined) { if (!this.isMonitoring) { // if monitoring stopped before the ping is sent, its safe to return this.logger.debug('stopped monitoring before outgoing ping message was finished'); return; } const error = new Error('no client found'); error.code = errors_1.ErrorCode.KeepAliveInconsistentState; throw error; } this.lastPing = messageId; this.logger.debug('setting pong timer'); this.pongTimer = setTimeout(() => { if (this.client === undefined) { // if monitoring stopped before the pong timer fires, its safe to return if (!this.isMonitoring) { this.logger.debug('stopped monitoring before pong timer fired'); return; } const error = new Error('no client found'); error.code = errors_1.ErrorCode.KeepAliveInconsistentState; throw error; } // signal that this pong is done being handled this.client.off('slack_event', this.attemptAcknowledgePong); // no pong received to acknowledge the last ping within the serverPongTimeout this.logger.debug('pong timer expired, recommend reconnect'); this.recommendReconnect = true; this.emit('recommend_reconnect'); }, this.serverPongTimeout); this.client.on('slack_event', this.attemptAcknowledgePong, this); }) .catch((error) => { this.logger.error(`Unhandled error: ${error.message}. Please report to @slack/rtm-api package maintainers.`); }); // biome-ignore lint/suspicious/noExplicitAny: errors can be anything } catch (error) { this.logger.error(`Unhandled error: ${error.message}. Please report to @slack/rtm-api package maintainers.`); } } /** * Clears the pong timer if its set, otherwise this is a noop. */ clearPreviousPongTimer() { if (this.pongTimer !== undefined) { clearTimeout(this.pongTimer); } } /** * Determines if a giving incoming event can be treated as an acknowledgement for the outstanding ping, and then * clears the ping if so. * @param event incoming slack event */ attemptAcknowledgePong(_type, event) { if (this.client === undefined) { const error = new Error('no client found'); error.code = errors_1.ErrorCode.KeepAliveInconsistentState; throw error; } if (this.lastPing !== undefined && event.reply_to !== undefined && event.reply_to >= this.lastPing) { // this message is a reply that acks the previous ping, clear the last ping this.logger.debug('received pong, clearing pong timer'); this.lastPing = undefined; // signal that this pong is done being handled this.clearPreviousPongTimer(); this.client.off('slack_event', this.attemptAcknowledgePong); } } } exports.KeepAlive = KeepAlive; /** * The name used to prefix all logging generated from this object */ KeepAlive.loggerName = 'KeepAlive'; //# sourceMappingURL=KeepAlive.js.map