first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
# `broker` — `createRedisWsBroker`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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, `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<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.