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

4.1 KiB

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

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

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.)