@slack/socket-mode
Advanced tools
Comparing version 1.2.0 to 1.3.0-rc.0
@@ -37,3 +37,5 @@ "use strict"; | ||
*/ | ||
function platformErrorFromEvent(event) { | ||
function platformErrorFromEvent( | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
event) { | ||
const error = errorWithCode(new Error(`An API error occurred: ${event.error.msg}`), ErrorCode.SendMessagePlatformError); | ||
@@ -57,3 +59,3 @@ error.data = event; | ||
function sendWhileDisconnectedError() { | ||
return errorWithCode(new Error('Cannot send message when client is not connected'), ErrorCode.NoReplyReceivedError); | ||
return errorWithCode(new Error('Failed to send a WebSocket message as the client is not connected'), ErrorCode.NoReplyReceivedError); | ||
} | ||
@@ -65,5 +67,5 @@ exports.sendWhileDisconnectedError = sendWhileDisconnectedError; | ||
function sendWhileNotReadyError() { | ||
return errorWithCode(new Error('Cannot send message when client is not ready'), ErrorCode.NoReplyReceivedError); | ||
return errorWithCode(new Error('Failed to send a WebSocket message as the client is not ready'), ErrorCode.NoReplyReceivedError); | ||
} | ||
exports.sendWhileNotReadyError = sendWhileNotReadyError; | ||
//# sourceMappingURL=errors.js.map |
/// <reference lib="es2017" /> | ||
export { SocketModeClient, } from './SocketModeClient'; | ||
export { SocketModeClient } from './SocketModeClient'; | ||
export { SocketModeOptions } from './SocketModeOptions'; | ||
export { UnrecoverableSocketModeStartError } from './UnrecoverableSocketModeStartError'; | ||
export { Logger, LogLevel } from './logger'; | ||
export { ErrorCode, SMPlatformError, SMWebsocketError, SMNoReplyReceivedError, SMSendWhileDisconnectedError, SMSendWhileNotReadyError, SMCallError, } from './errors'; | ||
//# sourceMappingURL=index.d.ts.map |
"use strict"; | ||
/// <reference lib="es2017" /> | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ErrorCode = exports.LogLevel = exports.SocketModeClient = void 0; | ||
exports.ErrorCode = exports.LogLevel = exports.UnrecoverableSocketModeStartError = exports.SocketModeClient = void 0; | ||
var SocketModeClient_1 = require("./SocketModeClient"); | ||
Object.defineProperty(exports, "SocketModeClient", { enumerable: true, get: function () { return SocketModeClient_1.SocketModeClient; } }); | ||
var UnrecoverableSocketModeStartError_1 = require("./UnrecoverableSocketModeStartError"); | ||
Object.defineProperty(exports, "UnrecoverableSocketModeStartError", { enumerable: true, get: function () { return UnrecoverableSocketModeStartError_1.UnrecoverableSocketModeStartError; } }); | ||
var logger_1 = require("./logger"); | ||
@@ -8,0 +10,0 @@ Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return logger_1.LogLevel; } }); |
import { EventEmitter } from 'eventemitter3'; | ||
import WebSocket from 'ws'; | ||
import { AppsConnectionsOpenResponse, WebClientOptions } from '@slack/web-api'; | ||
import { LogLevel, Logger } from './logger'; | ||
import { AppsConnectionsOpenResponse } from '@slack/web-api'; | ||
import { SocketModeOptions } from './SocketModeOptions'; | ||
/** | ||
* An Socket Mode Client allows programs to communicate with the | ||
* [Slack Platform's Events API](https://api.slack.com/events-api) over a websocket. | ||
* This object uses the EventEmitter pattern to dispatch incoming events and has a built in send method to | ||
* acknowledge incoming events over the websocket. | ||
* [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. | ||
*/ | ||
@@ -17,18 +17,32 @@ export declare class SocketModeClient extends EventEmitter { | ||
/** | ||
* 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. | ||
* 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. | ||
*/ | ||
authenticated: boolean; | ||
/** | ||
* Whether this client will automatically reconnect when (not manually) disconnected | ||
* Returns true if the underlying WebSocket connection is active. | ||
*/ | ||
private autoReconnectEnabled; | ||
isActive(): boolean; | ||
/** | ||
* The number of milliseconds to wait upon connection for reply messages from the previous connection. The default | ||
* value is 2 seconds. | ||
* The underlying WebSocket client instance | ||
*/ | ||
websocket?: WebSocket; | ||
constructor({ logger, logLevel, autoReconnectEnabled, pingPongLoggingEnabled, clientPingTimeout, serverPingTimeout, appToken, clientOptions, }?: SocketModeOptions); | ||
/** | ||
* Begin a Socket Mode session. | ||
* This method must be called before any messages can be sent or received. | ||
*/ | ||
start(): Promise<AppsConnectionsOpenResponse>; | ||
/** | ||
* End a Socket Mode session. After this method is called no messages will be sent or received | ||
* unless you call start() again later. | ||
*/ | ||
disconnect(): Promise<void>; | ||
/** | ||
* State machine that backs the transition and action behavior | ||
*/ | ||
private stateMachine; | ||
private connectingStateMachineConfig; | ||
private connectedStateMachineConfig; | ||
/** | ||
@@ -39,5 +53,5 @@ * Configuration for the state machine | ||
/** | ||
* The client's websockets | ||
* Whether this client will automatically reconnect when (not manually) disconnected | ||
*/ | ||
websocket?: WebSocket; | ||
private autoReconnectEnabled; | ||
private secondaryWebsocket?; | ||
@@ -54,11 +68,27 @@ private webClient; | ||
/** | ||
* Enables ping-pong detailed logging if true | ||
*/ | ||
private pingPongLoggingEnabled; | ||
/** | ||
* How long to wait for pings from server before timing out | ||
*/ | ||
private serverPingTimeoutMillis; | ||
/** | ||
* Reference to the timeout timer we use to listen to pings from the server | ||
*/ | ||
private serverPingTimeout; | ||
/** | ||
* How long to wait for pings from server before timing out | ||
*/ | ||
private clientPingTimeoutMillis; | ||
/** | ||
* Reference to the timeout timer we use to listen to pongs from the server | ||
*/ | ||
private clientPingTimeout; | ||
/** | ||
* reference to the timeout timer we use to listen to pings from the server | ||
* The last timetamp that this WebSocket client received pong from the server | ||
*/ | ||
private pingTimeout; | ||
private lastPongReceivedTimestamp; | ||
/** | ||
* Used to see if a websocket stops sending heartbeats and is deemed bad | ||
* Used to see if a WebSocket stops sending heartbeats and is deemed bad | ||
*/ | ||
@@ -68,39 +98,42 @@ private badConnection; | ||
* WebClient options we pass to our WebClient instance | ||
* We also reuse agent and tls for our websocket connection | ||
* We also reuse agent and tls for our WebSocket connection | ||
*/ | ||
private clientOptions; | ||
constructor({ logger, logLevel, autoReconnectEnabled, clientPingTimeout, appToken, clientOptions, }?: SocketModeOptions); | ||
/** | ||
* Begin an Socket Mode session. This method must be called before any messages can | ||
* be sent or received. | ||
*/ | ||
start(): Promise<AppsConnectionsOpenResponse>; | ||
/** | ||
* End a Socket Mode session. After this method is called no messages will be sent or received unless you call | ||
* start() again later. | ||
*/ | ||
disconnect(): Promise<void>; | ||
/** | ||
* Method for sending an outgoing message of an arbitrary type over the websocket connection. | ||
* 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 | ||
* @param body the message body or string text | ||
*/ | ||
private send; | ||
private retrieveWSSURL; | ||
private autoReconnectCondition; | ||
private reconnectingCondition; | ||
private configureAuthenticatedWebSocket; | ||
private handleConnectionFailure; | ||
private handleDisconnection; | ||
/** | ||
* Set up method for the client's websocket instance. This method will attach event listeners. | ||
* Set up method for the client's WebSocket instance. This method will attach event listeners. | ||
*/ | ||
private setupWebSocket; | ||
/** | ||
* Tear down method for the client's websocket instance. This method undoes the work in setupWebSocket(url). | ||
* Tear down the currently working heartbeat jobs. | ||
*/ | ||
private teardownWebsocket; | ||
private tearDownHeartBeatJobs; | ||
/** | ||
* confirms websocket connection is still active | ||
* Tear down method for the client's WebSocket instance. | ||
* This method undoes the work in setupWebSocket(url). | ||
*/ | ||
private tearDownWebSocket; | ||
private startPeriodicallySendingPingToSlack; | ||
private handlePingPongErrorReconnection; | ||
/** | ||
* Confirms WebSocket connection is still active | ||
* fires whenever a ping event is received | ||
*/ | ||
private heartbeat; | ||
private startMonitoringPingFromSlack; | ||
private isConnectionReady; | ||
/** | ||
* `onmessage` handler for the client's websocket. This will parse the payload and dispatch the relevant events for | ||
* each incoming message. | ||
* `onmessage` handler for the client's WebSocket. | ||
* This will parse the payload and dispatch the relevant events for each incoming message. | ||
*/ | ||
@@ -112,10 +145,2 @@ protected onWebSocketMessage({ data }: { | ||
export default SocketModeClient; | ||
export interface SocketModeOptions { | ||
appToken?: string; | ||
logger?: Logger; | ||
logLevel?: LogLevel; | ||
autoReconnectEnabled?: boolean; | ||
clientPingTimeout?: number; | ||
clientOptions?: Omit<WebClientOptions, 'logLevel' | 'logger'>; | ||
} | ||
//# sourceMappingURL=SocketModeClient.d.ts.map |
@@ -13,21 +13,40 @@ "use strict"; | ||
const errors_1 = require("./errors"); | ||
const UnrecoverableSocketModeStartError_1 = require("./UnrecoverableSocketModeStartError"); | ||
const packageJson = require('../package.json'); // eslint-disable-line import/no-commonjs, @typescript-eslint/no-var-requires | ||
// NOTE: there may be a better way to add metadata to an error about being "unrecoverable" than to keep an | ||
// independent enum, probably a Set (this isn't used as a type). | ||
var UnrecoverableSocketModeStartError; | ||
(function (UnrecoverableSocketModeStartError) { | ||
UnrecoverableSocketModeStartError["NotAuthed"] = "not_authed"; | ||
UnrecoverableSocketModeStartError["InvalidAuth"] = "invalid_auth"; | ||
UnrecoverableSocketModeStartError["AccountInactive"] = "account_inactive"; | ||
UnrecoverableSocketModeStartError["UserRemovedFromTeam"] = "user_removed_from_team"; | ||
UnrecoverableSocketModeStartError["TeamDisabled"] = "team_disabled"; | ||
})(UnrecoverableSocketModeStartError || (UnrecoverableSocketModeStartError = {})); | ||
// These enum values are used only in the state machine | ||
var State; | ||
(function (State) { | ||
State["Ready"] = "ready"; | ||
State["Connecting"] = "connecting"; | ||
State["Connected"] = "connected"; | ||
State["Handshaking"] = "handshaking"; | ||
State["Authenticating"] = "authenticating"; | ||
State["Authenticated"] = "authenticated"; | ||
State["Reconnecting"] = "reconnecting"; | ||
State["Disconnecting"] = "disconnecting"; | ||
State["Disconnected"] = "disconnected"; | ||
State["Failed"] = "failed"; | ||
})(State || (State = {})); | ||
// These enum values are used only in the state machine | ||
var Event; | ||
(function (Event) { | ||
Event["Start"] = "start"; | ||
Event["Failure"] = "failure"; | ||
Event["WebSocketOpen"] = "websocket open"; | ||
Event["WebSocketClose"] = "websocket close"; | ||
Event["ServerHello"] = "server hello"; | ||
Event["ServerDisconnectWarning"] = "server disconnect warning"; | ||
Event["ServerDisconnectOldSocket"] = "server disconnect old socket"; | ||
Event["ServerPingsNotReceived"] = "server pings not received"; | ||
Event["ServerPongsNotReceived"] = "server pongs not received"; | ||
Event["ExplicitDisconnect"] = "explicit disconnect"; | ||
})(Event || (Event = {})); | ||
/** | ||
* An Socket Mode Client allows programs to communicate with the | ||
* [Slack Platform's Events API](https://api.slack.com/events-api) over a websocket. | ||
* This object uses the EventEmitter pattern to dispatch incoming events and has a built in send method to | ||
* acknowledge incoming events over the websocket. | ||
* [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. | ||
*/ | ||
class SocketModeClient extends eventemitter3_1.EventEmitter { | ||
constructor({ logger = undefined, logLevel = undefined, autoReconnectEnabled = true, clientPingTimeout = 30000, appToken = undefined, clientOptions = {}, } = {}) { | ||
constructor({ logger = undefined, logLevel = undefined, autoReconnectEnabled = true, pingPongLoggingEnabled = false, clientPingTimeout = 5000, serverPingTimeout = 30000, appToken = undefined, clientOptions = {}, } = {}) { | ||
super(); | ||
@@ -39,196 +58,115 @@ /** | ||
/** | ||
* 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. | ||
* 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. | ||
*/ | ||
this.authenticated = false; | ||
/** | ||
* Configuration for the state machine | ||
*/ | ||
this.stateMachineConfig = finity_1.default | ||
.configure() | ||
/* eslint-disable @typescript-eslint/indent, newline-per-chained-call */ | ||
.initialState('disconnected') | ||
.on('start').transitionTo('connecting') | ||
// .onEnter(() => {}) | ||
.state('connecting') | ||
.submachine(finity_1.default.configure() | ||
.initialState('authenticating') | ||
.do(() => this.webClient.apps.connections.open() | ||
.then((result) => result) | ||
.catch((error) => { | ||
this.logger.error(error); | ||
// throw error; | ||
return Promise.reject(error); | ||
})) | ||
.onSuccess().transitionTo('authenticated') | ||
/* eslint-disable @typescript-eslint/indent, newline-per-chained-call */ | ||
this.connectingStateMachineConfig = finity_1.default.configure() | ||
.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('reconnecting').withCondition((context) => { | ||
const error = context.error; | ||
this.logger.info(`unable to Socket Mode start: ${error.message}`); | ||
// Observe this event when the error which causes reconnecting or disconnecting is meaningful | ||
this.emit('unable_to_socket_mode_start', error); | ||
let isRecoverable = true; | ||
if (error.code === web_api_1.ErrorCode.PlatformError && | ||
Object.values(UnrecoverableSocketModeStartError).includes(error.data.error)) { | ||
isRecoverable = false; | ||
} | ||
else if (error.code === web_api_1.ErrorCode.RequestError) { | ||
isRecoverable = false; | ||
} | ||
else if (error.code === web_api_1.ErrorCode.HTTPError) { | ||
isRecoverable = false; | ||
} | ||
return this.autoReconnectEnabled && isRecoverable; | ||
}) | ||
.transitionTo('failed') | ||
.state('authenticated') | ||
.onEnter((_state, context) => { | ||
this.authenticated = true; | ||
this.setupWebSocket(context.result.url); | ||
setImmediate(() => { | ||
this.emit('authenticated', context.result); | ||
}); | ||
}) | ||
.on('websocket open').transitionTo('handshaking') | ||
.state('handshaking') // a state in which to wait until the 'server hello' event | ||
.state('failed') | ||
.onEnter((_state, context) => { | ||
// dispatch 'failure' on parent machine to transition out of this submachine's states | ||
this.stateMachine.handle('failure', context.error); | ||
}) | ||
.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(); | ||
this.connectedStateMachineConfig = finity_1.default.configure() | ||
.global() | ||
.onStateEnter((state) => { | ||
this.logger.debug(`transitioning to state: connecting:${state}`); | ||
this.logger.debug(`Transitioning to state: ${State.Connected}:${state}`); | ||
}) | ||
.getConfig()) | ||
.on('server hello').transitionTo('connected') | ||
.on('websocket close') | ||
.transitionTo('reconnecting').withCondition(() => this.autoReconnectEnabled) | ||
.transitionTo('disconnected').withAction(() => { | ||
// this transition circumvents the 'disconnecting' state (since the websocket is already closed), so we need | ||
// to execute its onExit behavior here. | ||
this.teardownWebsocket(); | ||
}) | ||
.on('failure').transitionTo('disconnected') | ||
.on('explicit disconnect').transitionTo('disconnecting') | ||
.state('connected') | ||
.initialState(State.Ready) | ||
.onEnter(() => { | ||
this.connected = true; | ||
}) | ||
.submachine(finity_1.default.configure() | ||
.initialState('ready') | ||
.onEnter(() => { | ||
if (this.badConnection) { | ||
// arrived here because `server ping timeout` occurred and a new connection was created | ||
// tear down old connection | ||
this.teardownWebsocket(); | ||
// The state arrived here because Event.ServerPingTimeout occurred | ||
// and a new connection was created. | ||
// Tearing down the old connection. | ||
this.tearDownWebSocket(); | ||
this.badConnection = false; | ||
} | ||
// start heartbeat to keep track of the websocket connection continuing to be alive | ||
this.heartbeat(); | ||
// the transition isn't done yet, so we delay the following statement until after the event loop returns | ||
// 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('ready'); | ||
this.emit(State.Ready); | ||
}); | ||
}) | ||
.on('server disconnect warning').transitionTo('refreshing-connection').withCondition(() => this.autoReconnectEnabled) | ||
.on('server pings not received').transitionTo('refreshing-connection').withCondition(() => this.autoReconnectEnabled) | ||
.on('server disconnect old socket').transitionTo('closing-socket') | ||
.state('refreshing-connection') | ||
.submachine(finity_1.default.configure() | ||
.initialState('authenticating') | ||
.do(() => this.webClient.apps.connections.open() | ||
.then((result) => result) | ||
.catch((error) => { | ||
this.logger.error(error); | ||
// throw error; | ||
return Promise.reject(error); | ||
})) | ||
.onSuccess().transitionTo('authenticated') | ||
.onFailure() | ||
.transitionTo('authenticating').withCondition((context) => { | ||
const error = context.error; | ||
this.logger.info(`unable to Socket Mode start: ${error.message}`); | ||
// Observe this event when the error which causes reconnecting or disconnecting is meaningful | ||
this.emit('unable_to_socket_mode_start', error); | ||
let isRecoverable = true; | ||
if (error.code === web_api_1.ErrorCode.PlatformError && | ||
Object.values(UnrecoverableSocketModeStartError).includes(error.data.error)) { | ||
isRecoverable = false; | ||
.getConfig(); | ||
/** | ||
* Configuration for the state machine | ||
*/ | ||
this.stateMachineConfig = finity_1.default.configure() | ||
.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 if (error.code === web_api_1.ErrorCode.RequestError) { | ||
isRecoverable = false; | ||
else { | ||
// Emits events: `connecting`, `connected`, `disconnecting`, `reconnecting` | ||
this.emit(state); | ||
} | ||
else if (error.code === web_api_1.ErrorCode.HTTPError) { | ||
isRecoverable = false; | ||
} | ||
return this.autoReconnectEnabled && isRecoverable; | ||
}) | ||
.transitionTo('failed') | ||
.state('authenticated') | ||
.onEnter((_state, context) => { | ||
this.authenticated = true; | ||
this.setupWebSocket(context.result.url); | ||
setImmediate(() => { | ||
this.emit('authenticated', context.result); | ||
}); | ||
.initialState(State.Disconnected) | ||
.on(Event.Start) | ||
.transitionTo(State.Connecting) | ||
.state(State.Connecting) | ||
.onEnter(() => { | ||
this.logger.info('Going to establish a new connectiont to Slack ...'); | ||
}) | ||
.on('websocket open').transitionTo('handshaking') | ||
.state('handshaking') // a state in which to wait until the 'server hello' event | ||
.state('failed') | ||
.onEnter((_state, context) => { | ||
// dispatch 'failure' on parent machine to transition out of this submachine's states | ||
this.stateMachine.handle('failure', context.error); | ||
.submachine(this.connectingStateMachineConfig) | ||
.on(Event.ServerHello) | ||
.transitionTo(State.Connected) | ||
.on(Event.WebSocketClose) | ||
.transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this)) | ||
.transitionTo(State.Disconnected).withAction(this.handleDisconnection.bind(this)) | ||
.on(Event.Failure) | ||
.transitionTo(State.Disconnected) | ||
.on(Event.ExplicitDisconnect) | ||
.transitionTo(State.Disconnecting) | ||
.state(State.Connected) | ||
.onEnter(() => { | ||
this.connected = true; | ||
this.logger.info('Now connected to Slack'); | ||
}) | ||
.global() | ||
.onStateEnter((state) => { | ||
this.logger.debug(`transitioning to state: refreshing-connection:${state}`); | ||
}) | ||
.getConfig()) | ||
.on('server hello').transitionTo('ready') | ||
.on('websocket close') | ||
.transitionTo('authenticating').withCondition(() => this.autoReconnectEnabled) | ||
.transitionTo('disconnected').withAction(() => { | ||
// this transition circumvents the 'disconnecting' state (since the websocket is already closed), | ||
// so we need to execute its onExit behavior here. | ||
this.teardownWebsocket(); | ||
}) | ||
.on('failure').transitionTo('disconnected') | ||
.on('explicit disconnect').transitionTo('disconnecting') | ||
.state('closing-socket') | ||
.do(() => { | ||
// stop heartbeat | ||
if (this.pingTimeout !== undefined) { | ||
clearTimeout(this.pingTimeout); | ||
} | ||
return Promise.resolve(true); | ||
}) | ||
.onSuccess().transitionTo('ready') | ||
.onExit(() => this.teardownWebsocket()) | ||
.global() | ||
.onStateEnter((state) => { | ||
this.logger.debug(`transitioning to state: connected:${state}`); | ||
}) | ||
.getConfig()) | ||
.on('server disconnect warning') | ||
.transitionTo('refreshing-connection').withCondition(() => this.autoReconnectEnabled) | ||
.on('websocket close') | ||
.transitionTo('reconnecting').withCondition(() => this.autoReconnectEnabled) | ||
.transitionTo('disconnected').withAction(() => { | ||
// this transition circumvents the 'disconnecting' state (since the websocket is already closed), so we need | ||
// to execute its onExit behavior here. | ||
this.teardownWebsocket(); | ||
}) | ||
.on('explicit disconnect').transitionTo('disconnecting') | ||
.submachine(this.connectedStateMachineConfig) | ||
.on(Event.ServerDisconnectWarning) | ||
.transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this)) | ||
.on(Event.WebSocketClose) | ||
.transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this)) | ||
.transitionTo(State.Disconnected).withAction(this.handleDisconnection.bind(this)) | ||
.on(Event.ExplicitDisconnect) | ||
.transitionTo(State.Disconnecting) | ||
.on(Event.ServerDisconnectWarning) | ||
.transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this)) | ||
.on(Event.ServerPingsNotReceived) | ||
.transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this)) | ||
.on(Event.ServerPongsNotReceived) | ||
.transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this)) | ||
.on(Event.ServerDisconnectOldSocket) | ||
.transitionTo(State.Reconnecting).withCondition(this.autoReconnectCondition.bind(this)) | ||
.onExit(() => { | ||
this.connected = false; | ||
this.authenticated = false; | ||
if (this.pingTimeout !== undefined) { | ||
clearTimeout(this.pingTimeout); | ||
} | ||
this.tearDownHeartBeatJobs(); | ||
}) | ||
.state('disconnecting') | ||
.state(State.Disconnecting) | ||
.onEnter(() => { | ||
// Most of the time, a websocket will exist. The only time it does not is when transitioning from connecting, | ||
// before the client.start() has finished and the websocket hasn't been set up. | ||
// Most of the time, a WebSocket will exist. | ||
// The only time it does not is when transitioning from connecting, | ||
// before the client.start() has finished and the WebSocket hasn't been set up. | ||
if (this.websocket !== undefined) { | ||
@@ -238,36 +176,26 @@ this.websocket.close(); | ||
}) | ||
.on('websocket close').transitionTo('disconnected') | ||
.onExit(() => this.teardownWebsocket()) | ||
// reconnecting is just like disconnecting, except that the websocket should already be closed before we enter | ||
// this state, and that the next state should be connecting. | ||
.state('reconnecting') | ||
.do(() => { | ||
if (this.pingTimeout !== undefined) { | ||
clearTimeout(this.pingTimeout); | ||
} | ||
return Promise.resolve(true); | ||
.on(Event.WebSocketClose) | ||
.transitionTo(State.Disconnected) | ||
.onExit(() => this.tearDownWebSocket()) | ||
.state(State.Reconnecting) | ||
.onEnter(() => { | ||
this.logger.info('Reconnecting to Slack ...'); | ||
}) | ||
.onSuccess().transitionTo('connecting') | ||
.onExit(() => this.teardownWebsocket()) | ||
.global() | ||
.onStateEnter((state, context) => { | ||
this.logger.debug(`transitioning to state: ${state}`); | ||
if (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); | ||
} | ||
.do(async () => { | ||
this.badConnection = true; | ||
this.tearDownHeartBeatJobs(); | ||
}) | ||
.onSuccess().transitionTo(State.Connecting) | ||
.getConfig(); | ||
/** | ||
* Used to see if a websocket stops sending heartbeats and is deemed bad | ||
* Used to see if a WebSocket stops sending heartbeats and is deemed bad | ||
*/ | ||
this.badConnection = false; | ||
if (appToken === undefined) { | ||
throw new Error('Must provide an App Level Token when initializing a Socket Mode Client'); | ||
throw new Error('Must provide an App-Level Token when initializing a Socket Mode Client'); | ||
} | ||
this.clientPingTimeout = clientPingTimeout; | ||
this.pingPongLoggingEnabled = pingPongLoggingEnabled; | ||
this.clientPingTimeoutMillis = clientPingTimeout; | ||
this.lastPongReceivedTimestamp = undefined; | ||
this.serverPingTimeoutMillis = serverPingTimeout; | ||
// Setup the logger | ||
@@ -284,23 +212,34 @@ if (typeof logger !== 'undefined') { | ||
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 web_api_1.WebClient('', Object.assign({ logger, logLevel: this.logger.getLevel(), headers: { Authorization: `Bearer ${appToken}` } }, clientOptions)); | ||
this.autoReconnectEnabled = autoReconnectEnabled; | ||
this.stateMachine = finity_1.default.start(this.stateMachineConfig); | ||
this.logger.debug('initialized'); | ||
this.logger.debug('The Socket Mode client is successfully initialized'); | ||
} | ||
/** | ||
* Begin an Socket Mode session. This method must be called before any messages can | ||
* be sent or received. | ||
* Returns true if the underlying WebSocket connection is active. | ||
*/ | ||
isActive() { | ||
this.logger.debug(`Details of isActive() response (connected: ${this.connected}, authenticated: ${this.authenticated}, badConnection: ${this.badConnection})`); | ||
return this.connected && this.authenticated && !this.badConnection; | ||
} | ||
/** | ||
* Begin a Socket Mode session. | ||
* This method must be called before any messages can be sent or received. | ||
*/ | ||
start() { | ||
this.logger.debug('start()'); | ||
// delegate behavior to state machine | ||
this.stateMachine.handle('start'); | ||
// return a promise that resolves with the connection information | ||
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('authenticated', (result) => { | ||
this.removeListener('disconnected', reject); | ||
this.once(State.Authenticated, (result) => { | ||
this.removeListener(State.Disconnected, reject); | ||
resolve(result); | ||
}); | ||
this.once('disconnected', (err) => { | ||
this.removeListener('authenticated', resolve); | ||
this.once(State.Disconnected, (err) => { | ||
this.removeListener(State.Authenticated, resolve); | ||
reject(err); | ||
@@ -311,10 +250,10 @@ }); | ||
/** | ||
* End a Socket Mode session. After this method is called no messages will be sent or received unless you call | ||
* start() again later. | ||
* End a Socket Mode session. After this method is called no messages will be sent or received | ||
* unless you call start() again later. | ||
*/ | ||
disconnect() { | ||
return new Promise((resolve, reject) => { | ||
this.logger.debug('manual disconnect'); | ||
// resolve (or reject) on disconnect | ||
this.once('disconnected', (err) => { | ||
this.logger.debug('Manually disconnecting this Socket Mode client'); | ||
// Resolve (or reject) on disconnect | ||
this.once(State.Disconnected, (err) => { | ||
if (err instanceof Error) { | ||
@@ -327,23 +266,23 @@ reject(err); | ||
}); | ||
// delegate behavior to state machine | ||
this.stateMachine.handle('explicit disconnect'); | ||
// Delegate behavior to state machine | ||
this.stateMachine.handle(Event.ExplicitDisconnect); | ||
}); | ||
} | ||
/** | ||
* Method for sending an outgoing message of an arbitrary type over the websocket connection. | ||
* 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 | ||
* @param body the message body or string text | ||
*/ | ||
send(id, body = {}) { | ||
const message = { envelope_id: id, payload: Object.assign({}, body) }; | ||
const _body = typeof body === 'string' ? { text: body } : body; | ||
const message = { envelope_id: id, payload: Object.assign({}, _body) }; | ||
return new Promise((resolve, reject) => { | ||
this.logger.debug(`send() in state: ${this.stateMachine.getStateHierarchy()}`); | ||
this.logger.debug(`send() method was called in state: ${this.stateMachine.getCurrentState()}, state hierarchy: ${this.stateMachine.getStateHierarchy()}`); | ||
if (this.websocket === undefined) { | ||
this.logger.error('cannot send message when client is not connected'); | ||
this.logger.error('Failed to send a message as the client is not connected'); | ||
reject((0, errors_1.sendWhileDisconnectedError)()); | ||
} | ||
else if (!(this.stateMachine.getCurrentState() === 'connected' && | ||
this.stateMachine.getStateHierarchy()[1] === 'ready')) { | ||
this.logger.error('cannot send message when client is not ready'); | ||
else if (!this.isConnectionReady()) { | ||
this.logger.error('Failed to send a message as the client is not ready'); | ||
reject((0, errors_1.sendWhileNotReadyError)()); | ||
@@ -354,6 +293,6 @@ } | ||
const flatMessage = JSON.stringify(message); | ||
this.logger.debug(`sending message on websocket: ${flatMessage}`); | ||
this.logger.debug(`Sending a WebSocket message: ${flatMessage}`); | ||
this.websocket.send(flatMessage, (error) => { | ||
if (error !== undefined) { | ||
this.logger.error(`failed to send message on websocket: ${error.message}`); | ||
this.logger.error(`Failed to send a WebSocket message (error: ${error.message})`); | ||
return reject((0, errors_1.websocketErrorWithOriginal)(error)); | ||
@@ -366,4 +305,51 @@ } | ||
} | ||
async retrieveWSSURL() { | ||
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; | ||
} | ||
} | ||
autoReconnectCondition() { | ||
return this.autoReconnectEnabled; | ||
} | ||
reconnectingCondition(context) { | ||
const error = context.error; | ||
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('unable_to_socket_mode_start', error); | ||
let isRecoverable = true; | ||
if (error.code === web_api_1.ErrorCode.PlatformError && | ||
Object.values(UnrecoverableSocketModeStartError_1.UnrecoverableSocketModeStartError).includes(error.data.error)) { | ||
isRecoverable = false; | ||
} | ||
else if (error.code === web_api_1.ErrorCode.RequestError) { | ||
isRecoverable = false; | ||
} | ||
else if (error.code === web_api_1.ErrorCode.HTTPError) { | ||
isRecoverable = false; | ||
} | ||
return this.autoReconnectEnabled && isRecoverable; | ||
} | ||
configureAuthenticatedWebSocket(_state, context) { | ||
this.authenticated = true; | ||
this.setupWebSocket(context.result.url); | ||
setImmediate(() => { | ||
this.emit(State.Authenticated, context.result); | ||
}); | ||
} | ||
handleConnectionFailure(_state, context) { | ||
// dispatch 'failure' on parent machine to transition out of this submachine's states | ||
this.stateMachine.handle(Event.Failure, context.error); | ||
} | ||
handleDisconnection() { | ||
// This transition circumvents the 'disconnecting' state | ||
// (since the websocket is already closed), so we need to execute its onExit behavior here. | ||
this.tearDownWebSocket(); | ||
} | ||
/** | ||
* Set up method for the client's websocket instance. This method will attach event listeners. | ||
* Set up method for the client's WebSocket instance. This method will attach event listeners. | ||
*/ | ||
@@ -388,90 +374,196 @@ setupWebSocket(url) { | ||
// attach event listeners | ||
websocket.addEventListener('open', (event) => { this.stateMachine.handle('websocket open', event); }); | ||
websocket.addEventListener('close', (event) => { this.stateMachine.handle('websocket close', event); }); | ||
websocket.addEventListener('open', (event) => { | ||
this.stateMachine.handle(Event.WebSocketOpen, event); | ||
}); | ||
websocket.addEventListener('close', (event) => { | ||
this.stateMachine.handle(Event.WebSocketClose, event); | ||
}); | ||
websocket.addEventListener('error', (event) => { | ||
this.logger.error(`A websocket error occurred: ${event.message}`); | ||
this.logger.error(`A WebSocket error occurred: ${event.message}`); | ||
this.emit('error', (0, errors_1.websocketErrorWithOriginal)(event.error)); | ||
}); | ||
websocket.addEventListener('message', this.onWebSocketMessage.bind(this)); | ||
// Confirm websocket connection is still active | ||
websocket.addEventListener('ping', this.heartbeat.bind(this)); | ||
// Confirm WebSocket connection is still active | ||
websocket.addEventListener('ping', ((data) => { | ||
if (this.pingPongLoggingEnabled) { | ||
this.logger.debug(`Received ping from Slack server (data: ${data})`); | ||
} | ||
this.startMonitoringPingFromSlack(); | ||
// Since the `addEventListener` method does not accept listener with data arg in TypeScript, | ||
// we cast this function to any as a workaround | ||
})); // eslint-disable-line @typescript-eslint/no-explicit-any | ||
websocket.addEventListener('pong', ((data) => { | ||
if (this.pingPongLoggingEnabled) { | ||
this.logger.debug(`Received pong from Slack server (data: ${data})`); | ||
} | ||
this.lastPongReceivedTimestamp = new Date().getTime(); | ||
// Since the `addEventListener` method does not accept listener with data arg in TypeScript, | ||
// we cast this function to any as a workaround | ||
})); // eslint-disable-line @typescript-eslint/no-explicit-any | ||
} | ||
/** | ||
* Tear down method for the client's websocket instance. This method undoes the work in setupWebSocket(url). | ||
* Tear down the currently working heartbeat jobs. | ||
*/ | ||
teardownWebsocket() { | ||
tearDownHeartBeatJobs() { | ||
if (this.serverPingTimeout !== undefined) { | ||
clearTimeout(this.serverPingTimeout); | ||
} | ||
if (this.clientPingTimeout !== undefined) { | ||
clearTimeout(this.clientPingTimeout); | ||
} | ||
} | ||
/** | ||
* Tear down method for the client's WebSocket instance. | ||
* This method undoes the work in setupWebSocket(url). | ||
*/ | ||
tearDownWebSocket() { | ||
if (this.secondaryWebsocket !== undefined && this.websocket !== undefined) { | ||
this.logger.debug('secondary websocket exists, tear down first and assign second'); | ||
// currently have two websockets, so tear down the older one | ||
this.websocket.removeAllListeners('open'); | ||
this.websocket.removeAllListeners('close'); | ||
this.websocket.removeAllListeners('error'); | ||
this.websocket.removeAllListeners('message'); | ||
this.logger.debug('Since the secondary WebSocket exists, going to tear down the first and assign second ...'); | ||
// Currently have two WebSocket objects, so tear down the older one | ||
const oldWebsocket = this.websocket; | ||
// Switch to the new one here | ||
this.websocket = this.secondaryWebsocket; | ||
this.secondaryWebsocket = undefined; | ||
// Clean up the old one | ||
try { | ||
oldWebsocket.removeAllListeners('open'); | ||
oldWebsocket.removeAllListeners('close'); | ||
oldWebsocket.removeAllListeners('error'); | ||
oldWebsocket.removeAllListeners('message'); | ||
oldWebsocket.close(); | ||
oldWebsocket.terminate(); | ||
} | ||
catch (e) { | ||
this.logger.error(`Failed to terminate the old WS connection (error: ${e})`); | ||
} | ||
} | ||
else if (this.secondaryWebsocket === undefined && this.websocket !== undefined) { | ||
this.logger.debug('only primary websocket exists, tear it down'); | ||
// only one websocket to tear down | ||
this.websocket.removeAllListeners('open'); | ||
this.websocket.removeAllListeners('close'); | ||
this.websocket.removeAllListeners('error'); | ||
this.websocket.removeAllListeners('message'); | ||
this.logger.debug('Since only the primary WebSocket exists, going to tear it down ...'); | ||
// The only one WebSocket to tear down | ||
try { | ||
this.websocket.removeAllListeners('open'); | ||
this.websocket.removeAllListeners('close'); | ||
this.websocket.removeAllListeners('error'); | ||
this.websocket.removeAllListeners('message'); | ||
this.websocket.close(); | ||
this.websocket.terminate(); | ||
} | ||
catch (e) { | ||
this.logger.error(`Failed to terminate the old WS connection (error: ${e})`); | ||
} | ||
this.websocket = undefined; | ||
} | ||
this.logger.debug('Tearing down the old WebSocket connection has finished'); | ||
} | ||
startPeriodicallySendingPingToSlack() { | ||
if (this.clientPingTimeout !== undefined) { | ||
clearTimeout(this.clientPingTimeout); | ||
} | ||
// re-init for new monitoring loop | ||
this.lastPongReceivedTimestamp = undefined; | ||
let pingAttemptCount = 0; | ||
if (!this.badConnection) { | ||
this.clientPingTimeout = setInterval(() => { | ||
var _a; | ||
const nowMillis = new Date().getTime(); | ||
try { | ||
const pingMessage = `Ping from client (${nowMillis})`; | ||
(_a = this.websocket) === null || _a === void 0 ? void 0 : _a.ping(pingMessage); | ||
if (this.lastPongReceivedTimestamp === undefined) { | ||
pingAttemptCount += 1; | ||
} | ||
else { | ||
pingAttemptCount = 0; | ||
} | ||
if (this.pingPongLoggingEnabled) { | ||
this.logger.debug(`Sent ping to Slack: ${pingMessage}`); | ||
} | ||
} | ||
catch (e) { | ||
this.logger.error(`Failed to send ping to Slack (error: ${e})`); | ||
this.handlePingPongErrorReconnection(); | ||
return; | ||
} | ||
let isInvalid = pingAttemptCount > 5; | ||
if (this.lastPongReceivedTimestamp !== undefined) { | ||
const millis = nowMillis - this.lastPongReceivedTimestamp; | ||
isInvalid = millis > this.clientPingTimeoutMillis; | ||
} | ||
if (isInvalid) { | ||
this.logger.info(`A pong wasn't received from the server before the timeout of ${this.clientPingTimeoutMillis}ms!`); | ||
this.handlePingPongErrorReconnection(); | ||
} | ||
}, this.clientPingTimeoutMillis / 3); | ||
} | ||
} | ||
handlePingPongErrorReconnection() { | ||
try { | ||
this.stateMachine.handle(Event.ServerPongsNotReceived); | ||
} | ||
catch (e) { | ||
this.logger.error(`Failed to reconnect to Slack (error: ${e})`); | ||
} | ||
} | ||
/** | ||
* confirms websocket connection is still active | ||
* Confirms WebSocket connection is still active | ||
* fires whenever a ping event is received | ||
*/ | ||
heartbeat() { | ||
if (this.pingTimeout !== undefined) { | ||
clearTimeout(this.pingTimeout); | ||
startMonitoringPingFromSlack() { | ||
if (this.serverPingTimeout !== undefined) { | ||
clearTimeout(this.serverPingTimeout); | ||
} | ||
// Don't start heartbeat if connection is already deemed bad | ||
if (!this.badConnection) { | ||
this.pingTimeout = setTimeout(() => { | ||
this.logger.info(`A ping wasn't received from the server before the timeout of ${this.clientPingTimeout}ms!`); | ||
if (this.stateMachine.getCurrentState() === 'connected' && | ||
this.stateMachine.getStateHierarchy()[1] === 'ready') { | ||
this.serverPingTimeout = setTimeout(() => { | ||
this.logger.info(`A ping wasn't received from the server before the timeout of ${this.serverPingTimeoutMillis}ms!`); | ||
if (this.isConnectionReady()) { | ||
this.badConnection = true; | ||
// opens secondary websocket and teardown original once that is ready | ||
this.stateMachine.handle('server pings not received'); | ||
// Opens secondary WebSocket and teardown original once that is ready | ||
this.stateMachine.handle(Event.ServerPingsNotReceived); | ||
} | ||
}, this.clientPingTimeout); | ||
}, this.serverPingTimeoutMillis); | ||
} | ||
} | ||
isConnectionReady() { | ||
const currentState = this.stateMachine.getCurrentState(); | ||
const stateHierarchy = this.stateMachine.getStateHierarchy(); | ||
return currentState === State.Connected && | ||
stateHierarchy !== undefined && | ||
stateHierarchy.length >= 2 && | ||
stateHierarchy[1] === State.Ready; | ||
} | ||
/** | ||
* `onmessage` handler for the client's websocket. This will parse the payload and dispatch the relevant events for | ||
* each incoming message. | ||
* `onmessage` handler for the client's WebSocket. | ||
* This will parse the payload and dispatch the relevant events for each incoming message. | ||
*/ | ||
async onWebSocketMessage({ data }) { | ||
this.logger.debug(`received a message on the WebSocket: ${data}`); | ||
// parse message into slack event | ||
this.logger.debug(`Received a message on the WebSocket: ${data}`); | ||
// Parse message into slack event | ||
let event; | ||
try { | ||
event = JSON.parse(data); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
} | ||
catch (parseError) { | ||
// prevent application from crashing on a bad message, but log an error to bring attention | ||
this.logger.error(`unable to parse incoming websocket message: ${parseError.message}`); | ||
// Prevent application from crashing on a bad message, but log an error to bring attention | ||
this.logger.error(`Unable to parse an incoming WebSocket message: ${parseError.message}`); | ||
return; | ||
} | ||
// internal event handlers | ||
// Internal event handlers | ||
if (event.type === 'hello') { | ||
this.stateMachine.handle('server hello'); | ||
this.stateMachine.handle(Event.ServerHello); | ||
return; | ||
} | ||
// open second websocket connection in preparation for the existing websocket disconnecting | ||
// Open the second WebSocket connection in preparation for the existing WebSocket disconnecting | ||
if (event.type === 'disconnect' && event.reason === 'warning') { | ||
this.logger.debug('disconnect warning, creating second connection'); | ||
this.stateMachine.handle('server disconnect warning'); | ||
this.logger.debug('Received "disconnect" (warning) message - creating the second connection'); | ||
this.stateMachine.handle(Event.ServerDisconnectWarning); | ||
return; | ||
} | ||
// close primary websocket in favor of secondary websocket, assign secondary to primary | ||
// Close the primary WebSocket in favor of secondary WebSocket, assign secondary to primary | ||
if (event.type === 'disconnect' && event.reason === 'refresh_requested') { | ||
this.logger.debug('disconnect refresh requested, closing old websocket'); | ||
this.stateMachine.handle('server disconnect old socket'); | ||
this.logger.debug('Received "disconnect" (refresh requested) message - closing the old WebSocket connection'); | ||
this.stateMachine.handle(Event.ServerDisconnectOldSocket); | ||
// TODO: instead of using this event to reassign secondaryWebsocket to this.websocket, | ||
// use the websocket close event | ||
// use the WebSocket close event | ||
return; | ||
@@ -481,9 +573,10 @@ } | ||
const ack = async (response) => { | ||
this.logger.debug('calling ack', event.type); | ||
this.logger.debug(`Calling ack() - type: ${event.type}, envelope_id: ${event.envelope_id}, data: ${response}`); | ||
await this.send(event.envelope_id, response); | ||
}; | ||
// for events_api messages, expose the type of the event | ||
// For events_api messages, expose the type of the event | ||
if (event.type === 'events_api') { | ||
this.emit(event.payload.event.type, { | ||
ack, | ||
envelope_id: event.envelope_id, | ||
body: event.payload, | ||
@@ -497,5 +590,6 @@ event: event.payload.event, | ||
else { | ||
// emit just ack and body for all other types of messages | ||
// Emit just ack and body for all other types of messages | ||
this.emit(event.type, { | ||
ack, | ||
envelope_id: event.envelope_id, | ||
body: event.payload, | ||
@@ -505,6 +599,7 @@ accepts_response_payload: event.accepts_response_payload, | ||
} | ||
// emitter for all slack events | ||
// used in tools like bolt-js | ||
// Emitter for all slack events | ||
// (this can be used in tools like bolt-js) | ||
this.emit('slack_event', { | ||
ack, | ||
envelope_id: event.envelope_id, | ||
type: event.type, | ||
@@ -511,0 +606,0 @@ body: event.payload, |
{ | ||
"name": "@slack/socket-mode", | ||
"version": "1.2.0", | ||
"version": "1.3.0-rc.0", | ||
"description": "Official library for using the Slack Platform's Socket Mode API", | ||
@@ -5,0 +5,0 @@ "author": "Slack Technologies, LLC", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
80760
26
961
1