Initial Commit
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# `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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user