Files
skills/docs/api-stack/api/api-ws-broker-redis/src/broker/broker.md
2026-05-29 08:20:56 +01:00

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 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, PUNSUBSCRIBEs the pattern, and quits both sockets.
  • OTel counter + histogram surface (ws.broker.*) wired through the ./instruments module.

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 a RedisWsBroker<Payload>.
  • RedisWsBroker<Payload> extends WsBroker<Payload> — 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.