# @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. ## 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… ## 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 `` (default `suaveplan:ws:`). Within that namespace it owns: - `buffer::` — sorted-set holding the last `eventBufferMaxEvents` entries with scores keyed on `eventId`. Member format is `|`. Pub/sub channels are namespaced under `` (default `ws.broadcast.`): ``. Two `` 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 `*` 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).