Skip to main content

implement-command

Add a new Redis command (or command variant) to node-redis end-to-end — the `<NAME>.ts` Command file, its registration with JSDoc in the package `commands/index.ts`, and a co-located `<NAME>.spec.ts` with arg + behavior tests. Use when asked to implement, add, or wire up a Redis command in the client or a module package (json/search/bloom/time-series).

معلومات المصدر

المستودع
redis/node-redis
آخر نشاط في المصدر
١٦ سبتمبر ٢٠٢٦ في ١٥:٤٨
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٧٬٥٨٢
التفرعات
١٬٩٨٢

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
2 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
implement-command
description
Add a new Redis command (or command variant) to node-redis end-to-end — the `<NAME>.ts` Command file, its registration with JSDoc in the package `commands/index.ts`, and a co-located `<NAME>.spec.ts` with arg + behavior tests. Use when asked to implement, add, or wire up a Redis command in the client or a module package (json/search/bloom/time-series).
# Implement a node-redis Command ## Overview A command in node-redis is a single `Command` object exported from `packages/<pkg>/lib/commands/<NAME>.ts`. It declares how to serialize arguments onto the wire (`parseCommand`) and how to map the RESP reply to a JS value (`transformReply`). It becomes callable on clients only after it is registered in the package's `commands/index.ts`. Both the raw name and a camelCase alias are exposed (`HSET` and `hSet`). This skill is for `@redis/client` core commands and for module packages (`@redis/json`, `@redis/search`, `@redis/bloom`, `@redis/time-series`). It does not cover RESP codec changes or new client transports. Before writing, **read 2-3 existing commands with a similar shape** (simple key read, key + options, variadic, RESP2/3-divergent reply) and mirror them. Consult [redis.io/commands](https://redis.io/commands) for argument order and reply type, but treat existing `parseCommand`/`transformReply` files as the source of truth for repo conventions. ## File layout | Package | Command dir | Import paths | Wire name prefix | | --- | --- | --- | --- | | `client` | `packages/client/lib/commands/` | `../client/parser`, `../RESP/types`, `./generic-transformers` | none (`GET`) | | `json` | `packages/json/lib/commands/` | `@redis/client/dist/lib/...` | `JSON.` (`JSON.ARRAPPEND`) | | `search` | `packages/search/lib/commands/` | `@redis/client/dist/lib/...` | `FT.` | | `bloom` | `packages/bloom/lib/commands/<family>/` | `@redis/client/dist/lib/...` | e.g. `BF.`, `CF.`, `TOPK.` | | `time-series` | `packages/time-series/lib/commands/` | `@redis/client/dist/lib/...` | `TS.` | **Naming**: file name = raw wire name with subcommand `_` separators (`ACL_CAT`, `CONFIG_GET`, `CLUSTER_FORGET`). A distinct reply variant gets its own file (`HRANDFIELD_COUNT_WITHVALUES`). Module commands drop the dotted prefix from the file name (`ARRAPPEND.ts` → wire `JSON.ARRAPPEND`). ## Step 0 — Gather inputs (ask the user first) New commands are often implemented **before they are publicly released**, so redis.io may not document them yet and a default `redis:latest` may not have them. Before writing any code, ask the user for three things: 1. **The command spec.** Ask for the redis/redis JSON spec file — one per command under [`src/commands/<name>.json`](https://github.com/redis/redis/tree/unstable/src/commands) (subcommands use `-`, e.g. `client-info.json`, `acl-cat.json`). If the command is unreleased, ask the user to paste the spec from their branch. As a fallback on a live server: `redis-cli --json COMMAND DOCS <name>` and `COMMAND INFO <name>`. 2. **A running Redis instance that has the command.** This is the single most useful input — ask for it explicitly and up front. Ask for connection details (host/port, TLS, auth, module loaded). Use it to explore real behavior and confirm the implementation matches the spec — **do not rely on the spec alone**. Probe every argument branch and diff the real reply against your `transformReply`. A quick `redis-cli` session or a throwaway probe script (`packages/client` is already wired for `tsx`) is enough; never commit the probe. 3. **The first server version that ships the command.** Ask which Redis (or module) version introduced it — the spec's `since` field is the answer when present; otherwise ask the user directly. You need this to (a) write `@since` in the JSDoc (Step 2) and (b) gate the behavior tests with `minimumDockerVersion` (Step 3) so they don't run — and fail — on older servers in CI. If the user cannot provide a spec, derive arguments/reply from [redis.io/commands](https://redis.io/commands) but flag that it is unverified. If they cannot provide a live instance, implement from the spec but state that runtime behavior was not confirmed. If the introducing version is unknown, say so and leave `@since`/`minimumDockerVersion` out rather than guessing. ### Reading the spec JSON → mapping to a Command The redis/redis spec drives every part of the `Command` object. Example (`getex.json`, trimmed): ```json { "GETEX": { "since": "6.2.0", "arity": -2, "command_flags": ["WRITE", "FAST"], "key_specs": [{ "begin_search": { "index": { "pos": 1 } }, "flags": ["RW", "UPDATE"] }], "arguments": [ { "name": "key", "type": "key", "key_spec_index": 0 }, { "name": "expiration", "type": "oneof", "optional": true, "arguments": [ { "name": "seconds", "type": "integer", "token": "EX" }, { "name": "persist", "type": "pure-token", "token": "PERSIST" } ]} ], "reply_schema": { "oneOf": [ { "type": "string" }, { "type": "null" } ] } } } ``` | Spec field | Drives | | --- | --- | | `command_flags` contains `READONLY` (and **not** `WRITE`) | `IS_READ_ONLY: true`. `WRITE` → omit it. Pure read with no side effects → also `CACHEABLE: true`. For keyed commands the generated metadata table already implies these defaults, so module packages omit the flags; keyless reads must set `IS_READ_ONLY: true` explicitly (Step 4). | | `key_specs` empty / no `key`-type args | `NOT_KEYED_COMMAND: true`. | | `arguments[].type: "key"` | `parser.pushKey(...)` (one per key, in spec order). | | `type: "pure-token"` + `token` | a literal flag pushed only when its option is set (`parser.push('PERSIST')`). | | `token` + value type (`integer`/`string`/...) | push the token then the stringified value (`parser.push('EX', seconds.toString())`). | | `type: "oneof"` | mutually exclusive branch → `if/else if` in `parseCommand`; model as a union/`options` field. | | `optional: true` | goes in the `options` object (exported `interface`); guard with `if (options?.x)`. | | `multiple: true` | variadic → `parser.pushVariadic*`. | | `arity` | sanity-check arg count in `parseArgs` tests. | | `reply_schema` (JSON Schema) | the `transformReply` return type. `oneOf [string, null]` → `BlobStringReply \| NullReply`; `integer` → `NumberReply`; `array` → `ArrayReply<...>`; a map differing by RESP version → keyed `transformReply: { 2, 3 }`. | | `since` | the introducing server version → `@since` in the registry JSDoc (Step 2) **and** `minimumDockerVersion: [major, minor]` on the behavior tests (Step 3). | After implementing, run the command against the live instance and diff the real reply against `reply_schema` and your `transformReply` output. ## Step 1 — Write `<NAME>.ts` Minimal pass-through command (`packages/client/lib/commands/GET.ts`): ```typescript import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { CACHEABLE: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('GET'); parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; ``` `as const satisfies Command` is mandatory — it preserves the literal arg types for the public API while type-checking the shape. ### Command flags (all optional) - `IS_READ_ONLY: true` — read command; routable to replicas. Set for reads, omit/`false` for writes. - `CACHEABLE: true` — eligible for client-side caching. Only for pure reads with no side effects. - `NOT_KEYED_COMMAND: true` — command takes no key (server/connection level, e.g. `PING`, `CONFIG_GET`). - `IS_FORWARD_COMMAND` — internal; do not set on new commands. ### `parseCommand` — serialize args via `CommandParser` First arg is always `parser`. Push the wire name first, then args in order. Use the parser helpers — **do not** hand-build arrays: - `push(...args)` — raw args (the command token, flags, stringified numbers). - `pushKey(key)` — a key. Registers it for cluster slot routing. Use for **every** key, never `push` a key. - `pushKeys(keys)` / `pushKeysLength(keys)` — multiple keys; the `Length` variant prefixes the count. - `pushVariadic(vals)` — a `RedisVariadicArgument` (one value or array) as flat args. - `pushVariadicWithLength(vals)` — same, prefixed with the count (e.g. `FIELDS <n> ...`). - `pushVariadicNumber(vals)` — number or array of numbers, stringified. Numbers are not auto-stringified by `push` — call `.toString()`. Optional trailing args go in an `options` object; export its `interface` (see `SET.ts`'s `SetOptions`). Encode keyword flags conditionally: ```typescript parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument, options?: SetOptions) { parser.push('SET'); parser.pushKey(key); parser.push(value); if (options?.condition) parser.push(options.condition); // 'NX' | 'XX' } ``` ### `transformReply` — map RESP reply to JS - **Pass-through** (reply already the right shape): `transformReply: undefined as unknown as () => <ReplyType>`. - **Function**: `(reply: <RawType>) => <JsType>`. Use `UnwrapReply<...>` to read the raw RESP container. - **RESP-version keyed**: `{ 2: (reply) => ..., 3: (reply) => ... }` when RESP2 and RESP3 shapes differ (e.g. flat array vs map/tuple). See `HRANDFIELD_COUNT_WITHVALUES.ts`. #### Unify RESP2 onto the RESP3 shape When the server returns different shapes per protocol, the library exposes **one return type to callers**: the **RESP3 shape is the source of truth**, and the RESP2 reply is transformed to look like it. So the keyed form is almost always: - `3:` — **pass-through** (`undefined as unknown as () => <ReplyType>`), because RESP3 already has the target shape (map, tuple, big-number, double, ...). - `2:` — a **function** that reshapes the flat/legacy RESP2 reply into that same `<ReplyType>`. Type its input `UnwrapReply<Resp2Reply<ReplyType>>` so the raw RESP2 container is visible while the output type still matches RESP3. Canonical example — `HELLO.ts` turns the RESP2 flat array (`[k, v, k, v, ...]`) into the RESP3 map, while RESP3 passes through: ```typescript transformReply: { 2: (reply: UnwrapReply<Resp2Reply<HelloReply>>) => ({ server: reply[1], version: reply[3], proto: reply[5], /* ... */ }), 3: undefined as unknown as () => HelloReply } ``` Reuse shared transformers where one exists (`HGETALL.ts` uses `transformTuplesReply` for `2:`, map pass-through for `3:`). Only when RESP3 still needs reshaping does `3:` get its own function too. Verify the actual per-protocol shapes against the live instance (Step 0) — connect once with `RESP: 2` and once with `RESP: 3` and diff. Reply types live in `RESP/types`: `BlobStringReply`, `SimpleStringReply<'OK'>`, `NumberReply`, `DoubleReply`, `NullReply`, `BooleanReply`, `ArrayReply<T>`, `TuplesReply<[...]>`, `MapReply`, `UnwrapReply`. **RESP3 is the default.** No separate RESP3 test is needed for a new command; the default test setup already exercises RESP3. ### Type-mapping precision caveats - A `BLOB_STRING` reply cannot be remapped to `Number` via type mapping; only RESP3 `DOUBLE`/`BIG_NUMBER` are precision-risky. - If a `NumberReply` can exceed `Number.MAX_SAFE_INTEGER` (2^53-1), add a `@remarks` line to the JSDoc (Step 2) telling users to do `client.withTypeMapping({ [RESP_TYPES.NUMBER]: String })`. See the `ARGREP` entries in the client index for the exact wording. ### Module package commands Import from the published client subpath, prefix the wire name, and reuse shared transformers (`packages/json/lib/commands/ARRAPPEND.ts`): ```typescript import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: false, parseCommand(parser, key, path, value) { parser.push('JSON.ARRAPPEND'); parser.pushKey(key); parser.push(path, /* transform */ value); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; ``` ## Step 2 — Register in `commands/index.ts` `import` the command, then add it to the default-export map **twice**: the raw name (shorthand) and a camelCase alias. **Every entry MUST have a JSDoc block directly above it** — `npm run check:command-jsdoc` fails on any registry entry without an attached JSDoc comment (no blank-line gap allowed). ```typescript import GET from './GET'; // ... export default {
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub