<commit_before>import { EventEmitter } from 'eventemitter3';
import WebSocket from 'ws';
import Finity, { Context, StateMachine, Configuration } from 'finity';
import {
  WebClient,
  AppsConnectionsOpenResponse,
  WebAPICallError,
  ErrorCode as APICallErrorCode,
  addAppMetadata,
  WebClientOptions,
} from '@slack/web-api';
import { LogLevel, Logger, getLogger } from './logger';
import {
  websocketErrorWithOriginal,
  sendWhileDisconnectedError,
  sendWhileNotReadyError,
} from './errors';
import { UnrecoverableSocketModeStartError } from './UnrecoverableSocketModeStartError';
import { SocketModeOptions } from './SocketModeOptions';

const packageJson = require('../package.json'); // eslint-disable-line import/no-commonjs, @typescript-eslint/no-var-requires

// These enum values are used only in the state machine
enum State {
  Ready = 'ready',
  Connecting = 'connecting',
  Connected = 'connected',
  Handshaking = 'handshaking',
  Authenticating = 'authenticating',
  Authenticated = 'authenticated',
  Reconnecting = 'reconnecting',
  Disconnecting = 'disconnecting',
  Disconnected = 'disconnected',
  Failed = 'failed',
}

// These enum values are used only in the state machine
enum Event {
  Start = 'start',
  Failure = 'failure',
  WebSocketOpen = 'websocket open',
  WebSocketClose = 'websocket close',
  ServerHello = 'server hello',
  ServerDisconnectWarning = 'server disconnect warning',
  ServerDisconnectOldSocket = 'server disconnect old socket',
  ServerPingsNotReceived = 'server pings not received',
  ServerPongsNotReceived = 'server pongs not received',
  ExplicitDisconnect = 'explicit disconnect',
  UnableToSocketModeStart = 'unable_to_socket_mode_start',
}

/**
 * An Socket Mode Client allows programs to communicate with the
 * [Slack Platform's Events API](https://api.slack.com/events-api) over WebSocket connections.
 * This object uses the EventEmitter pattern to dispatch incoming events
 * and has a built in send method to acknowledge incoming events over the WebSocket connection.
 */
export class SocketModeClient extends EventEmitter {
  /**
   * Whether or not the client is currently connected to the web socket
   */
  public connected: boolean = false;

  /**
   * Whether or not the client has authenticated to the Socket Mode API.
   * This occurs when the connect method completes,
   * and a WebSocket URL is available for the client's connection.
   */
  public authenticated: boolean = false;

  /**
   * Returns true if the underlying WebSocket connection is active.
   */
  public isActive(): boolean {
    this.logger.debug(`Details of isActive() response (connected: ${this.connected}, authenticated: ${this.authenticated}, badConnection: ${this.badConnection})`);
    return this.connected && this.authenticated && !this.badConnection;
  }

  /**
   * The underlying WebSocket client instance
   */
  public websocket?: WebSocket;

  public constructor({
    logger = undefined,
    logLevel = undefined,
    autoReconnectEnabled = true,
    pingPongLoggingEnabled = false,
    clientPingTimeout = 5000,
    serverPingTimeout = 30000,
    appToken = undefined,
    clientOptions = {},
  }: SocketModeOptions = {}) {
    super();
    if (appToken === undefined) {
      throw new Error('Must provide an App-Level Token when initializing a Socket Mode Client');
    }
    this.pingPongLoggingEnabled = pingPongLoggingEnabled;
    this.clientPingTimeoutMillis = clientPingTimeout;
    this.lastPongReceivedTimestamp = undefined;
    this.serverPingTimeoutMillis = serverPingTimeout;
    // Setup the logger
    if (typeof logger !== 'undefined') {
      this.logger = logger;
      if (typeof logLevel !== 'undefined') {
        this.logger.debug('The logLevel given to Socket Mode was ignored as you also gave logger');
      }
    } else {
      this.logger = getLogger(SocketModeClient.loggerName, logLevel ?? LogLevel.INFO, logger);
    }
    this.clientOptions = clientOptions;
    if (this.clientOptions.retryConfig === undefined) {
      // For faster retries of apps.connections.open API calls for reconnecting
      this.clientOptions.retryConfig = { retries: 100, factor: 1.3 };
    }
    this.webClient = new WebClient('', {
      logger,
      logLevel: this.logger.getLevel(),
      headers: { Authorization: `Bearer ${appToken}` },
      ...clientOptions,
    });
    this.autoReconnectEnabled = autoReconnectEnabled;
    this.stateMachine = Finity.start(this.stateMachineConfig);
    this.logger.debug('The Socket Mode client is successfully initialized');
  }

  /**
   * Start a Socket Mode session app.
   * It may take a few milliseconds before being connected.
   * This method must be called before any messages can be sent or received.
   */
  public start(): Promise<AppsConnectionsOpenResponse> {
    this.logger.debug('Starting a Socket Mode client ...');
    // Delegate behavior to state machine
    this.stateMachine.handle(Event.Start);
    // Return a promise that resolves with the connection information
    return new Promise((resolve, reject) => {
      this.once(State.Authenticated, (result) => {
        this.removeListener(State.Disconnected, reject);
        resolve(result);
      });
      this.once(State.Disconnected, (err) => {
        this.removeListener(State.Authenticated, resolve);
        reject(err);
      });
    });
  }

  /**
   * End a Socket Mode session. After this method is called no messages will be sent or received
   * unless you call start() again later.
   */
  public disconnect(): Promise<void> {
    return new Promise((resolve, reject) => {
      this.logger.debug('Manually disconnecting this Socket Mode client');
      // Resolve (or reject) on disconnect
      this.once(State.Disconnected, (err) => {
        if (err instanceof Error) {
          reject(err);
        } else {
          resolve();
        }
      });
      // Delegate behavior to state machine
      this.stateMachine.handle(Event.ExplicitDisconnect);
    });
  }

  // --------------------------------------------
  // Private methods / properties
  // --------------------------------------------

  /**
   * State machine that backs the transition and action behavior
   */
  private stateMachine: StateMachine<State, Event>;

  /* eslint-disable @typescript-eslint/indent, newline-per-chained-call */

  private connectingStateMachineConfig: Configuration<State, Event> = Finity.configure<State, Event>()
    .global()
      .onStateEnter((state) => {
        this.logger.debug(`Transitioning to state: ${State.Connecting}:${state}`);
      })
    .initialState(State.Authenticating)
      .do(this.retrieveWSSURL.bind(this))
        .onSuccess().transitionTo(State.Authenticated)
        .onFailure()
          .transitionTo(State.Reconnecting).withCondition(this.reconnectingCondition.bind(this))
          .transitionTo(State.Failed)
    .state(State.Authenticated)
      .onEnter(this.configureAuthenticatedWebSocket.bind(this))
      .on(Event.WebSocketOpen).transitionTo(State.Handshaking)
    .state(State.Handshaking) // a state in which to wait until the Event.ServerHello event
    .state(State.Failed)
      .onEnter(this.handleConnectionFailure.bind(this))
  .getConfig();

  private connectedStateMachineConfig: Configuration<State, Event> = Finity.configure<State, Event>()
    .global()
      .onStateEnter((state) => {
        this.logger.debug(`Transitioning to state: ${State.Connected}:${state}`);
      })
    .initialState(State.Ready)
      .onEnter(() => {
        if (this.isSwitchingConnection) {
          this.switchWebSocketConnection();
          this.badConnection = false;
        }
        // Start heartbeat to keep track of the WebSocket connection continuing to be alive
        // Proactively verifying the connection health by sending ping from this client side
        this.startPeriodicallySendingPingToSlack();
        // Reactively verifying the connection health by checking the interval of ping from Slack
        this.startMonitoringPingFromSlack();
        // The transition isn't done yet, so we delay the following statement until after the event loop returns
        setImmediate(() => {
          this.emit(State.Ready);
        });
      })
  .getConfig();

  /**
   * Configuration for the state machine
   */
  private stateMachineConfig: Configuration<State, Event> = Finity.configure<State, Event>()
    .global()
      .onStateEnter((state, context) => {
        this.logger.debug(`Transitioning to state: ${state}`);
        if (state === State.Disconnected) {
          // Emits a `disconnected` event with a possible error object (might be undefined)
          this.emit(state, context.eventPayload);
        } else {
          // Emits events: `connecting`, `connected`, `disconnecting`, `reconnecting`
          this.emit(state);
        }
      })
    .initialState(State.Disconnected)
      .on(Event.Start)
        .transitionTo(State.Connecting)
    .state(State.Connecting)
      .onEnter(() => {
        this.logger.info('Going to establish a new connection to Slack ...');
      })
      .submachine(this.connectingStateMachineConfig)
      .on(Event.ServerHello)
        .transitionTo(State.Connected)
      .on(Event.WebSocketClose)
        .transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this))
        .transitionTo(State.Disconnecting)
      .on(Event.ExplicitDisconnect)
        .transitionTo(State.Disconnecting)
      .on(Event.Failure)
        .transitionTo(State.Disconnected)
    .state(State.Connected)
      .onEnter(() => {
        this.connected = true;
        this.logger.info('Now connected to Slack');
      })
      .submachine(this.connectedStateMachineConfig)
      .on(Event.WebSocketClose)
        .transitionTo(State.Reconnecting)
          .withCondition(this.autoReconnectCondition.bind(this))
          .withAction(() => this.markCurrentWebSocketAsInactive())
        .transitionTo(State.Disconnecting)
      .on(Event.ExplicitDisconnect)
        .transitionTo(State.Disconnecting)
        .withAction(() => this.markCurrentWebSocketAsInactive())
      .on(Event.ServerPingsNotReceived)
        .transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this))
        .transitionTo(State.Disconnecting)
      .on(Event.ServerPongsNotReceived)
        .transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this))
        .transitionTo(State.Disconnecting)
      .on(Event.ServerDisconnectWarning)
        .transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this))
        .transitionTo(State.Disconnecting)
      .on(Event.ServerDisconnectOldSocket)
        .transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this))
        .transitionTo(State.Disconnecting)
      .onExit(() => {
        this.terminateActiveHeartBeatJobs();
      })
    .state(State.Reconnecting)
      .onEnter(() => {
        this.logger.info('Reconnecting to Slack ...');
      })
      .do(async () => {
        this.isSwitchingConnection = true;
      })
        .onSuccess().transitionTo(State.Connecting)
        .onFailure().transitionTo(State.Failed)
    .state(State.Disconnecting)
      .onEnter(() => {
        this.logger.info('Disconnecting ...');
      })
      .do(async () => {
        this.terminateActiveHeartBeatJobs();
        this.terminateAllConnections();
        this.logger.info('Disconnected from Slack');
      })
        .onSuccess().transitionTo(State.Disconnected)
        .onFailure().transitionTo(State.Failed)
  .getConfig();

  /* eslint-enable @typescript-eslint/indent, newline-per-chained-call */

  /**
   * Whether this client will automatically reconnect when (not manually) disconnected
   */
  private autoReconnectEnabled: boolean;

  private secondaryWebsocket?: WebSocket;

  private webClient: WebClient;

  /**
   * The name used to prefix all logging generated from this object
   */
  private static loggerName = 'SocketModeClient';

  /**
   * This object's logger instance
   */
  private logger: Logger;

  /**
   * Enables ping-pong detailed logging if true
   */
  private pingPongLoggingEnabled: boolean;

  /**
   * How long to wait for pings from server before timing out
   */
  private serverPingTimeoutMillis: number;

  /**
   * Reference to the timeout timer we use to listen to pings from the server
   */
  private serverPingTimeout: NodeJS.Timeout | undefined;

  /**
   * How long to wait for pings from server before timing out
  */
  private clientPingTimeoutMillis: number;

  /**
   * Reference to the timeout timer we use to listen to pongs from the server
   */
  private clientPingTimeout: NodeJS.Timeout | undefined;

  /**
   * The last timetamp that this WebSocket client received pong from the server
   */
  private lastPongReceivedTimestamp: number | undefined;

  /**
   * Used to see if a WebSocket stops sending heartbeats and is deemed bad
   */
  private badConnection: boolean = false;

  /**
   * This flag can be true when this client is switching to a new connection.
   */
  private isSwitchingConnection: boolean = false;

  /**
   * WebClient options we pass to our WebClient instance
   * We also reuse agent and tls for our WebSocket connection
   */
  private clientOptions: WebClientOptions;

  /**
   * Method for sending an outgoing message of an arbitrary type over the WebSocket connection.
   * Primarily used to send acknowledgements back to slack for incoming events
   * @param id the envelope id
   * @param body the message body or string text
   */
  private send(id: string, body = {}): Promise<void> {
    const _body = typeof body === 'string' ? { text: body } : body;
    const message = { envelope_id: id, payload: { ..._body } };

    return new Promise((resolve, reject) => {
      this.logger.debug(`send() method was called in state: ${this.stateMachine.getCurrentState()}, state hierarchy: ${this.stateMachine.getStateHierarchy()}`);
      if (this.websocket === undefined) {
        this.logger.error('Failed to send a message as the client is not connected');
        reject(sendWhileDisconnectedError());
      } else if (!this.isConnectionReady()) {
        this.logger.error('Failed to send a message as the client is not ready');
        reject(sendWhileNotReadyError());
      } else {
        this.emit('outgoing_message', message);

        const flatMessage = JSON.stringify(message);
        this.logger.debug(`Sending a WebSocket message: ${flatMessage}`);
        this.websocket.send(flatMessage, (error) => {
          if (error !== undefined) {
            this.logger.error(`Failed to send a WebSocket message (error: ${error.message})`);
            return reject(websocketErrorWithOriginal(error));
          }
          return resolve();
        });
      }
    });
  }

  private async retrieveWSSURL(): Promise<AppsConnectionsOpenResponse> {
    try {
      this.logger.debug('Going to retrieve a new WSS URL ...');
      return await this.webClient.apps.connections.open();
    } catch (error) {
      this.logger.error(`Faled to retrieve a new WSS URL for reconnection (error: ${error})`);
      throw error;
    }
  }

  private autoReconnectCondition(): boolean {
    return this.autoReconnectEnabled;
  }

  private reconnectingCondition(context: Context<string, string>): boolean {
    const error = context.error as WebAPICallError;
    this.logger.warn(`Failed to start a Socket Mode connection (error: ${error.message})`);

    // Observe this event when the error which causes reconnecting or disconnecting is meaningful
    this.emit(Event.UnableToSocketModeStart, error);
    let isRecoverable = true;
    if (error.code === APICallErrorCode.PlatformError &&
        (Object.values(UnrecoverableSocketModeStartError) as string[]).includes(error.data.error)) {
      isRecoverable = false;
    } else if (error.code === APICallErrorCode.RequestError) {
      isRecoverable = false;
    } else if (error.code === APICallErrorCode.HTTPError) {
      isRecoverable = false;
    }
    return this.autoReconnectEnabled && isRecoverable;
  }

  private configureAuthenticatedWebSocket(_state: string, context: Context<string, string>) {
    this.authenticated = true;
    this.setupWebSocket(context.result.url);
    setImmediate(() => {
      this.emit(State.Authenticated, context.result);<commit_msg>Fix a typo in SocketModeClient log messages<commit_after>import { EventEmitter } from 'eventemitter3';
import WebSocket from 'ws';
import Finity, { Context, StateMachine, Configuration } from 'finity';
import {
  WebClient,
  AppsConnectionsOpenResponse,
  WebAPICallError,
  ErrorCode as APICallErrorCode,
  addAppMetadata,
  WebClientOptions,
} from '@slack/web-api';
import { LogLevel, Logger, getLogger } from './logger';
import {
  websocketErrorWithOriginal,
  sendWhileDisconnectedError,
  sendWhileNotReadyError,
} from './errors';
import { UnrecoverableSocketModeStartError } from './UnrecoverableSocketModeStartError';
import { SocketModeOptions } from './SocketModeOptions';

const packageJson = require('../package.json'); // eslint-disable-line import/no-commonjs, @typescript-eslint/no-var-requires

// These enum values are used only in the state machine
enum State {
  Ready = 'ready',
  Connecting = 'connecting',
  Connected = 'connected',
  Handshaking = 'handshaking',
  Authenticating = 'authenticating',
  Authenticated = 'authenticated',
  Reconnecting = 'reconnecting',
  Disconnecting = 'disconnecting',
  Disconnected = 'disconnected',
  Failed = 'failed',
}

// These enum values are used only in the state machine
enum Event {
  Start = 'start',
  Failure = 'failure',
  WebSocketOpen = 'websocket open',
  WebSocketClose = 'websocket close',
  ServerHello = 'server hello',
  ServerDisconnectWarning = 'server disconnect warning',
  ServerDisconnectOldSocket = 'server disconnect old socket',
  ServerPingsNotReceived = 'server pings not received',
  ServerPongsNotReceived = 'server pongs not received',
  ExplicitDisconnect = 'explicit disconnect',
  UnableToSocketModeStart = 'unable_to_socket_mode_start',
}

/**
 * An Socket Mode Client allows programs to communicate with the
 * [Slack Platform's Events API](https://api.slack.com/events-api) over WebSocket connections.
 * This object uses the EventEmitter pattern to dispatch incoming events
 * and has a built in send method to acknowledge incoming events over the WebSocket connection.
 */
export class SocketModeClient extends EventEmitter {
  /**
   * Whether or not the client is currently connected to the web socket
   */
  public connected: boolean = false;

  /**
   * Whether or not the client has authenticated to the Socket Mode API.
   * This occurs when the connect method completes,
   * and a WebSocket URL is available for the client's connection.
   */
  public authenticated: boolean = false;

  /**
   * Returns true if the underlying WebSocket connection is active.
   */
  public isActive(): boolean {
    this.logger.debug(`Details of isActive() response (connected: ${this.connected}, authenticated: ${this.authenticated}, badConnection: ${this.badConnection})`);
    return this.connected && this.authenticated && !this.badConnection;
  }

  /**
   * The underlying WebSocket client instance
   */
  public websocket?: WebSocket;

  public constructor({
    logger = undefined,
    logLevel = undefined,
    autoReconnectEnabled = true,
    pingPongLoggingEnabled = false,
    clientPingTimeout = 5000,
    serverPingTimeout = 30000,
    appToken = undefined,
    clientOptions = {},
  }: SocketModeOptions = {}) {
    super();
    if (appToken === undefined) {
      throw new Error('Must provide an App-Level Token when initializing a Socket Mode Client');
    }
    this.pingPongLoggingEnabled = pingPongLoggingEnabled;
    this.clientPingTimeoutMillis = clientPingTimeout;
    this.lastPongReceivedTimestamp = undefined;
    this.serverPingTimeoutMillis = serverPingTimeout;
    // Setup the logger
    if (typeof logger !== 'undefined') {
      this.logger = logger;
      if (typeof logLevel !== 'undefined') {
        this.logger.debug('The logLevel given to Socket Mode was ignored as you also gave logger');
      }
    } else {
      this.logger = getLogger(SocketModeClient.loggerName, logLevel ?? LogLevel.INFO, logger);
    }
    this.clientOptions = clientOptions;
    if (this.clientOptions.retryConfig === undefined) {
      // For faster retries of apps.connections.open API calls for reconnecting
      this.clientOptions.retryConfig = { retries: 100, factor: 1.3 };
    }
    this.webClient = new WebClient('', {
      logger,
      logLevel: this.logger.getLevel(),
      headers: { Authorization: `Bearer ${appToken}` },
      ...clientOptions,
    });
    this.autoReconnectEnabled = autoReconnectEnabled;
    this.stateMachine = Finity.start(this.stateMachineConfig);
    this.logger.debug('The Socket Mode client is successfully initialized');
  }

  /**
   * Start a Socket Mode session app.
   * It may take a few milliseconds before being connected.
   * This method must be called before any messages can be sent or received.
   */
  public start(): Promise<AppsConnectionsOpenResponse> {
    this.logger.debug('Starting a Socket Mode client ...');
    // Delegate behavior to state machine
    this.stateMachine.handle(Event.Start);
    // Return a promise that resolves with the connection information
    return new Promise((resolve, reject) => {
      this.once(State.Authenticated, (result) => {
        this.removeListener(State.Disconnected, reject);
        resolve(result);
      });
      this.once(State.Disconnected, (err) => {
        this.removeListener(State.Authenticated, resolve);
        reject(err);
      });
    });
  }

  /**
   * End a Socket Mode session. After this method is called no messages will be sent or received
   * unless you call start() again later.
   */
  public disconnect(): Promise<void> {
    return new Promise((resolve, reject) => {
      this.logger.debug('Manually disconnecting this Socket Mode client');
      // Resolve (or reject) on disconnect
      this.once(State.Disconnected, (err) => {
        if (err instanceof Error) {
          reject(err);
        } else {
          resolve();
        }
      });
      // Delegate behavior to state machine
      this.stateMachine.handle(Event.ExplicitDisconnect);
    });
  }

  // --------------------------------------------
  // Private methods / properties
  // --------------------------------------------

  /**
   * State machine that backs the transition and action behavior
   */
  private stateMachine: StateMachine<State, Event>;

  /* eslint-disable @typescript-eslint/indent, newline-per-chained-call */

  private connectingStateMachineConfig: Configuration<State, Event> = Finity.configure<State, Event>()
    .global()
      .onStateEnter((state) => {
        this.logger.debug(`Transitioning to state: ${State.Connecting}:${state}`);
      })
    .initialState(State.Authenticating)
      .do(this.retrieveWSSURL.bind(this))
        .onSuccess().transitionTo(State.Authenticated)
        .onFailure()
          .transitionTo(State.Reconnecting).withCondition(this.reconnectingCondition.bind(this))
          .transitionTo(State.Failed)
    .state(State.Authenticated)
      .onEnter(this.configureAuthenticatedWebSocket.bind(this))
      .on(Event.WebSocketOpen).transitionTo(State.Handshaking)
    .state(State.Handshaking) // a state in which to wait until the Event.ServerHello event
    .state(State.Failed)
      .onEnter(this.handleConnectionFailure.bind(this))
  .getConfig();

  private connectedStateMachineConfig: Configuration<State, Event> = Finity.configure<State, Event>()
    .global()
      .onStateEnter((state) => {
        this.logger.debug(`Transitioning to state: ${State.Connected}:${state}`);
      })
    .initialState(State.Ready)
      .onEnter(() => {
        if (this.isSwitchingConnection) {
          this.switchWebSocketConnection();
          this.badConnection = false;
        }
        // Start heartbeat to keep track of the WebSocket connection continuing to be alive
        // Proactively verifying the connection health by sending ping from this client side
        this.startPeriodicallySendingPingToSlack();
        // Reactively verifying the connection health by checking the interval of ping from Slack
        this.startMonitoringPingFromSlack();
        // The transition isn't done yet, so we delay the following statement until after the event loop returns
        setImmediate(() => {
          this.emit(State.Ready);
        });
      })
  .getConfig();

  /**
   * Configuration for the state machine
   */
  private stateMachineConfig: Configuration<State, Event> = Finity.configure<State, Event>()
    .global()
      .onStateEnter((state, context) => {
        this.logger.debug(`Transitioning to state: ${state}`);
        if (state === State.Disconnected) {
          // Emits a `disconnected` event with a possible error object (might be undefined)
          this.emit(state, context.eventPayload);
        } else {
          // Emits events: `connecting`, `connected`, `disconnecting`, `reconnecting`
          this.emit(state);
        }
      })
    .initialState(State.Disconnected)
      .on(Event.Start)
        .transitionTo(State.Connecting)
    .state(State.Connecting)
      .onEnter(() => {
        this.logger.info('Going to establish a new connection to Slack ...');
      })
      .submachine(this.connectingStateMachineConfig)
      .on(Event.ServerHello)
        .transitionTo(State.Connected)
      .on(Event.WebSocketClose)
        .transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this))
        .transitionTo(State.Disconnecting)
      .on(Event.ExplicitDisconnect)
        .transitionTo(State.Disconnecting)
      .on(Event.Failure)
        .transitionTo(State.Disconnected)
    .state(State.Connected)
      .onEnter(() => {
        this.connected = true;
        this.logger.info('Now connected to Slack');
      })
      .submachine(this.connectedStateMachineConfig)
      .on(Event.WebSocketClose)
        .transitionTo(State.Reconnecting)
          .withCondition(this.autoReconnectCondition.bind(this))
          .withAction(() => this.markCurrentWebSocketAsInactive())
        .transitionTo(State.Disconnecting)
      .on(Event.ExplicitDisconnect)
        .transitionTo(State.Disconnecting)
        .withAction(() => this.markCurrentWebSocketAsInactive())
      .on(Event.ServerPingsNotReceived)
        .transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this))
        .transitionTo(State.Disconnecting)
      .on(Event.ServerPongsNotReceived)
        .transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this))
        .transitionTo(State.Disconnecting)
      .on(Event.ServerDisconnectWarning)
        .transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this))
        .transitionTo(State.Disconnecting)
      .on(Event.ServerDisconnectOldSocket)
        .transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this))
        .transitionTo(State.Disconnecting)
      .onExit(() => {
        this.terminateActiveHeartBeatJobs();
      })
    .state(State.Reconnecting)
      .onEnter(() => {
        this.logger.info('Reconnecting to Slack ...');
      })
      .do(async () => {
        this.isSwitchingConnection = true;
      })
        .onSuccess().transitionTo(State.Connecting)
        .onFailure().transitionTo(State.Failed)
    .state(State.Disconnecting)
      .onEnter(() => {
        this.logger.info('Disconnecting ...');
      })
      .do(async () => {
        this.terminateActiveHeartBeatJobs();
        this.terminateAllConnections();
        this.logger.info('Disconnected from Slack');
      })
        .onSuccess().transitionTo(State.Disconnected)
        .onFailure().transitionTo(State.Failed)
  .getConfig();

  /* eslint-enable @typescript-eslint/indent, newline-per-chained-call */

  /**
   * Whether this client will automatically reconnect when (not manually) disconnected
   */
  private autoReconnectEnabled: boolean;

  private secondaryWebsocket?: WebSocket;

  private webClient: WebClient;

  /**
   * The name used to prefix all logging generated from this object
   */
  private static loggerName = 'SocketModeClient';

  /**
   * This object's logger instance
   */
  private logger: Logger;

  /**
   * Enables ping-pong detailed logging if true
   */
  private pingPongLoggingEnabled: boolean;

  /**
   * How long to wait for pings from server before timing out
   */
  private serverPingTimeoutMillis: number;

  /**
   * Reference to the timeout timer we use to listen to pings from the server
   */
  private serverPingTimeout: NodeJS.Timeout | undefined;

  /**
   * How long to wait for pings from server before timing out
  */
  private clientPingTimeoutMillis: number;

  /**
   * Reference to the timeout timer we use to listen to pongs from the server
   */
  private clientPingTimeout: NodeJS.Timeout | undefined;

  /**
   * The last timetamp that this WebSocket client received pong from the server
   */
  private lastPongReceivedTimestamp: number | undefined;

  /**
   * Used to see if a WebSocket stops sending heartbeats and is deemed bad
   */
  private badConnection: boolean = false;

  /**
   * This flag can be true when this client is switching to a new connection.
   */
  private isSwitchingConnection: boolean = false;

  /**
   * WebClient options we pass to our WebClient instance
   * We also reuse agent and tls for our WebSocket connection
   */
  private clientOptions: WebClientOptions;

  /**
   * Method for sending an outgoing message of an arbitrary type over the WebSocket connection.
   * Primarily used to send acknowledgements back to slack for incoming events
   * @param id the envelope id
   * @param body the message body or string text
   */
  private send(id: string, body = {}): Promise<void> {
    const _body = typeof body === 'string' ? { text: body } : body;
    const message = { envelope_id: id, payload: { ..._body } };

    return new Promise((resolve, reject) => {
      this.logger.debug(`send() method was called in state: ${this.stateMachine.getCurrentState()}, state hierarchy: ${this.stateMachine.getStateHierarchy()}`);
      if (this.websocket === undefined) {
        this.logger.error('Failed to send a message as the client is not connected');
        reject(sendWhileDisconnectedError());
      } else if (!this.isConnectionReady()) {
        this.logger.error('Failed to send a message as the client is not ready');
        reject(sendWhileNotReadyError());
      } else {
        this.emit('outgoing_message', message);

        const flatMessage = JSON.stringify(message);
        this.logger.debug(`Sending a WebSocket message: ${flatMessage}`);
        this.websocket.send(flatMessage, (error) => {
          if (error !== undefined) {
            this.logger.error(`Failed to send a WebSocket message (error: ${error.message})`);
            return reject(websocketErrorWithOriginal(error));
          }
          return resolve();
        });
      }
    });
  }

  private async retrieveWSSURL(): Promise<AppsConnectionsOpenResponse> {
    try {
      this.logger.debug('Going to retrieve a new WSS URL ...');
      return await this.webClient.apps.connections.open();
    } catch (error) {
      this.logger.error(`Failed to retrieve a new WSS URL for reconnection (error: ${error})`);
      throw error;
    }
  }

  private autoReconnectCondition(): boolean {
    return this.autoReconnectEnabled;
  }

  private reconnectingCondition(context: Context<string, string>): boolean {
    const error = context.error as WebAPICallError;
    this.logger.warn(`Failed to start a Socket Mode connection (error: ${error.message})`);

    // Observe this event when the error which causes reconnecting or disconnecting is meaningful
    this.emit(Event.UnableToSocketModeStart, error);
    let isRecoverable = true;
    if (error.code === APICallErrorCode.PlatformError &&
        (Object.values(UnrecoverableSocketModeStartError) as string[]).includes(error.data.error)) {
      isRecoverable = false;
    } else if (error.code === APICallErrorCode.RequestError) {
      isRecoverable = false;
    } else if (error.code === APICallErrorCode.HTTPError) {
      isRecoverable = false;
    }
    return this.autoReconnectEnabled && isRecoverable;
  }

  private configureAuthenticatedWebSocket(_state: string, context: Context<string, string>) {
    this.authenticated = true;
    this.setupWebSocket(context.result.url);
    setImmediate(() => {
      this.emit(State.Authenticated, context.result);
{'fraction_non_alphanumeric': 0.11483867028908086, 'fraction_numerical': 0.0009157788699288745, 'mean_word_length': 3.171654144912772, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 73, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2130414', 'n_tokens_mistral': 9086, 'n_tokens_neox': 8921, 'n_words': 2363}