import type { SlackEventCallback, SlackInteractivityPayload, SlackSlashCommandPayload, SlackWebApi } from "./types.js"; import type { SlackEventHandlingResult } from "./adapter.js"; /** * Handles a Slack Events API callback after the built-in Socket Mode runner has * acknowledged and admitted it at most once within that runner's bounded * instance-local window. Custom connection runners must provide equivalent * at-most-once admission before calling {@link handleEventCallback}. */ export interface SlackEventCallbackHandler { handleEventCallback(callback: SlackEventCallback): Promise; } /** * Handles a Slack interactivity payload — a shortcut, message action, or a * Block Kit button click (`block_actions`). Invoked AFTER the envelope is * acknowledged, so a slow handler never risks Slack's 3-second ack deadline. */ export type SlackInteractionHandler = (payload: SlackInteractivityPayload) => void | Promise; /** Handles a workspace-registered slash command after its Socket Mode ack. */ export type SlackSlashCommandHandler = (payload: SlackSlashCommandPayload) => void | Promise; export interface SlackSocketModeRunnerBackoffOptions { initialMs?: number; maxMs?: number; /** * Band jitter applied to each backoff sleep, as a fraction of the base delay * (0 disables jitter). The actual sleep is uniformly within ±ratio of the base — * `base * (1 - ratio + random() * 2 * ratio)` — so clients (or a process restarting * in a loop) that would otherwise reconnect in lockstep de-synchronize instead of * re-colliding on Slack's per-app connection budget. */ jitterRatio?: number; /** * How long a freshly opened socket must stay up before the backoff resets to * initial and a previously-degraded connection is reported recovered. The reset is * gated on this window (not on a mere connect) so a connect→immediate-drop flap * keeps accumulating backoff instead of re-hammering at the initial delay. Set to 0 * to disable the stability gate (the backoff then never auto-resets within a run). */ stabilityMs?: number; /** * Grace window after `start()` during which a non-graceful disconnect that arrives * before the first successful open is treated as an expected lingering prior-process * socket (common right after a restart): it is retried quietly with backoff and does * NOT flag the channel degraded. Set to 0 to disable the grace. */ startupGraceMs?: number; /** * Backstop: after a watchdog-triggered `terminate()`, if the socket emits neither * `close` nor `error` within this window, the attempt is force-settled so the * reconnect loop can never wedge on a stuck socket. Set to 0 to disable. */ drainDeadlineMs?: number; /** * Minimum (jittered) delay between reconnect attempts on the GRACEFUL path * (refresh/warning/clean close), which otherwise reconnects immediately. This is * NOT the failure backoff — it stays small — but it rate-limits the loop so a * degenerate server that drops every socket at/before open cannot spin into a * zero-delay reconnect storm (the very `too_many_websockets` failure mode this * adapter guards against). Set to 0 to reconnect with no floor. */ gracefulReconnectFloorMs?: number; } export interface SlackSocketModeRunnerHeartbeatOptions { /** * How often the watchdog wakes to probe an idle socket with a ping and to * check for silence. Because the silence check only runs on these ticks, * recycling can lag the `timeoutMs` deadline by up to one `intervalMs`. * Setting this to 0 disables the watchdog entirely. */ intervalMs?: number; /** * Silence budget: if no inbound frame (message, ping, or pong) arrives within * this window, the socket is treated as silently dead and force-recycled on * the next watchdog tick so the reconnect loop can re-establish it. The actual * recycle therefore happens between `timeoutMs` and `timeoutMs + intervalMs` * after the last frame. Setting this to 0 disables the watchdog entirely. */ timeoutMs?: number; } export interface SlackSocketModeRunnerLogger { debug?(message: string, metadata?: Record): void; info?(message: string, metadata?: Record): void; warn?(message: string, metadata?: Record): void; error?(message: string, metadata?: Record): void; } export interface SlackSocketModeRunnerOptions { api: SlackWebApi; handler: SlackEventCallbackHandler; reconnect?: SlackSocketModeRunnerBackoffOptions; heartbeat?: SlackSocketModeRunnerHeartbeatOptions; webSocketFactory?: SlackWebSocketFactory; onEventResult?: (result: SlackEventHandlingResult) => void | Promise; /** * Optional handler for shortcut interactivity payloads. When absent, interactive * envelopes are acknowledged and ignored (the historical behavior); when set, * shortcut payloads are routed to it after the envelope is acknowledged. */ onInteraction?: SlackInteractionHandler; /** Optional slash-command handler. Envelopes are acknowledged before dispatch. */ onSlashCommand?: SlackSlashCommandHandler; /** * Called once when an established connection drops into the reconnect/backoff loop * (a real degradation — `too_many_websockets`, a socket error, a heartbeat timeout, * an unknown disconnect reason). Suppressed for a graceful refresh and for a * lingering prior-process socket inside the startup grace window. Wire this to the * app's `onDegraded` so the channel reports `degraded` (responder kept alive) rather * than churning silently. */ onConnectionLost?: (reason: string) => void; /** * Called once a reconnect has stayed open for the stability window after a prior * loss (never on the first connect). Wire this to the app's `onRecovered`. */ onConnectionRestored?: () => void; /** Injected RNG in [0, 1) for backoff jitter; defaults to `Math.random`. */ random?: () => number; /** Injectable clock for event callback dedupe; defaults to `Date.now`. */ eventDedupeNow?: () => number; logger?: SlackSocketModeRunnerLogger; } export interface SlackSocketModeRunnerStartOptions { signal?: AbortSignal; } export type SlackWebSocketFactory = (url: string) => SlackWebSocketLike; export interface SlackWebSocketLike { send(data: string): void; close(code?: number, reason?: string): void; /** Send a WebSocket ping frame (keepalive). Optional: not every transport exposes it. */ ping?(): void; /** Forcibly destroy the socket without a closing handshake. Optional. */ terminate?(): void; on(event: "open", listener: () => void): this; on(event: "message", listener: (data: unknown) => void): this; on(event: "close", listener: (code?: number, reason?: unknown) => void): this; on(event: "error", listener: (error: unknown) => void): this; } export declare class SlackSocketModeRunner { private readonly api; private readonly handler; private readonly initialBackoffMs; private readonly maxBackoffMs; private readonly heartbeatIntervalMs; private readonly heartbeatTimeoutMs; private readonly stabilityMs; private readonly startupGraceMs; private readonly drainDeadlineMs; private readonly gracefulReconnectFloorMs; private readonly jitterRatio; private readonly random; private readonly eventDedupeNow; private readonly webSocketFactory; private readonly onEventResult; private readonly onInteraction; private readonly onSlashCommand; private readonly onConnectionLost; private readonly onConnectionRestored; private readonly logger; private activeSocket; private currentBackoffMs; private connectionDegraded; private hasEverConnected; private startedAt; private readonly admittedEventIds; private eventDedupeCapacityWarned; constructor(options: SlackSocketModeRunnerOptions); start(options?: SlackSocketModeRunnerStartOptions): Promise; /** * Apply band jitter to a backoff base: uniformly within ±`jitterRatio` of `baseMs`. * De-synchronizes reconnect attempts so colliding clients (or a restart loop) stop * re-triggering Slack's `too_many_websockets` in lockstep. */ private jitteredDelay; private connectOnce; private handleEnvelope; /** * Admit an exact, nonblank callback event id synchronously after ack. Returns * true when the callback is still inside its original admission window. */ private suppressDuplicateEventCallback; /** Remove only the expired insertion-order prefix; never scan or refresh hits. */ private pruneExpiredEventDedupePrefix; } //# sourceMappingURL=socket-mode-runner.d.ts.map