Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# `resp-client` — minimal built-in Redis client
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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.)
|
||||
Reference in New Issue
Block a user