diff --git a/packages/socket-mode/src/SocketModeClient.test.ts b/packages/socket-mode/src/SocketModeClient.test.ts index da73676d0..753a92aef 100644 --- a/packages/socket-mode/src/SocketModeClient.test.ts +++ b/packages/socket-mode/src/SocketModeClient.test.ts @@ -39,6 +39,42 @@ describe('SocketModeClient', () => { it('should resolve once Disconnected state emitted'); }); + describe('reconnect', () => { + it('should observe a rejected reconnect attempt', async () => { + const logger = new ConsoleLogger(); + const errorLog = sandbox.stub(logger, 'error'); + const client = new SocketModeClient({ + appToken: 'xapp-', + clientPingTimeout: 1, + logger, + }); + sandbox.stub(client, 'start').rejects(new Error('reconnect failed')); + + client.emit('close'); + await sleep(10); + + sinon.assert.calledWith(errorLog, 'Failed to reconnect Socket Mode client: Error: reconnect failed'); + }); + + it('should not reconnect or log an error while shutting down', async () => { + const logger = new ConsoleLogger(); + const errorLog = sandbox.stub(logger, 'error'); + const client = new SocketModeClient({ + appToken: 'xapp-', + clientPingTimeout: 1, + logger, + }); + const start = sandbox.stub(client, 'start').resolves({}); + + client.emit('close'); + await client.disconnect(); + await sleep(10); + + sinon.assert.notCalled(start); + sinon.assert.notCalled(errorLog); + }); + }); + describe('onWebSocketMessage', () => { // While this method is protected and cannot be invoked directly, emitting the 'message' event directly invokes it describe('slash_commands messages', () => { diff --git a/packages/socket-mode/src/SocketModeClient.ts b/packages/socket-mode/src/SocketModeClient.ts index ac2f86659..c13a44026 100644 --- a/packages/socket-mode/src/SocketModeClient.ts +++ b/packages/socket-mode/src/SocketModeClient.ts @@ -17,6 +17,8 @@ import { SlackWebSocket, WS_READY_STATES } from './SlackWebSocket'; import type { SocketModeOptions } from './SocketModeOptions'; import { UnrecoverableSocketModeStartError } from './UnrecoverableSocketModeStartError'; +class SocketModeClientShuttingDownError extends Error {} + // Lifecycle events as described in the README enum State { Connecting = 'connecting', @@ -142,7 +144,11 @@ export class SocketModeClient extends EventEmitter { this.on('close', () => { // Underlying WebSocket connection was closed, possibly reconnect. if (!this.shuttingDown && this.autoReconnectEnabled) { - this.delayReconnectAttempt(this.start); + void this.delayReconnectAttempt(this.start).catch((error) => { + if (!(error instanceof SocketModeClientShuttingDownError)) { + this.logger.error(`Failed to reconnect Socket Mode client: ${error}`); + } + }); } else { // If reconnect is disabled or user explicitly called `disconnect()`, emit a disconnected state. this.emit(State.Disconnected); @@ -228,14 +234,15 @@ export class SocketModeClient extends EventEmitter { this.numOfConsecutiveReconnectionFailures += 1; const msBeforeRetry = this.clientPingTimeoutMS * this.numOfConsecutiveReconnectionFailures; this.logger.debug(`Before trying to reconnect, this client will wait for ${msBeforeRetry} milliseconds`); - return new Promise((res, _rej) => { + return new Promise((resolve, reject) => { setTimeout(() => { if (this.shuttingDown) { this.logger.debug('Client shutting down, will not attempt reconnect.'); + reject(new SocketModeClientShuttingDownError('Socket Mode client is shutting down')); } else { this.logger.debug('Continuing with reconnect...'); this.emit(State.Reconnecting); - cb.apply(this).then(res); + cb.apply(this).then(resolve, reject); } }, msBeforeRetry); });