mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor(slack): extract socket reconnect policy helpers
This commit is contained in:
@@ -33,6 +33,13 @@ import { resolveSlackSlashCommandConfig } from "./commands.js";
|
||||
import { createSlackMonitorContext } from "./context.js";
|
||||
import { registerSlackMonitorEvents } from "./events.js";
|
||||
import { createSlackMessageHandler } from "./message-handler.js";
|
||||
import {
|
||||
formatUnknownError,
|
||||
getSocketEmitter,
|
||||
isNonRecoverableSlackAuthError,
|
||||
SLACK_SOCKET_RECONNECT_POLICY,
|
||||
waitForSlackSocketDisconnect,
|
||||
} from "./reconnect-policy.js";
|
||||
import { registerSlackMonitorSlashCommands } from "./slash.js";
|
||||
import type { MonitorSlackOpts } from "./types.js";
|
||||
|
||||
@@ -47,113 +54,6 @@ const { App, HTTPReceiver } = slackBolt;
|
||||
|
||||
const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
||||
const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
||||
const SLACK_SOCKET_RECONNECT_POLICY = {
|
||||
initialMs: 2_000,
|
||||
maxMs: 30_000,
|
||||
factor: 1.8,
|
||||
jitter: 0.25,
|
||||
maxAttempts: 12,
|
||||
} as const;
|
||||
|
||||
type SlackSocketDisconnectEvent = "disconnect" | "unable_to_socket_mode_start" | "error";
|
||||
|
||||
type EmitterLike = {
|
||||
on: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
||||
off: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
||||
};
|
||||
|
||||
function getSocketEmitter(app: unknown): EmitterLike | null {
|
||||
const receiver = (app as { receiver?: unknown }).receiver;
|
||||
const client =
|
||||
receiver && typeof receiver === "object"
|
||||
? (receiver as { client?: unknown }).client
|
||||
: undefined;
|
||||
if (!client || typeof client !== "object") {
|
||||
return null;
|
||||
}
|
||||
const on = (client as { on?: unknown }).on;
|
||||
const off = (client as { off?: unknown }).off;
|
||||
if (typeof on !== "function" || typeof off !== "function") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
on: (event, listener) =>
|
||||
(
|
||||
on as (this: unknown, event: string, listener: (...args: unknown[]) => void) => unknown
|
||||
).call(client, event, listener),
|
||||
off: (event, listener) =>
|
||||
(
|
||||
off as (this: unknown, event: string, listener: (...args: unknown[]) => void) => unknown
|
||||
).call(client, event, listener),
|
||||
};
|
||||
}
|
||||
|
||||
function waitForSlackSocketDisconnect(
|
||||
app: unknown,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<{
|
||||
event: SlackSocketDisconnectEvent;
|
||||
error?: unknown;
|
||||
}> {
|
||||
return new Promise((resolve) => {
|
||||
const emitter = getSocketEmitter(app);
|
||||
if (!emitter) {
|
||||
abortSignal?.addEventListener("abort", () => resolve({ event: "disconnect" }), {
|
||||
once: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const disconnectListener = () => resolveOnce({ event: "disconnect" });
|
||||
const startFailListener = (error?: unknown) =>
|
||||
resolveOnce({ event: "unable_to_socket_mode_start", error });
|
||||
const errorListener = (error: unknown) => resolveOnce({ event: "error", error });
|
||||
const abortListener = () => resolveOnce({ event: "disconnect" });
|
||||
|
||||
const cleanup = () => {
|
||||
emitter.off("disconnected", disconnectListener);
|
||||
emitter.off("unable_to_socket_mode_start", startFailListener);
|
||||
emitter.off("error", errorListener);
|
||||
abortSignal?.removeEventListener("abort", abortListener);
|
||||
};
|
||||
|
||||
const resolveOnce = (value: { event: SlackSocketDisconnectEvent; error?: unknown }) => {
|
||||
cleanup();
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
emitter.on("disconnected", disconnectListener);
|
||||
emitter.on("unable_to_socket_mode_start", startFailListener);
|
||||
emitter.on("error", errorListener);
|
||||
abortSignal?.addEventListener("abort", abortListener, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect non-recoverable Slack API / auth errors that should NOT be retried.
|
||||
* These indicate permanent credential problems (revoked bot, deactivated account, etc.)
|
||||
* and retrying will never succeed — continuing to retry blocks the entire gateway.
|
||||
*/
|
||||
export function isNonRecoverableSlackAuthError(error: unknown): boolean {
|
||||
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
||||
return /account_inactive|invalid_auth|token_revoked|token_expired|not_authed|org_login_required|team_access_not_granted|missing_scope|cannot_find_service|invalid_token/i.test(
|
||||
msg,
|
||||
);
|
||||
}
|
||||
|
||||
function formatUnknownError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch {
|
||||
return "unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
function parseApiAppIdFromAppToken(raw?: string) {
|
||||
const token = raw?.trim();
|
||||
@@ -572,6 +472,8 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export { isNonRecoverableSlackAuthError } from "./reconnect-policy.js";
|
||||
|
||||
export const __testing = {
|
||||
resolveSlackRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
|
||||
108
src/slack/monitor/reconnect-policy.ts
Normal file
108
src/slack/monitor/reconnect-policy.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
const SLACK_AUTH_ERROR_RE =
|
||||
/account_inactive|invalid_auth|token_revoked|token_expired|not_authed|org_login_required|team_access_not_granted|missing_scope|cannot_find_service|invalid_token/i;
|
||||
|
||||
export const SLACK_SOCKET_RECONNECT_POLICY = {
|
||||
initialMs: 2_000,
|
||||
maxMs: 30_000,
|
||||
factor: 1.8,
|
||||
jitter: 0.25,
|
||||
maxAttempts: 12,
|
||||
} as const;
|
||||
|
||||
export type SlackSocketDisconnectEvent = "disconnect" | "unable_to_socket_mode_start" | "error";
|
||||
|
||||
type EmitterLike = {
|
||||
on: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
||||
off: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
||||
};
|
||||
|
||||
export function getSocketEmitter(app: unknown): EmitterLike | null {
|
||||
const receiver = (app as { receiver?: unknown }).receiver;
|
||||
const client =
|
||||
receiver && typeof receiver === "object"
|
||||
? (receiver as { client?: unknown }).client
|
||||
: undefined;
|
||||
if (!client || typeof client !== "object") {
|
||||
return null;
|
||||
}
|
||||
const on = (client as { on?: unknown }).on;
|
||||
const off = (client as { off?: unknown }).off;
|
||||
if (typeof on !== "function" || typeof off !== "function") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
on: (event, listener) =>
|
||||
(
|
||||
on as (this: unknown, event: string, listener: (...args: unknown[]) => void) => unknown
|
||||
).call(client, event, listener),
|
||||
off: (event, listener) =>
|
||||
(
|
||||
off as (this: unknown, event: string, listener: (...args: unknown[]) => void) => unknown
|
||||
).call(client, event, listener),
|
||||
};
|
||||
}
|
||||
|
||||
export function waitForSlackSocketDisconnect(
|
||||
app: unknown,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<{
|
||||
event: SlackSocketDisconnectEvent;
|
||||
error?: unknown;
|
||||
}> {
|
||||
return new Promise((resolve) => {
|
||||
const emitter = getSocketEmitter(app);
|
||||
if (!emitter) {
|
||||
abortSignal?.addEventListener("abort", () => resolve({ event: "disconnect" }), {
|
||||
once: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const disconnectListener = () => resolveOnce({ event: "disconnect" });
|
||||
const startFailListener = (error?: unknown) =>
|
||||
resolveOnce({ event: "unable_to_socket_mode_start", error });
|
||||
const errorListener = (error: unknown) => resolveOnce({ event: "error", error });
|
||||
const abortListener = () => resolveOnce({ event: "disconnect" });
|
||||
|
||||
const cleanup = () => {
|
||||
emitter.off("disconnected", disconnectListener);
|
||||
emitter.off("unable_to_socket_mode_start", startFailListener);
|
||||
emitter.off("error", errorListener);
|
||||
abortSignal?.removeEventListener("abort", abortListener);
|
||||
};
|
||||
|
||||
const resolveOnce = (value: { event: SlackSocketDisconnectEvent; error?: unknown }) => {
|
||||
cleanup();
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
emitter.on("disconnected", disconnectListener);
|
||||
emitter.on("unable_to_socket_mode_start", startFailListener);
|
||||
emitter.on("error", errorListener);
|
||||
abortSignal?.addEventListener("abort", abortListener, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect non-recoverable Slack API / auth errors that should NOT be retried.
|
||||
* These indicate permanent credential problems (revoked bot, deactivated account, etc.)
|
||||
* and retrying will never succeed — continuing to retry blocks the entire gateway.
|
||||
*/
|
||||
export function isNonRecoverableSlackAuthError(error: unknown): boolean {
|
||||
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
||||
return SLACK_AUTH_ERROR_RE.test(msg);
|
||||
}
|
||||
|
||||
export function formatUnknownError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch {
|
||||
return "unknown error";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user