fix: reduce Slack WebClient retries
This commit is contained in:
@@ -44,6 +44,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik.
|
- Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik.
|
||||||
- Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff.
|
- Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff.
|
||||||
- Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
|
- Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
|
||||||
|
- Slack: reduce WebClient retries to avoid duplicate sends. (#1481)
|
||||||
- Slack: read thread replies for message reads when threadId is provided (replies-only). (#1450) Thanks @rodrigouroz.
|
- Slack: read thread replies for message reads when threadId is provided (replies-only). (#1450) Thanks @rodrigouroz.
|
||||||
- macOS: prefer linked channels in gateway summary to avoid false “not linked” status.
|
- macOS: prefer linked channels in gateway summary to avoid false “not linked” status.
|
||||||
- macOS/tests: fix gateway summary lookup after guard unwrap; prevent browser opens during tests. (ECID-1483)
|
- macOS/tests: fix gateway summary lookup after guard unwrap; prevent browser opens during tests. (ECID-1483)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { WebClient } from "@slack/web-api";
|
import type { WebClient } from "@slack/web-api";
|
||||||
|
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { logVerbose } from "../globals.js";
|
import { logVerbose } from "../globals.js";
|
||||||
import { resolveSlackAccount } from "./accounts.js";
|
import { resolveSlackAccount } from "./accounts.js";
|
||||||
|
import { createSlackWebClient } from "./client.js";
|
||||||
import { sendMessageSlack } from "./send.js";
|
import { sendMessageSlack } from "./send.js";
|
||||||
import { resolveSlackBotToken } from "./token.js";
|
import { resolveSlackBotToken } from "./token.js";
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ function normalizeEmoji(raw: string) {
|
|||||||
|
|
||||||
async function getClient(opts: SlackActionClientOpts = {}) {
|
async function getClient(opts: SlackActionClientOpts = {}) {
|
||||||
const token = resolveToken(opts.token, opts.accountId);
|
const token = resolveToken(opts.token, opts.accountId);
|
||||||
return opts.client ?? new WebClient(token);
|
return opts.client ?? createSlackWebClient(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveBotUserId(client: WebClient) {
|
async function resolveBotUserId(client: WebClient) {
|
||||||
|
|||||||
46
src/slack/client.test.ts
Normal file
46
src/slack/client.test.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("@slack/web-api", () => {
|
||||||
|
const WebClient = vi.fn(function WebClientMock(
|
||||||
|
this: Record<string, unknown>,
|
||||||
|
token: string,
|
||||||
|
options?: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
this.token = token;
|
||||||
|
this.options = options;
|
||||||
|
});
|
||||||
|
return { WebClient };
|
||||||
|
});
|
||||||
|
|
||||||
|
const slackWebApi = await import("@slack/web-api");
|
||||||
|
const { createSlackWebClient, resolveSlackWebClientOptions, SLACK_DEFAULT_RETRY_OPTIONS } =
|
||||||
|
await import("./client.js");
|
||||||
|
|
||||||
|
const WebClient = slackWebApi.WebClient as unknown as ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
|
describe("slack web client config", () => {
|
||||||
|
it("applies the default retry config when none is provided", () => {
|
||||||
|
const options = resolveSlackWebClientOptions();
|
||||||
|
|
||||||
|
expect(options.retryConfig).toEqual(SLACK_DEFAULT_RETRY_OPTIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects explicit retry config overrides", () => {
|
||||||
|
const customRetry = { retries: 0 };
|
||||||
|
const options = resolveSlackWebClientOptions({ retryConfig: customRetry });
|
||||||
|
|
||||||
|
expect(options.retryConfig).toBe(customRetry);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes merged options into WebClient", () => {
|
||||||
|
createSlackWebClient("xoxb-test", { timeout: 1234 });
|
||||||
|
|
||||||
|
expect(WebClient).toHaveBeenCalledWith(
|
||||||
|
"xoxb-test",
|
||||||
|
expect.objectContaining({
|
||||||
|
timeout: 1234,
|
||||||
|
retryConfig: SLACK_DEFAULT_RETRY_OPTIONS,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
20
src/slack/client.ts
Normal file
20
src/slack/client.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { type RetryOptions, type WebClientOptions, WebClient } from "@slack/web-api";
|
||||||
|
|
||||||
|
export const SLACK_DEFAULT_RETRY_OPTIONS: RetryOptions = {
|
||||||
|
retries: 2,
|
||||||
|
factor: 2,
|
||||||
|
minTimeout: 500,
|
||||||
|
maxTimeout: 3000,
|
||||||
|
randomize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveSlackWebClientOptions(options: WebClientOptions = {}): WebClientOptions {
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
retryConfig: options.retryConfig ?? SLACK_DEFAULT_RETRY_OPTIONS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSlackWebClient(token: string, options: WebClientOptions = {}) {
|
||||||
|
return new WebClient(token, resolveSlackWebClientOptions(options));
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { WebClient } from "@slack/web-api";
|
import { createSlackWebClient } from "./client.js";
|
||||||
|
|
||||||
import type { ChannelDirectoryEntry } from "../channels/plugins/types.js";
|
import type { ChannelDirectoryEntry } from "../channels/plugins/types.js";
|
||||||
import type { DirectoryConfigParams } from "../channels/plugins/directory-config.js";
|
import type { DirectoryConfigParams } from "../channels/plugins/directory-config.js";
|
||||||
@@ -61,7 +61,7 @@ export async function listSlackDirectoryPeersLive(
|
|||||||
): Promise<ChannelDirectoryEntry[]> {
|
): Promise<ChannelDirectoryEntry[]> {
|
||||||
const token = resolveReadToken(params);
|
const token = resolveReadToken(params);
|
||||||
if (!token) return [];
|
if (!token) return [];
|
||||||
const client = new WebClient(token);
|
const client = createSlackWebClient(token);
|
||||||
const query = normalizeQuery(params.query);
|
const query = normalizeQuery(params.query);
|
||||||
const members: SlackUser[] = [];
|
const members: SlackUser[] = [];
|
||||||
let cursor: string | undefined;
|
let cursor: string | undefined;
|
||||||
@@ -119,7 +119,7 @@ export async function listSlackDirectoryGroupsLive(
|
|||||||
): Promise<ChannelDirectoryEntry[]> {
|
): Promise<ChannelDirectoryEntry[]> {
|
||||||
const token = resolveReadToken(params);
|
const token = resolveReadToken(params);
|
||||||
if (!token) return [];
|
if (!token) return [];
|
||||||
const client = new WebClient(token);
|
const client = createSlackWebClient(token);
|
||||||
const query = normalizeQuery(params.query);
|
const query = normalizeQuery(params.query);
|
||||||
const channels: SlackChannel[] = [];
|
const channels: SlackChannel[] = [];
|
||||||
let cursor: string | undefined;
|
let cursor: string | undefined;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { resolveSlackChannelAllowlist } from "../resolve-channels.js";
|
|||||||
import { resolveSlackUserAllowlist } from "../resolve-users.js";
|
import { resolveSlackUserAllowlist } from "../resolve-users.js";
|
||||||
import { resolveSlackAppToken, resolveSlackBotToken } from "../token.js";
|
import { resolveSlackAppToken, resolveSlackBotToken } from "../token.js";
|
||||||
import { normalizeSlackWebhookPath, registerSlackHttpHandler } from "../http/index.js";
|
import { normalizeSlackWebhookPath, registerSlackHttpHandler } from "../http/index.js";
|
||||||
|
import { resolveSlackWebClientOptions } from "../client.js";
|
||||||
import { resolveSlackSlashCommandConfig } from "./commands.js";
|
import { resolveSlackSlashCommandConfig } from "./commands.js";
|
||||||
import { createSlackMonitorContext } from "./context.js";
|
import { createSlackMonitorContext } from "./context.js";
|
||||||
import { registerSlackMonitorEvents } from "./events.js";
|
import { registerSlackMonitorEvents } from "./events.js";
|
||||||
@@ -130,16 +131,19 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
endpoints: slackWebhookPath,
|
endpoints: slackWebhookPath,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
const clientOptions = resolveSlackWebClientOptions();
|
||||||
const app = new App(
|
const app = new App(
|
||||||
slackMode === "socket"
|
slackMode === "socket"
|
||||||
? {
|
? {
|
||||||
token: botToken,
|
token: botToken,
|
||||||
appToken,
|
appToken,
|
||||||
socketMode: true,
|
socketMode: true,
|
||||||
|
clientOptions,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
token: botToken,
|
token: botToken,
|
||||||
receiver: receiver ?? undefined,
|
receiver: receiver ?? undefined,
|
||||||
|
clientOptions,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const slackHttpHandler =
|
const slackHttpHandler =
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { WebClient } from "@slack/web-api";
|
import { createSlackWebClient } from "./client.js";
|
||||||
|
|
||||||
export type SlackProbe = {
|
export type SlackProbe = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
@@ -21,7 +21,7 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function probeSlack(token: string, timeoutMs = 2500): Promise<SlackProbe> {
|
export async function probeSlack(token: string, timeoutMs = 2500): Promise<SlackProbe> {
|
||||||
const client = new WebClient(token);
|
const client = createSlackWebClient(token);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
const result = await withTimeout(client.auth.test(), timeoutMs);
|
const result = await withTimeout(client.auth.test(), timeoutMs);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { WebClient } from "@slack/web-api";
|
import type { WebClient } from "@slack/web-api";
|
||||||
|
|
||||||
|
import { createSlackWebClient } from "./client.js";
|
||||||
|
|
||||||
export type SlackChannelLookup = {
|
export type SlackChannelLookup = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -84,7 +86,7 @@ export async function resolveSlackChannelAllowlist(params: {
|
|||||||
entries: string[];
|
entries: string[];
|
||||||
client?: WebClient;
|
client?: WebClient;
|
||||||
}): Promise<SlackChannelResolution[]> {
|
}): Promise<SlackChannelResolution[]> {
|
||||||
const client = params.client ?? new WebClient(params.token);
|
const client = params.client ?? createSlackWebClient(params.token);
|
||||||
const channels = await listSlackChannels(client);
|
const channels = await listSlackChannels(client);
|
||||||
const results: SlackChannelResolution[] = [];
|
const results: SlackChannelResolution[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { WebClient } from "@slack/web-api";
|
import type { WebClient } from "@slack/web-api";
|
||||||
|
|
||||||
|
import { createSlackWebClient } from "./client.js";
|
||||||
|
|
||||||
export type SlackUserLookup = {
|
export type SlackUserLookup = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -101,7 +103,7 @@ export async function resolveSlackUserAllowlist(params: {
|
|||||||
entries: string[];
|
entries: string[];
|
||||||
client?: WebClient;
|
client?: WebClient;
|
||||||
}): Promise<SlackUserResolution[]> {
|
}): Promise<SlackUserResolution[]> {
|
||||||
const client = params.client ?? new WebClient(params.token);
|
const client = params.client ?? createSlackWebClient(params.token);
|
||||||
const users = await listSlackUsers(client);
|
const users = await listSlackUsers(client);
|
||||||
const results: SlackUserResolution[] = [];
|
const results: SlackUserResolution[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { WebClient } from "@slack/web-api";
|
import type { WebClient } from "@slack/web-api";
|
||||||
|
|
||||||
|
import { createSlackWebClient } from "./client.js";
|
||||||
|
|
||||||
export type SlackScopesResult = {
|
export type SlackScopesResult = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
@@ -81,7 +83,7 @@ export async function fetchSlackScopes(
|
|||||||
token: string,
|
token: string,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
): Promise<SlackScopesResult> {
|
): Promise<SlackScopesResult> {
|
||||||
const client = new WebClient(token, { timeout: timeoutMs });
|
const client = createSlackWebClient(token, { timeout: timeoutMs });
|
||||||
const attempts: SlackScopesSource[] = ["auth.scopes", "apps.permissions.info"];
|
const attempts: SlackScopesSource[] = ["auth.scopes", "apps.permissions.info"];
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FilesUploadV2Arguments, WebClient } from "@slack/web-api";
|
import { type FilesUploadV2Arguments, type WebClient } from "@slack/web-api";
|
||||||
|
|
||||||
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
@@ -6,6 +6,7 @@ import { logVerbose } from "../globals.js";
|
|||||||
import { loadWebMedia } from "../web/media.js";
|
import { loadWebMedia } from "../web/media.js";
|
||||||
import type { SlackTokenSource } from "./accounts.js";
|
import type { SlackTokenSource } from "./accounts.js";
|
||||||
import { resolveSlackAccount } from "./accounts.js";
|
import { resolveSlackAccount } from "./accounts.js";
|
||||||
|
import { createSlackWebClient } from "./client.js";
|
||||||
import { markdownToSlackMrkdwnChunks } from "./format.js";
|
import { markdownToSlackMrkdwnChunks } from "./format.js";
|
||||||
import { parseSlackTarget } from "./targets.js";
|
import { parseSlackTarget } from "./targets.js";
|
||||||
import { resolveSlackBotToken } from "./token.js";
|
import { resolveSlackBotToken } from "./token.js";
|
||||||
@@ -137,7 +138,7 @@ export async function sendMessageSlack(
|
|||||||
fallbackToken: account.botToken,
|
fallbackToken: account.botToken,
|
||||||
fallbackSource: account.botTokenSource,
|
fallbackSource: account.botTokenSource,
|
||||||
});
|
});
|
||||||
const client = opts.client ?? new WebClient(token);
|
const client = opts.client ?? createSlackWebClient(token);
|
||||||
const recipient = parseRecipient(to);
|
const recipient = parseRecipient(to);
|
||||||
const { channelId } = await resolveChannelId(client, recipient);
|
const { channelId } = await resolveChannelId(client, recipient);
|
||||||
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId);
|
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId);
|
||||||
|
|||||||
Reference in New Issue
Block a user