# `resp-client` — minimal built-in Redis client _(no submodules)_ ## Purpose This module ships the broker's built-in RESP-protocol client so the package has no mandatory runtime dependency on an external Redis client. The client opens a single TCP socket via `node:net`, frames every command as an inline RESP array, and decodes replies incrementally — RESP guarantees FIFO replies on a single connection, so the client enqueues each command's promise and resolves them in arrival order. Subscribe-mode clients receive `pmessage` frames asynchronously and route them to registered listeners. The client implements {@link RedisPubSubLike} so production users who already operate `ioredis` or `node-redis` can ship an adapter against the same interface and skip the built-in client entirely. ## Features - Incremental RESP parser handling simple-string, error, integer, bulk string, and array reply types. Streams across multiple TCP `data` chunks. - Per-command timeout via `commandTimeoutMs` (default 5 s). - Socket-close detection rejects every in-flight command so callers do not hang indefinitely on a dropped connection. - Two role modes: reply-mode (default) and subscribe-mode (`isSubscriber: true`). Subscribe-mode handles `pmessage` / `psubscribe` / `punsubscribe` ack frames and forwards messages through the listener API. - Injectable `socketFactory` so tests can drive the parser end-to-end against a `FakeSocket` without booting a real TCP loopback — the parser code path exercised is exactly the production one. - Idempotent `quit()`; subsequent commands reject immediately with the documented closed-client message. ## Usage: Basic ```ts import { createRespClient } from "@suaveplan/api-ws-broker-redis/resp-client"; const client = createRespClient({ url: "redis://localhost:6379" }); await client.publish("orders", new Uint8Array([1, 2, 3])); await client.quit(); ``` ## Usage: Advanced ```ts import { createRespClient, type RedisPubSubLike, } from "@suaveplan/api-ws-broker-redis/resp-client"; // Subscribe-mode client wired to a listener. const subscriber: RedisPubSubLike = createRespClient({ url: "redis://localhost:6379", isSubscriber: true, commandTimeoutMs: 2_000, }); const off = subscriber.on((evt) => { if (evt.event === "message") { console.log("ws.* frame on", evt.channel, evt.payload); } else if (evt.event === "error") { metrics.connectionErrors.add(1); } }); await subscriber.psubscribe("ws.broadcast.*"); // ... later ... off(); await subscriber.quit(); ``` ## API Reference - `createRespClient(options): RedisPubSubLike` — factory. - `RedisPubSubLike` — the minimal Redis surface (publish, psubscribe, punsubscribe, zadd, zremrangebyrank, zrangebyscore, zcard, expire, ping, del, quit, on, isReady). - `RedisPubSubListener` — discriminated event union (`message`, `ready`, `close`, `error`). - `RespClientOptions { url, commandTimeoutMs?, socketFactory?, isSubscriber? }`. ## Implementation Notes The parser is a strict bottom-up recursive descent over the RESP grammar; the nested-array path covers `pmessage` (4-element) and `psubscribe`/`punsubscribe` ack (3-element) frames the broker subscribes to. The `+`, `-`, `:` types are single-line; bulk strings (`$`) carry an explicit byte length so binary payloads survive without coercion; arrays (`*`) recursively defer to the parser for each element. Subscribe-mode `PING` returns an array (`*2\r\n$4\r\npong\r\n$0\r\n\r\n`) rather than the reply-mode `+PONG\r\n` simple string. The dispatcher detects this via the leading bulk-string element and resolves to the canonical `"PONG"` so callers see the same value regardless of mode. Timeouts use `setTimeout` rather than `AbortController` because the pending queue index is the cancellation surface — on timeout the entry is spliced out before reject, so a late reply lands as an orphan and is dropped by the next reply handler's `pending.shift()` returning a defined but unrelated entry. (This is a known and accepted edge case under load: callers always see the timeout reject, never an out-of-order resolve.)