refactor(slack): reuse default write clients

This commit is contained in:
Peter Steinberger
2026-04-25 01:13:51 +01:00
parent 492dfdc7f2
commit b69e3b633b
5 changed files with 59 additions and 4 deletions

View File

@@ -12,6 +12,7 @@ vi.mock("./monitor/media.js", () => ({
vi.mock("./client.js", () => ({
createSlackWebClient: createSlackWebClientMock,
createSlackWriteClient: createSlackWebClientMock,
getSlackWriteClient: createSlackWebClientMock,
}));
let downloadSlackFile: typeof import("./actions.js").downloadSlackFile;

View File

@@ -4,7 +4,7 @@ import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { resolveSlackAccount } from "./accounts.js";
import { buildSlackBlocksFallbackText } from "./blocks-fallback.js";
import { validateSlackBlocksArray } from "./blocks-input.js";
import { createSlackWebClient, createSlackWriteClient } from "./client.js";
import { createSlackWebClient, getSlackWriteClient } from "./client.js";
import { resolveSlackMedia } from "./monitor/media.js";
import type { SlackMediaResult } from "./monitor/media.js";
import { sendMessageSlack } from "./send.js";
@@ -81,7 +81,7 @@ async function getClient(opts: SlackActionClientOpts = {}, mode: "read" | "write
return opts.client;
}
const token = resolveToken(opts.token, opts.accountId, opts.cfg);
return mode === "write" ? createSlackWriteClient(token) : createSlackWebClient(token);
return mode === "write" ? getSlackWriteClient(token) : createSlackWebClient(token);
}
async function resolveBotUserId(client: WebClient) {

View File

@@ -14,6 +14,8 @@ vi.mock("@slack/web-api", () => {
let createSlackWebClient: typeof import("./client.js").createSlackWebClient;
let createSlackWriteClient: typeof import("./client.js").createSlackWriteClient;
let getSlackWriteClient: typeof import("./client.js").getSlackWriteClient;
let clearSlackWriteClientCacheForTest: typeof import("./client.js").clearSlackWriteClientCacheForTest;
let resolveSlackWebClientOptions: typeof import("./client.js").resolveSlackWebClientOptions;
let resolveSlackWriteClientOptions: typeof import("./client.js").resolveSlackWriteClientOptions;
let SLACK_DEFAULT_RETRY_OPTIONS: typeof import("./client.js").SLACK_DEFAULT_RETRY_OPTIONS;
@@ -25,6 +27,8 @@ beforeAll(async () => {
({
createSlackWebClient,
createSlackWriteClient,
getSlackWriteClient,
clearSlackWriteClientCacheForTest,
resolveSlackWebClientOptions,
resolveSlackWriteClientOptions,
SLACK_DEFAULT_RETRY_OPTIONS,
@@ -35,6 +39,7 @@ beforeAll(async () => {
beforeEach(() => {
WebClient.mockClear();
clearSlackWriteClientCacheForTest();
});
describe("slack web client config", () => {
@@ -93,6 +98,29 @@ describe("slack web client config", () => {
}),
);
});
it("reuses default write clients per token", () => {
const first = getSlackWriteClient("xoxb-test");
const second = getSlackWriteClient("xoxb-test");
expect(second).toBe(first);
expect(WebClient).toHaveBeenCalledTimes(1);
expect(WebClient).toHaveBeenCalledWith(
"xoxb-test",
expect.objectContaining({
maxRequestConcurrency: 1,
retryConfig: SLACK_WRITE_RETRY_OPTIONS,
}),
);
});
it("keeps default write clients separated by token", () => {
const first = getSlackWriteClient("xoxb-one");
const second = getSlackWriteClient("xoxb-two");
expect(second).not.toBe(first);
expect(WebClient).toHaveBeenCalledTimes(2);
});
});
describe("slack proxy agent", () => {

View File

@@ -1,5 +1,9 @@
import { type WebClientOptions, WebClient } from "@slack/web-api";
import { resolveSlackWebClientOptions, resolveSlackWriteClientOptions } from "./client-options.js";
const SLACK_WRITE_CLIENT_CACHE_MAX = 32;
const slackWriteClientCache = new Map<string, WebClient>();
export {
resolveSlackWebClientOptions,
resolveSlackWriteClientOptions,
@@ -14,3 +18,25 @@ export function createSlackWebClient(token: string, options: WebClientOptions =
export function createSlackWriteClient(token: string, options: WebClientOptions = {}) {
return new WebClient(token, resolveSlackWriteClientOptions(options));
}
export function getSlackWriteClient(token: string): WebClient {
const cached = slackWriteClientCache.get(token);
if (cached) {
slackWriteClientCache.delete(token);
slackWriteClientCache.set(token, cached);
return cached;
}
const client = createSlackWriteClient(token);
if (slackWriteClientCache.size >= SLACK_WRITE_CLIENT_CACHE_MAX) {
const oldestToken = slackWriteClientCache.keys().next().value;
if (oldestToken) {
slackWriteClientCache.delete(oldestToken);
}
}
slackWriteClientCache.set(token, client);
return client;
}
export function clearSlackWriteClientCacheForTest(): void {
slackWriteClientCache.clear();
}

View File

@@ -19,7 +19,7 @@ import type { SlackTokenSource } from "./accounts.js";
import { resolveSlackAccount } from "./accounts.js";
import { buildSlackBlocksFallbackText } from "./blocks-fallback.js";
import { validateSlackBlocksArray } from "./blocks-input.js";
import { createSlackWriteClient } from "./client.js";
import { getSlackWriteClient } from "./client.js";
import { markdownToSlackMrkdwnChunks } from "./format.js";
import { SLACK_TEXT_LIMIT } from "./limits.js";
import { loadOutboundMediaFromUrl } from "./runtime-api.js";
@@ -397,7 +397,7 @@ async function sendMessageSlackQueued(params: {
blocks?: (Block | KnownBlock)[];
}): Promise<SlackSendResult> {
const { opts, cfg, account, token, recipient, blocks, trimmedMessage } = params;
const client = opts.client ?? createSlackWriteClient(token);
const client = opts.client ?? getSlackWriteClient(token);
const { channelId } = await resolveChannelId(client, recipient, {
accountId: account.accountId,
token,