163 lines
9.6 KiB
Markdown
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).
|
|
|