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

9.6 KiB

@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 — This module is the orchestrator for @suaveplan/api-ws-broker-redis. It wires two Redis clients (one in publish mode,…
    • broker — This module is the orchestrator for @suaveplan/api-ws-broker-redis. It wires two Redis clients (one in publish mode,…
  • error-codes — This module defines the stable error-code registry for @suaveplan/api-ws-broker-redis. Every failure surface the brok…
    • error-codes — This module defines the stable error-code registry for @suaveplan/api-ws-broker-redis. Every failure surface the brok…
  • event-buffer — This module backs the broker's reconnect event buffer. Each (tenant, topic) pair maps to one Redis sorted-set keyed b…
    • event-buffer — This module backs the broker's reconnect event buffer. Each (tenant, topic) pair maps to one Redis sorted-set keyed b…
  • health — This module is the pure, side-effect-free evaluator for the broker's health status. It inspects the two underlying RESP…
    • health — This module is the pure, side-effect-free evaluator for the broker's health status. It inspects the two underlying RESP…
  • instruments — This module owns the OpenTelemetry instrument surface for the Redis broker. Six instruments cover every documented oper…
    • instruments — This module owns the OpenTelemetry instrument surface for the Redis broker. Six instruments cover every documented oper…
  • options — This module defines the configuration shape consumed by createRedisWsBroker(...). Every operational knob the broker e…
    • options — This module defines the configuration shape consumed by createRedisWsBroker(...). Every operational knob the broker e…
  • publish — This module handles wire-encoding for the Redis broker. Every published frame gets a one-byte encoding tag (`ENCODING_J…
    • publish — This module handles wire-encoding for the Redis broker. Every published frame gets a one-byte encoding tag (`ENCODING_J…
  • reconnect — This module is the deterministic policy evaluator for the broker's reconnect loop. It computes the next backoff delay i…
    • reconnect — This module is the deterministic policy evaluator for the broker's reconnect loop. It computes the next backoff delay i…
  • resp-client — This module ships the broker's built-in RESP-protocol client so the package has no mandatory runtime dependency on an e…
    • resp-client — This module ships the broker's built-in RESP-protocol client so the package has no mandatory runtime dependency on an e…
  • subscribe — This module owns the broker's local routing table — the in-process map from topic name to handler set that the inbound…
    • subscribe — 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
./error-codes WS_BROKER_REDIS_CODES + WsBrokerRedisError src/error-codes/error-codes.md
./options option schema + defaults src/options/options.md
./resp-client built-in minimal RESP client (createRespClient) 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

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:

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 PSUBSCRIBEs <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).