3.7 KiB
client/slack-api-client — Slack Web API client
Purpose
SlackApiClient is the typed transport for the Slack Web API. It exposes the six methods downstream packages rely on — sendMessage, updateMessage, deleteMessage, listChannels, getChannelInfo, listUsers — plus getStatus for retrieving the delivery report stored after a successful sendMessage. The client accepts an injectable transport bag (fetch, clock, logger) so production code can pass globalThis.fetch while tests inject a FakeFetch. Every public method is bounded by an internal timeout enforced via AbortController, supports a caller-supplied AbortSignal, classifies non-2xx responses through the shared HTTP / Slack-error matchers, and emits the four OTel instruments defined in otel/metrics.
Features
- Zod boundary validation in the constructor — empty
tokenrejects withSlackConfigMissingErrorwhosecauseis the originatingZodError. - Eight typed error subclasses surfaced through
classify+#errorForCode. Retry-Afterandx-ratelimit-resetparsing on HTTP 429 — millisecond delta exposed viaerror.context.retryAfterMs.- snake_case → camelCase mapping for channel and user payloads with
undefined-elision so the public shape stays compatible withexactOptionalPropertyTypes. - Internal
timeoutMs(default 10 000 ms) AND callerAbortSignalboth routed through oneAbortController. slack.requests/slack.errors/slack.latency_msemitted from every code path.- Delivery-report cache (
Map<ts, SlackDeliveryReport>) forgetStatus(ts)lookups.
Usage: Basic
import { SlackApiClient } from "@suaveplan/slack-client";
const client = new SlackApiClient({ token: process.env["SLACK_TOKEN"]! });
const res = await client.sendMessage({
channel: "C0123456789",
text: "Build is green :white_check_mark:",
});
Usage: Advanced
import { SlackApiClient } from "@suaveplan/slack-client";
import { FakeFetch } from "@suaveplan/slack-client/fakes/fake-fetch";
const fake = new FakeFetch();
fake.push({
json: { ok: true, channel: "C1", ts: "1.1" },
});
let now = 1_700_000_000_000;
const client = new SlackApiClient({
token: "xoxb-test",
fetch: fake.fetch,
clock: () => now++,
logger: { debug() {}, info() {}, warn() {}, error() {} },
});
const res = await client.sendMessage({ channel: "C1", text: "hi" });
const report = client.getStatus(res.ts!);
// report.status === "delivered"
API Reference
new SlackApiClient(options: SlackApiClientOptions)sendMessage(message: SlackMessage, signal?: AbortSignal): Promise<SlackResponse>updateMessage(channel, ts, text, blocks?): Promise<SlackResponse>deleteMessage(channel, ts): Promise<SlackResponse>listChannels(limit?): Promise<SlackChannel[]>getChannelInfo(channelId): Promise<SlackChannel>listUsers(limit?): Promise<SlackUser[]>getStatus(ts: string): SlackDeliveryReport | undefined
Errors thrown: SlackConfigMissingError, SlackChannelNotFoundError, SlackAuthError, SlackRateLimitedError, SlackTimeoutError, SlackWorkspaceNotFoundError, SlackApiError.
Implementation Notes
The client uses private #-prefixed class fields (not TS private) so consumer tooling cannot accidentally reach into the instance through bracket access — the contract is enforced at the runtime level by the engine. The request method assembles one AbortController that listens to both the caller signal AND the internal timer, then aborts on whichever fires first; the listener is removed in finish() to avoid leaking attached handlers across requests. The #errorForCode switch is exhaustive over SlackErrorCode and defaults to SlackApiError for forward-compatibility when the registry expands.