# `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 *` 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 `PSUBSCRIBE` at construction time so a subsequent `publish` is 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 by `api-ws-bun` and `api-sse-bun` for `Last-Event-Id` resume. - 10 stable `WS_BROKER_REDIS.*` error codes; every production path attaches the original `cause`. - Idempotent `dispose()` that detaches listeners, `PUNSUBSCRIBE`s the pattern, and quits both sockets. - OTel counter + histogram surface (`ws.broker.*`) wired through the `./instruments` module. ## Usage: Basic ```ts 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 ```ts 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(options)` — async factory returning a `RedisWsBroker`. - `RedisWsBroker extends WsBroker` — adds `writeBuffer`, `readBuffer`, `healthz`, `isDisposed`, and `options` (the resolved bundle). - `CreateRedisWsBrokerOptions` — `RedisWsBrokerOptions` plus the factory-only `clientFactory` and `readyTimeoutMs` knobs. - `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.