Files
2026-05-27 23:29:53 +01:00

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 token rejects with SlackConfigMissingError whose cause is the originating ZodError.
  • Eight typed error subclasses surfaced through classify + #errorForCode.
  • Retry-After and x-ratelimit-reset parsing on HTTP 429 — millisecond delta exposed via error.context.retryAfterMs.
  • snake_case → camelCase mapping for channel and user payloads with undefined-elision so the public shape stays compatible with exactOptionalPropertyTypes.
  • Internal timeoutMs (default 10 000 ms) AND caller AbortSignal both routed through one AbortController.
  • slack.requests / slack.errors / slack.latency_ms emitted from every code path.
  • Delivery-report cache (Map<ts, SlackDeliveryReport>) for getStatus(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.