Files
suaveplan/packages/api-stack/api/api-ws-broker-redis/README.md
2026-05-27 23:29:53 +01:00

163 lines
9.6 KiB
Markdown

# @suaveplan/api-ws-broker-redis - Redis pub/sub broker for @suaveplan/api-ws — cross-host WebSocket and SSE fan-out. Publishes BrokerEnvelopes via Redis Pub/Sub channels, maintains per-(tenant,topic) reconnect event-buffers in sorted-sets with cap + TTL, gzip-compresses payloads above a configurable threshold, exposes a healthz probe + auto-reconnect, and emits the standard ws.broker.* OTel surface. Ships a minimal built-in RESP client so integration tests can drive a real Redis container without an external client dependency; accepts any pluggable RedisPubSubLike (e.g. ioredis or node-redis) in production.
Cross-host Redis pub/sub broker for `@suaveplan/api-ws` and `@suaveplan/api-sse`.
Implements the `WsBroker` contract from `@suaveplan/api-ws` against a real
Redis instance: every `broker.broadcast(topic, envelope)` is published to a
shared Redis channel pattern (default `ws.broadcast.*`), and every host
subscribed to that pattern delivers the broadcast to its locally-connected
subscribers via the local `ConnectionRegistry`. Connections are NOT migrated
across hosts — the broker just makes broadcasts visible everywhere.
The same broker serves the reconnect event-buffer required by `api-ws-bun`
(WebSocket) and `api-sse-bun` (SSE): a per-`(tenant, topic)` Redis sorted-set
storing the most-recent `eventBufferMaxEvents` events with TTL
`eventBufferTtlSec`, queryable by `Last-Event-Id` for `WS` reconnect or
`Last-Event-Id` header for SSE resume.
<!-- begin:auto-docs -->
## Documentation tree
- [broker](./src/broker/broker.md) — This module is the orchestrator for `@suaveplan/api-ws-broker-redis`. It wires two Redis clients (one in publish mode,…
- [broker](./src/broker/broker.md) — This module is the orchestrator for `@suaveplan/api-ws-broker-redis`. It wires two Redis clients (one in publish mode,…
- [error-codes](./src/error-codes/error-codes.md) — This module defines the stable error-code registry for `@suaveplan/api-ws-broker-redis`. Every failure surface the brok…
- [error-codes](./src/error-codes/error-codes.md) — This module defines the stable error-code registry for `@suaveplan/api-ws-broker-redis`. Every failure surface the brok…
- [event-buffer](./src/event-buffer/event-buffer.md) — This module backs the broker's reconnect event buffer. Each `(tenant, topic)` pair maps to one Redis sorted-set keyed b…
- [event-buffer](./src/event-buffer/event-buffer.md) — This module backs the broker's reconnect event buffer. Each `(tenant, topic)` pair maps to one Redis sorted-set keyed b…
- [health](./src/health/health.md) — This module is the pure, side-effect-free evaluator for the broker's health status. It inspects the two underlying RESP…
- [health](./src/health/health.md) — This module is the pure, side-effect-free evaluator for the broker's health status. It inspects the two underlying RESP…
- [instruments](./src/instruments/instruments.md) — This module owns the OpenTelemetry instrument surface for the Redis broker. Six instruments cover every documented oper…
- [instruments](./src/instruments/instruments.md) — This module owns the OpenTelemetry instrument surface for the Redis broker. Six instruments cover every documented oper…
- [options](./src/options/options.md) — This module defines the configuration shape consumed by `createRedisWsBroker(...)`. Every operational knob the broker e…
- [options](./src/options/options.md) — This module defines the configuration shape consumed by `createRedisWsBroker(...)`. Every operational knob the broker e…
- [publish](./src/publish/publish.md) — This module handles wire-encoding for the Redis broker. Every published frame gets a one-byte encoding tag (`ENCODING_J…
- [publish](./src/publish/publish.md) — This module handles wire-encoding for the Redis broker. Every published frame gets a one-byte encoding tag (`ENCODING_J…
- [reconnect](./src/reconnect/reconnect.md) — This module is the deterministic policy evaluator for the broker's reconnect loop. It computes the next backoff delay i…
- [reconnect](./src/reconnect/reconnect.md) — This module is the deterministic policy evaluator for the broker's reconnect loop. It computes the next backoff delay i…
- [resp-client](./src/resp-client/resp-client.md) — This module ships the broker's built-in RESP-protocol client so the package has no mandatory runtime dependency on an e…
- [resp-client](./src/resp-client/resp-client.md) — This module ships the broker's built-in RESP-protocol client so the package has no mandatory runtime dependency on an e…
- [subscribe](./src/subscribe/subscribe.md) — This module owns the broker's local routing table — the in-process map from topic name to handler set that the inbound…
- [subscribe](./src/subscribe/subscribe.md) — This module owns the broker's local routing table — the in-process map from topic name to handler set that the inbound…
<!-- end:auto-docs -->
## What ships
| Subpath | Module | Doc |
|---|---|---|
| `./broker` | `createRedisWsBroker(opts)` factory | [`src/broker/broker.md`](src/broker/broker.md) |
| `./error-codes` | `WS_BROKER_REDIS_CODES` + `WsBrokerRedisError` | [`src/error-codes/error-codes.md`](src/error-codes/error-codes.md) |
| `./options` | option schema + defaults | [`src/options/options.md`](src/options/options.md) |
| `./resp-client` | built-in minimal RESP client (`createRespClient`) | [`src/resp-client/resp-client.md`](src/resp-client/resp-client.md) |
## Surface
- `createRedisWsBroker(opts)` — async factory returning a `RedisWsBroker`
(the `WsBroker` contract plus `writeBuffer`, `readBuffer`, `healthz`, and
`isDisposed`).
- `RedisWsBrokerOptions` — Zod-validated strict option shape. Required:
`connection`. Optional: `keyPrefix`, `channelPrefix`, `eventBufferTtlSec`,
`eventBufferMaxEvents`, `compressionThresholdBytes`, `reconnectStrategy`,
`commandTimeoutMs`, `disableCompression`, `hostId`.
- `WS_BROKER_REDIS_CODES` — closed set of 10 stable error codes
(`CONNECTION_LOST`, `PUBLISH_FAILED`, `SUBSCRIBE_FAILED`,
`BUFFER_WRITE_FAILED`, `BUFFER_READ_FAILED`, `PAYLOAD_TOO_LARGE`,
`DESERIALISE_FAILED`, `CONFIG_INVALID`, `RECONNECT_FAILED`, `TIMEOUT`).
- `createRespClient(opts)` — minimal built-in RESP client implementing
`RedisPubSubLike`. Use this when you do not already have an `ioredis` or
`node-redis` instance in your runtime.
- `RedisPubSubLike` — pluggable client interface; pass any conforming
implementation via `clientFactory` to use `ioredis` / `node-redis` /
another adapter.
## Quickstart
```ts
import { createRedisWsBroker } from "@suaveplan/api-ws-broker-redis";
import type { TenantId } from "@suaveplan/api-types/tenant";
const broker = await createRedisWsBroker<{ id: number }>({
connection: "redis://localhost:6379",
});
const sub = await broker.subscribe("orders", (envelope) => {
console.log("delivered", envelope.payload.id);
});
await broker.publish("orders", {
topic: "orders",
tenant: "t1" as unknown as TenantId,
payload: { id: 1 },
});
sub.unsubscribe();
await broker.dispose();
```
## Operational notes
### Key-prefix conventions
The broker namespaces every Redis key under `<keyPrefix>` (default
`suaveplan:ws:`). Within that namespace it owns:
- `<keyPrefix>buffer:<tenant>:<topic>` — sorted-set holding the last
`eventBufferMaxEvents` entries with scores keyed on `eventId`. Member
format is `<eventId>|<base64-payload>`.
Pub/sub channels are namespaced under `<channelPrefix>` (default
`ws.broadcast.`): `<channelPrefix><topic>`.
Two `<keyPrefix>` choices must be in lock-step across every host replica
that participates in the same fan-out fabric — otherwise broadcasts will
land on different channel names and never be delivered.
### Redis memory sizing
The peak Redis memory consumed by a single broker is bounded by:
```text
hosts * tenants * topics * eventBufferMaxEvents * average_event_size
```
with TTL `eventBufferTtlSec` ensuring abandoned topics expire on their own.
For default settings (cap 256, TTL 5 min, ~1 KiB events) that is
~256 KiB per active `(tenant, topic)`. A 10k-active-topic deployment
budgets ~2.5 GiB of Redis memory for buffers alone, before pub/sub
traffic. Tune the cap and TTL down for memory-constrained deployments.
### Multi-host topology
Every host eagerly `PSUBSCRIBE`s `<channelPrefix>*` at construction time
so the first publish is immediately visible to peers. Adding a host is
purely additive — no configuration change to existing hosts is required;
the new host comes up, subscribes to the pattern, and starts delivering
broadcasts to its locally-connected subscribers.
### Compression
Payloads above `compressionThresholdBytes` (default 1 KiB) are
gzipped before publish. The wire format carries a one-byte encoding
magic so subscribers can decode either form transparently. The 4 MiB
hard cap (`MAX_PAYLOAD_BYTES`) refuses runaway payloads with
`WS_BROKER_REDIS.PAYLOAD_TOO_LARGE`.
### Pluggable clients
The default broker opens two sockets via the built-in RESP client
(`./resp-client`). Pass `clientFactory: (url, role) => myAdapter(url, role)`
to swap in an `ioredis` or `node-redis` adapter — the adapter must satisfy
the {@link RedisPubSubLike} interface.
## Quality bars
- 100% line / branch / function / statement coverage (Istanbul).
- 100% docs coverage — every implementation `.ts` has a sibling `.md` with
the required headings.
- Zero direct imports of `vitest` / `bun:test` — all test files import
from `@suaveplan/testing/runner`.
- Stable, namespaced error codes (`WS_BROKER_REDIS.*`).
- Real-Redis integration tests via `@suaveplan/testing-integration-redis`
with the testcontainers-backed `RedisFixture` (gracefully skipped when
Docker is unavailable).