5.1 KiB
broker — createRedisWsBroker
(no submodules)
Purpose
This module is the orchestrator for @suaveplan/api-ws-broker-redis. It wires
two Redis clients (one in publish mode, one held in subscribe mode), establishes
a PSUBSCRIBE <channelPrefix>* pattern, and exposes the WsBroker surface plus
broker-specific operations (writeBuffer, readBuffer, healthz, isDisposed).
The broker is the production cross-host adapter for the @suaveplan/api-ws
contract — the in-memory broker handles single-host fan-out and this package
extends that to many hosts by riding Redis pub/sub.
The broker is intentionally pluggable: a clientFactory option lets callers
swap in ioredis, node-redis, or any client implementing
{@link RedisPubSubLike}. When unset the broker defaults to the built-in RESP
client shipped from ./resp-client, which keeps the package free of mandatory
runtime dependencies on an external Redis client.
Features
- Two Redis sockets, one per role — replies on the publisher socket, async pub/sub frames on the subscriber socket. Redis itself enforces that subscribe sockets reject non-subscribe commands; the broker structure mirrors that constraint exactly.
- Eager
PSUBSCRIBEat construction time so a subsequentpublishis immediately visible to peers. - Compression-aware envelope encoding via
./publish(gzip above the configurable threshold, hard 4 MiB cap, magic-byte framing). - Reconnect-friendly event buffer: per-
(tenant, topic)sorted-set with cap + TTL, consumed byapi-ws-bunandapi-sse-bunforLast-Event-Idresume. - 10 stable
WS_BROKER_REDIS.*error codes; every production path attaches the originalcause. - Idempotent
dispose()that detaches listeners,PUNSUBSCRIBEs the pattern, and quits both sockets. - OTel counter + histogram surface (
ws.broker.*) wired through the./instrumentsmodule.
Usage: Basic
import { createRedisWsBroker } from "@suaveplan/api-ws-broker-redis/broker";
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();
Usage: Advanced
import { createRedisWsBroker } from "@suaveplan/api-ws-broker-redis/broker";
// Multi-host setup with custom prefixes and a fixed reconnect strategy.
const broker = await createRedisWsBroker({
connection: process.env.REDIS_URL!,
keyPrefix: "prod:ws:",
channelPrefix: "prod.ws.broadcast.",
eventBufferMaxEvents: 512,
eventBufferTtlSec: 600,
compressionThresholdBytes: 2048,
commandTimeoutMs: 2_000,
reconnectStrategy: {
initialDelayMs: 50,
maxDelayMs: 2_000,
maxAttempts: 200,
multiplier: 2,
},
hostId: process.env.HOSTNAME ?? "host-unknown",
});
// Pluggable client (ioredis adapter, etc.):
const ioredisBroker = await createRedisWsBroker({
connection: process.env.REDIS_URL!,
clientFactory: (url, role) =>
// Wrap an ioredis instance into the RedisPubSubLike shape — production
// teams who already use ioredis can avoid maintaining two clients.
myIoredisAdapter(url, role),
});
API Reference
createRedisWsBroker<Payload>(options)— async factory returning aRedisWsBroker<Payload>.RedisWsBroker<Payload> extends WsBroker<Payload>— addswriteBuffer,readBuffer,healthz,isDisposed, andoptions(the resolved bundle).CreateRedisWsBrokerOptions—RedisWsBrokerOptionsplus the factory-onlyclientFactoryandreadyTimeoutMsknobs.RedisClientFactory—(url, role) => RedisPubSubLike.
Implementation Notes
The broker normalises all timing through Date.now() — production deployments
that want a synthetic clock should layer that at the OTel reader level rather
than reaching into the broker. The constructor strips factory-only fields
(clientFactory, readyTimeoutMs) before handing the rest to the strict
Zod schema so unknown keys still fail fast for the genuine option shape.
The waitForReady helper races the client's ready event against a timeout
and against error/close. This means a flaky network at startup surfaces
fast (CONNECTION_LOST) rather than wedging the broker in an unbuilt state.
Once construction returns, every method short-circuits with the same code on
post-dispose calls — the broker is single-use, like the in-memory variant.
The subscribe path runs dispatchInboundFrame per topic via the local
registry, which snapshots the handler set before delivery so a handler that
unsubscribes mid-fan-out does not silently skip its peers. Decode failures
and per-handler rejections route through the onError callback (currently
captured into healthz().lastError) rather than propagating into the pub/sub
loop, because Redis will not stop pushing frames just because one of them
was malformed.