From 71caca76d04010f4e1df8d161f03dec6c46f676a Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 30 Jul 2026 14:14:11 +0300 Subject: [PATCH 01/17] chore(client): regenerate command metadata from server COMMAND Regenerate the static command-metadata table from a live server COMMAND dump and add the generator + overrides tooling that produces it. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../command-metadata/command-metadata-data.ts | 5839 +++++++++++++++++ packages/client/package.json | 1 + .../scripts/command-metadata-overrides.ts | 121 + .../scripts/generate-command-metadata-data.ts | 129 + 4 files changed, 6090 insertions(+) create mode 100644 packages/client/lib/command-metadata/command-metadata-data.ts create mode 100644 packages/client/scripts/command-metadata-overrides.ts create mode 100644 packages/client/scripts/generate-command-metadata-data.ts diff --git a/packages/client/lib/command-metadata/command-metadata-data.ts b/packages/client/lib/command-metadata/command-metadata-data.ts new file mode 100644 index 00000000000..4a48ef1db9c --- /dev/null +++ b/packages/client/lib/command-metadata/command-metadata-data.ts @@ -0,0 +1,5839 @@ +// This file is auto-generated by scripts/generate-command-metadata-data.ts — do not edit manually. +// Source: Redis 8.9.241, 426 commands. +import { ModuleMetadataRecords } from "./types"; + +export const COMMAND_METADATA: ModuleMetadataRecords = { + "ft": { + "aggregate": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "aliasadd": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "denyoom", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "aliasdel": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "aliaslist": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "aliasupdate": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "denyoom", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "alter": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "denyoom", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "create": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "denyoom", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "cursor": { + "request": "special", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "subcommands": { + "del": { + "request": "special", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "gc": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "read": { + "request": "special", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + } + } + }, + "dictadd": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "denyoom", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "dictdel": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "dictdump": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "drop": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "module" + ] + }, + "dropindex": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "explain": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "explaincli": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "info": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "profile": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "search": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "spellcheck": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "sugadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "sugdel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "sugget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "suglen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "syndump": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "synupdate": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "denyoom", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "tagvals": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + } + }, + "bf": { + "add": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "card": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "debug": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ], + "tips": [ + "dont_cache" + ] + }, + "exists": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ], + "tips": [ + "dont_cache" + ] + }, + "insert": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "loadchunk": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "madd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "mexists": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "reserve": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "scandump": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ], + "tips": [ + "dont_cache" + ] + } + }, + "cf": { + "add": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "addnx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "compact": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "count": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "debug": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ], + "tips": [ + "dont_cache" + ] + }, + "del": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module", + "fast" + ] + }, + "exists": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ], + "tips": [ + "dont_cache" + ] + }, + "insert": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "insertnx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "loadchunk": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "mexists": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "reserve": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "scandump": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ], + "tips": [ + "dont_cache" + ] + } + }, + "cms": { + "incrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "initbydim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "initbyprob": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "merge": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "query": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + } + }, + "json": { + "arrappend": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "arrindex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "arrinsert": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "arrlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "arrpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "arrtrim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "clear": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "debug": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "movablekeys" + ], + "tips": [ + "dont_cache" + ] + }, + "del": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "forget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "get": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "merge": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "mget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "mset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "numincrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "nummultby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "numpowby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "objkeys": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "objlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "resp": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "set": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "strappend": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "strlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "toggle": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "type": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + } + }, + "std": { + "vadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "vcard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "vdim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "vemb": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "vgetattr": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "vinfo": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "vismember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "vlinks": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "fast" + ] + }, + "vrandmember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "vrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "vrem": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "vsetattr": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module", + "fast" + ] + }, + "vsim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "acl": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "cat": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "deluser": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "dryrun": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "genpass": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "getuser": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "load": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "log": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "save": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "setuser": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "users": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "whoami": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + } + } + }, + "append": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "arcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "ardel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "ardelrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ] + }, + "arget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "argetrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "argrep": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "arinfo": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "arinsert": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "arlastitems": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "arlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "armget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "armset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "arnext": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "arop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "arring": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "arscan": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "arseek": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "arset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "asking": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "fast" + ] + }, + "auth": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale", + "fast", + "no_auth", + "allow_busy" + ] + }, + "backup": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "abort": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript" + ] + }, + "cleanup": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale" + ] + }, + "seal": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript" + ] + }, + "start": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript" + ] + }, + "status": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale" + ] + } + } + }, + "bgrewriteaof": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "no_async_loading" + ] + }, + "bgsave": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "no_async_loading" + ] + }, + "bitcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "bitfield": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "bitfield_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "bitop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "bitpos": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "blmove": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "blocking" + ] + }, + "blmovem": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "blocking" + ] + }, + "blmpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "blocking", + "movablekeys" + ] + }, + "blpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "blocking" + ] + }, + "brpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "blocking" + ] + }, + "brpoplpush": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "blocking" + ] + }, + "bzmpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "blocking", + "movablekeys" + ] + }, + "bzpopmax": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "blocking", + "fast" + ] + }, + "bzpopmin": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "blocking", + "fast" + ] + }, + "client": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "caching": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "getname": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "getredir": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "id": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "info": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "kill": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "no-evict": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "no-touch": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "pause": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "reply": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "setinfo": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "setname": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "tracking": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "trackinginfo": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale" + ] + }, + "unblock": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "unpause": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + } + } + }, + "cluster": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "addslots": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "addslotsrange": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "bumpepoch": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "count-failure-reports": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "countkeysinslot": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "stale" + ] + }, + "delslots": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "delslotsrange": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "failover": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "flushslots": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "forget": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "getkeysinslot": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "info": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "keyslot": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "links": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "meet": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "migration": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "myid": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "myshardid": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "nodes": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "replicas": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "replicate": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "reset": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "stale" + ] + }, + "saveconfig": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "set-config-epoch": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "setslot": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ] + }, + "shards": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "slaves": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "slot-stats": { + "request": "all_shards", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "slots": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "syncslots": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "stale", + "no_async_loading" + ], + "tips": [ + "nondeterministic_output" + ] + } + } + }, + "command": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output_order" + ], + "subcommands": { + "count": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "docs": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "getkeys": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "getkeysandflags": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "info": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output_order" + ] + } + } + }, + "config": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "get": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "resetstat": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "rewrite": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "set": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + } + } + }, + "copy": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "dbsize": { + "request": "all_shards", + "response": "agg_sum", + "isKeyless": true, + "flags": [ + "readonly", + "fast" + ] + }, + "debug": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "decr": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "decrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "del": { + "request": "multi_shard", + "response": "agg_sum", + "isKeyless": false, + "flags": [ + "write" + ], + "keySpecs": [ + { + "beginSearch": { + "type": "index", + "index": 1 + }, + "findKeys": { + "type": "range", + "lastKey": -1, + "keyStep": 1, + "limit": 0 + } + } + ] + }, + "delex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "digest": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "discard": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale", + "fast", + "allow_busy" + ] + }, + "dump": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "echo": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale", + "fast" + ] + }, + "eval": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "noscript", + "stale", + "skip_monitor", + "no_mandatory_keys", + "movablekeys", + "script_runner" + ] + }, + "eval_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "noscript", + "stale", + "skip_monitor", + "no_mandatory_keys", + "movablekeys", + "script_runner" + ] + }, + "evalsha": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "noscript", + "stale", + "skip_monitor", + "no_mandatory_keys", + "movablekeys", + "script_runner" + ] + }, + "evalsha_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "noscript", + "stale", + "skip_monitor", + "no_mandatory_keys", + "movablekeys", + "script_runner" + ] + }, + "exec": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale", + "skip_slowlog" + ] + }, + "exists": { + "request": "multi_shard", + "response": "agg_sum", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ], + "keySpecs": [ + { + "beginSearch": { + "type": "index", + "index": 1 + }, + "findKeys": { + "type": "range", + "lastKey": -1, + "keyStep": 1, + "limit": 0 + } + } + ] + }, + "expire": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "expireat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "expiretime": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "failover": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "stale" + ] + }, + "fcall": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "noscript", + "stale", + "skip_monitor", + "no_mandatory_keys", + "movablekeys", + "script_runner" + ] + }, + "fcall_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "noscript", + "stale", + "skip_monitor", + "no_mandatory_keys", + "movablekeys", + "script_runner" + ] + }, + "flushall": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "write" + ] + }, + "flushdb": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "write" + ] + }, + "function": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "delete": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "write", + "noscript" + ] + }, + "dump": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript" + ] + }, + "flush": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "write", + "noscript" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "kill": { + "request": "all_shards", + "response": "one_succeeded", + "isKeyless": true, + "flags": [ + "noscript", + "allow_busy" + ] + }, + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "load": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "write", + "denyoom", + "noscript" + ] + }, + "restore": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "write", + "denyoom", + "noscript" + ] + }, + "stats": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "allow_busy" + ], + "tips": [ + "nondeterministic_output" + ] + } + } + }, + "geoadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "geodist": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "geohash": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "geopos": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "georadius": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "movablekeys" + ] + }, + "georadius_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "georadiusbymember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "movablekeys" + ] + }, + "georadiusbymember_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "geosearch": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "geosearchstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "get": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "getbit": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "getdel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "getex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "getrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "getset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "hdel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "hello": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale", + "fast", + "no_auth", + "allow_busy" + ] + }, + "hexists": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "hexpire": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "hexpireat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "hexpiretime": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "hget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "hgetall": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "hgetdel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "hgetex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "himport": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "discard": { + "request": "all_shards", + "response": "agg_max", + "isKeyless": true, + "flags": [] + }, + "discardall": { + "request": "all_shards", + "response": "agg_max", + "isKeyless": true, + "flags": [] + }, + "prepare": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "denyoom" + ] + }, + "set": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + } + } + }, + "hincrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "hincrbyfloat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "hkeys": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "hlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "hmget": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "hmset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "hotkeys": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "get": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "reset": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript" + ] + }, + "start": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript" + ] + }, + "stop": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript" + ] + } + } + }, + "hpersist": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "hpexpire": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "hpexpireat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "hpexpiretime": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "hpttl": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "hrandfield": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "hscan": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "hset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "hsetex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "hsetnx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "hstrlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "httl": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "hvals": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "incr": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "incrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "incrbyfloat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "increx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "info": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "keys": { + "request": "all_shards", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "lastsave": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale", + "fast" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "latency": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "doctor": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "graph": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "histogram": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "history": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "latest": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "reset": { + "request": "all_nodes", + "response": "agg_sum", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + } + } + }, + "lcs": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "lindex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "linsert": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "llen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "lmove": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "lmovem": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "lmpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "movablekeys" + ] + }, + "lolwut": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "fast" + ] + }, + "lpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "lpos": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "lpush": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "lpushx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "lrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "lrem": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ] + }, + "lset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "ltrim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ] + }, + "memory": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "doctor": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "tips": [ + "nondeterministic_output" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "malloc-stats": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "tips": [ + "nondeterministic_output" + ] + }, + "purge": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true, + "flags": [] + }, + "stats": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "tips": [ + "nondeterministic_output" + ] + }, + "usage": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + } + } + }, + "mget": { + "request": "multi_shard", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ], + "keySpecs": [ + { + "beginSearch": { + "type": "index", + "index": 1 + }, + "findKeys": { + "type": "range", + "lastKey": -1, + "keyStep": 1, + "limit": 0 + } + } + ] + }, + "migrate": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "movablekeys" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "module": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "list": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "load": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "no_async_loading" + ] + }, + "loadex": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "no_async_loading" + ] + }, + "unload": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "no_async_loading" + ] + } + } + }, + "monitor": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale" + ] + }, + "move": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "mset": { + "request": "multi_shard", + "response": "all_succeeded", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ], + "keySpecs": [ + { + "beginSearch": { + "type": "index", + "index": 1 + }, + "findKeys": { + "type": "range", + "lastKey": -1, + "keyStep": 2, + "limit": 0 + } + } + ] + }, + "msetex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "movablekeys" + ] + }, + "msetnx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "multi": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale", + "fast", + "allow_busy" + ] + }, + "object": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "encoding": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "freq": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "idletime": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "refcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + } + } + }, + "persist": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "pexpire": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "pexpireat": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "pexpiretime": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "pfadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "pfcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "pfdebug": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "admin" + ] + }, + "pfmerge": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "pfselftest": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin" + ] + }, + "ping": { + "request": "all_shards", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "fast" + ] + }, + "psetex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "psubscribe": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "denyoom", + "pubsub", + "noscript", + "loading", + "stale" + ] + }, + "psync": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "no_async_loading", + "no_multi" + ] + }, + "pttl": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "publish": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "pubsub", + "loading", + "stale", + "fast" + ] + }, + "pubsub": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "channels": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "pubsub", + "loading", + "stale" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "numpat": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "pubsub", + "loading", + "stale" + ] + }, + "numsub": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "pubsub", + "loading", + "stale" + ] + }, + "shardchannels": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "pubsub", + "loading", + "stale" + ] + }, + "shardnumsub": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "pubsub", + "loading", + "stale" + ] + } + } + }, + "punsubscribe": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "pubsub", + "noscript", + "loading", + "stale" + ] + }, + "quit": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale", + "fast", + "no_auth", + "allow_busy" + ] + }, + "randomkey": { + "request": "all_shards", + "response": "special", + "isKeyless": true, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "readonly": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale", + "fast" + ] + }, + "readwrite": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale", + "fast" + ] + }, + "rename": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ] + }, + "renamenx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "replconf": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale", + "allow_busy" + ] + }, + "replicaof": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "stale", + "no_async_loading" + ] + }, + "reset": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale", + "fast", + "no_auth", + "allow_busy" + ] + }, + "restore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "restore-asking": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "asking" + ] + }, + "role": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale", + "fast" + ] + }, + "rpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "rpoplpush": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "rpush": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "rpushx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "sadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "save": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "no_async_loading", + "no_multi" + ] + }, + "scan": { + "request": "special", + "response": "special", + "isKeyless": true, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "scard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "script": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "debug": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript" + ] + }, + "exists": { + "request": "all_shards", + "response": "agg_logical_and", + "isKeyless": true, + "flags": [ + "noscript" + ] + }, + "flush": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "noscript" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "kill": { + "request": "all_shards", + "response": "one_succeeded", + "isKeyless": true, + "flags": [ + "noscript", + "allow_busy" + ] + }, + "load": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "noscript", + "stale" + ] + } + } + }, + "sdiff": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "sdiffcard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "movablekeys" + ] + }, + "sdiffstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "select": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale", + "fast" + ] + }, + "set": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "setbit": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "setex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "setnx": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "setrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "shutdown": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "loading", + "stale", + "no_multi", + "allow_busy" + ] + }, + "sinter": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "sintercard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "movablekeys" + ] + }, + "sinterstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "sismember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "slaveof": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "stale", + "no_async_loading" + ] + }, + "slowlog": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "get": { + "request": "all_nodes", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "len": { + "request": "all_nodes", + "response": "agg_sum", + "isKeyless": true, + "flags": [ + "admin", + "loading", + "stale" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "reset": { + "request": "all_nodes", + "response": "all_succeeded", + "isKeyless": true, + "flags": [ + "admin", + "loading", + "stale" + ] + } + } + }, + "smembers": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "smismember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "smove": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "sort": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "movablekeys" + ] + }, + "sort_ro": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "movablekeys" + ] + }, + "spop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "spublish": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "pubsub", + "loading", + "stale", + "fast" + ] + }, + "srandmember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "srem": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "sscan": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "ssubscribe": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "denyoom", + "pubsub", + "noscript", + "loading", + "stale" + ] + }, + "strlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "subscribe": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "denyoom", + "pubsub", + "noscript", + "loading", + "stale" + ] + }, + "substr": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "sunion": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output_order" + ] + }, + "sunioncard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "movablekeys" + ] + }, + "sunionstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "sunsubscribe": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "pubsub", + "noscript", + "loading", + "stale" + ] + }, + "swapdb": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write", + "fast" + ] + }, + "sync": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "admin", + "noscript", + "no_async_loading", + "no_multi" + ] + }, + "time": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale", + "fast" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "touch": { + "request": "multi_shard", + "response": "agg_sum", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ], + "keySpecs": [ + { + "beginSearch": { + "type": "index", + "index": 1 + }, + "findKeys": { + "type": "range", + "lastKey": -1, + "keyStep": 1, + "limit": 0 + } + } + ] + }, + "trimslots": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "write" + ] + }, + "ttl": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "type": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "unlink": { + "request": "multi_shard", + "response": "agg_sum", + "isKeyless": false, + "flags": [ + "write", + "fast" + ], + "keySpecs": [ + { + "beginSearch": { + "type": "index", + "index": 1 + }, + "findKeys": { + "type": "range", + "lastKey": -1, + "keyStep": 1, + "limit": 0 + } + } + ] + }, + "unsubscribe": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "pubsub", + "noscript", + "loading", + "stale" + ] + }, + "unwatch": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "noscript", + "loading", + "stale", + "fast", + "allow_busy" + ] + }, + "wait": { + "request": "all_shards", + "response": "agg_min", + "isKeyless": true, + "flags": [ + "blocking" + ] + }, + "waitaof": { + "request": "all_shards", + "response": "agg_min", + "isKeyless": true, + "flags": [ + "blocking" + ] + }, + "watch": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "noscript", + "loading", + "stale", + "fast", + "allow_busy" + ] + }, + "xack": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "xackdel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "xadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "xautoclaim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "xcfgset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "xclaim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "xdel": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "xdelex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "xgroup": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "create": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "createconsumer": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "delconsumer": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ] + }, + "destroy": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "setid": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ] + } + } + }, + "xidmprecord": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "xinfo": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [], + "subcommands": { + "consumers": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "groups": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "help": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "loading", + "stale" + ] + }, + "stream": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + } + } + }, + "xlen": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "xnack": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "xpending": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "xrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "xread": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "blocking", + "movablekeys" + ] + }, + "xreadgroup": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "blocking", + "movablekeys" + ] + }, + "xrevrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "xsetid": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "xtrim": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "zadd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "zcard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "zcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "zdiff": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "movablekeys" + ] + }, + "zdiffstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "movablekeys" + ] + }, + "zincrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "fast" + ] + }, + "zinter": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "movablekeys" + ] + }, + "zintercard": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "movablekeys" + ] + }, + "zinterstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "movablekeys" + ] + }, + "zlexcount": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "zmpop": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "movablekeys" + ] + }, + "zmscore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "zpopmax": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "zpopmin": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "zrandmember": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "zrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "zrangebylex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "zrangebyscore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "zrangestore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom" + ] + }, + "zrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "zrem": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "fast" + ] + }, + "zremrangebylex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ] + }, + "zremrangebyrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ] + }, + "zremrangebyscore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write" + ] + }, + "zrevrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "zrevrangebylex": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "zrevrangebyscore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ] + }, + "zrevrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "zscan": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly" + ], + "tips": [ + "nondeterministic_output" + ] + }, + "zscore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "fast" + ] + }, + "zunion": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "movablekeys" + ] + }, + "zunionstore": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "movablekeys" + ] + } + }, + "tdigest": { + "add": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "byrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "byrevrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "cdf": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "create": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "max": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "merge": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module", + "movablekeys" + ] + }, + "min": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "quantile": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "rank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "reset": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "revrank": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "trimmed_mean": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + } + }, + "timeseries": { + "refreshcluster": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module", + "noscript" + ] + } + }, + "topk": { + "add": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "count": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "incrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "list": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "query": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "reserve": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + } + }, + "ts": { + "add": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "alter": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "create": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "createrule": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module", + "fast" + ] + }, + "decrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "del": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "deleterule": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "module" + ] + }, + "get": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "incrby": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "info": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "madd": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "write", + "denyoom", + "module" + ] + }, + "mget": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "mrange": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "mrevrange": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "nrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "movablekeys" + ] + }, + "nrevrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module", + "movablekeys" + ] + }, + "queryindex": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "querylabels": { + "request": "default-keyless", + "response": "default-keyless", + "isKeyless": true, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "range": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + }, + "read": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ], + "tips": [ + "dont_cache" + ] + }, + "revrange": { + "request": "default-keyed", + "response": "default-keyed", + "isKeyless": false, + "flags": [ + "readonly", + "module" + ] + } + } +} as const; diff --git a/packages/client/package.json b/packages/client/package.json index 48fa9d97695..346ceace3fa 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -11,6 +11,7 @@ "scripts": { "test": "npm run test:types && nyc -r text-summary -r lcov mocha -r tsx --reporter mocha-multi-reporters --reporter-options configFile=mocha-multi-reporter-config.json --exit './lib/**/*.spec.ts'", "test:types": "tsc -p tsconfig.types-test.json", + "generate:metadata": "tsx ./scripts/generate-command-metadata-data.ts", "release": "release-it" }, "dependencies": { diff --git a/packages/client/scripts/command-metadata-overrides.ts b/packages/client/scripts/command-metadata-overrides.ts new file mode 100644 index 00000000000..72137a1287a --- /dev/null +++ b/packages/client/scripts/command-metadata-overrides.ts @@ -0,0 +1,121 @@ +/** + * Curation applied on top of the raw COMMAND dump when regenerating + * `command-metadata-data.ts`, keeping the static data aligned with the HLD + * "Command Routing Policy Table" (see ft-policies.spec.ts). + * + * Rationale: the server reports internal, debug, deprecated and cluster-admin + * commands that the HLD deliberately omits from client routing. They are + * excluded here so the static phase refuses to resolve them (they fall through + * to the fallback resolver instead). + * + * Scope: this file curates TABLE SHAPE only — which entries exist and their + * routing policies (request/response/isKeyless). Per-command value intent + * (`IS_READ_ONLY`, `CACHEABLE`) does NOT belong here: it lives in the command + * definitions and wins over the table via the override-first predicates + * (`isReplicaSafe` / `isCacheable` in `lib/command-metadata/predicates.ts`). + */ +import type { CommandMetadata } from '../lib/command-metadata/policies-constants'; + +/** Entire modules to drop (internal / cluster-admin command namespaces). */ +export const EXCLUDED_MODULES: ReadonlySet = new Set(['_ft', 'search']); + +/** Individual `module.command` entries to drop. */ +export const EXCLUDED_COMMANDS: ReadonlySet = new Set([ + // FT internal conditional variants + 'ft._aliasaddifnx', + 'ft._aliasdelifx', + 'ft._alterifnx', + 'ft._createifnx', + 'ft._dropifx', + 'ft._dropindexifx', + // FT debug/introspection not in the HLD table + 'ft._list', + 'ft.config', + // FT deprecated legacy (pre-2.0) commands + 'ft.add', + 'ft.del', + 'ft.get', + 'ft.mget', + 'ft.safeadd', + 'ft.synadd', + // Not yet in the HLD routing table + 'ft.hybrid', + // Cluster-admin + 'timeseries.clusterset' +]); + +/** + * Partial-entry overrides, keyed by `module.command`, merged onto the + * generated entry (override keys win; unspecified keys keep the server value). + * `subcommands` is deep-merged per subcommand, so an override touching one + * subcommand keeps its siblings' server-derived policies. + * + * `ft.cursor` is pinned to the HLD `special` request policy (sticky cursor): + * FT.CURSOR READ/DEL must reach the node that served the FT.AGGREGATE that + * minted the cursor. The client-side sticky machinery lives in + * `lib/cluster/request-response-policies/ft-cursor.ts` (router + capture) and + * the cursor binding map on `cluster-slots.ts`. The response stays + * `default-keyless` (single-node pass-through). Pinned as an override so the + * static table is deterministic regardless of what a given server reports for + * the container command's subcommands. + * + * `KEYLESS` reverts: the server tips these commands `special` (request and/or + * response), but there is no meaningful client-side interpretation for them + * (the HLD gives a recipe only for FT.CURSOR; SCAN and RANDOMKEY have obvious + * client semantics and are implemented — see + * `lib/cluster/request-response-policies/scan-cursor.ts` and + * `reduceRandomKey` in `dispatch.ts` — so they keep their server-reported + * policies). Without an interpretation they are pinned back to master's + * behavior — `default-keyless` routes to a single random node and passes the + * sole reply through. `memory purge`/`usage`, `latency reset` and + * `hotkeys help` are untouched: the per-subcommand deep-merge keeps their + * server-derived policies. + */ +const KEYLESS = { request: 'default-keyless', response: 'default-keyless', isKeyless: true } as const; + +export const COMMAND_OVERRIDES: Readonly>> = { + // MSETEX is pinned back to master's single-node routing (server tips it + // multi_shard). Splitting breaks its documented atomic contract twice over: + // NX/XX is all-or-nothing across ALL keys, but each shard would evaluate the + // condition against its own subset (partial writes), and the 0/1 per-shard + // replies cannot be aggregated honestly. The server itself excludes the + // conditional sibling MSETNX from multi_shard for the same reason. Pinned + // default-keyed: single-slot calls keep full server-side atomicity, + // cross-slot calls get the server's own CROSSSLOT error (use hash tags). + // keySpecs explicitly unset — only the multi_shard splitter consumes them, + // and the shallow merge would otherwise keep the server-derived specs. + 'std.msetex': { request: 'default-keyed', response: 'default-keyed', keySpecs: undefined }, + 'ft.cursor': { + request: 'special', + response: 'default-keyless', + isKeyless: true, + subcommands: { + read: { request: 'special', response: 'default-keyless', isKeyless: true }, + del: { request: 'special', response: 'default-keyless', isKeyless: true } + } + }, + // HIMPORT session subcommands fan out to all masters (server-tipped + // request_policy:all_shards) but the server defines no response_policy, and the + // default-keyless reducer crashes on scalar fan-out replies (`aggregateMerge` throws + // 'Unsupported reply type for merge aggregation' for simple strings/integers whenever + // ≥2 masters reply). These reducers implement the HLD's "default fan-out handling" + // client-side with semantics preserved: + // - prepare → all_succeeded: every master must acknowledge; first `OK` is the reply. + // - discard/discardall → agg_max: the client hook substitutes registry-based replies + // (exactly one node client performs the registry mutation and returns the real 1/count, + // siblings return 0), so max recovers the registry answer. Request policies are NOT + // overridden — all_shards comes from the server's own tips. HIMPORT SET carries a key + // at position 2 and keeps default keyed routing. + 'std.himport': { + subcommands: { + prepare: { response: 'all_succeeded' }, + discard: { response: 'agg_max' }, + discardall: { response: 'agg_max' } + } + }, + 'std.info': KEYLESS, + 'std.memory': { subcommands: { doctor: KEYLESS, 'malloc-stats': KEYLESS, stats: KEYLESS } }, + 'std.latency': { subcommands: { doctor: KEYLESS, graph: KEYLESS, histogram: KEYLESS, history: KEYLESS, latest: KEYLESS } }, + 'std.function': { subcommands: { stats: KEYLESS } }, + 'std.hotkeys': { subcommands: { get: KEYLESS, reset: KEYLESS, start: KEYLESS, stop: KEYLESS } } +}; diff --git a/packages/client/scripts/generate-command-metadata-data.ts b/packages/client/scripts/generate-command-metadata-data.ts new file mode 100644 index 00000000000..8df9057ceaa --- /dev/null +++ b/packages/client/scripts/generate-command-metadata-data.ts @@ -0,0 +1,129 @@ +/** + * Regenerates `lib/command-metadata/command-metadata-data.ts` + * from a live Redis server's COMMAND reply. + * + * The metadata derivation is shared with `DynamicPolicyResolverFactory`, so the + * generated static data is exactly what the dynamic resolver would build at + * runtime against the same server, minus the HLD curation defined in + * `command-metadata-overrides.ts` (internal/deprecated/cluster-admin commands). + * + * Usage: + * npm run generate:metadata --workspace=packages/client -- redis://localhost:6379 + * + * The Redis URL is taken from the first CLI argument, then the REDIS_URL + * environment variable, and defaults to redis://localhost:6379. + * + * Run against a server with all bundled modules loaded (e.g. Redis 8.8) so + * module commands (ft, json, bf, ...) are included. + */ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { createClient } from '../index'; +import { transformCommandReply, type CommandRawReply } from '../lib/commands/generic-transformers'; +import { DynamicPolicyResolverFactory } from '../lib/command-metadata/dynamic-policy-resolver-factory'; +import type { CommandMetadataRecords, ModuleMetadataRecords } from '../lib/command-metadata/types'; +import type { CommandMetadata } from '../lib/command-metadata/policies-constants'; +import { EXCLUDED_MODULES, EXCLUDED_COMMANDS, COMMAND_OVERRIDES } from './command-metadata-overrides'; + +const OUTPUT_PATH = resolve(__dirname, '../lib/command-metadata/command-metadata-data.ts'); + +function sortedByKey(record: Record, mapValue: (value: T) => T): Record { + const sorted: Record = {}; + // Lowercased to match StaticMetadataResolver's lookup normalization. + for (const key of Object.keys(record).sort()) { + sorted[key.toLowerCase()] = mapValue(record[key]); + } + return sorted; +} + +function sortCommandMetadata(policies: CommandMetadata): CommandMetadata { + return { + ...policies, + subcommands: policies.subcommands + ? sortedByKey(policies.subcommands, sortCommandMetadata) + : undefined + }; +} + +// Sort modules, commands and subcommands alphabetically so regeneration +// produces stable diffs regardless of the order the server lists commands in. +function sortModuleMetadataRecords(records: ModuleMetadataRecords): ModuleMetadataRecords { + return sortedByKey(records, (commands: CommandMetadataRecords) => + sortedByKey(commands, sortCommandMetadata) + ); +} + +// Merges an override onto a generated entry. Top-level keys are shallow-merged +// (override keys win), except `subcommands`, which is deep-merged per +// subcommand so an override touching one subcommand doesn't drop its siblings. +function applyOverride( + policies: CommandMetadata, + override: Partial | undefined +): CommandMetadata { + if (!override) return policies; + const merged = { ...policies, ...override }; + if (override.subcommands && policies.subcommands) { + const subcommands = { ...policies.subcommands }; + for (const [name, sub] of Object.entries(override.subcommands)) { + subcommands[name] = { ...policies.subcommands[name], ...sub }; + } + merged.subcommands = subcommands; + } + return merged; +} + +// Applies the HLD curation from command-metadata-overrides.ts. Expects +// lowercased records (i.e. run after sortModuleMetadataRecords). +function curate(records: ModuleMetadataRecords): ModuleMetadataRecords { + const curated: ModuleMetadataRecords = {}; + + for (const [moduleName, commands] of Object.entries(records)) { + if (EXCLUDED_MODULES.has(moduleName)) continue; + + curated[moduleName] = {}; + for (const [commandName, policies] of Object.entries(commands)) { + const fullName = `${moduleName}.${commandName}`; + if (EXCLUDED_COMMANDS.has(fullName)) continue; + + curated[moduleName][commandName] = applyOverride(policies, COMMAND_OVERRIDES[fullName]); + } + } + + return curated; +} + +async function main() { + const url = process.argv[2] ?? process.env.REDIS_URL ?? 'redis://localhost:6379'; + const client = createClient({ url }); + await client.connect(); + + try { + const rawCommands = await client.sendCommand>(['COMMAND']); + const commands = rawCommands.map(transformCommandReply); + const policies = curate(sortModuleMetadataRecords( + DynamicPolicyResolverFactory.buildModuleMetadataRecords(commands) + )); + + const info = await client.sendCommand(['INFO', 'server']); + const version = /redis_version:(\S+)/.exec(info)?.[1] ?? 'unknown'; + + const content = [ + '// This file is auto-generated by scripts/generate-command-metadata-data.ts — do not edit manually.', + `// Source: Redis ${version}, ${Object.values(policies).reduce((sum, commands) => sum + Object.keys(commands).length, 0)} commands.`, + 'import { ModuleMetadataRecords } from "./types";', + '', + `export const COMMAND_METADATA: ModuleMetadataRecords = ${JSON.stringify(policies, null, 2)} as const;`, + '' + ].join('\n'); + + writeFileSync(OUTPUT_PATH, content); + console.log(`Wrote ${OUTPUT_PATH} (Redis ${version})`); + } finally { + client.destroy(); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); From 215812215e2cce96d0be9f2de9588840170091c9 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 30 Jul 2026 14:14:11 +0300 Subject: [PATCH 02/17] refactor(client): derive IS_READ_ONLY/CACHEABLE from server metadata Drive each command's routing and client-side-cache eligibility from the regenerated server COMMAND metadata instead of per-file hand annotations, updating the command definitions across all packages accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/bloom/lib/commands/bloom/ADD.ts | 1 - packages/bloom/lib/commands/bloom/CARD.ts | 1 - packages/bloom/lib/commands/bloom/EXISTS.ts | 1 - packages/bloom/lib/commands/bloom/INFO.ts | 1 - packages/bloom/lib/commands/bloom/INSERT.ts | 1 - .../bloom/lib/commands/bloom/LOADCHUNK.ts | 1 - packages/bloom/lib/commands/bloom/MADD.ts | 1 - packages/bloom/lib/commands/bloom/MEXISTS.ts | 1 - packages/bloom/lib/commands/bloom/RESERVE.ts | 1 - packages/bloom/lib/commands/bloom/SCANDUMP.ts | 1 - .../lib/commands/count-min-sketch/INCRBY.ts | 1 - .../lib/commands/count-min-sketch/INFO.ts | 1 - .../commands/count-min-sketch/INITBYDIM.ts | 1 - .../commands/count-min-sketch/INITBYPROB.ts | 1 - .../lib/commands/count-min-sketch/MERGE.ts | 1 - .../lib/commands/count-min-sketch/QUERY.ts | 1 - packages/bloom/lib/commands/cuckoo/ADD.ts | 1 - packages/bloom/lib/commands/cuckoo/ADDNX.ts | 1 - packages/bloom/lib/commands/cuckoo/COUNT.ts | 1 - packages/bloom/lib/commands/cuckoo/DEL.ts | 1 - packages/bloom/lib/commands/cuckoo/EXISTS.ts | 1 - packages/bloom/lib/commands/cuckoo/INFO.ts | 1 - packages/bloom/lib/commands/cuckoo/INSERT.ts | 1 - .../bloom/lib/commands/cuckoo/INSERTNX.ts | 3 +- .../bloom/lib/commands/cuckoo/LOADCHUNK.ts | 1 - packages/bloom/lib/commands/cuckoo/RESERVE.ts | 1 - .../bloom/lib/commands/cuckoo/SCANDUMP.ts | 1 - packages/bloom/lib/commands/t-digest/ADD.ts | 1 - .../bloom/lib/commands/t-digest/BYRANK.ts | 1 - .../bloom/lib/commands/t-digest/BYREVRANK.ts | 1 - packages/bloom/lib/commands/t-digest/CDF.ts | 1 - .../bloom/lib/commands/t-digest/CREATE.ts | 1 - packages/bloom/lib/commands/t-digest/INFO.ts | 1 - packages/bloom/lib/commands/t-digest/MAX.ts | 1 - packages/bloom/lib/commands/t-digest/MERGE.ts | 1 - packages/bloom/lib/commands/t-digest/MIN.ts | 1 - .../bloom/lib/commands/t-digest/QUANTILE.ts | 1 - packages/bloom/lib/commands/t-digest/RANK.ts | 1 - packages/bloom/lib/commands/t-digest/RESET.ts | 1 - .../bloom/lib/commands/t-digest/REVRANK.ts | 1 - .../lib/commands/t-digest/TRIMMED_MEAN.ts | 1 - packages/bloom/lib/commands/top-k/ADD.ts | 1 - packages/bloom/lib/commands/top-k/COUNT.ts | 1 - packages/bloom/lib/commands/top-k/INCRBY.ts | 1 - packages/bloom/lib/commands/top-k/INFO.ts | 1 - packages/bloom/lib/commands/top-k/LIST.ts | 1 - .../lib/commands/top-k/LIST_WITHCOUNT.ts | 1 - packages/bloom/lib/commands/top-k/QUERY.ts | 1 - packages/bloom/lib/commands/top-k/RESERVE.ts | 1 - packages/client/lib/RESP/types.ts | 12 ++----- packages/client/lib/client/index.ts | 32 ++++++++++++------- packages/client/lib/commands/ACL_CAT.ts | 1 - packages/client/lib/commands/ACL_DELUSER.ts | 2 -- packages/client/lib/commands/ACL_DRYRUN.ts | 1 - packages/client/lib/commands/ACL_GENPASS.ts | 1 - packages/client/lib/commands/ACL_GETUSER.ts | 1 - packages/client/lib/commands/ACL_LIST.ts | 1 - packages/client/lib/commands/ACL_LOAD.ts | 2 -- packages/client/lib/commands/ACL_LOG.ts | 1 - packages/client/lib/commands/ACL_LOG_RESET.ts | 3 -- packages/client/lib/commands/ACL_SAVE.ts | 2 -- packages/client/lib/commands/ACL_SETUSER.ts | 2 -- packages/client/lib/commands/ACL_USERS.ts | 1 - packages/client/lib/commands/ACL_WHOAMI.ts | 1 - packages/client/lib/commands/APPEND.ts | 1 - packages/client/lib/commands/ARCOUNT.ts | 1 - packages/client/lib/commands/ARGET.ts | 1 - packages/client/lib/commands/ARGETRANGE.ts | 1 - packages/client/lib/commands/ARGREP.ts | 1 - .../client/lib/commands/ARGREP_WITHVALUES.ts | 1 - packages/client/lib/commands/ARINFO.ts | 1 - packages/client/lib/commands/ARLASTITEMS.ts | 1 - packages/client/lib/commands/ARLEN.ts | 1 - packages/client/lib/commands/ARMGET.ts | 1 - packages/client/lib/commands/ARNEXT.ts | 1 - packages/client/lib/commands/AROP.ts | 1 - packages/client/lib/commands/ARSCAN.ts | 1 - packages/client/lib/commands/ASKING.ts | 1 - packages/client/lib/commands/AUTH.ts | 1 - packages/client/lib/commands/BGREWRITEAOF.ts | 2 -- packages/client/lib/commands/BGSAVE.ts | 2 -- packages/client/lib/commands/BITCOUNT.ts | 2 -- packages/client/lib/commands/BITFIELD.ts | 1 - packages/client/lib/commands/BITFIELD_RO.ts | 2 -- packages/client/lib/commands/BITOP.ts | 1 - packages/client/lib/commands/BITPOS.ts | 2 -- packages/client/lib/commands/BLMOVE.ts | 1 - packages/client/lib/commands/BLMPOP.ts | 1 - packages/client/lib/commands/BLPOP.ts | 1 - packages/client/lib/commands/BRPOP.ts | 1 - packages/client/lib/commands/BRPOPLPUSH.ts | 1 - packages/client/lib/commands/BZMPOP.ts | 1 - packages/client/lib/commands/BZPOPMAX.ts | 1 - packages/client/lib/commands/BZPOPMIN.ts | 1 - .../client/lib/commands/CLIENT_CACHING.ts | 1 - .../client/lib/commands/CLIENT_GETNAME.ts | 1 - .../client/lib/commands/CLIENT_GETREDIR.ts | 1 - packages/client/lib/commands/CLIENT_ID.ts | 1 - packages/client/lib/commands/CLIENT_INFO.ts | 1 - packages/client/lib/commands/CLIENT_KILL.ts | 2 -- packages/client/lib/commands/CLIENT_LIST.ts | 1 - .../client/lib/commands/CLIENT_NO-EVICT.ts | 1 - .../client/lib/commands/CLIENT_NO-TOUCH.ts | 1 - packages/client/lib/commands/CLIENT_PAUSE.ts | 2 -- .../client/lib/commands/CLIENT_SETNAME.ts | 1 - .../client/lib/commands/CLIENT_TRACKING.ts | 1 - .../lib/commands/CLIENT_TRACKINGINFO.ts | 1 - .../client/lib/commands/CLIENT_UNBLOCK.ts | 2 -- .../client/lib/commands/CLIENT_UNPAUSE.ts | 2 -- .../client/lib/commands/CLUSTER_ADDSLOTS.ts | 2 -- .../lib/commands/CLUSTER_ADDSLOTSRANGE.ts | 2 -- .../client/lib/commands/CLUSTER_BUMPEPOCH.ts | 2 -- .../commands/CLUSTER_COUNT-FAILURE-REPORTS.ts | 1 - .../lib/commands/CLUSTER_COUNTKEYSINSLOT.ts | 1 - .../client/lib/commands/CLUSTER_DELSLOTS.ts | 2 -- .../lib/commands/CLUSTER_DELSLOTSRANGE.ts | 2 -- .../client/lib/commands/CLUSTER_FAILOVER.ts | 2 -- .../client/lib/commands/CLUSTER_FLUSHSLOTS.ts | 2 -- .../client/lib/commands/CLUSTER_FORGET.ts | 2 -- .../lib/commands/CLUSTER_GETKEYSINSLOT.ts | 1 - packages/client/lib/commands/CLUSTER_INFO.ts | 1 - .../client/lib/commands/CLUSTER_KEYSLOT.ts | 5 ++- packages/client/lib/commands/CLUSTER_LINKS.ts | 1 - packages/client/lib/commands/CLUSTER_MEET.ts | 2 -- packages/client/lib/commands/CLUSTER_MYID.ts | 1 - .../client/lib/commands/CLUSTER_MYSHARDID.ts | 1 - packages/client/lib/commands/CLUSTER_NODES.ts | 1 - .../client/lib/commands/CLUSTER_REPLICAS.ts | 1 - .../client/lib/commands/CLUSTER_REPLICATE.ts | 2 -- packages/client/lib/commands/CLUSTER_RESET.ts | 2 -- .../client/lib/commands/CLUSTER_SAVECONFIG.ts | 2 -- .../lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts | 2 -- .../client/lib/commands/CLUSTER_SETSLOT.ts | 2 -- packages/client/lib/commands/CLUSTER_SLOTS.ts | 1 - packages/client/lib/commands/COMMAND.ts | 1 - packages/client/lib/commands/COMMAND_COUNT.ts | 1 - .../client/lib/commands/COMMAND_GETKEYS.ts | 1 - .../lib/commands/COMMAND_GETKEYSANDFLAGS.ts | 1 - packages/client/lib/commands/COMMAND_INFO.ts | 1 - packages/client/lib/commands/COMMAND_LIST.ts | 1 - packages/client/lib/commands/CONFIG_GET.ts | 1 - .../client/lib/commands/CONFIG_RESETSTAT.ts | 2 -- .../client/lib/commands/CONFIG_REWRITE.ts | 2 -- packages/client/lib/commands/CONFIG_SET.ts | 2 -- packages/client/lib/commands/COPY.ts | 1 - packages/client/lib/commands/DBSIZE.ts | 2 -- packages/client/lib/commands/DEL.ts | 1 - packages/client/lib/commands/DELEX.ts | 1 - packages/client/lib/commands/DIGEST.ts | 1 - packages/client/lib/commands/DUMP.ts | 1 - packages/client/lib/commands/ECHO.ts | 1 - packages/client/lib/commands/EVAL.ts | 1 - packages/client/lib/commands/EVALSHA.ts | 1 - packages/client/lib/commands/EXISTS.ts | 2 -- packages/client/lib/commands/EXPIRETIME.ts | 1 - packages/client/lib/commands/FCALL.ts | 1 - packages/client/lib/commands/FCALL_RO.ts | 2 +- packages/client/lib/commands/FLUSHALL.ts | 2 -- packages/client/lib/commands/FLUSHDB.ts | 2 -- .../client/lib/commands/FUNCTION_DELETE.ts | 2 -- packages/client/lib/commands/FUNCTION_DUMP.ts | 1 - .../client/lib/commands/FUNCTION_FLUSH.ts | 2 -- packages/client/lib/commands/FUNCTION_KILL.ts | 2 -- packages/client/lib/commands/FUNCTION_LIST.ts | 3 +- .../lib/commands/FUNCTION_LIST_WITHCODE.ts | 3 +- packages/client/lib/commands/FUNCTION_LOAD.ts | 2 -- .../client/lib/commands/FUNCTION_RESTORE.ts | 2 -- .../client/lib/commands/FUNCTION_STATS.ts | 1 - packages/client/lib/commands/GEOADD.ts | 1 - packages/client/lib/commands/GEODIST.ts | 2 -- packages/client/lib/commands/GEOHASH.ts | 2 -- packages/client/lib/commands/GEOPOS.ts | 2 -- packages/client/lib/commands/GEORADIUS.ts | 1 - .../client/lib/commands/GEORADIUSBYMEMBER.ts | 1 - .../lib/commands/GEORADIUSBYMEMBER_RO.ts | 2 -- .../lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts | 2 -- .../lib/commands/GEORADIUSBYMEMBER_STORE.ts | 3 +- .../lib/commands/GEORADIUSBYMEMBER_WITH.ts | 2 -- packages/client/lib/commands/GEORADIUS_RO.ts | 2 -- .../client/lib/commands/GEORADIUS_RO_WITH.ts | 2 -- .../client/lib/commands/GEORADIUS_STORE.ts | 3 +- .../client/lib/commands/GEORADIUS_WITH.ts | 3 +- packages/client/lib/commands/GEOSEARCH.ts | 1 - .../client/lib/commands/GEOSEARCHSTORE.ts | 1 - .../client/lib/commands/GEOSEARCH_WITH.ts | 1 - packages/client/lib/commands/GET.ts | 2 -- packages/client/lib/commands/GETBIT.ts | 2 -- packages/client/lib/commands/GETDEL.ts | 1 - packages/client/lib/commands/GETEX.ts | 1 - packages/client/lib/commands/GETRANGE.ts | 2 -- packages/client/lib/commands/GETSET.ts | 1 - packages/client/lib/commands/HEXISTS.ts | 2 -- packages/client/lib/commands/HEXPIRETIME.ts | 1 - packages/client/lib/commands/HGET.ts | 2 -- packages/client/lib/commands/HGETALL.ts | 2 -- packages/client/lib/commands/HKEYS.ts | 2 -- packages/client/lib/commands/HLEN.ts | 2 -- packages/client/lib/commands/HMGET.ts | 2 -- packages/client/lib/commands/HOTKEYS_GET.ts | 1 - packages/client/lib/commands/HOTKEYS_RESET.ts | 2 -- packages/client/lib/commands/HOTKEYS_START.ts | 2 -- packages/client/lib/commands/HOTKEYS_STOP.ts | 2 -- packages/client/lib/commands/HPEXPIREAT.ts | 1 - packages/client/lib/commands/HPEXPIRETIME.ts | 1 - packages/client/lib/commands/HPTTL.ts | 1 - packages/client/lib/commands/HRANDFIELD.ts | 1 - .../client/lib/commands/HRANDFIELD_COUNT.ts | 1 - .../commands/HRANDFIELD_COUNT_WITHVALUES.ts | 1 - packages/client/lib/commands/HSCAN.ts | 1 - .../client/lib/commands/HSCAN_NOVALUES.ts | 1 - packages/client/lib/commands/HSETNX.ts | 1 - packages/client/lib/commands/HSTRLEN.ts | 2 -- packages/client/lib/commands/HTTL.ts | 1 - packages/client/lib/commands/HVALS.ts | 2 -- packages/client/lib/commands/INFO.ts | 1 - packages/client/lib/commands/KEYS.ts | 2 -- packages/client/lib/commands/LASTSAVE.ts | 1 - .../client/lib/commands/LATENCY_DOCTOR.ts | 1 - packages/client/lib/commands/LATENCY_GRAPH.ts | 1 - .../client/lib/commands/LATENCY_HISTOGRAM.ts | 1 - .../client/lib/commands/LATENCY_HISTORY.ts | 1 - .../client/lib/commands/LATENCY_LATEST.ts | 1 - packages/client/lib/commands/LATENCY_RESET.ts | 2 -- packages/client/lib/commands/LCS.ts | 1 - packages/client/lib/commands/LCS_IDX.ts | 1 - .../lib/commands/LCS_IDX_WITHMATCHLEN.ts | 1 - packages/client/lib/commands/LCS_LEN.ts | 1 - packages/client/lib/commands/LINDEX.ts | 2 -- packages/client/lib/commands/LINSERT.ts | 1 - packages/client/lib/commands/LLEN.ts | 2 -- packages/client/lib/commands/LMOVE.ts | 1 - packages/client/lib/commands/LMPOP.ts | 1 - packages/client/lib/commands/LOLWUT.ts | 2 -- packages/client/lib/commands/LPOP_COUNT.ts | 1 - packages/client/lib/commands/LPOS.ts | 2 -- packages/client/lib/commands/LPOS_COUNT.ts | 2 -- packages/client/lib/commands/LRANGE.ts | 2 -- packages/client/lib/commands/LREM.ts | 1 - packages/client/lib/commands/LSET.ts | 1 - packages/client/lib/commands/MEMORY_DOCTOR.ts | 1 - .../lib/commands/MEMORY_MALLOC-STATS.ts | 1 - packages/client/lib/commands/MEMORY_PURGE.ts | 2 -- packages/client/lib/commands/MEMORY_STATS.ts | 1 - packages/client/lib/commands/MEMORY_USAGE.ts | 1 - packages/client/lib/commands/MGET.ts | 2 -- packages/client/lib/commands/MIGRATE.ts | 1 - packages/client/lib/commands/MODULE_LIST.ts | 1 - packages/client/lib/commands/MODULE_LOAD.ts | 2 -- packages/client/lib/commands/MODULE_UNLOAD.ts | 2 -- packages/client/lib/commands/MSET.ts | 1 - packages/client/lib/commands/MSETNX.ts | 1 - .../client/lib/commands/OBJECT_ENCODING.ts | 1 - packages/client/lib/commands/OBJECT_FREQ.ts | 1 - .../client/lib/commands/OBJECT_IDLETIME.ts | 1 - .../client/lib/commands/OBJECT_REFCOUNT.ts | 1 - packages/client/lib/commands/PEXPIRE.ts | 1 - packages/client/lib/commands/PEXPIREAT.ts | 1 - packages/client/lib/commands/PEXPIRETIME.ts | 1 - packages/client/lib/commands/PFADD.ts | 1 - packages/client/lib/commands/PFCOUNT.ts | 1 - packages/client/lib/commands/PING.ts | 1 - packages/client/lib/commands/PTTL.ts | 1 - packages/client/lib/commands/PUBLISH.ts | 2 -- .../client/lib/commands/PUBSUB_CHANNELS.ts | 1 - packages/client/lib/commands/PUBSUB_NUMPAT.ts | 1 - packages/client/lib/commands/PUBSUB_NUMSUB.ts | 1 - .../lib/commands/PUBSUB_SHARDCHANNELS.ts | 1 - packages/client/lib/commands/RANDOMKEY.ts | 2 -- packages/client/lib/commands/READONLY.ts | 1 - packages/client/lib/commands/READWRITE.ts | 1 - packages/client/lib/commands/RENAME.ts | 1 - packages/client/lib/commands/RENAMENX.ts | 1 - packages/client/lib/commands/REPLICAOF.ts | 2 -- .../client/lib/commands/RESTORE-ASKING.ts | 2 -- packages/client/lib/commands/RESTORE.ts | 1 - packages/client/lib/commands/ROLE.ts | 1 - packages/client/lib/commands/SAVE.ts | 2 -- packages/client/lib/commands/SCAN.ts | 2 -- packages/client/lib/commands/SCARD.ts | 2 -- packages/client/lib/commands/SCRIPT_DEBUG.ts | 1 - packages/client/lib/commands/SCRIPT_EXISTS.ts | 1 - packages/client/lib/commands/SCRIPT_FLUSH.ts | 2 -- packages/client/lib/commands/SCRIPT_KILL.ts | 2 -- packages/client/lib/commands/SCRIPT_LOAD.ts | 2 -- packages/client/lib/commands/SDIFF.ts | 2 -- packages/client/lib/commands/SETBIT.ts | 1 - packages/client/lib/commands/SHUTDOWN.ts | 2 -- packages/client/lib/commands/SINTER.ts | 2 -- packages/client/lib/commands/SINTERCARD.ts | 1 - packages/client/lib/commands/SINTERSTORE.ts | 1 - packages/client/lib/commands/SISMEMBER.ts | 2 -- packages/client/lib/commands/SMEMBERS.ts | 2 -- packages/client/lib/commands/SMISMEMBER.ts | 2 -- packages/client/lib/commands/SMOVE.ts | 1 - packages/client/lib/commands/SORT.ts | 1 - packages/client/lib/commands/SORT_RO.ts | 1 - packages/client/lib/commands/SORT_STORE.ts | 1 - packages/client/lib/commands/SPOP.ts | 1 - packages/client/lib/commands/SPOP_COUNT.ts | 1 - packages/client/lib/commands/SPUBLISH.ts | 1 - packages/client/lib/commands/SRANDMEMBER.ts | 1 - .../client/lib/commands/SRANDMEMBER_COUNT.ts | 1 - packages/client/lib/commands/SREM.ts | 1 - packages/client/lib/commands/SSCAN.ts | 1 - packages/client/lib/commands/STRLEN.ts | 2 -- packages/client/lib/commands/SUNION.ts | 2 -- packages/client/lib/commands/SUNIONSTORE.ts | 1 - packages/client/lib/commands/SWAPDB.ts | 2 -- packages/client/lib/commands/TIME.ts | 1 - packages/client/lib/commands/TOUCH.ts | 2 +- packages/client/lib/commands/TTL.ts | 1 - packages/client/lib/commands/TYPE.ts | 2 -- packages/client/lib/commands/UNLINK.ts | 1 - packages/client/lib/commands/VCARD.ts | 1 - packages/client/lib/commands/VDIM.ts | 1 - packages/client/lib/commands/VEMB.ts | 1 - packages/client/lib/commands/VEMB_RAW.ts | 1 - packages/client/lib/commands/VGETATTR.ts | 1 - packages/client/lib/commands/VINFO.ts | 1 - packages/client/lib/commands/VLINKS.ts | 1 - .../client/lib/commands/VLINKS_WITHSCORES.ts | 1 - packages/client/lib/commands/VRANDMEMBER.ts | 7 +++- packages/client/lib/commands/VRANGE.ts | 1 - packages/client/lib/commands/VSIM.ts | 1 - .../client/lib/commands/VSIM_WITHSCORES.ts | 1 - packages/client/lib/commands/WAIT.ts | 2 -- packages/client/lib/commands/XACK.ts | 1 - packages/client/lib/commands/XACKDEL.ts | 1 - packages/client/lib/commands/XADD.ts | 1 - .../client/lib/commands/XADD_NOMKSTREAM.ts | 1 - packages/client/lib/commands/XAUTOCLAIM.ts | 1 - .../client/lib/commands/XAUTOCLAIM_JUSTID.ts | 1 - packages/client/lib/commands/XCFGSET.ts | 1 - packages/client/lib/commands/XCLAIM.ts | 1 - packages/client/lib/commands/XCLAIM_JUSTID.ts | 1 - packages/client/lib/commands/XDEL.ts | 1 - packages/client/lib/commands/XDELEX.ts | 1 - packages/client/lib/commands/XGROUP_CREATE.ts | 1 - .../lib/commands/XGROUP_CREATECONSUMER.ts | 1 - .../client/lib/commands/XGROUP_DELCONSUMER.ts | 1 - .../client/lib/commands/XGROUP_DESTROY.ts | 1 - packages/client/lib/commands/XGROUP_SETID.ts | 1 - .../client/lib/commands/XINFO_CONSUMERS.ts | 1 - packages/client/lib/commands/XINFO_GROUPS.ts | 1 - packages/client/lib/commands/XINFO_STREAM.ts | 1 - packages/client/lib/commands/XLEN.ts | 2 -- packages/client/lib/commands/XNACK.ts | 1 - packages/client/lib/commands/XPENDING.ts | 2 -- .../client/lib/commands/XPENDING_RANGE.ts | 2 -- packages/client/lib/commands/XRANGE.ts | 2 -- packages/client/lib/commands/XREAD.ts | 1 - packages/client/lib/commands/XREADGROUP.ts | 1 - packages/client/lib/commands/XREVRANGE.ts | 2 -- packages/client/lib/commands/XSETID.ts | 1 - packages/client/lib/commands/XTRIM.ts | 1 - packages/client/lib/commands/ZCARD.ts | 2 -- packages/client/lib/commands/ZCOUNT.ts | 2 -- packages/client/lib/commands/ZDIFF.ts | 1 - packages/client/lib/commands/ZDIFFSTORE.ts | 1 - .../client/lib/commands/ZDIFF_WITHSCORES.ts | 1 - packages/client/lib/commands/ZINTER.ts | 1 - packages/client/lib/commands/ZINTERCARD.ts | 1 - packages/client/lib/commands/ZINTERSTORE.ts | 1 - .../client/lib/commands/ZINTER_WITHSCORES.ts | 1 - packages/client/lib/commands/ZLEXCOUNT.ts | 2 -- packages/client/lib/commands/ZMPOP.ts | 1 - packages/client/lib/commands/ZMSCORE.ts | 2 -- packages/client/lib/commands/ZPOPMAX.ts | 1 - packages/client/lib/commands/ZPOPMAX_COUNT.ts | 1 - packages/client/lib/commands/ZPOPMIN.ts | 1 - packages/client/lib/commands/ZPOPMIN_COUNT.ts | 1 - packages/client/lib/commands/ZRANDMEMBER.ts | 1 - .../client/lib/commands/ZRANDMEMBER_COUNT.ts | 1 - .../commands/ZRANDMEMBER_COUNT_WITHSCORES.ts | 1 - packages/client/lib/commands/ZRANGE.ts | 2 -- packages/client/lib/commands/ZRANGEBYLEX.ts | 2 -- packages/client/lib/commands/ZRANGEBYSCORE.ts | 2 -- .../lib/commands/ZRANGEBYSCORE_WITHSCORES.ts | 2 -- packages/client/lib/commands/ZRANGESTORE.ts | 1 - .../client/lib/commands/ZRANGE_WITHSCORES.ts | 2 -- packages/client/lib/commands/ZRANK.ts | 2 -- .../client/lib/commands/ZRANK_WITHSCORE.ts | 2 -- packages/client/lib/commands/ZREM.ts | 1 - .../client/lib/commands/ZREMRANGEBYLEX.ts | 1 - .../client/lib/commands/ZREMRANGEBYRANK.ts | 1 - .../client/lib/commands/ZREMRANGEBYSCORE.ts | 1 - packages/client/lib/commands/ZREVRANK.ts | 2 -- .../client/lib/commands/ZREVRANK_WITHSCORE.ts | 2 -- packages/client/lib/commands/ZSCAN.ts | 1 - packages/client/lib/commands/ZSCORE.ts | 2 -- packages/client/lib/commands/ZUNION.ts | 1 - packages/client/lib/commands/ZUNIONSTORE.ts | 1 - .../client/lib/commands/ZUNION_WITHSCORES.ts | 1 - packages/client/lib/sentinel/utils.ts | 14 ++++++-- packages/json/lib/commands/ARRAPPEND.ts | 1 - packages/json/lib/commands/ARRINDEX.ts | 1 - packages/json/lib/commands/ARRINSERT.ts | 1 - packages/json/lib/commands/ARRLEN.ts | 1 - packages/json/lib/commands/ARRPOP.ts | 1 - packages/json/lib/commands/ARRTRIM.ts | 1 - packages/json/lib/commands/CLEAR.ts | 1 - packages/json/lib/commands/DEBUG_MEMORY.ts | 1 - packages/json/lib/commands/DEL.ts | 1 - packages/json/lib/commands/FORGET.ts | 1 - packages/json/lib/commands/GET.ts | 1 - packages/json/lib/commands/MERGE.ts | 1 - packages/json/lib/commands/MGET.ts | 1 - packages/json/lib/commands/MSET.ts | 1 - packages/json/lib/commands/NUMINCRBY.ts | 1 - packages/json/lib/commands/NUMMULTBY.ts | 1 - packages/json/lib/commands/OBJKEYS.ts | 1 - packages/json/lib/commands/OBJLEN.ts | 1 - packages/json/lib/commands/RESP.ts | 1 - packages/json/lib/commands/SET.ts | 1 - packages/json/lib/commands/STRAPPEND.ts | 1 - packages/json/lib/commands/STRLEN.ts | 1 - packages/json/lib/commands/TOGGLE.ts | 1 - packages/json/lib/commands/TYPE.ts | 1 - packages/search/lib/commands/AGGREGATE.ts | 2 -- .../lib/commands/AGGREGATE_WITHCURSOR.ts | 1 - packages/search/lib/commands/ALIASADD.ts | 2 -- packages/search/lib/commands/ALIASDEL.ts | 2 -- packages/search/lib/commands/ALIASUPDATE.ts | 2 -- packages/search/lib/commands/ALTER.ts | 2 -- packages/search/lib/commands/CREATE.ts | 2 -- packages/search/lib/commands/CURSOR_DEL.ts | 2 -- packages/search/lib/commands/CURSOR_READ.ts | 2 -- packages/search/lib/commands/DICTADD.ts | 2 -- packages/search/lib/commands/DICTDEL.ts | 2 -- packages/search/lib/commands/DICTDUMP.ts | 2 -- packages/search/lib/commands/DROPINDEX.ts | 2 -- packages/search/lib/commands/EXPLAIN.ts | 2 -- packages/search/lib/commands/EXPLAINCLI.ts | 2 -- packages/search/lib/commands/INFO.ts | 2 -- .../search/lib/commands/PROFILE_AGGREGATE.ts | 2 -- .../search/lib/commands/PROFILE_SEARCH.ts | 2 -- packages/search/lib/commands/SEARCH.ts | 2 -- .../search/lib/commands/SEARCH_NOCONTENT.ts | 2 -- packages/search/lib/commands/SPELLCHECK.ts | 2 -- packages/search/lib/commands/SUGADD.ts | 1 - packages/search/lib/commands/SUGDEL.ts | 1 - packages/search/lib/commands/SUGGET.ts | 1 - .../lib/commands/SUGGET_WITHPAYLOADS.ts | 1 - .../search/lib/commands/SUGGET_WITHSCORES.ts | 1 - .../SUGGET_WITHSCORES_WITHPAYLOADS.ts | 1 - packages/search/lib/commands/SUGLEN.ts | 1 - packages/search/lib/commands/SYNDUMP.ts | 2 -- packages/search/lib/commands/SYNUPDATE.ts | 2 -- packages/search/lib/commands/TAGVALS.ts | 2 -- packages/time-series/lib/commands/ADD.ts | 1 - packages/time-series/lib/commands/ALTER.ts | 1 - packages/time-series/lib/commands/CREATE.ts | 1 - .../time-series/lib/commands/CREATERULE.ts | 1 - packages/time-series/lib/commands/DECRBY.ts | 1 - packages/time-series/lib/commands/DEL.ts | 1 - .../time-series/lib/commands/DELETERULE.ts | 1 - packages/time-series/lib/commands/GET.ts | 1 - packages/time-series/lib/commands/INCRBY.ts | 1 - packages/time-series/lib/commands/INFO.ts | 1 - .../time-series/lib/commands/INFO_DEBUG.ts | 1 - packages/time-series/lib/commands/MADD.ts | 1 - packages/time-series/lib/commands/MGET.ts | 2 -- .../lib/commands/MGET_SELECTED_LABELS.ts | 1 - .../lib/commands/MGET_WITHLABELS.ts | 1 - packages/time-series/lib/commands/MRANGE.ts | 2 -- .../lib/commands/MRANGE_GROUPBY.ts | 1 - .../lib/commands/MRANGE_MULTIAGGR.ts | 2 -- .../lib/commands/MRANGE_SELECTED_LABELS.ts | 1 - .../MRANGE_SELECTED_LABELS_GROUPBY.ts | 1 - .../MRANGE_SELECTED_LABELS_MULTIAGGR.ts | 2 -- .../lib/commands/MRANGE_WITHLABELS.ts | 2 -- .../lib/commands/MRANGE_WITHLABELS_GROUPBY.ts | 1 - .../commands/MRANGE_WITHLABELS_MULTIAGGR.ts | 2 -- .../time-series/lib/commands/MREVRANGE.ts | 2 -- .../lib/commands/MREVRANGE_GROUPBY.ts | 1 - .../lib/commands/MREVRANGE_MULTIAGGR.ts | 2 -- .../lib/commands/MREVRANGE_SELECTED_LABELS.ts | 1 - .../MREVRANGE_SELECTED_LABELS_GROUPBY.ts | 1 - .../MREVRANGE_SELECTED_LABELS_MULTIAGGR.ts | 2 -- .../lib/commands/MREVRANGE_WITHLABELS.ts | 2 -- .../commands/MREVRANGE_WITHLABELS_GROUPBY.ts | 1 - .../MREVRANGE_WITHLABELS_MULTIAGGR.ts | 2 -- .../time-series/lib/commands/QUERYINDEX.ts | 2 -- packages/time-series/lib/commands/RANGE.ts | 1 - .../lib/commands/RANGE_MULTIAGGR.ts | 1 - packages/time-series/lib/commands/READ.ts | 1 - packages/time-series/lib/commands/REVRANGE.ts | 1 - .../lib/commands/REVRANGE_MULTIAGGR.ts | 1 - 488 files changed, 51 insertions(+), 666 deletions(-) diff --git a/packages/bloom/lib/commands/bloom/ADD.ts b/packages/bloom/lib/commands/bloom/ADD.ts index e12d9cfa1d2..0ab480d9ba4 100644 --- a/packages/bloom/lib/commands/bloom/ADD.ts +++ b/packages/bloom/lib/commands/bloom/ADD.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('BF.ADD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/CARD.ts b/packages/bloom/lib/commands/bloom/CARD.ts index c2f9aeb00fc..ac8a7ccf497 100644 --- a/packages/bloom/lib/commands/bloom/CARD.ts +++ b/packages/bloom/lib/commands/bloom/CARD.ts @@ -2,7 +2,6 @@ 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: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('BF.CARD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/EXISTS.ts b/packages/bloom/lib/commands/bloom/EXISTS.ts index b3f19af9516..492718ccfc8 100644 --- a/packages/bloom/lib/commands/bloom/EXISTS.ts +++ b/packages/bloom/lib/commands/bloom/EXISTS.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('BF.EXISTS'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/INFO.ts b/packages/bloom/lib/commands/bloom/INFO.ts index 7074885a41e..e7135411a26 100644 --- a/packages/bloom/lib/commands/bloom/INFO.ts +++ b/packages/bloom/lib/commands/bloom/INFO.ts @@ -11,7 +11,6 @@ export type BfInfoReplyMap = TuplesToMapReply<[ ]>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('BF.INFO'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/INSERT.ts b/packages/bloom/lib/commands/bloom/INSERT.ts index b8dcef325f8..cdd525ed8ec 100644 --- a/packages/bloom/lib/commands/bloom/INSERT.ts +++ b/packages/bloom/lib/commands/bloom/INSERT.ts @@ -12,7 +12,6 @@ export interface BfInsertOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts index ef3cc4a3e12..6ece1af3562 100644 --- a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, iterator: number, chunk: RedisArgument) { parser.push('BF.LOADCHUNK'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/MADD.ts b/packages/bloom/lib/commands/bloom/MADD.ts index efbd932b403..c80585ac482 100644 --- a/packages/bloom/lib/commands/bloom/MADD.ts +++ b/packages/bloom/lib/commands/bloom/MADD.ts @@ -4,7 +4,6 @@ import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-t import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('BF.MADD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/MEXISTS.ts b/packages/bloom/lib/commands/bloom/MEXISTS.ts index a5a311a8e4c..e2683932069 100644 --- a/packages/bloom/lib/commands/bloom/MEXISTS.ts +++ b/packages/bloom/lib/commands/bloom/MEXISTS.ts @@ -4,7 +4,6 @@ import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-t import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('BF.MEXISTS'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/bloom/RESERVE.ts b/packages/bloom/lib/commands/bloom/RESERVE.ts index 00f17c1889f..05469d6fa6f 100644 --- a/packages/bloom/lib/commands/bloom/RESERVE.ts +++ b/packages/bloom/lib/commands/bloom/RESERVE.ts @@ -7,7 +7,6 @@ export interface BfReserveOptions { } export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/bloom/SCANDUMP.ts b/packages/bloom/lib/commands/bloom/SCANDUMP.ts index d0472b649c5..242a0f04236 100644 --- a/packages/bloom/lib/commands/bloom/SCANDUMP.ts +++ b/packages/bloom/lib/commands/bloom/SCANDUMP.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, iterator: number) { parser.push('BF.SCANDUMP'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts index a011957ece6..a1c60deb77b 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts @@ -7,7 +7,6 @@ export interface BfIncrByItem { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/count-min-sketch/INFO.ts b/packages/bloom/lib/commands/count-min-sketch/INFO.ts index fef1cac97e7..cd02d108722 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INFO.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INFO.ts @@ -15,7 +15,6 @@ export interface CmsInfoReply { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('CMS.INFO'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts index 44e6a75952f..34295b181fe 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, width: number, depth: number) { parser.push('CMS.INITBYDIM'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts index 3b96120bd04..b17a63b5aec 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, error: number, probability: number) { parser.push('CMS.INITBYPROB'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts index 4d959bd619d..15461038333 100644 --- a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts +++ b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts @@ -9,7 +9,6 @@ interface BfMergeSketch { export type BfMergeSketches = Array | Array; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts index b55b51d1bbd..07a2512aebb 100644 --- a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts @@ -3,7 +3,6 @@ import { ArrayReply, NumberReply, Command, RedisArgument } from '@redis/client/d import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('CMS.QUERY'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/ADD.ts b/packages/bloom/lib/commands/cuckoo/ADD.ts index 37a5d1b5b86..00bbbf37f5e 100644 --- a/packages/bloom/lib/commands/cuckoo/ADD.ts +++ b/packages/bloom/lib/commands/cuckoo/ADD.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('CF.ADD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/ADDNX.ts b/packages/bloom/lib/commands/cuckoo/ADDNX.ts index ceaf62be21c..5206e85f9e7 100644 --- a/packages/bloom/lib/commands/cuckoo/ADDNX.ts +++ b/packages/bloom/lib/commands/cuckoo/ADDNX.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('CF.ADDNX'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/COUNT.ts b/packages/bloom/lib/commands/cuckoo/COUNT.ts index f0cd5a72105..6a87ed063ce 100644 --- a/packages/bloom/lib/commands/cuckoo/COUNT.ts +++ b/packages/bloom/lib/commands/cuckoo/COUNT.ts @@ -2,7 +2,6 @@ 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: true, parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('CF.COUNT'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/DEL.ts b/packages/bloom/lib/commands/cuckoo/DEL.ts index c97b7c2d9fc..2865764bf3c 100644 --- a/packages/bloom/lib/commands/cuckoo/DEL.ts +++ b/packages/bloom/lib/commands/cuckoo/DEL.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('CF.DEL'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/EXISTS.ts b/packages/bloom/lib/commands/cuckoo/EXISTS.ts index 2299cb3de99..54569de79de 100644 --- a/packages/bloom/lib/commands/cuckoo/EXISTS.ts +++ b/packages/bloom/lib/commands/cuckoo/EXISTS.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { parser.push('CF.EXISTS'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/INFO.ts b/packages/bloom/lib/commands/cuckoo/INFO.ts index 6a8f06f1e77..05c60f78184 100644 --- a/packages/bloom/lib/commands/cuckoo/INFO.ts +++ b/packages/bloom/lib/commands/cuckoo/INFO.ts @@ -14,7 +14,6 @@ export type CfInfoReplyMap = TuplesToMapReply<[ ]>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('CF.INFO'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/INSERT.ts b/packages/bloom/lib/commands/cuckoo/INSERT.ts index 3ad3feee16d..ba848ed170f 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERT.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERT.ts @@ -28,7 +28,6 @@ export function parseCfInsertArguments( } export default { - IS_READ_ONLY: false, parseCommand(...args: Parameters) { args[0].push('CF.INSERT'); parseCfInsertArguments(...args); diff --git a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts index 81b1f88a422..af749a2599b 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts @@ -1,5 +1,5 @@ import { ArrayReply, Command, NumberReply } from '@redis/client/dist/lib/RESP/types'; -import INSERT, { parseCfInsertArguments } from './INSERT'; +import { parseCfInsertArguments } from './INSERT'; /** * Adds one or more items to a Cuckoo Filter only if they do not exist yet, creating the filter if needed @@ -11,7 +11,6 @@ import INSERT, { parseCfInsertArguments } from './INSERT'; * @param options.NOCREATE - If true, prevents automatic filter creation */ export default { - IS_READ_ONLY: INSERT.IS_READ_ONLY, parseCommand(...args: Parameters) { args[0].push('CF.INSERTNX'); parseCfInsertArguments(...args); diff --git a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts index 8fb21be8e0d..feb4228efb1 100644 --- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, iterator: number, chunk: RedisArgument) { parser.push('CF.LOADCHUNK'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/cuckoo/RESERVE.ts b/packages/bloom/lib/commands/cuckoo/RESERVE.ts index 2685b0db06d..96dc797c536 100644 --- a/packages/bloom/lib/commands/cuckoo/RESERVE.ts +++ b/packages/bloom/lib/commands/cuckoo/RESERVE.ts @@ -8,7 +8,6 @@ export interface CfReserveOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts index 25ef2c3f6da..520971e207b 100644 --- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts +++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, iterator: number) { parser.push('CF.SCANDUMP'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/ADD.ts b/packages/bloom/lib/commands/t-digest/ADD.ts index 5534d58065b..01ed86c82a4 100644 --- a/packages/bloom/lib/commands/t-digest/ADD.ts +++ b/packages/bloom/lib/commands/t-digest/ADD.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, values: Array) { parser.push('TDIGEST.ADD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/BYRANK.ts b/packages/bloom/lib/commands/t-digest/BYRANK.ts index 9c1ab0059f3..b272f9b029d 100644 --- a/packages/bloom/lib/commands/t-digest/BYRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYRANK.ts @@ -15,7 +15,6 @@ export function transformByRankArguments( } export default { - IS_READ_ONLY: true, parseCommand(...args: Parameters) { args[0].push('TDIGEST.BYRANK'); transformByRankArguments(...args); diff --git a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts index a94e5566bb1..9a3f33be00f 100644 --- a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts @@ -8,7 +8,6 @@ import BYRANK, { transformByRankArguments } from './BYRANK'; * @param ranks - Array of ranks to get value estimates for (descending order) */ export default { - IS_READ_ONLY: BYRANK.IS_READ_ONLY, parseCommand(...args: Parameters) { args[0].push('TDIGEST.BYREVRANK'); transformByRankArguments(...args); diff --git a/packages/bloom/lib/commands/t-digest/CDF.ts b/packages/bloom/lib/commands/t-digest/CDF.ts index 4d1d8ea2786..adb36617eda 100644 --- a/packages/bloom/lib/commands/t-digest/CDF.ts +++ b/packages/bloom/lib/commands/t-digest/CDF.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, values: Array) { parser.push('TDIGEST.CDF'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/CREATE.ts b/packages/bloom/lib/commands/t-digest/CREATE.ts index 58b1e008284..4782b0adfa7 100644 --- a/packages/bloom/lib/commands/t-digest/CREATE.ts +++ b/packages/bloom/lib/commands/t-digest/CREATE.ts @@ -6,7 +6,6 @@ export interface TDigestCreateOptions { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, options?: TDigestCreateOptions) { parser.push('TDIGEST.CREATE'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/INFO.ts b/packages/bloom/lib/commands/t-digest/INFO.ts index 2cb9e93443c..087cda22bca 100644 --- a/packages/bloom/lib/commands/t-digest/INFO.ts +++ b/packages/bloom/lib/commands/t-digest/INFO.ts @@ -15,7 +15,6 @@ export type TdInfoReplyMap = TuplesToMapReply<[ ]>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TDIGEST.INFO'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/MAX.ts b/packages/bloom/lib/commands/t-digest/MAX.ts index 140db6a3e48..0425b8c81f5 100644 --- a/packages/bloom/lib/commands/t-digest/MAX.ts +++ b/packages/bloom/lib/commands/t-digest/MAX.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TDIGEST.MAX'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/MERGE.ts b/packages/bloom/lib/commands/t-digest/MERGE.ts index 80049d1e540..aa232d63e12 100644 --- a/packages/bloom/lib/commands/t-digest/MERGE.ts +++ b/packages/bloom/lib/commands/t-digest/MERGE.ts @@ -8,7 +8,6 @@ export interface TDigestMergeOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/bloom/lib/commands/t-digest/MIN.ts b/packages/bloom/lib/commands/t-digest/MIN.ts index d6e56fb672e..8ba54c8ee36 100644 --- a/packages/bloom/lib/commands/t-digest/MIN.ts +++ b/packages/bloom/lib/commands/t-digest/MIN.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TDIGEST.MIN'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/QUANTILE.ts b/packages/bloom/lib/commands/t-digest/QUANTILE.ts index 1c27b5f6ec6..367a831c246 100644 --- a/packages/bloom/lib/commands/t-digest/QUANTILE.ts +++ b/packages/bloom/lib/commands/t-digest/QUANTILE.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, quantiles: Array) { parser.push('TDIGEST.QUANTILE'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/RANK.ts b/packages/bloom/lib/commands/t-digest/RANK.ts index 053c0c544e9..8eca3eb9ff2 100644 --- a/packages/bloom/lib/commands/t-digest/RANK.ts +++ b/packages/bloom/lib/commands/t-digest/RANK.ts @@ -14,7 +14,6 @@ export function transformRankArguments( } export default { - IS_READ_ONLY: true, parseCommand(...args: Parameters) { args[0].push('TDIGEST.RANK'); transformRankArguments(...args); diff --git a/packages/bloom/lib/commands/t-digest/RESET.ts b/packages/bloom/lib/commands/t-digest/RESET.ts index c2bda72d6d4..adeead207a4 100644 --- a/packages/bloom/lib/commands/t-digest/RESET.ts +++ b/packages/bloom/lib/commands/t-digest/RESET.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TDIGEST.RESET'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/t-digest/REVRANK.ts b/packages/bloom/lib/commands/t-digest/REVRANK.ts index e323e10190a..756188fe9e5 100644 --- a/packages/bloom/lib/commands/t-digest/REVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/REVRANK.ts @@ -8,7 +8,6 @@ import RANK, { transformRankArguments } from './RANK'; * @param values - Array of values to get reverse ranks for */ export default { - IS_READ_ONLY: RANK.IS_READ_ONLY, parseCommand(...args: Parameters) { args[0].push('TDIGEST.REVRANK'); transformRankArguments(...args); diff --git a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts index 1fd6360ab65..218a51722d4 100644 --- a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts +++ b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/top-k/ADD.ts b/packages/bloom/lib/commands/top-k/ADD.ts index 244e6209c91..0bd63d28893 100644 --- a/packages/bloom/lib/commands/top-k/ADD.ts +++ b/packages/bloom/lib/commands/top-k/ADD.ts @@ -3,7 +3,6 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/clie import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('TOPK.ADD'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/COUNT.ts b/packages/bloom/lib/commands/top-k/COUNT.ts index 7e75a3b68aa..c70666c5b64 100644 --- a/packages/bloom/lib/commands/top-k/COUNT.ts +++ b/packages/bloom/lib/commands/top-k/COUNT.ts @@ -3,7 +3,6 @@ import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/d import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('TOPK.COUNT'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/INCRBY.ts b/packages/bloom/lib/commands/top-k/INCRBY.ts index 9e9a49e18f9..0dcffc83ffb 100644 --- a/packages/bloom/lib/commands/top-k/INCRBY.ts +++ b/packages/bloom/lib/commands/top-k/INCRBY.ts @@ -11,7 +11,6 @@ function pushIncrByItem(parser: CommandParser, { item, incrementBy }: TopKIncrBy } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/bloom/lib/commands/top-k/INFO.ts b/packages/bloom/lib/commands/top-k/INFO.ts index 52ceb2a552a..5e587a381ba 100644 --- a/packages/bloom/lib/commands/top-k/INFO.ts +++ b/packages/bloom/lib/commands/top-k/INFO.ts @@ -11,7 +11,6 @@ export type TopKInfoReplyMap = TuplesToMapReply<[ ]>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TOPK.INFO'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/LIST.ts b/packages/bloom/lib/commands/top-k/LIST.ts index d7adeaa193c..a04b99cae36 100644 --- a/packages/bloom/lib/commands/top-k/LIST.ts +++ b/packages/bloom/lib/commands/top-k/LIST.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TOPK.LIST'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts index 2c0f10e785b..804ccc5c543 100644 --- a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts +++ b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TOPK.LIST'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/QUERY.ts b/packages/bloom/lib/commands/top-k/QUERY.ts index a6fb4bae69e..32d1971becb 100644 --- a/packages/bloom/lib/commands/top-k/QUERY.ts +++ b/packages/bloom/lib/commands/top-k/QUERY.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { RedisVariadicArgument, transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { parser.push('TOPK.QUERY'); parser.pushKey(key); diff --git a/packages/bloom/lib/commands/top-k/RESERVE.ts b/packages/bloom/lib/commands/top-k/RESERVE.ts index ee3ee9a8cf4..ee44da0147b 100644 --- a/packages/bloom/lib/commands/top-k/RESERVE.ts +++ b/packages/bloom/lib/commands/top-k/RESERVE.ts @@ -8,7 +8,6 @@ export interface TopKReserveOptions { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, topK: number, options?: TopKReserveOptions) { parser.push('TOPK.RESERVE'); parser.pushKey(key); diff --git a/packages/client/lib/RESP/types.ts b/packages/client/lib/RESP/types.ts index e50dc382309..c4180e1185c 100644 --- a/packages/client/lib/RESP/types.ts +++ b/packages/client/lib/RESP/types.ts @@ -283,21 +283,13 @@ export type CommandArguments = Array & { preserve?: unknown }; // export type ResponsePolicies = RESPONSE_POLICIES[keyof RESPONSE_POLICIES]; -// export type CommandPolicies = { -// request?: RequestPolicies | null; -// response?: ResponsePolicies | null; -// }; - export type Command = { CACHEABLE?: boolean; IS_READ_ONLY?: boolean; - /** - * @internal - * TODO: remove once `POLICIES` is implemented - */ + /** @deprecated Unused; superseded by request/response policies. */ IS_FORWARD_COMMAND?: boolean; + /** @deprecated Unused; keyless-ness is derived from server metadata (`isKeyless`). */ NOT_KEYED_COMMAND?: true; - // POLICIES?: CommandPolicies; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- arbitrary arg list per command parseCommand(this: void, parser: CommandParser, ...args: Array): void; TRANSFORM_LEGACY_REPLY?: boolean; diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 84f124178d5..ff168a88b4b 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -4,6 +4,7 @@ import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsP import RedisCommandsQueue, { CommandOptions } from './commands-queue'; import { EventEmitter } from 'node:events'; import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; +import { defaultCommandMetadata, isCacheable } from '../command-metadata'; import { AbortError, ClientClosedError, ClientOfflineError, DisconnectsClientError, WatchError } from '../errors'; import { URL } from 'node:url'; import { TcpSocketConnectOpts } from 'node:net'; @@ -1297,22 +1298,31 @@ export default class RedisClient< } const csc = this._self.#clientSideCache; - const defaultTypeMapping = this._self.#options.commandOptions === commandOptions || - (this._self.#options.commandOptions?.typeMapping === commandOptions?.typeMapping); - const fn = () => { return this.sendCommand(parser.redisArgs, commandOptions) }; - if (csc && command.CACHEABLE && defaultTypeMapping) { - return await csc.handleCache(this._self, parser as BasicCommandParser, fn, transformReply, commandOptions?.typeMapping); - } else { - const reply = await fn(); - - const finalReply = transformReply ? transformReply(reply, parser.preserve, commandOptions?.typeMapping) : reply; + // Eligibility is only worth computing when caching is enabled — the + // metadata lookup decodes the command identifier (Buffer args included) + // and must not tax the common no-CSC path. + if (csc) { + const defaultTypeMapping = this._self.#options.commandOptions === commandOptions || + (this._self.#options.commandOptions?.typeMapping === commandOptions?.typeMapping); - publish(CHANNELS.COMMAND_REPLY, () => ({ args: sanitizeArgs(parser.redisArgs), reply: finalReply, clientId: this._self._clientId })); + // Override-first: a defined `Command.CACHEABLE` wins; otherwise CSC + // eligibility derives from the server flags/tips (see `isCacheable`). + const cacheable = isCacheable(defaultCommandMetadata.lookup(parser.commandIdentifier), command.CACHEABLE); - return finalReply; + if (cacheable && defaultTypeMapping) { + return await csc.handleCache(this._self, parser as BasicCommandParser, fn, transformReply, commandOptions?.typeMapping); + } } + + const reply = await fn(); + + const finalReply = transformReply ? transformReply(reply, parser.preserve, commandOptions?.typeMapping) : reply; + + publish(CHANNELS.COMMAND_REPLY, () => ({ args: sanitizeArgs(parser.redisArgs), reply: finalReply, clientId: this._self._clientId })); + + return finalReply; } /** diff --git a/packages/client/lib/commands/ACL_CAT.ts b/packages/client/lib/commands/ACL_CAT.ts index ae094b732b8..50cbbb37995 100644 --- a/packages/client/lib/commands/ACL_CAT.ts +++ b/packages/client/lib/commands/ACL_CAT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, categoryName?: RedisArgument) { parser.push('ACL', 'CAT'); diff --git a/packages/client/lib/commands/ACL_DELUSER.ts b/packages/client/lib/commands/ACL_DELUSER.ts index 5aa66becf75..4214b983427 100644 --- a/packages/client/lib/commands/ACL_DELUSER.ts +++ b/packages/client/lib/commands/ACL_DELUSER.ts @@ -3,8 +3,6 @@ import { NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, username: RedisVariadicArgument) { parser.push('ACL', 'DELUSER'); parser.pushVariadic(username); diff --git a/packages/client/lib/commands/ACL_DRYRUN.ts b/packages/client/lib/commands/ACL_DRYRUN.ts index 09a51bc36f8..542aa9e196b 100644 --- a/packages/client/lib/commands/ACL_DRYRUN.ts +++ b/packages/client/lib/commands/ACL_DRYRUN.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, username: RedisArgument, command: Array) { parser.push('ACL', 'DRYRUN', username, ...command); diff --git a/packages/client/lib/commands/ACL_GENPASS.ts b/packages/client/lib/commands/ACL_GENPASS.ts index b5caa29b9b2..77aa54a7abf 100644 --- a/packages/client/lib/commands/ACL_GENPASS.ts +++ b/packages/client/lib/commands/ACL_GENPASS.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, bits?: number) { parser.push('ACL', 'GENPASS'); diff --git a/packages/client/lib/commands/ACL_GETUSER.ts b/packages/client/lib/commands/ACL_GETUSER.ts index b4764ad744e..d0c2572ee98 100644 --- a/packages/client/lib/commands/ACL_GETUSER.ts +++ b/packages/client/lib/commands/ACL_GETUSER.ts @@ -18,7 +18,6 @@ type AclUser = TuplesToMapReply<[ ]>; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, username: RedisArgument) { parser.push('ACL', 'GETUSER', username); diff --git a/packages/client/lib/commands/ACL_LIST.ts b/packages/client/lib/commands/ACL_LIST.ts index b5f82cf272c..da9c76c166f 100644 --- a/packages/client/lib/commands/ACL_LIST.ts +++ b/packages/client/lib/commands/ACL_LIST.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('ACL', 'LIST'); diff --git a/packages/client/lib/commands/ACL_LOAD.ts b/packages/client/lib/commands/ACL_LOAD.ts index dc4320b99fc..e9e961a8b66 100644 --- a/packages/client/lib/commands/ACL_LOAD.ts +++ b/packages/client/lib/commands/ACL_LOAD.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('ACL', 'LOAD'); }, diff --git a/packages/client/lib/commands/ACL_LOG.ts b/packages/client/lib/commands/ACL_LOG.ts index d6f95cec834..bb60f712640 100644 --- a/packages/client/lib/commands/ACL_LOG.ts +++ b/packages/client/lib/commands/ACL_LOG.ts @@ -19,7 +19,6 @@ export type AclLogReply = ArrayReply>; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, count?: number) { parser.push('ACL', 'LOG'); diff --git a/packages/client/lib/commands/ACL_LOG_RESET.ts b/packages/client/lib/commands/ACL_LOG_RESET.ts index 9a692129bd2..391fbe85f08 100644 --- a/packages/client/lib/commands/ACL_LOG_RESET.ts +++ b/packages/client/lib/commands/ACL_LOG_RESET.ts @@ -1,10 +1,7 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; -import ACL_LOG from './ACL_LOG'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: ACL_LOG.IS_READ_ONLY, parseCommand(parser: CommandParser) { parser.push('ACL', 'LOG', 'RESET'); }, diff --git a/packages/client/lib/commands/ACL_SAVE.ts b/packages/client/lib/commands/ACL_SAVE.ts index ec24522724a..8c30bb61787 100644 --- a/packages/client/lib/commands/ACL_SAVE.ts +++ b/packages/client/lib/commands/ACL_SAVE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('ACL', 'SAVE'); }, diff --git a/packages/client/lib/commands/ACL_SETUSER.ts b/packages/client/lib/commands/ACL_SETUSER.ts index cad013f4d15..db4e615b566 100644 --- a/packages/client/lib/commands/ACL_SETUSER.ts +++ b/packages/client/lib/commands/ACL_SETUSER.ts @@ -3,8 +3,6 @@ import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, username: RedisArgument, rule: RedisVariadicArgument) { parser.push('ACL', 'SETUSER', username); parser.pushVariadic(rule); diff --git a/packages/client/lib/commands/ACL_USERS.ts b/packages/client/lib/commands/ACL_USERS.ts index 6ce4c6d84ef..86ece6e9eaa 100644 --- a/packages/client/lib/commands/ACL_USERS.ts +++ b/packages/client/lib/commands/ACL_USERS.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('ACL', 'USERS'); diff --git a/packages/client/lib/commands/ACL_WHOAMI.ts b/packages/client/lib/commands/ACL_WHOAMI.ts index eb21a75af5f..057cabbbc1f 100644 --- a/packages/client/lib/commands/ACL_WHOAMI.ts +++ b/packages/client/lib/commands/ACL_WHOAMI.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('ACL', 'WHOAMI'); diff --git a/packages/client/lib/commands/APPEND.ts b/packages/client/lib/commands/APPEND.ts index d4b6eda83f4..05b83db26d7 100644 --- a/packages/client/lib/commands/APPEND.ts +++ b/packages/client/lib/commands/APPEND.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) { parser.push('APPEND'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ARCOUNT.ts b/packages/client/lib/commands/ARCOUNT.ts index 301b675a3c7..4182def44af 100644 --- a/packages/client/lib/commands/ARCOUNT.ts +++ b/packages/client/lib/commands/ARCOUNT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ARCOUNT'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ARGET.ts b/packages/client/lib/commands/ARGET.ts index 6a563ce6754..290f7b906c0 100644 --- a/packages/client/lib/commands/ARGET.ts +++ b/packages/client/lib/commands/ARGET.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, index: number | string) { parser.push('ARGET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ARGETRANGE.ts b/packages/client/lib/commands/ARGETRANGE.ts index 800bbd6e210..a55b01997a3 100644 --- a/packages/client/lib/commands/ARGETRANGE.ts +++ b/packages/client/lib/commands/ARGETRANGE.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, start: number | string, end: number | string) { parser.push('ARGETRANGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ARGREP.ts b/packages/client/lib/commands/ARGREP.ts index decbb5fb397..3e5f63bf344 100644 --- a/packages/client/lib/commands/ARGREP.ts +++ b/packages/client/lib/commands/ARGREP.ts @@ -67,7 +67,6 @@ export function parseArGrepArguments( export type ArGrepArguments = Tail>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, ...args: ArGrepArguments) { parser.push('ARGREP'); parseArGrepArguments(parser, ...args); diff --git a/packages/client/lib/commands/ARGREP_WITHVALUES.ts b/packages/client/lib/commands/ARGREP_WITHVALUES.ts index e27aa46ff89..2d13a5f39b6 100644 --- a/packages/client/lib/commands/ARGREP_WITHVALUES.ts +++ b/packages/client/lib/commands/ARGREP_WITHVALUES.ts @@ -8,7 +8,6 @@ export type ArGrepWithValuesReply = Array<{ }>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, ...args: ArGrepArguments) { parser.push('ARGREP'); parseArGrepArguments(parser, ...args); diff --git a/packages/client/lib/commands/ARINFO.ts b/packages/client/lib/commands/ARINFO.ts index 1c3e78bf00f..b917dd1a11a 100644 --- a/packages/client/lib/commands/ARINFO.ts +++ b/packages/client/lib/commands/ARINFO.ts @@ -9,7 +9,6 @@ export interface ArInfoOptions { export type ArInfoReply = MapReply; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, options?: ArInfoOptions) { parser.push('ARINFO'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ARLASTITEMS.ts b/packages/client/lib/commands/ARLASTITEMS.ts index c617a723cf9..d26ba3a6a17 100644 --- a/packages/client/lib/commands/ARLASTITEMS.ts +++ b/packages/client/lib/commands/ARLASTITEMS.ts @@ -6,7 +6,6 @@ export interface ArLastItemsOptions { } export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ARLEN.ts b/packages/client/lib/commands/ARLEN.ts index eaa8a7b5bea..765fd4a4db3 100644 --- a/packages/client/lib/commands/ARLEN.ts +++ b/packages/client/lib/commands/ARLEN.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ARLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ARMGET.ts b/packages/client/lib/commands/ARMGET.ts index 900d58861cc..f27a72614f0 100644 --- a/packages/client/lib/commands/ARMGET.ts +++ b/packages/client/lib/commands/ARMGET.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, indices: number | string | Array) { parser.push('ARMGET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ARNEXT.ts b/packages/client/lib/commands/ARNEXT.ts index 92e74f7b9be..07920f598d4 100644 --- a/packages/client/lib/commands/ARNEXT.ts +++ b/packages/client/lib/commands/ARNEXT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ARNEXT'); parser.pushKey(key); diff --git a/packages/client/lib/commands/AROP.ts b/packages/client/lib/commands/AROP.ts index 3716f75b9a5..e2576a2b5fc 100644 --- a/packages/client/lib/commands/AROP.ts +++ b/packages/client/lib/commands/AROP.ts @@ -15,7 +15,6 @@ export const AR_OPERATIONS = { export type ArOperation = typeof AR_OPERATIONS[keyof typeof AR_OPERATIONS]; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ARSCAN.ts b/packages/client/lib/commands/ARSCAN.ts index 765e86eebd8..45c5d9cd89c 100644 --- a/packages/client/lib/commands/ARSCAN.ts +++ b/packages/client/lib/commands/ARSCAN.ts @@ -11,7 +11,6 @@ export type ArScanReply = Array<{ }>; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ASKING.ts b/packages/client/lib/commands/ASKING.ts index 92ce8f72390..13efce9dd8f 100644 --- a/packages/client/lib/commands/ASKING.ts +++ b/packages/client/lib/commands/ASKING.ts @@ -4,7 +4,6 @@ import { SimpleStringReply, Command } from '../RESP/types'; export const ASKING_CMD = 'ASKING'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push(ASKING_CMD); diff --git a/packages/client/lib/commands/AUTH.ts b/packages/client/lib/commands/AUTH.ts index 85b48b0026e..3f2ee99af08 100644 --- a/packages/client/lib/commands/AUTH.ts +++ b/packages/client/lib/commands/AUTH.ts @@ -7,7 +7,6 @@ export interface AuthOptions { } export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, { username, password }: AuthOptions) { parser.push('AUTH'); diff --git a/packages/client/lib/commands/BGREWRITEAOF.ts b/packages/client/lib/commands/BGREWRITEAOF.ts index c658f3e8529..b587bad1fcc 100644 --- a/packages/client/lib/commands/BGREWRITEAOF.ts +++ b/packages/client/lib/commands/BGREWRITEAOF.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('BGREWRITEAOF'); }, diff --git a/packages/client/lib/commands/BGSAVE.ts b/packages/client/lib/commands/BGSAVE.ts index 1fd6c6b5bdb..445a591e5cb 100644 --- a/packages/client/lib/commands/BGSAVE.ts +++ b/packages/client/lib/commands/BGSAVE.ts @@ -6,8 +6,6 @@ export interface BgSaveOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, options?: BgSaveOptions) { parser.push('BGSAVE'); if (options?.SCHEDULE) { diff --git a/packages/client/lib/commands/BITCOUNT.ts b/packages/client/lib/commands/BITCOUNT.ts index decfb754db5..e139564098e 100644 --- a/packages/client/lib/commands/BITCOUNT.ts +++ b/packages/client/lib/commands/BITCOUNT.ts @@ -8,8 +8,6 @@ export interface BitCountRange { } export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, range?: BitCountRange) { parser.push('BITCOUNT'); parser.pushKey(key); diff --git a/packages/client/lib/commands/BITFIELD.ts b/packages/client/lib/commands/BITFIELD.ts index f095b4cf7a6..c96508e8c1b 100644 --- a/packages/client/lib/commands/BITFIELD.ts +++ b/packages/client/lib/commands/BITFIELD.ts @@ -40,7 +40,6 @@ export type BitFieldRoOperations = Array< >; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, operations: BitFieldOperations) { parser.push('BITFIELD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/BITFIELD_RO.ts b/packages/client/lib/commands/BITFIELD_RO.ts index 66001718b80..7b737aa4247 100644 --- a/packages/client/lib/commands/BITFIELD_RO.ts +++ b/packages/client/lib/commands/BITFIELD_RO.ts @@ -7,8 +7,6 @@ export type BitFieldRoOperations = Array< >; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, operations: BitFieldRoOperations) { parser.push('BITFIELD_RO'); parser.pushKey(key); diff --git a/packages/client/lib/commands/BITOP.ts b/packages/client/lib/commands/BITOP.ts index a766ef963df..7168af5822e 100644 --- a/packages/client/lib/commands/BITOP.ts +++ b/packages/client/lib/commands/BITOP.ts @@ -5,7 +5,6 @@ import { RedisVariadicArgument } from './generic-transformers'; export type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT' | 'DIFF' | 'DIFF1' | 'ANDOR' | 'ONE'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, operation: BitOperations, diff --git a/packages/client/lib/commands/BITPOS.ts b/packages/client/lib/commands/BITPOS.ts index 57e3a63b681..9d28474b28a 100644 --- a/packages/client/lib/commands/BITPOS.ts +++ b/packages/client/lib/commands/BITPOS.ts @@ -3,8 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { BitValue } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, bit: BitValue, diff --git a/packages/client/lib/commands/BLMOVE.ts b/packages/client/lib/commands/BLMOVE.ts index b0ada7cdb20..f7ee3821f09 100644 --- a/packages/client/lib/commands/BLMOVE.ts +++ b/packages/client/lib/commands/BLMOVE.ts @@ -3,7 +3,6 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type import { ListSide } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, source: RedisArgument, diff --git a/packages/client/lib/commands/BLMPOP.ts b/packages/client/lib/commands/BLMPOP.ts index 15d03f8d822..30a01499af6 100644 --- a/packages/client/lib/commands/BLMPOP.ts +++ b/packages/client/lib/commands/BLMPOP.ts @@ -3,7 +3,6 @@ import { Command } from '../RESP/types'; import LMPOP, { LMPopArguments, parseLMPopArguments } from './LMPOP'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, timeout: number, ...args: LMPopArguments) { parser.push('BLMPOP', timeout.toString()); parseLMPopArguments(parser, ...args); diff --git a/packages/client/lib/commands/BLPOP.ts b/packages/client/lib/commands/BLPOP.ts index aa0b30e768e..a01be83fc18 100644 --- a/packages/client/lib/commands/BLPOP.ts +++ b/packages/client/lib/commands/BLPOP.ts @@ -3,7 +3,6 @@ import { UnwrapReply, NullReply, TuplesReply, BlobStringReply, Command } from '. import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisVariadicArgument, timeout: number) { parser.push('BLPOP'); parser.pushKeys(key); diff --git a/packages/client/lib/commands/BRPOP.ts b/packages/client/lib/commands/BRPOP.ts index 401a951556d..b4aae1d67a7 100644 --- a/packages/client/lib/commands/BRPOP.ts +++ b/packages/client/lib/commands/BRPOP.ts @@ -4,7 +4,6 @@ import { RedisVariadicArgument } from './generic-transformers'; import BLPOP from './BLPOP'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisVariadicArgument, timeout: number) { parser.push('BRPOP'); parser.pushKeys(key); diff --git a/packages/client/lib/commands/BRPOPLPUSH.ts b/packages/client/lib/commands/BRPOPLPUSH.ts index 72f63a1c1e5..9a799db9af9 100644 --- a/packages/client/lib/commands/BRPOPLPUSH.ts +++ b/packages/client/lib/commands/BRPOPLPUSH.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, timeout: number) { parser.push('BRPOPLPUSH'); parser.pushKeys([source, destination]); diff --git a/packages/client/lib/commands/BZMPOP.ts b/packages/client/lib/commands/BZMPOP.ts index 98079b7a20d..523db06280b 100644 --- a/packages/client/lib/commands/BZMPOP.ts +++ b/packages/client/lib/commands/BZMPOP.ts @@ -3,7 +3,6 @@ import { Command } from '../RESP/types'; import ZMPOP, { parseZMPopArguments, ZMPopArguments } from './ZMPOP'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, timeout: number, ...args: ZMPopArguments) { parser.push('BZMPOP', timeout.toString()); parseZMPopArguments(parser, ...args); diff --git a/packages/client/lib/commands/BZPOPMAX.ts b/packages/client/lib/commands/BZPOPMAX.ts index e87bc2ca98e..f4becbd3035 100644 --- a/packages/client/lib/commands/BZPOPMAX.ts +++ b/packages/client/lib/commands/BZPOPMAX.ts @@ -3,7 +3,6 @@ import { NullReply, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, Comm import { RedisVariadicArgument, transformDoubleReply } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, keys: RedisVariadicArgument, timeout: number) { parser.push('BZPOPMAX'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/BZPOPMIN.ts b/packages/client/lib/commands/BZPOPMIN.ts index 9dc4c47e13d..a1c870d16ea 100644 --- a/packages/client/lib/commands/BZPOPMIN.ts +++ b/packages/client/lib/commands/BZPOPMIN.ts @@ -4,7 +4,6 @@ import { RedisVariadicArgument } from './generic-transformers'; import BZPOPMAX from './BZPOPMAX'; export default { - IS_READ_ONLY: BZPOPMAX.IS_READ_ONLY, parseCommand(parser: CommandParser, keys: RedisVariadicArgument, timeout: number) { parser.push('BZPOPMIN'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/CLIENT_CACHING.ts b/packages/client/lib/commands/CLIENT_CACHING.ts index 9987e49c99b..0a9592e8bf9 100644 --- a/packages/client/lib/commands/CLIENT_CACHING.ts +++ b/packages/client/lib/commands/CLIENT_CACHING.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, value: boolean) { parser.push( diff --git a/packages/client/lib/commands/CLIENT_GETNAME.ts b/packages/client/lib/commands/CLIENT_GETNAME.ts index 2e18c43cd5f..208ee763839 100644 --- a/packages/client/lib/commands/CLIENT_GETNAME.ts +++ b/packages/client/lib/commands/CLIENT_GETNAME.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLIENT', 'GETNAME'); diff --git a/packages/client/lib/commands/CLIENT_GETREDIR.ts b/packages/client/lib/commands/CLIENT_GETREDIR.ts index 80cc6418dab..0d2d29bb5e4 100644 --- a/packages/client/lib/commands/CLIENT_GETREDIR.ts +++ b/packages/client/lib/commands/CLIENT_GETREDIR.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLIENT', 'GETREDIR'); diff --git a/packages/client/lib/commands/CLIENT_ID.ts b/packages/client/lib/commands/CLIENT_ID.ts index da58786ec3c..950d05ad0a8 100644 --- a/packages/client/lib/commands/CLIENT_ID.ts +++ b/packages/client/lib/commands/CLIENT_ID.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLIENT', 'ID'); diff --git a/packages/client/lib/commands/CLIENT_INFO.ts b/packages/client/lib/commands/CLIENT_INFO.ts index 8908bdb2600..5e0dbd25008 100644 --- a/packages/client/lib/commands/CLIENT_INFO.ts +++ b/packages/client/lib/commands/CLIENT_INFO.ts @@ -65,7 +65,6 @@ export interface ClientInfoReply { const CLIENT_INFO_REGEX = /([^\s=]+)=([^\s]*)/g; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLIENT', 'INFO'); diff --git a/packages/client/lib/commands/CLIENT_KILL.ts b/packages/client/lib/commands/CLIENT_KILL.ts index 24f8f0873f1..1a64d079fa1 100644 --- a/packages/client/lib/commands/CLIENT_KILL.ts +++ b/packages/client/lib/commands/CLIENT_KILL.ts @@ -48,8 +48,6 @@ export interface ClientKillMaxAge extends ClientKillFilterCommon) { parser.push('CLIENT', 'KILL'); diff --git a/packages/client/lib/commands/CLIENT_LIST.ts b/packages/client/lib/commands/CLIENT_LIST.ts index 1e7f3d9ab40..2b774109ebf 100644 --- a/packages/client/lib/commands/CLIENT_LIST.ts +++ b/packages/client/lib/commands/CLIENT_LIST.ts @@ -15,7 +15,6 @@ export interface ListFilterId { export type ListFilter = ListFilterType | ListFilterId; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, filter?: ListFilter) { parser.push('CLIENT', 'LIST'); diff --git a/packages/client/lib/commands/CLIENT_NO-EVICT.ts b/packages/client/lib/commands/CLIENT_NO-EVICT.ts index de2f65270e2..55d298e5d53 100644 --- a/packages/client/lib/commands/CLIENT_NO-EVICT.ts +++ b/packages/client/lib/commands/CLIENT_NO-EVICT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, value: boolean) { parser.push( diff --git a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts index 8c6deff4af5..77b053c9a1b 100644 --- a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts +++ b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, value: boolean) { parser.push( diff --git a/packages/client/lib/commands/CLIENT_PAUSE.ts b/packages/client/lib/commands/CLIENT_PAUSE.ts index ae6e4376364..17d5843de27 100644 --- a/packages/client/lib/commands/CLIENT_PAUSE.ts +++ b/packages/client/lib/commands/CLIENT_PAUSE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, timeout: number, mode?: 'WRITE' | 'ALL') { parser.push('CLIENT', 'PAUSE', timeout.toString()); if (mode) { diff --git a/packages/client/lib/commands/CLIENT_SETNAME.ts b/packages/client/lib/commands/CLIENT_SETNAME.ts index 335891e8308..bad3c02e490 100644 --- a/packages/client/lib/commands/CLIENT_SETNAME.ts +++ b/packages/client/lib/commands/CLIENT_SETNAME.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, name: RedisArgument) { parser.push('CLIENT', 'SETNAME', name); diff --git a/packages/client/lib/commands/CLIENT_TRACKING.ts b/packages/client/lib/commands/CLIENT_TRACKING.ts index df70a3705f9..89798fe8d6e 100644 --- a/packages/client/lib/commands/CLIENT_TRACKING.ts +++ b/packages/client/lib/commands/CLIENT_TRACKING.ts @@ -27,7 +27,6 @@ export type ClientTrackingOptions = CommonOptions & ( ); export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand( parser: CommandParser, diff --git a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts index fe6e090455c..4ec42be4b49 100644 --- a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts +++ b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts @@ -8,7 +8,6 @@ type TrackingInfo = TuplesToMapReply<[ ]>; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLIENT', 'TRACKINGINFO'); diff --git a/packages/client/lib/commands/CLIENT_UNBLOCK.ts b/packages/client/lib/commands/CLIENT_UNBLOCK.ts index 7e285909500..b9282f70538 100644 --- a/packages/client/lib/commands/CLIENT_UNBLOCK.ts +++ b/packages/client/lib/commands/CLIENT_UNBLOCK.ts @@ -9,8 +9,6 @@ export const CLIENT_UNBLOCK_MODES = { export type ClientUnblockMode = typeof CLIENT_UNBLOCK_MODES[keyof typeof CLIENT_UNBLOCK_MODES]; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, clientId: number | `${number}`, mode?: ClientUnblockMode) { parser.push( 'CLIENT', diff --git a/packages/client/lib/commands/CLIENT_UNPAUSE.ts b/packages/client/lib/commands/CLIENT_UNPAUSE.ts index c202e50a5df..c96a316cad4 100644 --- a/packages/client/lib/commands/CLIENT_UNPAUSE.ts +++ b/packages/client/lib/commands/CLIENT_UNPAUSE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLIENT', 'UNPAUSE'); }, diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts index 0f5c4513d1d..d491fb1aaa0 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, slots: number | Array) { parser.push('CLUSTER', 'ADDSLOTS'); parser.pushVariadicNumber(slots); diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts index 40780731981..99f75bf89d9 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts @@ -3,8 +3,6 @@ import { SimpleStringReply, Command } from '../RESP/types'; import { parseSlotRangesArguments, SlotRange } from './generic-transformers'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, ranges: SlotRange | Array) { parser.push('CLUSTER', 'ADDSLOTSRANGE'); parseSlotRangesArguments(parser, ranges); diff --git a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts index 04b62f85424..be35f733f49 100644 --- a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts +++ b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'BUMPEPOCH'); }, diff --git a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts index 0ac311f7ecd..2e069c07939 100644 --- a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts +++ b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, nodeId: RedisArgument) { parser.push('CLUSTER', 'COUNT-FAILURE-REPORTS', nodeId); diff --git a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts index 63b4a8e02e2..38b39c8922a 100644 --- a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, slot: number) { parser.push('CLUSTER', 'COUNTKEYSINSLOT', slot.toString()); diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts index 9be6e962a18..48aed216358 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, slots: number | Array) { parser.push('CLUSTER', 'DELSLOTS'); parser.pushVariadicNumber(slots); diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts index 64c04021ba1..c49c2b19bad 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts @@ -3,8 +3,6 @@ import { SimpleStringReply, Command } from '../RESP/types'; import { parseSlotRangesArguments, SlotRange } from './generic-transformers'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser:CommandParser, ranges: SlotRange | Array) { parser.push('CLUSTER', 'DELSLOTSRANGE'); parseSlotRangesArguments(parser, ranges); diff --git a/packages/client/lib/commands/CLUSTER_FAILOVER.ts b/packages/client/lib/commands/CLUSTER_FAILOVER.ts index f74d65bd691..0e124e2a52c 100644 --- a/packages/client/lib/commands/CLUSTER_FAILOVER.ts +++ b/packages/client/lib/commands/CLUSTER_FAILOVER.ts @@ -13,8 +13,6 @@ export interface ClusterFailoverOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser:CommandParser, options?: ClusterFailoverOptions) { parser.push('CLUSTER', 'FAILOVER'); diff --git a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts index dab22b2e740..de5a8b0d008 100644 --- a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'FLUSHSLOTS'); }, diff --git a/packages/client/lib/commands/CLUSTER_FORGET.ts b/packages/client/lib/commands/CLUSTER_FORGET.ts index 2928c3e9075..3593a7babd7 100644 --- a/packages/client/lib/commands/CLUSTER_FORGET.ts +++ b/packages/client/lib/commands/CLUSTER_FORGET.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, nodeId: RedisArgument) { parser.push('CLUSTER', 'FORGET', nodeId); }, diff --git a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts index 2fd630ea1af..16982e6ea4e 100644 --- a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, slot: number, count: number) { parser.push('CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()); diff --git a/packages/client/lib/commands/CLUSTER_INFO.ts b/packages/client/lib/commands/CLUSTER_INFO.ts index 53140b38819..a0d0576563e 100644 --- a/packages/client/lib/commands/CLUSTER_INFO.ts +++ b/packages/client/lib/commands/CLUSTER_INFO.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { VerbatimStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'INFO'); diff --git a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts index a7f7de2d7bd..774a031a280 100644 --- a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts @@ -2,13 +2,12 @@ import { CommandParser } from '../client/parser'; import { Command, NumberReply, RedisArgument } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('CLUSTER', 'KEYSLOT'); // Use pushKey so a configured `keyPrefix` is applied to the reported key: the returned - // slot then matches where prefixed commands for `key` are actually routed. The command - // stays NOT_KEYED_COMMAND because its result is identical on any node. + // slot then matches where prefixed commands for `key` are actually routed. The command's + // result is identical on any node, so key-based routing is irrelevant to correctness. parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply diff --git a/packages/client/lib/commands/CLUSTER_LINKS.ts b/packages/client/lib/commands/CLUSTER_LINKS.ts index e98f61e762b..1e3c0e46546 100644 --- a/packages/client/lib/commands/CLUSTER_LINKS.ts +++ b/packages/client/lib/commands/CLUSTER_LINKS.ts @@ -11,7 +11,6 @@ type ClusterLinksReply = ArrayReply>; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'LINKS'); diff --git a/packages/client/lib/commands/CLUSTER_MEET.ts b/packages/client/lib/commands/CLUSTER_MEET.ts index 804e5963d19..b7a4cc9e1f8 100644 --- a/packages/client/lib/commands/CLUSTER_MEET.ts +++ b/packages/client/lib/commands/CLUSTER_MEET.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, host: string, port: number) { parser.push('CLUSTER', 'MEET', host, port.toString()); }, diff --git a/packages/client/lib/commands/CLUSTER_MYID.ts b/packages/client/lib/commands/CLUSTER_MYID.ts index 2aae7cdd8e0..e517f1ba3d8 100644 --- a/packages/client/lib/commands/CLUSTER_MYID.ts +++ b/packages/client/lib/commands/CLUSTER_MYID.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'MYID'); diff --git a/packages/client/lib/commands/CLUSTER_MYSHARDID.ts b/packages/client/lib/commands/CLUSTER_MYSHARDID.ts index ccde3ee249b..005d3e634d1 100644 --- a/packages/client/lib/commands/CLUSTER_MYSHARDID.ts +++ b/packages/client/lib/commands/CLUSTER_MYSHARDID.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'MYSHARDID'); diff --git a/packages/client/lib/commands/CLUSTER_NODES.ts b/packages/client/lib/commands/CLUSTER_NODES.ts index c8b59f88224..9e202233a31 100644 --- a/packages/client/lib/commands/CLUSTER_NODES.ts +++ b/packages/client/lib/commands/CLUSTER_NODES.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { VerbatimStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'NODES'); diff --git a/packages/client/lib/commands/CLUSTER_REPLICAS.ts b/packages/client/lib/commands/CLUSTER_REPLICAS.ts index eb60e560b45..f6d86b1175f 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICAS.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICAS.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, nodeId: RedisArgument) { parser.push('CLUSTER', 'REPLICAS', nodeId); diff --git a/packages/client/lib/commands/CLUSTER_REPLICATE.ts b/packages/client/lib/commands/CLUSTER_REPLICATE.ts index d7312ae108e..5bffde3ee70 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICATE.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICATE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, nodeId: RedisArgument) { parser.push('CLUSTER', 'REPLICATE', nodeId); }, diff --git a/packages/client/lib/commands/CLUSTER_RESET.ts b/packages/client/lib/commands/CLUSTER_RESET.ts index 2ba1a6eaf20..209e424e7a3 100644 --- a/packages/client/lib/commands/CLUSTER_RESET.ts +++ b/packages/client/lib/commands/CLUSTER_RESET.ts @@ -6,8 +6,6 @@ export interface ClusterResetOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, options?: ClusterResetOptions) { parser.push('CLUSTER', 'RESET'); diff --git a/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts b/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts index 08da2a45b89..dca1f589d46 100644 --- a/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts +++ b/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'SAVECONFIG'); }, diff --git a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts index ba423df7fb7..108a46f40bb 100644 --- a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts +++ b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, configEpoch: number) { parser.push('CLUSTER', 'SET-CONFIG-EPOCH', configEpoch.toString()); }, diff --git a/packages/client/lib/commands/CLUSTER_SETSLOT.ts b/packages/client/lib/commands/CLUSTER_SETSLOT.ts index 1f74316a3f3..25d368b4d8a 100644 --- a/packages/client/lib/commands/CLUSTER_SETSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_SETSLOT.ts @@ -11,8 +11,6 @@ export const CLUSTER_SLOT_STATES = { export type ClusterSlotState = typeof CLUSTER_SLOT_STATES[keyof typeof CLUSTER_SLOT_STATES]; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, slot: number, state: ClusterSlotState, nodeId?: RedisArgument) { parser.push('CLUSTER', 'SETSLOT', slot.toString(), state); diff --git a/packages/client/lib/commands/CLUSTER_SLOTS.ts b/packages/client/lib/commands/CLUSTER_SLOTS.ts index f6f967abe28..60d9d2f28a3 100644 --- a/packages/client/lib/commands/CLUSTER_SLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_SLOTS.ts @@ -17,7 +17,6 @@ type ClusterSlotsRawReply = ArrayReply<[ export type ClusterSlotsNode = ReturnType; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CLUSTER', 'SLOTS'); diff --git a/packages/client/lib/commands/COMMAND.ts b/packages/client/lib/commands/COMMAND.ts index 52eb7eb2fea..2148b584160 100644 --- a/packages/client/lib/commands/COMMAND.ts +++ b/packages/client/lib/commands/COMMAND.ts @@ -3,7 +3,6 @@ import { ArrayReply, Command, UnwrapReply } from '../RESP/types'; import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('COMMAND'); diff --git a/packages/client/lib/commands/COMMAND_COUNT.ts b/packages/client/lib/commands/COMMAND_COUNT.ts index ef561920b0b..6c2ff8a59b9 100644 --- a/packages/client/lib/commands/COMMAND_COUNT.ts +++ b/packages/client/lib/commands/COMMAND_COUNT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('COMMAND', 'COUNT'); diff --git a/packages/client/lib/commands/COMMAND_GETKEYS.ts b/packages/client/lib/commands/COMMAND_GETKEYS.ts index 97c5cb69ce2..09ac4682489 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYS.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYS.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, args: Array) { parser.push('COMMAND', 'GETKEYS'); diff --git a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts index 72c1e16a2d1..92d72f03813 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts @@ -7,7 +7,6 @@ export type CommandGetKeysAndFlagsRawReply = ArrayReply>; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, args: Array) { parser.push('COMMAND', 'GETKEYSANDFLAGS'); diff --git a/packages/client/lib/commands/COMMAND_INFO.ts b/packages/client/lib/commands/COMMAND_INFO.ts index fdf03780652..ef6dd0b7f94 100644 --- a/packages/client/lib/commands/COMMAND_INFO.ts +++ b/packages/client/lib/commands/COMMAND_INFO.ts @@ -3,7 +3,6 @@ import { ArrayReply, Command, UnwrapReply } from '../RESP/types'; import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, commands: Array) { parser.push('COMMAND', 'INFO', ...commands); diff --git a/packages/client/lib/commands/COMMAND_LIST.ts b/packages/client/lib/commands/COMMAND_LIST.ts index ba518b70eca..a283fde4a58 100644 --- a/packages/client/lib/commands/COMMAND_LIST.ts +++ b/packages/client/lib/commands/COMMAND_LIST.ts @@ -17,7 +17,6 @@ export interface CommandListOptions { } export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, options?: CommandListOptions) { parser.push('COMMAND', 'LIST'); diff --git a/packages/client/lib/commands/CONFIG_GET.ts b/packages/client/lib/commands/CONFIG_GET.ts index e8339c4d9a0..c8ba899743b 100644 --- a/packages/client/lib/commands/CONFIG_GET.ts +++ b/packages/client/lib/commands/CONFIG_GET.ts @@ -3,7 +3,6 @@ import { MapReply, BlobStringReply, Command } from '../RESP/types'; import { RedisVariadicArgument, transformTuplesReply } from './generic-transformers'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, parameters: RedisVariadicArgument) { parser.push('CONFIG', 'GET'); diff --git a/packages/client/lib/commands/CONFIG_RESETSTAT.ts b/packages/client/lib/commands/CONFIG_RESETSTAT.ts index 15de5ba7808..c6c108e9cc3 100644 --- a/packages/client/lib/commands/CONFIG_RESETSTAT.ts +++ b/packages/client/lib/commands/CONFIG_RESETSTAT.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CONFIG', 'RESETSTAT'); }, diff --git a/packages/client/lib/commands/CONFIG_REWRITE.ts b/packages/client/lib/commands/CONFIG_REWRITE.ts index ae6712ffb57..98a53adf294 100644 --- a/packages/client/lib/commands/CONFIG_REWRITE.ts +++ b/packages/client/lib/commands/CONFIG_REWRITE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('CONFIG', 'REWRITE'); }, diff --git a/packages/client/lib/commands/CONFIG_SET.ts b/packages/client/lib/commands/CONFIG_SET.ts index dd1bbc29ef2..039a32e04d5 100644 --- a/packages/client/lib/commands/CONFIG_SET.ts +++ b/packages/client/lib/commands/CONFIG_SET.ts @@ -6,8 +6,6 @@ type SingleParameter = [parameter: RedisArgument, value: RedisArgument]; type MultipleParameters = [config: Record]; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, ...[parameterOrConfig, value]: SingleParameter | MultipleParameters diff --git a/packages/client/lib/commands/COPY.ts b/packages/client/lib/commands/COPY.ts index 8caa66822ef..3419e94ac05 100644 --- a/packages/client/lib/commands/COPY.ts +++ b/packages/client/lib/commands/COPY.ts @@ -7,7 +7,6 @@ export interface CopyCommandOptions { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, options?: CopyCommandOptions) { parser.push('COPY'); parser.pushKeys([source, destination]); diff --git a/packages/client/lib/commands/DBSIZE.ts b/packages/client/lib/commands/DBSIZE.ts index 1ba1f060476..330507894b0 100644 --- a/packages/client/lib/commands/DBSIZE.ts +++ b/packages/client/lib/commands/DBSIZE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('DBSIZE'); }, diff --git a/packages/client/lib/commands/DEL.ts b/packages/client/lib/commands/DEL.ts index da0803f4d1b..a1a86ab85bb 100644 --- a/packages/client/lib/commands/DEL.ts +++ b/packages/client/lib/commands/DEL.ts @@ -3,7 +3,6 @@ import { NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('DEL'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/DELEX.ts b/packages/client/lib/commands/DELEX.ts index 10b1cfee0c4..3946af8d165 100644 --- a/packages/client/lib/commands/DELEX.ts +++ b/packages/client/lib/commands/DELEX.ts @@ -23,7 +23,6 @@ export const DelexCondition = { type DelexCondition = (typeof DelexCondition)[keyof typeof DelexCondition]; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/DIGEST.ts b/packages/client/lib/commands/DIGEST.ts index 64c32da983a..4d6dc1ee4b3 100644 --- a/packages/client/lib/commands/DIGEST.ts +++ b/packages/client/lib/commands/DIGEST.ts @@ -2,7 +2,6 @@ import { CommandParser } from "../client/parser"; import { Command, RedisArgument, SimpleStringReply } from "../RESP/types"; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push("DIGEST"); parser.pushKey(key); diff --git a/packages/client/lib/commands/DUMP.ts b/packages/client/lib/commands/DUMP.ts index e442c1cdb2f..cb719fa0f46 100644 --- a/packages/client/lib/commands/DUMP.ts +++ b/packages/client/lib/commands/DUMP.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('DUMP'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ECHO.ts b/packages/client/lib/commands/ECHO.ts index 7935bdc0101..bf8847d5cc9 100644 --- a/packages/client/lib/commands/ECHO.ts +++ b/packages/client/lib/commands/ECHO.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, message: RedisArgument) { parser.push('ECHO', message); diff --git a/packages/client/lib/commands/EVAL.ts b/packages/client/lib/commands/EVAL.ts index cdb8025b0be..490b786599c 100644 --- a/packages/client/lib/commands/EVAL.ts +++ b/packages/client/lib/commands/EVAL.ts @@ -24,7 +24,6 @@ export function parseEvalArguments( } export default { - IS_READ_ONLY: false, parseCommand(...args: Parameters) { args[0].push('EVAL'); parseEvalArguments(...args); diff --git a/packages/client/lib/commands/EVALSHA.ts b/packages/client/lib/commands/EVALSHA.ts index 5a9cc771358..c0738ee439e 100644 --- a/packages/client/lib/commands/EVALSHA.ts +++ b/packages/client/lib/commands/EVALSHA.ts @@ -2,7 +2,6 @@ import { Command } from '../RESP/types'; import EVAL, { parseEvalArguments } from './EVAL'; export default { - IS_READ_ONLY: false, parseCommand(...args: Parameters) { args[0].push('EVALSHA'); parseEvalArguments(...args); diff --git a/packages/client/lib/commands/EXISTS.ts b/packages/client/lib/commands/EXISTS.ts index 8ebb28269fe..91090c8ac48 100644 --- a/packages/client/lib/commands/EXISTS.ts +++ b/packages/client/lib/commands/EXISTS.ts @@ -3,8 +3,6 @@ import { NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('EXISTS'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/EXPIRETIME.ts b/packages/client/lib/commands/EXPIRETIME.ts index 2bb97fb737b..ca8c7d1dbe8 100644 --- a/packages/client/lib/commands/EXPIRETIME.ts +++ b/packages/client/lib/commands/EXPIRETIME.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('EXPIRETIME'); parser.pushKey(key); diff --git a/packages/client/lib/commands/FCALL.ts b/packages/client/lib/commands/FCALL.ts index 622060f693c..871b823252f 100644 --- a/packages/client/lib/commands/FCALL.ts +++ b/packages/client/lib/commands/FCALL.ts @@ -2,7 +2,6 @@ import { Command } from '../RESP/types'; import EVAL, { parseEvalArguments } from './EVAL'; export default { - IS_READ_ONLY: false, parseCommand(...args: Parameters) { args[0].push('FCALL'); parseEvalArguments(...args); diff --git a/packages/client/lib/commands/FCALL_RO.ts b/packages/client/lib/commands/FCALL_RO.ts index 95effb0e698..28da0ebd869 100644 --- a/packages/client/lib/commands/FCALL_RO.ts +++ b/packages/client/lib/commands/FCALL_RO.ts @@ -2,7 +2,7 @@ import { Command } from '../RESP/types'; import EVAL, { parseEvalArguments } from './EVAL'; export default { - IS_READ_ONLY: false, + IS_READ_ONLY: true, parseCommand(...args: Parameters) { args[0].push('FCALL_RO'); parseEvalArguments(...args); diff --git a/packages/client/lib/commands/FLUSHALL.ts b/packages/client/lib/commands/FLUSHALL.ts index c39535e8864..e9997b2b6ef 100644 --- a/packages/client/lib/commands/FLUSHALL.ts +++ b/packages/client/lib/commands/FLUSHALL.ts @@ -9,8 +9,6 @@ export const REDIS_FLUSH_MODES = { export type RedisFlushMode = typeof REDIS_FLUSH_MODES[keyof typeof REDIS_FLUSH_MODES]; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, mode?: RedisFlushMode) { parser.push('FLUSHALL'); if (mode) { diff --git a/packages/client/lib/commands/FLUSHDB.ts b/packages/client/lib/commands/FLUSHDB.ts index 5639f69a611..fb1143e86fa 100644 --- a/packages/client/lib/commands/FLUSHDB.ts +++ b/packages/client/lib/commands/FLUSHDB.ts @@ -3,8 +3,6 @@ import { SimpleStringReply, Command } from '../RESP/types'; import { RedisFlushMode } from './FLUSHALL'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, mode?: RedisFlushMode) { parser.push('FLUSHDB'); if (mode) { diff --git a/packages/client/lib/commands/FUNCTION_DELETE.ts b/packages/client/lib/commands/FUNCTION_DELETE.ts index dbfb044928e..16dfcebd348 100644 --- a/packages/client/lib/commands/FUNCTION_DELETE.ts +++ b/packages/client/lib/commands/FUNCTION_DELETE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, library: RedisArgument) { parser.push('FUNCTION', 'DELETE', library); }, diff --git a/packages/client/lib/commands/FUNCTION_DUMP.ts b/packages/client/lib/commands/FUNCTION_DUMP.ts index 2d0dbdd4455..030c771a16a 100644 --- a/packages/client/lib/commands/FUNCTION_DUMP.ts +++ b/packages/client/lib/commands/FUNCTION_DUMP.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('FUNCTION', 'DUMP') diff --git a/packages/client/lib/commands/FUNCTION_FLUSH.ts b/packages/client/lib/commands/FUNCTION_FLUSH.ts index 4ca59e4464e..d07ae23785e 100644 --- a/packages/client/lib/commands/FUNCTION_FLUSH.ts +++ b/packages/client/lib/commands/FUNCTION_FLUSH.ts @@ -3,8 +3,6 @@ import { SimpleStringReply, Command } from '../RESP/types'; import { RedisFlushMode } from './FLUSHALL'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, mode?: RedisFlushMode) { parser.push('FUNCTION', 'FLUSH'); diff --git a/packages/client/lib/commands/FUNCTION_KILL.ts b/packages/client/lib/commands/FUNCTION_KILL.ts index 8b5351e93ab..2e427c46623 100644 --- a/packages/client/lib/commands/FUNCTION_KILL.ts +++ b/packages/client/lib/commands/FUNCTION_KILL.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('FUNCTION', 'KILL'); }, diff --git a/packages/client/lib/commands/FUNCTION_LIST.ts b/packages/client/lib/commands/FUNCTION_LIST.ts index 82e3697eadc..ae8d8c5050d 100644 --- a/packages/client/lib/commands/FUNCTION_LIST.ts +++ b/packages/client/lib/commands/FUNCTION_LIST.ts @@ -18,8 +18,7 @@ export type FunctionListReplyItem = [ export type FunctionListReply = ArrayReply>; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, + IS_READ_ONLY: true, parseCommand(parser: CommandParser, options?: FunctionListOptions) { parser.push('FUNCTION', 'LIST'); diff --git a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts index 208bc5fd303..03a0d42e64a 100644 --- a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts +++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts @@ -7,8 +7,7 @@ export type FunctionListWithCodeReply = ArrayReply>; export default { - NOT_KEYED_COMMAND: FUNCTION_LIST.NOT_KEYED_COMMAND, - IS_READ_ONLY: FUNCTION_LIST.IS_READ_ONLY, + IS_READ_ONLY: true, parseCommand(...args: Parameters) { FUNCTION_LIST.parseCommand(...args); args[0].push('WITHCODE'); diff --git a/packages/client/lib/commands/FUNCTION_LOAD.ts b/packages/client/lib/commands/FUNCTION_LOAD.ts index 40b8ea8c0f4..2b2c21164ad 100644 --- a/packages/client/lib/commands/FUNCTION_LOAD.ts +++ b/packages/client/lib/commands/FUNCTION_LOAD.ts @@ -6,8 +6,6 @@ export interface FunctionLoadOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, code: RedisArgument, options?: FunctionLoadOptions) { parser.push('FUNCTION', 'LOAD'); diff --git a/packages/client/lib/commands/FUNCTION_RESTORE.ts b/packages/client/lib/commands/FUNCTION_RESTORE.ts index 944813f25e5..6191e0e31e6 100644 --- a/packages/client/lib/commands/FUNCTION_RESTORE.ts +++ b/packages/client/lib/commands/FUNCTION_RESTORE.ts @@ -6,8 +6,6 @@ export interface FunctionRestoreOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, dump: RedisArgument, options?: FunctionRestoreOptions) { parser.push('FUNCTION', 'RESTORE', dump); diff --git a/packages/client/lib/commands/FUNCTION_STATS.ts b/packages/client/lib/commands/FUNCTION_STATS.ts index 9e418e80860..cf89a3d9bf6 100644 --- a/packages/client/lib/commands/FUNCTION_STATS.ts +++ b/packages/client/lib/commands/FUNCTION_STATS.ts @@ -21,7 +21,6 @@ type FunctionStatsReply = TuplesToMapReply<[ ]>; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('FUNCTION', 'STATS'); diff --git a/packages/client/lib/commands/GEOADD.ts b/packages/client/lib/commands/GEOADD.ts index 31bf457e158..89df0dcbe82 100644 --- a/packages/client/lib/commands/GEOADD.ts +++ b/packages/client/lib/commands/GEOADD.ts @@ -20,7 +20,6 @@ export interface GeoAddOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEODIST.ts b/packages/client/lib/commands/GEODIST.ts index ba4d3080a71..64e9cba8cdb 100644 --- a/packages/client/lib/commands/GEODIST.ts +++ b/packages/client/lib/commands/GEODIST.ts @@ -3,8 +3,6 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type import { GeoUnits } from './GEOSEARCH'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, member1: RedisArgument, diff --git a/packages/client/lib/commands/GEOHASH.ts b/packages/client/lib/commands/GEOHASH.ts index c3265d13157..2aeba39095e 100644 --- a/packages/client/lib/commands/GEOHASH.ts +++ b/packages/client/lib/commands/GEOHASH.ts @@ -3,8 +3,6 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ import { RedisVariadicArgument } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { parser.push('GEOHASH'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GEOPOS.ts b/packages/client/lib/commands/GEOPOS.ts index 6bdbb65ac46..b457bdb9121 100644 --- a/packages/client/lib/commands/GEOPOS.ts +++ b/packages/client/lib/commands/GEOPOS.ts @@ -3,8 +3,6 @@ import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NullReply, Unw import { RedisVariadicArgument } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { parser.push('GEOPOS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GEORADIUS.ts b/packages/client/lib/commands/GEORADIUS.ts index 5e8d880ab5e..2de623a1ba1 100644 --- a/packages/client/lib/commands/GEORADIUS.ts +++ b/packages/client/lib/commands/GEORADIUS.ts @@ -17,7 +17,6 @@ export function parseGeoRadiusArguments( } export default { - IS_READ_ONLY: false, parseCommand(...args: Parameters) { args[0].push('GEORADIUS'); return parseGeoRadiusArguments(...args); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts index be4ca54650c..622b0d1fa55 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts @@ -17,7 +17,6 @@ export function parseGeoRadiusByMemberArguments( } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts index 335eea08133..8c9956c6d10 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts @@ -2,8 +2,6 @@ import { Command } from '../RESP/types'; import GEORADIUSBYMEMBER, { parseGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(...args: Parameters) { const parser = args[0]; parser.push('GEORADIUSBYMEMBER_RO'); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts index 06835438016..a1e4be74629 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts @@ -2,8 +2,6 @@ import { Command } from '../RESP/types'; import GEORADIUSBYMEMBER_WITH, { parseGeoRadiusByMemberWithArguments } from './GEORADIUSBYMEMBER_WITH'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(...args: Parameters) { const parser = args[0]; parser.push('GEORADIUSBYMEMBER_RO'); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts index 676df34dd5a..22f4bb1f4bb 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts @@ -1,6 +1,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import GEORADIUSBYMEMBER, { parseGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; +import { parseGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; import { GeoSearchOptions, GeoUnits } from './GEOSEARCH'; export interface GeoRadiusStoreOptions extends GeoSearchOptions { @@ -8,7 +8,6 @@ export interface GeoRadiusStoreOptions extends GeoSearchOptions { } export default { - IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts index eefae0b27a9..011014af1ab 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts @@ -1,6 +1,5 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, Command } from '../RESP/types'; -import GEORADIUSBYMEMBER from './GEORADIUSBYMEMBER'; import { GeoSearchOptions, GeoUnits, parseGeoSearchOptions } from './GEOSEARCH'; import GEOSEARCH_WITH, { GeoReplyWith } from './GEOSEARCH_WITH'; @@ -22,7 +21,6 @@ export function parseGeoRadiusByMemberWithArguments( } export default { - IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEORADIUS_RO.ts b/packages/client/lib/commands/GEORADIUS_RO.ts index 5db65d9dc9b..230c49fc22a 100644 --- a/packages/client/lib/commands/GEORADIUS_RO.ts +++ b/packages/client/lib/commands/GEORADIUS_RO.ts @@ -2,8 +2,6 @@ import { Command } from '../RESP/types'; import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(...args: Parameters) { args[0].push('GEORADIUS_RO'); parseGeoRadiusArguments(...args); diff --git a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts index cee1679382b..7c683dcaffd 100644 --- a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts +++ b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts @@ -3,8 +3,6 @@ import { parseGeoRadiusWithArguments } from './GEORADIUS_WITH'; import GEORADIUS_WITH from './GEORADIUS_WITH'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(...args: Parameters) { args[0].push('GEORADIUS_RO'); parseGeoRadiusWithArguments(...args); diff --git a/packages/client/lib/commands/GEORADIUS_STORE.ts b/packages/client/lib/commands/GEORADIUS_STORE.ts index 18459d44217..f9dfca5e55a 100644 --- a/packages/client/lib/commands/GEORADIUS_STORE.ts +++ b/packages/client/lib/commands/GEORADIUS_STORE.ts @@ -1,6 +1,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS'; +import { parseGeoRadiusArguments } from './GEORADIUS'; import { GeoCoordinates, GeoSearchOptions, GeoUnits } from './GEOSEARCH'; export interface GeoRadiusStoreOptions extends GeoSearchOptions { @@ -8,7 +8,6 @@ export interface GeoRadiusStoreOptions extends GeoSearchOptions { } export default { - IS_READ_ONLY: GEORADIUS.IS_READ_ONLY, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEORADIUS_WITH.ts b/packages/client/lib/commands/GEORADIUS_WITH.ts index ac4c8b7bb1b..c21ea5f556d 100644 --- a/packages/client/lib/commands/GEORADIUS_WITH.ts +++ b/packages/client/lib/commands/GEORADIUS_WITH.ts @@ -1,6 +1,6 @@ import { CommandParser } from '../client/parser'; import { Command, RedisArgument } from '../RESP/types'; -import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS'; +import { parseGeoRadiusArguments } from './GEORADIUS'; import { GeoCoordinates, GeoSearchOptions, GeoUnits } from './GEOSEARCH'; import GEOSEARCH_WITH, { GeoReplyWith } from './GEOSEARCH_WITH'; @@ -19,7 +19,6 @@ export function parseGeoRadiusWithArguments( } export default { - IS_READ_ONLY: GEORADIUS.IS_READ_ONLY, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEOSEARCH.ts b/packages/client/lib/commands/GEOSEARCH.ts index 869dc60bec9..348bc1834e3 100644 --- a/packages/client/lib/commands/GEOSEARCH.ts +++ b/packages/client/lib/commands/GEOSEARCH.ts @@ -79,7 +79,6 @@ export function parseGeoSearchOptions( } export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GEOSEARCHSTORE.ts b/packages/client/lib/commands/GEOSEARCHSTORE.ts index 34c6e0988e2..f080019366d 100644 --- a/packages/client/lib/commands/GEOSEARCHSTORE.ts +++ b/packages/client/lib/commands/GEOSEARCHSTORE.ts @@ -7,7 +7,6 @@ export interface GeoSearchStoreOptions extends GeoSearchOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/client/lib/commands/GEOSEARCH_WITH.ts b/packages/client/lib/commands/GEOSEARCH_WITH.ts index 2cf7132f7ae..224b9e22227 100644 --- a/packages/client/lib/commands/GEOSEARCH_WITH.ts +++ b/packages/client/lib/commands/GEOSEARCH_WITH.ts @@ -23,7 +23,6 @@ export interface GeoReplyWithMember { } export default { - IS_READ_ONLY: GEOSEARCH.IS_READ_ONLY, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/GET.ts b/packages/client/lib/commands/GET.ts index ca013752ae5..b4670c0bd53 100644 --- a/packages/client/lib/commands/GET.ts +++ b/packages/client/lib/commands/GET.ts @@ -2,8 +2,6 @@ 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); diff --git a/packages/client/lib/commands/GETBIT.ts b/packages/client/lib/commands/GETBIT.ts index 023ba0fb607..6f729b8d484 100644 --- a/packages/client/lib/commands/GETBIT.ts +++ b/packages/client/lib/commands/GETBIT.ts @@ -3,8 +3,6 @@ import { NumberReply, Command, RedisArgument } from '../RESP/types'; import { BitValue } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, offset: number) { parser.push('GETBIT'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GETDEL.ts b/packages/client/lib/commands/GETDEL.ts index a39014109f1..68a54b1c2cc 100644 --- a/packages/client/lib/commands/GETDEL.ts +++ b/packages/client/lib/commands/GETDEL.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('GETDEL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GETEX.ts b/packages/client/lib/commands/GETEX.ts index 87319e17136..8f64ac70606 100644 --- a/packages/client/lib/commands/GETEX.ts +++ b/packages/client/lib/commands/GETEX.ts @@ -38,7 +38,6 @@ export type GetExOptions = { }; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, options: GetExOptions) { parser.push('GETEX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GETRANGE.ts b/packages/client/lib/commands/GETRANGE.ts index ce0db6e3c03..9c41bbf88d4 100644 --- a/packages/client/lib/commands/GETRANGE.ts +++ b/packages/client/lib/commands/GETRANGE.ts @@ -2,8 +2,6 @@ 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, start: number, end: number) { parser.push('GETRANGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/GETSET.ts b/packages/client/lib/commands/GETSET.ts index 1b3312548e4..b4f59d8bd9b 100644 --- a/packages/client/lib/commands/GETSET.ts +++ b/packages/client/lib/commands/GETSET.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) { parser.push('GETSET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HEXISTS.ts b/packages/client/lib/commands/HEXISTS.ts index 9bb517b7df4..692cf0ee8ba 100644 --- a/packages/client/lib/commands/HEXISTS.ts +++ b/packages/client/lib/commands/HEXISTS.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) { parser.push('HEXISTS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HEXPIRETIME.ts b/packages/client/lib/commands/HEXPIRETIME.ts index 697d327db16..e29c5e42db9 100644 --- a/packages/client/lib/commands/HEXPIRETIME.ts +++ b/packages/client/lib/commands/HEXPIRETIME.ts @@ -10,7 +10,6 @@ export const HASH_EXPIRATION_TIME = { } as const; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HGET.ts b/packages/client/lib/commands/HGET.ts index fcd9334eb0a..22c861072df 100644 --- a/packages/client/lib/commands/HGET.ts +++ b/packages/client/lib/commands/HGET.ts @@ -2,8 +2,6 @@ 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, field: RedisArgument) { parser.push('HGET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HGETALL.ts b/packages/client/lib/commands/HGETALL.ts index 8d53669cdd4..00efbc7566d 100644 --- a/packages/client/lib/commands/HGETALL.ts +++ b/packages/client/lib/commands/HGETALL.ts @@ -3,8 +3,6 @@ import { RedisArgument, MapReply, BlobStringReply, Command } from '../RESP/types import { transformTuplesReply } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('HGETALL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HKEYS.ts b/packages/client/lib/commands/HKEYS.ts index f07a1ac127f..fde0521d546 100644 --- a/packages/client/lib/commands/HKEYS.ts +++ b/packages/client/lib/commands/HKEYS.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('HKEYS') parser.pushKey(key); diff --git a/packages/client/lib/commands/HLEN.ts b/packages/client/lib/commands/HLEN.ts index e3b89da3e7d..5b73171606a 100644 --- a/packages/client/lib/commands/HLEN.ts +++ b/packages/client/lib/commands/HLEN.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('HLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HMGET.ts b/packages/client/lib/commands/HMGET.ts index 51ba937339f..6515bb24cd3 100644 --- a/packages/client/lib/commands/HMGET.ts +++ b/packages/client/lib/commands/HMGET.ts @@ -3,8 +3,6 @@ import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from ' import { RedisVariadicArgument } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument) { parser.push('HMGET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HOTKEYS_GET.ts b/packages/client/lib/commands/HOTKEYS_GET.ts index dcc5b5ca59c..93de87fc7f1 100644 --- a/packages/client/lib/commands/HOTKEYS_GET.ts +++ b/packages/client/lib/commands/HOTKEYS_GET.ts @@ -175,7 +175,6 @@ function transformHotkeysGetReply(reply: unknown | null): HotkeysGetReply | null * server-side payload is treated as a fixed schema, not a generic map. */ export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('HOTKEYS', 'GET'); diff --git a/packages/client/lib/commands/HOTKEYS_RESET.ts b/packages/client/lib/commands/HOTKEYS_RESET.ts index 42221235f68..e6965021c1c 100644 --- a/packages/client/lib/commands/HOTKEYS_RESET.ts +++ b/packages/client/lib/commands/HOTKEYS_RESET.ts @@ -10,8 +10,6 @@ import { SimpleStringReply, Command } from '../RESP/types'; * - ACTIVE -> ERROR (must stop first) */ export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser) { parser.push('HOTKEYS', 'RESET'); }, diff --git a/packages/client/lib/commands/HOTKEYS_START.ts b/packages/client/lib/commands/HOTKEYS_START.ts index 5f9736473f6..2dd5a799dc8 100644 --- a/packages/client/lib/commands/HOTKEYS_START.ts +++ b/packages/client/lib/commands/HOTKEYS_START.ts @@ -55,8 +55,6 @@ export interface HotkeysStartOptions { * - ACTIVE -> ERROR */ export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, options: HotkeysStartOptions) { parser.push('HOTKEYS', 'START'); diff --git a/packages/client/lib/commands/HOTKEYS_STOP.ts b/packages/client/lib/commands/HOTKEYS_STOP.ts index 05c2a42dd2f..f19c791489e 100644 --- a/packages/client/lib/commands/HOTKEYS_STOP.ts +++ b/packages/client/lib/commands/HOTKEYS_STOP.ts @@ -12,8 +12,6 @@ import { SimpleStringReply, NullReply, Command } from '../RESP/types'; * Note: Returns null if no session was started or is already stopped. */ export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser) { parser.push('HOTKEYS', 'STOP'); }, diff --git a/packages/client/lib/commands/HPEXPIREAT.ts b/packages/client/lib/commands/HPEXPIREAT.ts index 14288d7ae90..ff4df264092 100644 --- a/packages/client/lib/commands/HPEXPIREAT.ts +++ b/packages/client/lib/commands/HPEXPIREAT.ts @@ -4,7 +4,6 @@ import { RedisVariadicArgument, transformPXAT } from './generic-transformers'; import { HashExpiration } from './HEXPIRE'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HPEXPIRETIME.ts b/packages/client/lib/commands/HPEXPIRETIME.ts index cacce25a85f..4d800eecab4 100644 --- a/packages/client/lib/commands/HPEXPIRETIME.ts +++ b/packages/client/lib/commands/HPEXPIRETIME.ts @@ -3,7 +3,6 @@ import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../R import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HPTTL.ts b/packages/client/lib/commands/HPTTL.ts index b9cd54a850d..23b03413900 100644 --- a/packages/client/lib/commands/HPTTL.ts +++ b/packages/client/lib/commands/HPTTL.ts @@ -3,7 +3,6 @@ import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../R import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HRANDFIELD.ts b/packages/client/lib/commands/HRANDFIELD.ts index 3383b94dcb2..88c9c28e276 100644 --- a/packages/client/lib/commands/HRANDFIELD.ts +++ b/packages/client/lib/commands/HRANDFIELD.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('HRANDFIELD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT.ts b/packages/client/lib/commands/HRANDFIELD_COUNT.ts index 62abe97e350..270a10da54e 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('HRANDFIELD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts index aa8ebad1b93..ff163b44923 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts @@ -7,7 +7,6 @@ export type HRandFieldCountWithValuesReply = Array<{ }>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('HRANDFIELD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HSCAN.ts b/packages/client/lib/commands/HSCAN.ts index e1e40663a07..5c6406ec5bd 100644 --- a/packages/client/lib/commands/HSCAN.ts +++ b/packages/client/lib/commands/HSCAN.ts @@ -8,7 +8,6 @@ export interface HScanEntry { } export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HSCAN_NOVALUES.ts b/packages/client/lib/commands/HSCAN_NOVALUES.ts index eff61a7aab0..04018543ca2 100644 --- a/packages/client/lib/commands/HSCAN_NOVALUES.ts +++ b/packages/client/lib/commands/HSCAN_NOVALUES.ts @@ -2,7 +2,6 @@ import { BlobStringReply, Command } from '../RESP/types'; import HSCAN from './HSCAN'; export default { - IS_READ_ONLY: true, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/HSETNX.ts b/packages/client/lib/commands/HSETNX.ts index 130d7cd81d3..0c73b912cce 100644 --- a/packages/client/lib/commands/HSETNX.ts +++ b/packages/client/lib/commands/HSETNX.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, Command, NumberReply } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HSTRLEN.ts b/packages/client/lib/commands/HSTRLEN.ts index 2468747d4c9..05152b66904 100644 --- a/packages/client/lib/commands/HSTRLEN.ts +++ b/packages/client/lib/commands/HSTRLEN.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) { parser.push('HSTRLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/HTTL.ts b/packages/client/lib/commands/HTTL.ts index 4b8fe5d7e85..f3d094eecdf 100644 --- a/packages/client/lib/commands/HTTL.ts +++ b/packages/client/lib/commands/HTTL.ts @@ -3,7 +3,6 @@ import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../R import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/HVALS.ts b/packages/client/lib/commands/HVALS.ts index ab17e47f533..09dea4fc5a0 100644 --- a/packages/client/lib/commands/HVALS.ts +++ b/packages/client/lib/commands/HVALS.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('HVALS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/INFO.ts b/packages/client/lib/commands/INFO.ts index 82cbd497a5b..075af99d2fd 100644 --- a/packages/client/lib/commands/INFO.ts +++ b/packages/client/lib/commands/INFO.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, section?: RedisArgument) { parser.push('INFO'); diff --git a/packages/client/lib/commands/KEYS.ts b/packages/client/lib/commands/KEYS.ts index e516245d2ee..57ac442a6c5 100644 --- a/packages/client/lib/commands/KEYS.ts +++ b/packages/client/lib/commands/KEYS.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, pattern: RedisArgument) { parser.push('KEYS', pattern); }, diff --git a/packages/client/lib/commands/LASTSAVE.ts b/packages/client/lib/commands/LASTSAVE.ts index 447cb95ab6d..297a13a77f2 100644 --- a/packages/client/lib/commands/LASTSAVE.ts +++ b/packages/client/lib/commands/LASTSAVE.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('LASTSAVE'); diff --git a/packages/client/lib/commands/LATENCY_DOCTOR.ts b/packages/client/lib/commands/LATENCY_DOCTOR.ts index 49c830b3065..b4dc8af65a5 100644 --- a/packages/client/lib/commands/LATENCY_DOCTOR.ts +++ b/packages/client/lib/commands/LATENCY_DOCTOR.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('LATENCY', 'DOCTOR'); diff --git a/packages/client/lib/commands/LATENCY_GRAPH.ts b/packages/client/lib/commands/LATENCY_GRAPH.ts index 20251c3cded..3e8b7a9418e 100644 --- a/packages/client/lib/commands/LATENCY_GRAPH.ts +++ b/packages/client/lib/commands/LATENCY_GRAPH.ts @@ -23,7 +23,6 @@ export const LATENCY_EVENTS = { export type LatencyEvent = typeof LATENCY_EVENTS[keyof typeof LATENCY_EVENTS]; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, event: LatencyEvent) { parser.push('LATENCY', 'GRAPH', event); diff --git a/packages/client/lib/commands/LATENCY_HISTOGRAM.ts b/packages/client/lib/commands/LATENCY_HISTOGRAM.ts index 209560b0428..49c8f5cd1d5 100644 --- a/packages/client/lib/commands/LATENCY_HISTOGRAM.ts +++ b/packages/client/lib/commands/LATENCY_HISTOGRAM.ts @@ -12,7 +12,6 @@ type Histogram = Record n; export default { - CACHEABLE: false, IS_READ_ONLY: true, parseCommand(parser: CommandParser, ...commands: string[]) { const args = ['LATENCY', 'HISTOGRAM']; diff --git a/packages/client/lib/commands/LATENCY_HISTORY.ts b/packages/client/lib/commands/LATENCY_HISTORY.ts index 6e0e4d5c560..3bdfcdeac12 100644 --- a/packages/client/lib/commands/LATENCY_HISTORY.ts +++ b/packages/client/lib/commands/LATENCY_HISTORY.ts @@ -21,7 +21,6 @@ export type LatencyEventType = ( ); export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, event: LatencyEventType) { parser.push('LATENCY', 'HISTORY', event); diff --git a/packages/client/lib/commands/LATENCY_LATEST.ts b/packages/client/lib/commands/LATENCY_LATEST.ts index 2ce3efd291c..fe7fffeadfc 100644 --- a/packages/client/lib/commands/LATENCY_LATEST.ts +++ b/packages/client/lib/commands/LATENCY_LATEST.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('LATENCY', 'LATEST'); diff --git a/packages/client/lib/commands/LATENCY_RESET.ts b/packages/client/lib/commands/LATENCY_RESET.ts index b6e9165c5c5..69fcbfa8bc4 100644 --- a/packages/client/lib/commands/LATENCY_RESET.ts +++ b/packages/client/lib/commands/LATENCY_RESET.ts @@ -5,8 +5,6 @@ import { LATENCY_EVENTS, LatencyEvent } from './LATENCY_GRAPH'; export { LATENCY_EVENTS, LatencyEvent }; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, ...events: Array) { const args = ['LATENCY', 'RESET']; if (events.length > 0) { diff --git a/packages/client/lib/commands/LCS.ts b/packages/client/lib/commands/LCS.ts index ed4f11ad990..14e93b48104 100644 --- a/packages/client/lib/commands/LCS.ts +++ b/packages/client/lib/commands/LCS.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key1: RedisArgument, diff --git a/packages/client/lib/commands/LCS_IDX.ts b/packages/client/lib/commands/LCS_IDX.ts index cb0a6b07657..fd840dec9d8 100644 --- a/packages/client/lib/commands/LCS_IDX.ts +++ b/packages/client/lib/commands/LCS_IDX.ts @@ -24,7 +24,6 @@ export type LcsIdxReply = TuplesToMapReply<[ ]>; export default { - IS_READ_ONLY: LCS.IS_READ_ONLY, parseCommand( parser: CommandParser, key1: RedisArgument, diff --git a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts index d2a743983e1..2101293e2cf 100644 --- a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts +++ b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts @@ -15,7 +15,6 @@ export type LcsIdxWithMatchLenReply = TuplesToMapReply<[ ]>; export default { - IS_READ_ONLY: LCS_IDX.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; LCS_IDX.parseCommand(...args); diff --git a/packages/client/lib/commands/LCS_LEN.ts b/packages/client/lib/commands/LCS_LEN.ts index a1f92d914a4..c4e64a11fdc 100644 --- a/packages/client/lib/commands/LCS_LEN.ts +++ b/packages/client/lib/commands/LCS_LEN.ts @@ -2,7 +2,6 @@ import { NumberReply, Command } from '../RESP/types'; import LCS from './LCS'; export default { - IS_READ_ONLY: LCS.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/LINDEX.ts b/packages/client/lib/commands/LINDEX.ts index 6335fc40c2c..29024a69355 100644 --- a/packages/client/lib/commands/LINDEX.ts +++ b/packages/client/lib/commands/LINDEX.ts @@ -2,8 +2,6 @@ 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, index: number) { parser.push('LINDEX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LINSERT.ts b/packages/client/lib/commands/LINSERT.ts index 8a40ac66630..5cd2fe482cf 100644 --- a/packages/client/lib/commands/LINSERT.ts +++ b/packages/client/lib/commands/LINSERT.ts @@ -4,7 +4,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; type LInsertPosition = 'BEFORE' | 'AFTER'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/LLEN.ts b/packages/client/lib/commands/LLEN.ts index 674e022e60d..b4253054408 100644 --- a/packages/client/lib/commands/LLEN.ts +++ b/packages/client/lib/commands/LLEN.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('LLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LMOVE.ts b/packages/client/lib/commands/LMOVE.ts index f3ac847e900..7559e5d27d6 100644 --- a/packages/client/lib/commands/LMOVE.ts +++ b/packages/client/lib/commands/LMOVE.ts @@ -3,7 +3,6 @@ import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/type import { ListSide } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, source: RedisArgument, diff --git a/packages/client/lib/commands/LMPOP.ts b/packages/client/lib/commands/LMPOP.ts index c8095e42e75..606c2a65a9d 100644 --- a/packages/client/lib/commands/LMPOP.ts +++ b/packages/client/lib/commands/LMPOP.ts @@ -23,7 +23,6 @@ export function parseLMPopArguments( export type LMPopArguments = Tail>; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, ...args: LMPopArguments) { parser.push('LMPOP'); parseLMPopArguments(parser, ...args); diff --git a/packages/client/lib/commands/LOLWUT.ts b/packages/client/lib/commands/LOLWUT.ts index 372bf536967..f36db5e28f2 100644 --- a/packages/client/lib/commands/LOLWUT.ts +++ b/packages/client/lib/commands/LOLWUT.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, version?: number, ...optionalArguments: Array) { parser.push('LOLWUT'); if (version) { diff --git a/packages/client/lib/commands/LPOP_COUNT.ts b/packages/client/lib/commands/LPOP_COUNT.ts index 6d9aba42c21..965a91319a2 100644 --- a/packages/client/lib/commands/LPOP_COUNT.ts +++ b/packages/client/lib/commands/LPOP_COUNT.ts @@ -3,7 +3,6 @@ import { RedisArgument, NullReply, ArrayReply, BlobStringReply, Command } from ' import LPOP from './LPOP'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, count: number) { LPOP.parseCommand(parser, key); parser.push(count.toString()) diff --git a/packages/client/lib/commands/LPOS.ts b/packages/client/lib/commands/LPOS.ts index bb05ba6555d..f81e785d2e6 100644 --- a/packages/client/lib/commands/LPOS.ts +++ b/packages/client/lib/commands/LPOS.ts @@ -7,8 +7,6 @@ export interface LPosOptions { } export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/LPOS_COUNT.ts b/packages/client/lib/commands/LPOS_COUNT.ts index e782a2d26ee..ebe61230075 100644 --- a/packages/client/lib/commands/LPOS_COUNT.ts +++ b/packages/client/lib/commands/LPOS_COUNT.ts @@ -3,8 +3,6 @@ import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; import LPOS, { LPosOptions } from './LPOS'; export default { - CACHEABLE: LPOS.CACHEABLE, - IS_READ_ONLY: LPOS.IS_READ_ONLY, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/LRANGE.ts b/packages/client/lib/commands/LRANGE.ts index ab033dd88a4..257dd0dd154 100644 --- a/packages/client/lib/commands/LRANGE.ts +++ b/packages/client/lib/commands/LRANGE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, start: number, stop: number) { parser.push('LRANGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LREM.ts b/packages/client/lib/commands/LREM.ts index bb97e3882e7..18e17531c5e 100644 --- a/packages/client/lib/commands/LREM.ts +++ b/packages/client/lib/commands/LREM.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, count: number, element: RedisArgument) { parser.push('LREM'); parser.pushKey(key); diff --git a/packages/client/lib/commands/LSET.ts b/packages/client/lib/commands/LSET.ts index 0fe646fbb73..f11f4269f59 100644 --- a/packages/client/lib/commands/LSET.ts +++ b/packages/client/lib/commands/LSET.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, index: number, element: RedisArgument) { parser.push('LSET'); parser.pushKey(key); diff --git a/packages/client/lib/commands/MEMORY_DOCTOR.ts b/packages/client/lib/commands/MEMORY_DOCTOR.ts index 3a2d808db10..f67a5607d9d 100644 --- a/packages/client/lib/commands/MEMORY_DOCTOR.ts +++ b/packages/client/lib/commands/MEMORY_DOCTOR.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('MEMORY', 'DOCTOR'); diff --git a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts index af6b5db3347..a46bbcc28f9 100644 --- a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts +++ b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('MEMORY', 'MALLOC-STATS'); diff --git a/packages/client/lib/commands/MEMORY_PURGE.ts b/packages/client/lib/commands/MEMORY_PURGE.ts index bbd02890786..14fd9555b11 100644 --- a/packages/client/lib/commands/MEMORY_PURGE.ts +++ b/packages/client/lib/commands/MEMORY_PURGE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser) { parser.push('MEMORY', 'PURGE'); }, diff --git a/packages/client/lib/commands/MEMORY_STATS.ts b/packages/client/lib/commands/MEMORY_STATS.ts index dd5eca7000a..c397d3ac9d0 100644 --- a/packages/client/lib/commands/MEMORY_STATS.ts +++ b/packages/client/lib/commands/MEMORY_STATS.ts @@ -36,7 +36,6 @@ export type MemoryStatsReply = TuplesToMapReply<[ ]>; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('MEMORY', 'STATS'); diff --git a/packages/client/lib/commands/MEMORY_USAGE.ts b/packages/client/lib/commands/MEMORY_USAGE.ts index ff5336d32e5..9996e58ea53 100644 --- a/packages/client/lib/commands/MEMORY_USAGE.ts +++ b/packages/client/lib/commands/MEMORY_USAGE.ts @@ -10,7 +10,6 @@ export interface MemoryUsageOptions { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, options?: MemoryUsageOptions) { parser.push('MEMORY', 'USAGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/MGET.ts b/packages/client/lib/commands/MGET.ts index ce1e9ba7781..4f3e457b123 100644 --- a/packages/client/lib/commands/MGET.ts +++ b/packages/client/lib/commands/MGET.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, keys: Array) { parser.push('MGET'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/MIGRATE.ts b/packages/client/lib/commands/MIGRATE.ts index ee03b97acd8..cf3e691d8a5 100644 --- a/packages/client/lib/commands/MIGRATE.ts +++ b/packages/client/lib/commands/MIGRATE.ts @@ -9,7 +9,6 @@ export interface MigrateOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, host: RedisArgument, diff --git a/packages/client/lib/commands/MODULE_LIST.ts b/packages/client/lib/commands/MODULE_LIST.ts index 791c1a53e54..2804fb0b518 100644 --- a/packages/client/lib/commands/MODULE_LIST.ts +++ b/packages/client/lib/commands/MODULE_LIST.ts @@ -46,7 +46,6 @@ function transformModuleListReply(reply: Array) { } export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('MODULE', 'LIST'); diff --git a/packages/client/lib/commands/MODULE_LOAD.ts b/packages/client/lib/commands/MODULE_LOAD.ts index ceb90c1c353..1c29a7d5c82 100644 --- a/packages/client/lib/commands/MODULE_LOAD.ts +++ b/packages/client/lib/commands/MODULE_LOAD.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, path: RedisArgument, moduleArguments?: Array) { parser.push('MODULE', 'LOAD', path); diff --git a/packages/client/lib/commands/MODULE_UNLOAD.ts b/packages/client/lib/commands/MODULE_UNLOAD.ts index 1acc359d0d4..98c2884bc2e 100644 --- a/packages/client/lib/commands/MODULE_UNLOAD.ts +++ b/packages/client/lib/commands/MODULE_UNLOAD.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, name: RedisArgument) { parser.push('MODULE', 'UNLOAD', name); }, diff --git a/packages/client/lib/commands/MSET.ts b/packages/client/lib/commands/MSET.ts index f761854f09c..173e4f42a66 100644 --- a/packages/client/lib/commands/MSET.ts +++ b/packages/client/lib/commands/MSET.ts @@ -32,7 +32,6 @@ export function parseMSetArguments(parser: CommandParser, toSet: MSetArguments) } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, toSet: MSetArguments) { parser.push('MSET'); return parseMSetArguments(parser, toSet); diff --git a/packages/client/lib/commands/MSETNX.ts b/packages/client/lib/commands/MSETNX.ts index 3ecce9525de..031520b5999 100644 --- a/packages/client/lib/commands/MSETNX.ts +++ b/packages/client/lib/commands/MSETNX.ts @@ -3,7 +3,6 @@ import { SimpleStringReply, Command } from '../RESP/types'; import { MSetArguments, parseMSetArguments } from './MSET'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, toSet: MSetArguments) { parser.push('MSETNX'); return parseMSetArguments(parser, toSet); diff --git a/packages/client/lib/commands/OBJECT_ENCODING.ts b/packages/client/lib/commands/OBJECT_ENCODING.ts index 3a795f6fb64..90477059d85 100644 --- a/packages/client/lib/commands/OBJECT_ENCODING.ts +++ b/packages/client/lib/commands/OBJECT_ENCODING.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('OBJECT', 'ENCODING'); parser.pushKey(key); diff --git a/packages/client/lib/commands/OBJECT_FREQ.ts b/packages/client/lib/commands/OBJECT_FREQ.ts index dad1124b101..0b49d39b580 100644 --- a/packages/client/lib/commands/OBJECT_FREQ.ts +++ b/packages/client/lib/commands/OBJECT_FREQ.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('OBJECT', 'FREQ'); parser.pushKey(key); diff --git a/packages/client/lib/commands/OBJECT_IDLETIME.ts b/packages/client/lib/commands/OBJECT_IDLETIME.ts index 2bd32f4e65d..dca0c7c47c2 100644 --- a/packages/client/lib/commands/OBJECT_IDLETIME.ts +++ b/packages/client/lib/commands/OBJECT_IDLETIME.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('OBJECT', 'IDLETIME'); parser.pushKey(key); diff --git a/packages/client/lib/commands/OBJECT_REFCOUNT.ts b/packages/client/lib/commands/OBJECT_REFCOUNT.ts index 4bee4dea60c..be2401bd7bd 100644 --- a/packages/client/lib/commands/OBJECT_REFCOUNT.ts +++ b/packages/client/lib/commands/OBJECT_REFCOUNT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('OBJECT', 'REFCOUNT'); parser.pushKey(key); diff --git a/packages/client/lib/commands/PEXPIRE.ts b/packages/client/lib/commands/PEXPIRE.ts index 4053f46c8e2..de0002e9cf0 100644 --- a/packages/client/lib/commands/PEXPIRE.ts +++ b/packages/client/lib/commands/PEXPIRE.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/PEXPIREAT.ts b/packages/client/lib/commands/PEXPIREAT.ts index e454447c970..7db96ddb881 100644 --- a/packages/client/lib/commands/PEXPIREAT.ts +++ b/packages/client/lib/commands/PEXPIREAT.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformPXAT } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/PEXPIRETIME.ts b/packages/client/lib/commands/PEXPIRETIME.ts index b5d04eae230..df0d79cb314 100644 --- a/packages/client/lib/commands/PEXPIRETIME.ts +++ b/packages/client/lib/commands/PEXPIRETIME.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('PEXPIRETIME'); parser.pushKey(key); diff --git a/packages/client/lib/commands/PFADD.ts b/packages/client/lib/commands/PFADD.ts index 94c2d1d5ae6..30b018f4a6f 100644 --- a/packages/client/lib/commands/PFADD.ts +++ b/packages/client/lib/commands/PFADD.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, element?: RedisVariadicArgument) { parser.push('PFADD') parser.pushKey(key); diff --git a/packages/client/lib/commands/PFCOUNT.ts b/packages/client/lib/commands/PFCOUNT.ts index 46d2e2ed71f..f508c9d3994 100644 --- a/packages/client/lib/commands/PFCOUNT.ts +++ b/packages/client/lib/commands/PFCOUNT.ts @@ -3,7 +3,6 @@ import { NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('PFCOUNT'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/PING.ts b/packages/client/lib/commands/PING.ts index 26807eeeba4..b9ef51407d3 100644 --- a/packages/client/lib/commands/PING.ts +++ b/packages/client/lib/commands/PING.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, message?: RedisArgument) { parser.push('PING'); diff --git a/packages/client/lib/commands/PTTL.ts b/packages/client/lib/commands/PTTL.ts index 5717c51179f..3ca9718eccc 100644 --- a/packages/client/lib/commands/PTTL.ts +++ b/packages/client/lib/commands/PTTL.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('PTTL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/PUBLISH.ts b/packages/client/lib/commands/PUBLISH.ts index 1b38dde15d1..bb5b0a49198 100644 --- a/packages/client/lib/commands/PUBLISH.ts +++ b/packages/client/lib/commands/PUBLISH.ts @@ -2,9 +2,7 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - IS_FORWARD_COMMAND: true, parseCommand(parser: CommandParser, channel: RedisArgument, message: RedisArgument) { parser.push('PUBLISH', channel, message); }, diff --git a/packages/client/lib/commands/PUBSUB_CHANNELS.ts b/packages/client/lib/commands/PUBSUB_CHANNELS.ts index 0f53c79a78a..94013f2b452 100644 --- a/packages/client/lib/commands/PUBSUB_CHANNELS.ts +++ b/packages/client/lib/commands/PUBSUB_CHANNELS.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, pattern?: RedisArgument) { parser.push('PUBSUB', 'CHANNELS'); diff --git a/packages/client/lib/commands/PUBSUB_NUMPAT.ts b/packages/client/lib/commands/PUBSUB_NUMPAT.ts index 173446e023b..bec270b2cc1 100644 --- a/packages/client/lib/commands/PUBSUB_NUMPAT.ts +++ b/packages/client/lib/commands/PUBSUB_NUMPAT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('PUBSUB', 'NUMPAT'); diff --git a/packages/client/lib/commands/PUBSUB_NUMSUB.ts b/packages/client/lib/commands/PUBSUB_NUMSUB.ts index 845f587a834..49717ff3253 100644 --- a/packages/client/lib/commands/PUBSUB_NUMSUB.ts +++ b/packages/client/lib/commands/PUBSUB_NUMSUB.ts @@ -3,7 +3,6 @@ import { ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from ' import { RedisVariadicArgument } from './generic-transformers'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, channels?: RedisVariadicArgument) { parser.push('PUBSUB', 'NUMSUB'); diff --git a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts index 46ac2005fc3..f32bce69c81 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, pattern?: RedisArgument) { parser.push('PUBSUB', 'SHARDCHANNELS'); diff --git a/packages/client/lib/commands/RANDOMKEY.ts b/packages/client/lib/commands/RANDOMKEY.ts index 97d040a0d1d..adb25f6610c 100644 --- a/packages/client/lib/commands/RANDOMKEY.ts +++ b/packages/client/lib/commands/RANDOMKEY.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('RANDOMKEY'); }, diff --git a/packages/client/lib/commands/READONLY.ts b/packages/client/lib/commands/READONLY.ts index ce3300c5321..9b7016d2d05 100644 --- a/packages/client/lib/commands/READONLY.ts +++ b/packages/client/lib/commands/READONLY.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('READONLY'); diff --git a/packages/client/lib/commands/READWRITE.ts b/packages/client/lib/commands/READWRITE.ts index 7d9d8c7e00a..054b9a52c45 100644 --- a/packages/client/lib/commands/READWRITE.ts +++ b/packages/client/lib/commands/READWRITE.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('READWRITE'); diff --git a/packages/client/lib/commands/RENAME.ts b/packages/client/lib/commands/RENAME.ts index 245851ca31a..4cb0c4346e1 100644 --- a/packages/client/lib/commands/RENAME.ts +++ b/packages/client/lib/commands/RENAME.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, newKey: RedisArgument) { parser.push('RENAME'); parser.pushKeys([key, newKey]); diff --git a/packages/client/lib/commands/RENAMENX.ts b/packages/client/lib/commands/RENAMENX.ts index 0e8d4f73cf3..77d827d0098 100644 --- a/packages/client/lib/commands/RENAMENX.ts +++ b/packages/client/lib/commands/RENAMENX.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, newKey: RedisArgument) { parser.push('RENAMENX'); parser.pushKeys([key, newKey]); diff --git a/packages/client/lib/commands/REPLICAOF.ts b/packages/client/lib/commands/REPLICAOF.ts index c4b09bc4fb8..45a0c9dc5ab 100644 --- a/packages/client/lib/commands/REPLICAOF.ts +++ b/packages/client/lib/commands/REPLICAOF.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, host: string, port: number) { parser.push('REPLICAOF', host, port.toString()); }, diff --git a/packages/client/lib/commands/RESTORE-ASKING.ts b/packages/client/lib/commands/RESTORE-ASKING.ts index e8de532b6a4..e7aa91435f1 100644 --- a/packages/client/lib/commands/RESTORE-ASKING.ts +++ b/packages/client/lib/commands/RESTORE-ASKING.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('RESTORE-ASKING'); }, diff --git a/packages/client/lib/commands/RESTORE.ts b/packages/client/lib/commands/RESTORE.ts index 88d41e9aee5..002069f24f1 100644 --- a/packages/client/lib/commands/RESTORE.ts +++ b/packages/client/lib/commands/RESTORE.ts @@ -17,7 +17,6 @@ export interface RestoreOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ROLE.ts b/packages/client/lib/commands/ROLE.ts index 71079f28ccc..8635680f0b0 100644 --- a/packages/client/lib/commands/ROLE.ts +++ b/packages/client/lib/commands/ROLE.ts @@ -35,7 +35,6 @@ type SentinelRole = [ type Role = TuplesReply; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('ROLE'); diff --git a/packages/client/lib/commands/SAVE.ts b/packages/client/lib/commands/SAVE.ts index ee78884083c..6a9dbecc39f 100644 --- a/packages/client/lib/commands/SAVE.ts +++ b/packages/client/lib/commands/SAVE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('SAVE'); }, diff --git a/packages/client/lib/commands/SCAN.ts b/packages/client/lib/commands/SCAN.ts index c07436326c7..3a86cf9a138 100644 --- a/packages/client/lib/commands/SCAN.ts +++ b/packages/client/lib/commands/SCAN.ts @@ -70,8 +70,6 @@ export interface ScanOptions extends ScanCommonOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, cursor: RedisArgument, options?: ScanOptions) { parser.push('SCAN'); parseScanArguments(parser, cursor, options); diff --git a/packages/client/lib/commands/SCARD.ts b/packages/client/lib/commands/SCARD.ts index 61d4792d996..d9d0c5d84ea 100644 --- a/packages/client/lib/commands/SCARD.ts +++ b/packages/client/lib/commands/SCARD.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('SCARD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SCRIPT_DEBUG.ts b/packages/client/lib/commands/SCRIPT_DEBUG.ts index b0d3079068f..ac7be498124 100644 --- a/packages/client/lib/commands/SCRIPT_DEBUG.ts +++ b/packages/client/lib/commands/SCRIPT_DEBUG.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, mode: 'YES' | 'SYNC' | 'NO') { parser.push('SCRIPT', 'DEBUG', mode); diff --git a/packages/client/lib/commands/SCRIPT_EXISTS.ts b/packages/client/lib/commands/SCRIPT_EXISTS.ts index b0f6cbe2275..2e414e78741 100644 --- a/packages/client/lib/commands/SCRIPT_EXISTS.ts +++ b/packages/client/lib/commands/SCRIPT_EXISTS.ts @@ -3,7 +3,6 @@ import { ArrayReply, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser, sha1: RedisVariadicArgument) { parser.push('SCRIPT', 'EXISTS'); diff --git a/packages/client/lib/commands/SCRIPT_FLUSH.ts b/packages/client/lib/commands/SCRIPT_FLUSH.ts index 1e05a619bad..24224debde2 100644 --- a/packages/client/lib/commands/SCRIPT_FLUSH.ts +++ b/packages/client/lib/commands/SCRIPT_FLUSH.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, mode?: 'ASYNC' | 'SYNC') { parser.push('SCRIPT', 'FLUSH'); diff --git a/packages/client/lib/commands/SCRIPT_KILL.ts b/packages/client/lib/commands/SCRIPT_KILL.ts index 26953506235..d99b805dd62 100644 --- a/packages/client/lib/commands/SCRIPT_KILL.ts +++ b/packages/client/lib/commands/SCRIPT_KILL.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('SCRIPT', 'KILL'); }, diff --git a/packages/client/lib/commands/SCRIPT_LOAD.ts b/packages/client/lib/commands/SCRIPT_LOAD.ts index 58f7c00dfcd..6eeedc00853 100644 --- a/packages/client/lib/commands/SCRIPT_LOAD.ts +++ b/packages/client/lib/commands/SCRIPT_LOAD.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command, RedisArgument } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, script: RedisArgument) { parser.push('SCRIPT', 'LOAD', script); }, diff --git a/packages/client/lib/commands/SDIFF.ts b/packages/client/lib/commands/SDIFF.ts index bd78edc93db..5330bac3dd0 100644 --- a/packages/client/lib/commands/SDIFF.ts +++ b/packages/client/lib/commands/SDIFF.ts @@ -3,8 +3,6 @@ import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('SDIFF'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/SETBIT.ts b/packages/client/lib/commands/SETBIT.ts index 5cd29260071..00484f2391a 100644 --- a/packages/client/lib/commands/SETBIT.ts +++ b/packages/client/lib/commands/SETBIT.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { BitValue } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, offset: number, value: BitValue) { parser.push('SETBIT'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SHUTDOWN.ts b/packages/client/lib/commands/SHUTDOWN.ts index efea081e8eb..8a8ea1bbab1 100644 --- a/packages/client/lib/commands/SHUTDOWN.ts +++ b/packages/client/lib/commands/SHUTDOWN.ts @@ -17,8 +17,6 @@ export interface ShutdownOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, options?: ShutdownOptions) { parser.push('SHUTDOWN'); diff --git a/packages/client/lib/commands/SINTER.ts b/packages/client/lib/commands/SINTER.ts index 19ecdbb41ca..e9ad7c2c76f 100644 --- a/packages/client/lib/commands/SINTER.ts +++ b/packages/client/lib/commands/SINTER.ts @@ -3,8 +3,6 @@ import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('SINTER'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/SINTERCARD.ts b/packages/client/lib/commands/SINTERCARD.ts index f3b80e82dd3..88ef66e4b92 100644 --- a/packages/client/lib/commands/SINTERCARD.ts +++ b/packages/client/lib/commands/SINTERCARD.ts @@ -12,7 +12,6 @@ export interface SInterCardOptions { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, keys: RedisVariadicArgument, options?: SInterCardOptions | number) { parser.push('SINTERCARD'); parser.pushKeysLength(keys); diff --git a/packages/client/lib/commands/SINTERSTORE.ts b/packages/client/lib/commands/SINTERSTORE.ts index 06db0af9cb0..de1ead71f6f 100644 --- a/packages/client/lib/commands/SINTERSTORE.ts +++ b/packages/client/lib/commands/SINTERSTORE.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) { parser.push('SINTERSTORE'); parser.pushKey(destination) diff --git a/packages/client/lib/commands/SISMEMBER.ts b/packages/client/lib/commands/SISMEMBER.ts index 6192ca2605f..827f16a9c3d 100644 --- a/packages/client/lib/commands/SISMEMBER.ts +++ b/packages/client/lib/commands/SISMEMBER.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { parser.push('SISMEMBER'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SMEMBERS.ts b/packages/client/lib/commands/SMEMBERS.ts index 6d018e999f4..5e1f69a0b62 100644 --- a/packages/client/lib/commands/SMEMBERS.ts +++ b/packages/client/lib/commands/SMEMBERS.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, SetReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('SMEMBERS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SMISMEMBER.ts b/packages/client/lib/commands/SMISMEMBER.ts index f0f3a143c7f..7f4aa430b43 100644 --- a/packages/client/lib/commands/SMISMEMBER.ts +++ b/packages/client/lib/commands/SMISMEMBER.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, members: Array) { parser.push('SMISMEMBER'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SMOVE.ts b/packages/client/lib/commands/SMOVE.ts index d87eeefdfbf..1cc6a436c75 100644 --- a/packages/client/lib/commands/SMOVE.ts +++ b/packages/client/lib/commands/SMOVE.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, member: RedisArgument) { parser.push('SMOVE'); parser.pushKeys([source, destination]); diff --git a/packages/client/lib/commands/SORT.ts b/packages/client/lib/commands/SORT.ts index 43790151470..e4f0da6e6cb 100644 --- a/packages/client/lib/commands/SORT.ts +++ b/packages/client/lib/commands/SORT.ts @@ -67,7 +67,6 @@ export function parseSortArguments( } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, options?: SortOptions) { parser.push('SORT'); parseSortArguments(parser, key, options); diff --git a/packages/client/lib/commands/SORT_RO.ts b/packages/client/lib/commands/SORT_RO.ts index 9901907c223..b6512e9065d 100644 --- a/packages/client/lib/commands/SORT_RO.ts +++ b/packages/client/lib/commands/SORT_RO.ts @@ -2,7 +2,6 @@ import { Command } from '../RESP/types'; import SORT, { parseSortArguments } from './SORT'; export default { - IS_READ_ONLY: true, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/SORT_STORE.ts b/packages/client/lib/commands/SORT_STORE.ts index 4d0d9b99dfc..eecd0c6a5e7 100644 --- a/packages/client/lib/commands/SORT_STORE.ts +++ b/packages/client/lib/commands/SORT_STORE.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import SORT, { SortOptions } from './SORT'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, options?: SortOptions) { SORT.parseCommand(parser, source, options); parser.push('STORE'); diff --git a/packages/client/lib/commands/SPOP.ts b/packages/client/lib/commands/SPOP.ts index 38f40989e63..ac907b0cdda 100644 --- a/packages/client/lib/commands/SPOP.ts +++ b/packages/client/lib/commands/SPOP.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('SPOP'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SPOP_COUNT.ts b/packages/client/lib/commands/SPOP_COUNT.ts index 62acc621ac9..5fcab402d73 100644 --- a/packages/client/lib/commands/SPOP_COUNT.ts +++ b/packages/client/lib/commands/SPOP_COUNT.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, Command, ArrayReply } from '../RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('SPOP'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SPUBLISH.ts b/packages/client/lib/commands/SPUBLISH.ts index d664c034ae4..ca8734605c7 100644 --- a/packages/client/lib/commands/SPUBLISH.ts +++ b/packages/client/lib/commands/SPUBLISH.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, channel: RedisArgument, message: RedisArgument) { parser.push('SPUBLISH'); // The channel routes the command to the correct shard (like a key) but must NOT be diff --git a/packages/client/lib/commands/SRANDMEMBER.ts b/packages/client/lib/commands/SRANDMEMBER.ts index 4285f7aa17c..50277094e97 100644 --- a/packages/client/lib/commands/SRANDMEMBER.ts +++ b/packages/client/lib/commands/SRANDMEMBER.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('SRANDMEMBER') parser.pushKey(key); diff --git a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts index dd72245c3b3..876308bf76f 100644 --- a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts +++ b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts @@ -3,7 +3,6 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ import SRANDMEMBER from './SRANDMEMBER'; export default { - IS_READ_ONLY: SRANDMEMBER.IS_READ_ONLY, parseCommand(parser: CommandParser, key: RedisArgument, count: number) { SRANDMEMBER.parseCommand(parser, key); parser.push(count.toString()); diff --git a/packages/client/lib/commands/SREM.ts b/packages/client/lib/commands/SREM.ts index 75053474cce..6c79eb7b57d 100644 --- a/packages/client/lib/commands/SREM.ts +++ b/packages/client/lib/commands/SREM.ts @@ -3,7 +3,6 @@ import { NumberReply, Command, RedisArgument } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, members: RedisVariadicArgument) { parser.push('SREM'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SSCAN.ts b/packages/client/lib/commands/SSCAN.ts index 43971519bad..aeb27c74412 100644 --- a/packages/client/lib/commands/SSCAN.ts +++ b/packages/client/lib/commands/SSCAN.ts @@ -3,7 +3,6 @@ import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; import { ScanCommonOptions, parseScanArguments} from './SCAN'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/STRLEN.ts b/packages/client/lib/commands/STRLEN.ts index 34e0430fc9e..a72769165c5 100644 --- a/packages/client/lib/commands/STRLEN.ts +++ b/packages/client/lib/commands/STRLEN.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('STRLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/SUNION.ts b/packages/client/lib/commands/SUNION.ts index 3d9a5954a7c..4552fc36528 100644 --- a/packages/client/lib/commands/SUNION.ts +++ b/packages/client/lib/commands/SUNION.ts @@ -3,8 +3,6 @@ import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('SUNION'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/SUNIONSTORE.ts b/packages/client/lib/commands/SUNIONSTORE.ts index e2f43ecb1c8..4b5689d4855 100644 --- a/packages/client/lib/commands/SUNIONSTORE.ts +++ b/packages/client/lib/commands/SUNIONSTORE.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) { parser.push('SUNIONSTORE'); parser.pushKey(destination); diff --git a/packages/client/lib/commands/SWAPDB.ts b/packages/client/lib/commands/SWAPDB.ts index e59c75715cd..313f0055841 100644 --- a/packages/client/lib/commands/SWAPDB.ts +++ b/packages/client/lib/commands/SWAPDB.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, index1: number, index2: number) { parser.push('SWAPDB', index1.toString(), index2.toString()); }, diff --git a/packages/client/lib/commands/TIME.ts b/packages/client/lib/commands/TIME.ts index b25af710e1c..2dcc8adadd7 100644 --- a/packages/client/lib/commands/TIME.ts +++ b/packages/client/lib/commands/TIME.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('TIME'); diff --git a/packages/client/lib/commands/TOUCH.ts b/packages/client/lib/commands/TOUCH.ts index c765c9f8347..10f27e33ab9 100644 --- a/packages/client/lib/commands/TOUCH.ts +++ b/packages/client/lib/commands/TOUCH.ts @@ -3,7 +3,7 @@ import { NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: false, + CACHEABLE: false, parseCommand(parser: CommandParser, key: RedisVariadicArgument) { parser.push('TOUCH'); parser.pushKeys(key); diff --git a/packages/client/lib/commands/TTL.ts b/packages/client/lib/commands/TTL.ts index 8420089fcb9..b8f420ade76 100644 --- a/packages/client/lib/commands/TTL.ts +++ b/packages/client/lib/commands/TTL.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TTL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/TYPE.ts b/packages/client/lib/commands/TYPE.ts index ffc592994db..4d2f5fd1bd4 100644 --- a/packages/client/lib/commands/TYPE.ts +++ b/packages/client/lib/commands/TYPE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('TYPE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/UNLINK.ts b/packages/client/lib/commands/UNLINK.ts index 14d1e700277..e2396d8cea0 100644 --- a/packages/client/lib/commands/UNLINK.ts +++ b/packages/client/lib/commands/UNLINK.ts @@ -3,7 +3,6 @@ import { NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('UNLINK'); parser.pushKeys(keys); diff --git a/packages/client/lib/commands/VCARD.ts b/packages/client/lib/commands/VCARD.ts index 5ae86cdbfd2..b4ea49dfbb4 100644 --- a/packages/client/lib/commands/VCARD.ts +++ b/packages/client/lib/commands/VCARD.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('VCARD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/VDIM.ts b/packages/client/lib/commands/VDIM.ts index d0b26a06575..8f0d6eaf3d2 100644 --- a/packages/client/lib/commands/VDIM.ts +++ b/packages/client/lib/commands/VDIM.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('VDIM'); parser.pushKey(key); diff --git a/packages/client/lib/commands/VEMB.ts b/packages/client/lib/commands/VEMB.ts index 3fd6ba3fc34..db724536a61 100644 --- a/packages/client/lib/commands/VEMB.ts +++ b/packages/client/lib/commands/VEMB.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '../RESP/types'; import { transformDoubleArrayReply } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { parser.push('VEMB'); parser.pushKey(key); diff --git a/packages/client/lib/commands/VEMB_RAW.ts b/packages/client/lib/commands/VEMB_RAW.ts index d5d106cbcac..74bce9cf784 100644 --- a/packages/client/lib/commands/VEMB_RAW.ts +++ b/packages/client/lib/commands/VEMB_RAW.ts @@ -36,7 +36,6 @@ const transformRawVembReply = { }; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/VGETATTR.ts b/packages/client/lib/commands/VGETATTR.ts index 55239da1590..659c08cbfa9 100644 --- a/packages/client/lib/commands/VGETATTR.ts +++ b/packages/client/lib/commands/VGETATTR.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '../RESP/types'; import { transformRedisJsonNullReply } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { parser.push('VGETATTR'); parser.pushKey(key); diff --git a/packages/client/lib/commands/VINFO.ts b/packages/client/lib/commands/VINFO.ts index 1b719d338f1..bd456081ffd 100644 --- a/packages/client/lib/commands/VINFO.ts +++ b/packages/client/lib/commands/VINFO.ts @@ -11,7 +11,6 @@ export type VInfoReplyMap = TuplesToMapReply<[ ]>; export default { - IS_READ_ONLY: true, /** * Retrieve metadata and internal details about a vector set, including size, dimensions, quantization type, and graph structure * diff --git a/packages/client/lib/commands/VLINKS.ts b/packages/client/lib/commands/VLINKS.ts index 69e0c5d3eb0..71231af25ba 100644 --- a/packages/client/lib/commands/VLINKS.ts +++ b/packages/client/lib/commands/VLINKS.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) { parser.push('VLINKS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/VLINKS_WITHSCORES.ts b/packages/client/lib/commands/VLINKS_WITHSCORES.ts index 48e7c5b4660..e1c1fc7cbf4 100644 --- a/packages/client/lib/commands/VLINKS_WITHSCORES.ts +++ b/packages/client/lib/commands/VLINKS_WITHSCORES.ts @@ -23,7 +23,6 @@ function transformVLinksWithScoresReply(reply: Array>): A } export default { - IS_READ_ONLY: VLINKS.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/VRANDMEMBER.ts b/packages/client/lib/commands/VRANDMEMBER.ts index 84a3543d1aa..5a525cf06a8 100644 --- a/packages/client/lib/commands/VRANDMEMBER.ts +++ b/packages/client/lib/commands/VRANDMEMBER.ts @@ -2,7 +2,12 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, ArrayReply, Command, NullReply } from '../RESP/types'; export default { - IS_READ_ONLY: true, + // The server metadata makes VRANDMEMBER look cacheable (readonly + keyed) + // because, unlike its siblings (SRANDMEMBER/HRANDFIELD/ZRANDMEMBER), it is + // not tagged with the `nondeterministic_output` tip yet. Caching a random + // pick would freeze it until the key changes — override until the server + // tags it. + CACHEABLE: false, parseCommand(parser: CommandParser, key: RedisArgument, count?: number) { parser.push('VRANDMEMBER'); parser.pushKey(key); diff --git a/packages/client/lib/commands/VRANGE.ts b/packages/client/lib/commands/VRANGE.ts index 3aaf5e1ec1e..a3efc24ef0e 100644 --- a/packages/client/lib/commands/VRANGE.ts +++ b/packages/client/lib/commands/VRANGE.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/VSIM.ts b/packages/client/lib/commands/VSIM.ts index d8fba1e843c..cb19484ceef 100644 --- a/packages/client/lib/commands/VSIM.ts +++ b/packages/client/lib/commands/VSIM.ts @@ -13,7 +13,6 @@ export interface VSimOptions { } export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/VSIM_WITHSCORES.ts b/packages/client/lib/commands/VSIM_WITHSCORES.ts index d5dd4d638e0..7beca97f825 100644 --- a/packages/client/lib/commands/VSIM_WITHSCORES.ts +++ b/packages/client/lib/commands/VSIM_WITHSCORES.ts @@ -10,7 +10,6 @@ import { transformDoubleReply } from './generic-transformers'; import VSIM from './VSIM'; export default { - IS_READ_ONLY: VSIM.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/WAIT.ts b/packages/client/lib/commands/WAIT.ts index df45a12373d..765f6cf75d1 100644 --- a/packages/client/lib/commands/WAIT.ts +++ b/packages/client/lib/commands/WAIT.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, numberOfReplicas: number, timeout: number) { parser.push('WAIT', numberOfReplicas.toString(), timeout.toString()); }, diff --git a/packages/client/lib/commands/XACK.ts b/packages/client/lib/commands/XACK.ts index 2500134f1c8..304a669a9c1 100644 --- a/packages/client/lib/commands/XACK.ts +++ b/packages/client/lib/commands/XACK.ts @@ -3,7 +3,6 @@ import { NumberReply, Command, RedisArgument } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument, id: RedisVariadicArgument) { parser.push('XACK'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XACKDEL.ts b/packages/client/lib/commands/XACKDEL.ts index 2afd5206fc6..0b30b1e3d07 100644 --- a/packages/client/lib/commands/XACKDEL.ts +++ b/packages/client/lib/commands/XACKDEL.ts @@ -10,7 +10,6 @@ import { RedisVariadicArgument } from "./generic-transformers"; * Acknowledges and deletes one or multiple messages for a stream consumer group */ export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XADD.ts b/packages/client/lib/commands/XADD.ts index 4d5873ea26f..9d6aefd3d51 100644 --- a/packages/client/lib/commands/XADD.ts +++ b/packages/client/lib/commands/XADD.ts @@ -106,7 +106,6 @@ export function parseXAddArguments( } export default { - IS_READ_ONLY: false, parseCommand(...args: Tail>) { return parseXAddArguments(undefined, ...args); }, diff --git a/packages/client/lib/commands/XADD_NOMKSTREAM.ts b/packages/client/lib/commands/XADD_NOMKSTREAM.ts index 6184de8aa55..f5f312256e0 100644 --- a/packages/client/lib/commands/XADD_NOMKSTREAM.ts +++ b/packages/client/lib/commands/XADD_NOMKSTREAM.ts @@ -6,7 +6,6 @@ import { parseXAddArguments } from './XADD'; * Command for adding entries to an existing stream without creating it if it doesn't exist */ export default { - IS_READ_ONLY: false, parseCommand(...args: Tail>) { return parseXAddArguments('NOMKSTREAM', ...args); }, diff --git a/packages/client/lib/commands/XAUTOCLAIM.ts b/packages/client/lib/commands/XAUTOCLAIM.ts index ebd884a89cf..b338f5af57e 100644 --- a/packages/client/lib/commands/XAUTOCLAIM.ts +++ b/packages/client/lib/commands/XAUTOCLAIM.ts @@ -25,7 +25,6 @@ export type XAutoClaimRawReply = TuplesReply<[ ]>; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts index 0dca8620689..2f36b3a69eb 100644 --- a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts +++ b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts @@ -15,7 +15,6 @@ type XAutoClaimJustIdRawReply = TuplesReply<[ ]>; export default { - IS_READ_ONLY: XAUTOCLAIM.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; XAUTOCLAIM.parseCommand(...args); diff --git a/packages/client/lib/commands/XCFGSET.ts b/packages/client/lib/commands/XCFGSET.ts index 2aa2de641d5..e88cf448806 100644 --- a/packages/client/lib/commands/XCFGSET.ts +++ b/packages/client/lib/commands/XCFGSET.ts @@ -29,7 +29,6 @@ export interface XCfgSetOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XCLAIM.ts b/packages/client/lib/commands/XCLAIM.ts index af79d74e8aa..d28014378ee 100644 --- a/packages/client/lib/commands/XCLAIM.ts +++ b/packages/client/lib/commands/XCLAIM.ts @@ -20,7 +20,6 @@ export interface XClaimOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XCLAIM_JUSTID.ts b/packages/client/lib/commands/XCLAIM_JUSTID.ts index cd6052e1534..d02bb43723f 100644 --- a/packages/client/lib/commands/XCLAIM_JUSTID.ts +++ b/packages/client/lib/commands/XCLAIM_JUSTID.ts @@ -5,7 +5,6 @@ import XCLAIM from './XCLAIM'; * Command variant for XCLAIM that returns only message IDs */ export default { - IS_READ_ONLY: XCLAIM.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; XCLAIM.parseCommand(...args); diff --git a/packages/client/lib/commands/XDEL.ts b/packages/client/lib/commands/XDEL.ts index db7fb4fbfaa..424bb5f8e44 100644 --- a/packages/client/lib/commands/XDEL.ts +++ b/packages/client/lib/commands/XDEL.ts @@ -6,7 +6,6 @@ import { RedisVariadicArgument } from './generic-transformers'; * Command for removing messages from a stream */ export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, id: RedisVariadicArgument) { parser.push('XDEL'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XDELEX.ts b/packages/client/lib/commands/XDELEX.ts index b41f52b0ae6..0a396baf308 100644 --- a/packages/client/lib/commands/XDELEX.ts +++ b/packages/client/lib/commands/XDELEX.ts @@ -10,7 +10,6 @@ import { RedisVariadicArgument } from "./generic-transformers"; * Deletes one or multiple entries from the stream */ export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XGROUP_CREATE.ts b/packages/client/lib/commands/XGROUP_CREATE.ts index fbbaf1b67be..99934dfd8d3 100644 --- a/packages/client/lib/commands/XGROUP_CREATE.ts +++ b/packages/client/lib/commands/XGROUP_CREATE.ts @@ -16,7 +16,6 @@ export interface XGroupCreateOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts index 37af9a181ae..5a09705cf24 100644 --- a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts +++ b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts @@ -5,7 +5,6 @@ import { RedisArgument, Command, NumberReply } from '../RESP/types'; * Command for creating a new consumer in a consumer group */ export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts index 37bfb028dc4..9594901eab1 100644 --- a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts +++ b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts @@ -5,7 +5,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; * Command for removing a consumer from a consumer group */ export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XGROUP_DESTROY.ts b/packages/client/lib/commands/XGROUP_DESTROY.ts index 9af3a61843c..fcf1569608c 100644 --- a/packages/client/lib/commands/XGROUP_DESTROY.ts +++ b/packages/client/lib/commands/XGROUP_DESTROY.ts @@ -5,7 +5,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; * Command for removing a consumer group */ export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) { parser.push('XGROUP', 'DESTROY'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XGROUP_SETID.ts b/packages/client/lib/commands/XGROUP_SETID.ts index dea7c981aba..b28f27867c3 100644 --- a/packages/client/lib/commands/XGROUP_SETID.ts +++ b/packages/client/lib/commands/XGROUP_SETID.ts @@ -12,7 +12,6 @@ export interface XGroupSetIdOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XINFO_CONSUMERS.ts b/packages/client/lib/commands/XINFO_CONSUMERS.ts index 2e7052d34f7..8d902b30dd7 100644 --- a/packages/client/lib/commands/XINFO_CONSUMERS.ts +++ b/packages/client/lib/commands/XINFO_CONSUMERS.ts @@ -18,7 +18,6 @@ export type XInfoConsumersReply = ArrayReply>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) { parser.push('XINFO', 'CONSUMERS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XINFO_GROUPS.ts b/packages/client/lib/commands/XINFO_GROUPS.ts index b2469c8551f..ab00df11f0f 100644 --- a/packages/client/lib/commands/XINFO_GROUPS.ts +++ b/packages/client/lib/commands/XINFO_GROUPS.ts @@ -16,7 +16,6 @@ export type XInfoGroupsReply = ArrayReply>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('XINFO', 'GROUPS'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XINFO_STREAM.ts b/packages/client/lib/commands/XINFO_STREAM.ts index 4a5b0310d65..4a31a6fdcc8 100644 --- a/packages/client/lib/commands/XINFO_STREAM.ts +++ b/packages/client/lib/commands/XINFO_STREAM.ts @@ -51,7 +51,6 @@ export type XInfoStreamReply = TuplesToMapReply<[ ]>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('XINFO', 'STREAM'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XLEN.ts b/packages/client/lib/commands/XLEN.ts index 34b40a57606..d13b31b1793 100644 --- a/packages/client/lib/commands/XLEN.ts +++ b/packages/client/lib/commands/XLEN.ts @@ -5,8 +5,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; * Command for getting the length of a stream */ export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('XLEN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XNACK.ts b/packages/client/lib/commands/XNACK.ts index 5de397c049b..7081be9fab1 100644 --- a/packages/client/lib/commands/XNACK.ts +++ b/packages/client/lib/commands/XNACK.ts @@ -9,7 +9,6 @@ export interface XNackOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XPENDING.ts b/packages/client/lib/commands/XPENDING.ts index 41e71ed1a7f..5724afcc141 100644 --- a/packages/client/lib/commands/XPENDING.ts +++ b/packages/client/lib/commands/XPENDING.ts @@ -20,8 +20,6 @@ type XPendingRawReply = TuplesReply<[ ]>; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) { parser.push('XPENDING'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XPENDING_RANGE.ts b/packages/client/lib/commands/XPENDING_RANGE.ts index 3610a0d42a8..aa72281d179 100644 --- a/packages/client/lib/commands/XPENDING_RANGE.ts +++ b/packages/client/lib/commands/XPENDING_RANGE.ts @@ -28,8 +28,6 @@ type XPendingRangeRawReply = ArrayReply>; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XRANGE.ts b/packages/client/lib/commands/XRANGE.ts index 3ab4836919a..c562a33423c 100644 --- a/packages/client/lib/commands/XRANGE.ts +++ b/packages/client/lib/commands/XRANGE.ts @@ -34,8 +34,6 @@ export function xRangeArguments( } export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, ...args: Parameters) { parser.push('XRANGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XREAD.ts b/packages/client/lib/commands/XREAD.ts index a484edf5058..e67a64b81e2 100644 --- a/packages/client/lib/commands/XREAD.ts +++ b/packages/client/lib/commands/XREAD.ts @@ -53,7 +53,6 @@ export interface XReadOptions { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, streams: XReadStreams, options?: XReadOptions) { parser.push('XREAD'); diff --git a/packages/client/lib/commands/XREADGROUP.ts b/packages/client/lib/commands/XREADGROUP.ts index 9f3b1974ea2..8f7f404fbac 100644 --- a/packages/client/lib/commands/XREADGROUP.ts +++ b/packages/client/lib/commands/XREADGROUP.ts @@ -23,7 +23,6 @@ export interface XReadGroupOptions { } export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, group: RedisArgument, diff --git a/packages/client/lib/commands/XREVRANGE.ts b/packages/client/lib/commands/XREVRANGE.ts index d579f3af5ad..f1340e2e782 100644 --- a/packages/client/lib/commands/XREVRANGE.ts +++ b/packages/client/lib/commands/XREVRANGE.ts @@ -15,8 +15,6 @@ export interface XRevRangeOptions { * Command for reading stream entries in reverse order */ export default { - CACHEABLE: XRANGE.CACHEABLE, - IS_READ_ONLY: XRANGE.IS_READ_ONLY, parseCommand(parser: CommandParser, key: RedisArgument, ...args: Parameters) { parser.push('XREVRANGE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/XSETID.ts b/packages/client/lib/commands/XSETID.ts index 55112dc668c..48762ee8c2e 100644 --- a/packages/client/lib/commands/XSETID.ts +++ b/packages/client/lib/commands/XSETID.ts @@ -8,7 +8,6 @@ export interface XSetIdOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/XTRIM.ts b/packages/client/lib/commands/XTRIM.ts index ef93c21c5b7..a26430c6e30 100644 --- a/packages/client/lib/commands/XTRIM.ts +++ b/packages/client/lib/commands/XTRIM.ts @@ -21,7 +21,6 @@ export interface XTrimOptions { * Command for trimming a stream to a specified length or minimum ID */ export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZCARD.ts b/packages/client/lib/commands/ZCARD.ts index b36ea685993..584b891ed30 100644 --- a/packages/client/lib/commands/ZCARD.ts +++ b/packages/client/lib/commands/ZCARD.ts @@ -5,8 +5,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; * Command for getting the number of members in a sorted set */ export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ZCARD'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZCOUNT.ts b/packages/client/lib/commands/ZCOUNT.ts index ccbc3d13d9b..33f1ea1c610 100644 --- a/packages/client/lib/commands/ZCOUNT.ts +++ b/packages/client/lib/commands/ZCOUNT.ts @@ -3,8 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformStringDoubleArgument } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZDIFF.ts b/packages/client/lib/commands/ZDIFF.ts index 28135dc9c13..5e5ad7e5c0a 100644 --- a/packages/client/lib/commands/ZDIFF.ts +++ b/packages/client/lib/commands/ZDIFF.ts @@ -3,7 +3,6 @@ import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { parser.push('ZDIFF'); parser.pushKeysLength(keys); diff --git a/packages/client/lib/commands/ZDIFFSTORE.ts b/packages/client/lib/commands/ZDIFFSTORE.ts index d83a4bdc851..cb94fc0da8d 100644 --- a/packages/client/lib/commands/ZDIFFSTORE.ts +++ b/packages/client/lib/commands/ZDIFFSTORE.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, destination: RedisArgument, inputKeys: RedisVariadicArgument) { parser.push('ZDIFFSTORE'); parser.pushKey(destination); diff --git a/packages/client/lib/commands/ZDIFF_WITHSCORES.ts b/packages/client/lib/commands/ZDIFF_WITHSCORES.ts index 4088f106dc6..ad2c7d4ea23 100644 --- a/packages/client/lib/commands/ZDIFF_WITHSCORES.ts +++ b/packages/client/lib/commands/ZDIFF_WITHSCORES.ts @@ -5,7 +5,6 @@ import ZDIFF from './ZDIFF'; export default { - IS_READ_ONLY: ZDIFF.IS_READ_ONLY, parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { ZDIFF.parseCommand(parser, keys); parser.push('WITHSCORES'); diff --git a/packages/client/lib/commands/ZINTER.ts b/packages/client/lib/commands/ZINTER.ts index 8df33f7ab51..55225e23b06 100644 --- a/packages/client/lib/commands/ZINTER.ts +++ b/packages/client/lib/commands/ZINTER.ts @@ -29,7 +29,6 @@ export function parseZInterArguments( } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, keys: ZInterKeysType, options?: ZInterOptions) { parser.push('ZINTER'); parseZInterArguments(parser, keys, options); diff --git a/packages/client/lib/commands/ZINTERCARD.ts b/packages/client/lib/commands/ZINTERCARD.ts index 8c2e98d12cb..6bc593b0b39 100644 --- a/packages/client/lib/commands/ZINTERCARD.ts +++ b/packages/client/lib/commands/ZINTERCARD.ts @@ -7,7 +7,6 @@ export interface ZInterCardOptions { } export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, keys: RedisVariadicArgument, diff --git a/packages/client/lib/commands/ZINTERSTORE.ts b/packages/client/lib/commands/ZINTERSTORE.ts index dcbe153cfc7..7bf03ae409d 100644 --- a/packages/client/lib/commands/ZINTERSTORE.ts +++ b/packages/client/lib/commands/ZINTERSTORE.ts @@ -5,7 +5,6 @@ import { ZKeys } from './generic-transformers'; import { parseZInterArguments, ZInterOptions } from './ZINTER'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/client/lib/commands/ZINTER_WITHSCORES.ts b/packages/client/lib/commands/ZINTER_WITHSCORES.ts index d3a6614b3c2..355286ce885 100644 --- a/packages/client/lib/commands/ZINTER_WITHSCORES.ts +++ b/packages/client/lib/commands/ZINTER_WITHSCORES.ts @@ -4,7 +4,6 @@ import ZINTER from './ZINTER'; export default { - IS_READ_ONLY: ZINTER.IS_READ_ONLY, parseCommand(...args: Parameters) { ZINTER.parseCommand(...args); args[0].push('WITHSCORES'); diff --git a/packages/client/lib/commands/ZLEXCOUNT.ts b/packages/client/lib/commands/ZLEXCOUNT.ts index 7536590c168..d389db43cb4 100644 --- a/packages/client/lib/commands/ZLEXCOUNT.ts +++ b/packages/client/lib/commands/ZLEXCOUNT.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZMPOP.ts b/packages/client/lib/commands/ZMPOP.ts index dde2ee8a262..d6946fcb15e 100644 --- a/packages/client/lib/commands/ZMPOP.ts +++ b/packages/client/lib/commands/ZMPOP.ts @@ -32,7 +32,6 @@ export function parseZMPopArguments( export type ZMPopArguments = Tail>; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, keys: RedisVariadicArgument, diff --git a/packages/client/lib/commands/ZMSCORE.ts b/packages/client/lib/commands/ZMSCORE.ts index 053cfa91e15..d77890fdf63 100644 --- a/packages/client/lib/commands/ZMSCORE.ts +++ b/packages/client/lib/commands/ZMSCORE.ts @@ -3,8 +3,6 @@ import { RedisArgument, ArrayReply, NullReply, BlobStringReply, DoubleReply, Unw import { createTransformNullableDoubleReplyResp2Func, RedisVariadicArgument } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { parser.push('ZMSCORE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZPOPMAX.ts b/packages/client/lib/commands/ZPOPMAX.ts index 856d6f10908..142c1cbb1b7 100644 --- a/packages/client/lib/commands/ZPOPMAX.ts +++ b/packages/client/lib/commands/ZPOPMAX.ts @@ -3,7 +3,6 @@ import { RedisArgument, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, import { transformDoubleReply } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ZPOPMAX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZPOPMAX_COUNT.ts b/packages/client/lib/commands/ZPOPMAX_COUNT.ts index 888ce039fbe..68ebd63b14b 100644 --- a/packages/client/lib/commands/ZPOPMAX_COUNT.ts +++ b/packages/client/lib/commands/ZPOPMAX_COUNT.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '../RESP/types'; import { transformSortedSetReply } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('ZPOPMAX'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZPOPMIN.ts b/packages/client/lib/commands/ZPOPMIN.ts index 6295925aef1..29253f2a1d9 100644 --- a/packages/client/lib/commands/ZPOPMIN.ts +++ b/packages/client/lib/commands/ZPOPMIN.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '../RESP/types'; import ZPOPMAX from './ZPOPMAX'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ZPOPMIN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZPOPMIN_COUNT.ts b/packages/client/lib/commands/ZPOPMIN_COUNT.ts index 2b6abf580b9..1bfa660bc36 100644 --- a/packages/client/lib/commands/ZPOPMIN_COUNT.ts +++ b/packages/client/lib/commands/ZPOPMIN_COUNT.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '../RESP/types'; import { transformSortedSetReply } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, count: number) { parser.push('ZPOPMIN'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZRANDMEMBER.ts b/packages/client/lib/commands/ZRANDMEMBER.ts index 2abd9d3684c..f560bad990a 100644 --- a/packages/client/lib/commands/ZRANDMEMBER.ts +++ b/packages/client/lib/commands/ZRANDMEMBER.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('ZRANDMEMBER'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts index 42ef8110639..fe98783a92d 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts @@ -3,7 +3,6 @@ import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/typ import ZRANDMEMBER from './ZRANDMEMBER'; export default { - IS_READ_ONLY: ZRANDMEMBER.IS_READ_ONLY, parseCommand(parser: CommandParser, key: RedisArgument, count: number) { ZRANDMEMBER.parseCommand(parser, key); parser.push(count.toString()); diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts index f096e9d807d..90a632e3248 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts @@ -4,7 +4,6 @@ import { transformSortedSetReply } from './generic-transformers'; import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; export default { - IS_READ_ONLY: ZRANDMEMBER_COUNT.IS_READ_ONLY, parseCommand(parser: CommandParser, key: RedisArgument, count: number) { ZRANDMEMBER_COUNT.parseCommand(parser, key, count); parser.push('WITHSCORES'); diff --git a/packages/client/lib/commands/ZRANGE.ts b/packages/client/lib/commands/ZRANGE.ts index d1bc3433a50..0f29a3ed4fa 100644 --- a/packages/client/lib/commands/ZRANGE.ts +++ b/packages/client/lib/commands/ZRANGE.ts @@ -47,8 +47,6 @@ export function zRangeArgument( } export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZRANGEBYLEX.ts b/packages/client/lib/commands/ZRANGEBYLEX.ts index 316d9745c7e..4a9d32dfcc9 100644 --- a/packages/client/lib/commands/ZRANGEBYLEX.ts +++ b/packages/client/lib/commands/ZRANGEBYLEX.ts @@ -10,8 +10,6 @@ export interface ZRangeByLexOptions { } export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZRANGEBYSCORE.ts b/packages/client/lib/commands/ZRANGEBYSCORE.ts index 4d5471fdc0b..2ecb8351942 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE.ts @@ -12,8 +12,6 @@ export interface ZRangeByScoreOptions { export declare function transformReply(): Array; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts index 1a759b23dce..b2b43951dac 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts @@ -3,8 +3,6 @@ import { transformSortedSetReply } from './generic-transformers'; import ZRANGEBYSCORE from './ZRANGEBYSCORE'; export default { - CACHEABLE: ZRANGEBYSCORE.CACHEABLE, - IS_READ_ONLY: ZRANGEBYSCORE.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/ZRANGESTORE.ts b/packages/client/lib/commands/ZRANGESTORE.ts index f73e93a506f..098695c6208 100644 --- a/packages/client/lib/commands/ZRANGESTORE.ts +++ b/packages/client/lib/commands/ZRANGESTORE.ts @@ -12,7 +12,6 @@ export interface ZRangeStoreOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts index 7e6cf00cf2e..8e6eee68369 100644 --- a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts @@ -3,8 +3,6 @@ import { transformSortedSetReply } from './generic-transformers'; import ZRANGE from './ZRANGE'; export default { - CACHEABLE: ZRANGE.CACHEABLE, - IS_READ_ONLY: ZRANGE.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/ZRANK.ts b/packages/client/lib/commands/ZRANK.ts index 045e9ef8c25..14e517a0245 100644 --- a/packages/client/lib/commands/ZRANK.ts +++ b/packages/client/lib/commands/ZRANK.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { parser.push('ZRANK'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZRANK_WITHSCORE.ts b/packages/client/lib/commands/ZRANK_WITHSCORE.ts index dc2e48b362d..18e30413b6b 100644 --- a/packages/client/lib/commands/ZRANK_WITHSCORE.ts +++ b/packages/client/lib/commands/ZRANK_WITHSCORE.ts @@ -2,8 +2,6 @@ import { NullReply, TuplesReply, NumberReply, BlobStringReply, DoubleReply, Unwr import ZRANK from './ZRANK'; export default { - CACHEABLE: ZRANK.CACHEABLE, - IS_READ_ONLY: ZRANK.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/ZREM.ts b/packages/client/lib/commands/ZREM.ts index c8ba0ec02a6..987353bb097 100644 --- a/packages/client/lib/commands/ZREM.ts +++ b/packages/client/lib/commands/ZREM.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZREMRANGEBYLEX.ts b/packages/client/lib/commands/ZREMRANGEBYLEX.ts index 5d7e1a21bb0..8c76c7ed9da 100644 --- a/packages/client/lib/commands/ZREMRANGEBYLEX.ts +++ b/packages/client/lib/commands/ZREMRANGEBYLEX.ts @@ -3,7 +3,6 @@ import { NumberReply, Command, RedisArgument } from '../RESP/types'; import { transformStringDoubleArgument } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZREMRANGEBYRANK.ts b/packages/client/lib/commands/ZREMRANGEBYRANK.ts index 0a2eb3fadf3..e24ab9b9f05 100644 --- a/packages/client/lib/commands/ZREMRANGEBYRANK.ts +++ b/packages/client/lib/commands/ZREMRANGEBYRANK.ts @@ -2,7 +2,6 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts index 3d23d875948..c78342ce59d 100644 --- a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts +++ b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformStringDoubleArgument } from './generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZREVRANK.ts b/packages/client/lib/commands/ZREVRANK.ts index d48dc68adc2..f27879951e1 100644 --- a/packages/client/lib/commands/ZREVRANK.ts +++ b/packages/client/lib/commands/ZREVRANK.ts @@ -2,8 +2,6 @@ import { CommandParser } from '../client/parser'; import { NumberReply, NullReply, Command, RedisArgument } from '../RESP/types'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { parser.push('ZREVRANK'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZREVRANK_WITHSCORE.ts b/packages/client/lib/commands/ZREVRANK_WITHSCORE.ts index 8a1701b5cc9..2a4c05c26e9 100644 --- a/packages/client/lib/commands/ZREVRANK_WITHSCORE.ts +++ b/packages/client/lib/commands/ZREVRANK_WITHSCORE.ts @@ -3,8 +3,6 @@ import ZREVRANK from './ZREVRANK'; import { Command } from '../RESP/types'; export default { - CACHEABLE: ZREVRANK.CACHEABLE, - IS_READ_ONLY: ZREVRANK.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/commands/ZSCAN.ts b/packages/client/lib/commands/ZSCAN.ts index 051235033eb..47513de1b37 100644 --- a/packages/client/lib/commands/ZSCAN.ts +++ b/packages/client/lib/commands/ZSCAN.ts @@ -9,7 +9,6 @@ export interface HScanEntry { } export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/client/lib/commands/ZSCORE.ts b/packages/client/lib/commands/ZSCORE.ts index 23b52901078..bfa2a8b20ce 100644 --- a/packages/client/lib/commands/ZSCORE.ts +++ b/packages/client/lib/commands/ZSCORE.ts @@ -4,8 +4,6 @@ import { RedisArgument, Command } from '../RESP/types'; import { transformNullableDoubleReply } from './generic-transformers'; export default { - CACHEABLE: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { parser.push('ZSCORE'); parser.pushKey(key); diff --git a/packages/client/lib/commands/ZUNION.ts b/packages/client/lib/commands/ZUNION.ts index 716ee508674..98b95322bdd 100644 --- a/packages/client/lib/commands/ZUNION.ts +++ b/packages/client/lib/commands/ZUNION.ts @@ -8,7 +8,6 @@ export interface ZUnionOptions { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, keys: ZKeys, options?: ZUnionOptions) { parser.push('ZUNION'); parseZKeysArguments(parser, keys); diff --git a/packages/client/lib/commands/ZUNIONSTORE.ts b/packages/client/lib/commands/ZUNIONSTORE.ts index d3738d4f0bb..e51397fba4d 100644 --- a/packages/client/lib/commands/ZUNIONSTORE.ts +++ b/packages/client/lib/commands/ZUNIONSTORE.ts @@ -4,7 +4,6 @@ import { ZKeys, parseZKeysArguments } from './generic-transformers'; import { ZUnionOptions } from './ZUNION'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, destination: RedisArgument, diff --git a/packages/client/lib/commands/ZUNION_WITHSCORES.ts b/packages/client/lib/commands/ZUNION_WITHSCORES.ts index c62df55518f..5945526e6f6 100644 --- a/packages/client/lib/commands/ZUNION_WITHSCORES.ts +++ b/packages/client/lib/commands/ZUNION_WITHSCORES.ts @@ -4,7 +4,6 @@ import ZUNION from './ZUNION'; export default { - IS_READ_ONLY: ZUNION.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/client/lib/sentinel/utils.ts b/packages/client/lib/sentinel/utils.ts index c04b9df8e9e..b22195d7e86 100644 --- a/packages/client/lib/sentinel/utils.ts +++ b/packages/client/lib/sentinel/utils.ts @@ -2,6 +2,7 @@ import { ArrayReply, Command, RedisFunction, RedisScript, RespVersions, UnwrapRe import { BasicCommandParser } from '../client/parser'; import { RedisSocketOptions, RedisTcpSocketOptions } from '../client/socket'; import { functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; +import { defaultCommandMetadata, isReplicaSafe } from '../command-metadata'; import { NamespaceProxySentinel, NamespaceProxySentinelClient, NodeAddressMap, ProxySentinel, ProxySentinelClient, RedisNode } from './types'; /* TODO: should use map interface, would need a transform reply probably? as resp2 is list form, which this depends on */ @@ -65,13 +66,19 @@ export function clientSocketToNode(socket: RedisSocketOptions): RedisNode { export function createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + // Resolved once from the wire identifier (known only after the first parse) + // and reused — the command function is a shared prototype method. A defined + // `IS_READ_ONLY` wins over the table (see `isReplicaSafe`). + let replicaSafe: boolean | undefined; return async function (this: T, ...args: Array) { const parser = new BasicCommandParser(this._self._keyPrefix); command.parseCommand(parser, ...args); + replicaSafe ??= isReplicaSafe(defaultCommandMetadata.lookup(parser.commandIdentifier), command.IS_READ_ONLY); + return this._self._execute( - command.IS_READ_ONLY, + replicaSafe, client => client._executeCommand(command, parser, this.commandOptions, transformReply) ); }; @@ -95,13 +102,16 @@ export function createFunctionCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + let replicaSafe: boolean | undefined; return async function (this: T, ...args: Array) { const parser = new BasicCommandParser(this._self._keyPrefix); command.parseCommand(parser, ...args); + replicaSafe ??= isReplicaSafe(defaultCommandMetadata.lookup(parser.commandIdentifier), command.IS_READ_ONLY); + return this._self._execute( - command.IS_READ_ONLY, + replicaSafe, client => client._executeCommand(command, parser, this._self.commandOptions, transformReply) ); } diff --git a/packages/json/lib/commands/ARRAPPEND.ts b/packages/json/lib/commands/ARRAPPEND.ts index f54c0749cd6..2075b0eb4fb 100644 --- a/packages/json/lib/commands/ARRAPPEND.ts +++ b/packages/json/lib/commands/ARRAPPEND.ts @@ -3,7 +3,6 @@ import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/co import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/json/lib/commands/ARRINDEX.ts b/packages/json/lib/commands/ARRINDEX.ts index 20236df8472..7da9ece07d0 100644 --- a/packages/json/lib/commands/ARRINDEX.ts +++ b/packages/json/lib/commands/ARRINDEX.ts @@ -10,7 +10,6 @@ export interface JsonArrIndexOptions { } export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/json/lib/commands/ARRINSERT.ts b/packages/json/lib/commands/ARRINSERT.ts index 6b96e2ccf71..79785011135 100644 --- a/packages/json/lib/commands/ARRINSERT.ts +++ b/packages/json/lib/commands/ARRINSERT.ts @@ -3,7 +3,6 @@ import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@red import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/json/lib/commands/ARRLEN.ts b/packages/json/lib/commands/ARRLEN.ts index f49166c218d..c4adae04181 100644 --- a/packages/json/lib/commands/ARRLEN.ts +++ b/packages/json/lib/commands/ARRLEN.ts @@ -6,7 +6,6 @@ export interface JsonArrLenOptions { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonArrLenOptions) { parser.push('JSON.ARRLEN'); parser.pushKey(key); diff --git a/packages/json/lib/commands/ARRPOP.ts b/packages/json/lib/commands/ARRPOP.ts index 2d35d50504e..9a358ca56aa 100644 --- a/packages/json/lib/commands/ARRPOP.ts +++ b/packages/json/lib/commands/ARRPOP.ts @@ -10,7 +10,6 @@ export type RedisArrPopOptions = { ); export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, options?: RedisArrPopOptions) { parser.push('JSON.ARRPOP'); parser.pushKey(key); diff --git a/packages/json/lib/commands/ARRTRIM.ts b/packages/json/lib/commands/ARRTRIM.ts index 573fa787507..8ed0533f7cd 100644 --- a/packages/json/lib/commands/ARRTRIM.ts +++ b/packages/json/lib/commands/ARRTRIM.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, start: number, stop: number) { parser.push('JSON.ARRTRIM'); parser.pushKey(key); diff --git a/packages/json/lib/commands/CLEAR.ts b/packages/json/lib/commands/CLEAR.ts index b86513cc219..9774496d546 100644 --- a/packages/json/lib/commands/CLEAR.ts +++ b/packages/json/lib/commands/CLEAR.ts @@ -6,7 +6,6 @@ export interface JsonClearOptions { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonClearOptions) { parser.push('JSON.CLEAR'); parser.pushKey(key); diff --git a/packages/json/lib/commands/DEBUG_MEMORY.ts b/packages/json/lib/commands/DEBUG_MEMORY.ts index aa36d74c077..a9efee9f068 100644 --- a/packages/json/lib/commands/DEBUG_MEMORY.ts +++ b/packages/json/lib/commands/DEBUG_MEMORY.ts @@ -6,7 +6,6 @@ export interface JsonDebugMemoryOptions { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonDebugMemoryOptions) { parser.push('JSON.DEBUG', 'MEMORY'); parser.pushKey(key); diff --git a/packages/json/lib/commands/DEL.ts b/packages/json/lib/commands/DEL.ts index e86366bebe6..70425aa8001 100644 --- a/packages/json/lib/commands/DEL.ts +++ b/packages/json/lib/commands/DEL.ts @@ -6,7 +6,6 @@ export interface JsonDelOptions { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonDelOptions) { parser.push('JSON.DEL'); parser.pushKey(key); diff --git a/packages/json/lib/commands/FORGET.ts b/packages/json/lib/commands/FORGET.ts index 0a8ed3d91c4..cd24794bcd3 100644 --- a/packages/json/lib/commands/FORGET.ts +++ b/packages/json/lib/commands/FORGET.ts @@ -6,7 +6,6 @@ export interface JsonForgetOptions { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonForgetOptions) { parser.push('JSON.FORGET'); parser.pushKey(key); diff --git a/packages/json/lib/commands/GET.ts b/packages/json/lib/commands/GET.ts index 81d512be234..d75bf069d02 100644 --- a/packages/json/lib/commands/GET.ts +++ b/packages/json/lib/commands/GET.ts @@ -8,7 +8,6 @@ export interface JsonGetOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/json/lib/commands/MERGE.ts b/packages/json/lib/commands/MERGE.ts index 78f713c2223..ea5caf69c95 100644 --- a/packages/json/lib/commands/MERGE.ts +++ b/packages/json/lib/commands/MERGE.ts @@ -3,7 +3,6 @@ import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/li import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, value: RedisJSON) { parser.push('JSON.MERGE'); parser.pushKey(key); diff --git a/packages/json/lib/commands/MGET.ts b/packages/json/lib/commands/MGET.ts index a8b26513a99..413266dc372 100644 --- a/packages/json/lib/commands/MGET.ts +++ b/packages/json/lib/commands/MGET.ts @@ -13,7 +13,6 @@ import { } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, keys: Array, diff --git a/packages/json/lib/commands/MSET.ts b/packages/json/lib/commands/MSET.ts index 0cf819eea5d..92aeac7877a 100644 --- a/packages/json/lib/commands/MSET.ts +++ b/packages/json/lib/commands/MSET.ts @@ -9,7 +9,6 @@ export interface JsonMSetItem { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, items: Array) { parser.push('JSON.MSET'); diff --git a/packages/json/lib/commands/NUMINCRBY.ts b/packages/json/lib/commands/NUMINCRBY.ts index 02c1c17dbc9..1f883a857f9 100644 --- a/packages/json/lib/commands/NUMINCRBY.ts +++ b/packages/json/lib/commands/NUMINCRBY.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, NumberReply, DoubleReply, NullReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, by: number) { parser.push('JSON.NUMINCRBY'); parser.pushKey(key); diff --git a/packages/json/lib/commands/NUMMULTBY.ts b/packages/json/lib/commands/NUMMULTBY.ts index c3621908a4c..80376ffe42d 100644 --- a/packages/json/lib/commands/NUMMULTBY.ts +++ b/packages/json/lib/commands/NUMMULTBY.ts @@ -3,7 +3,6 @@ import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import NUMINCRBY from './NUMINCRBY'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, by: number) { parser.push('JSON.NUMMULTBY'); parser.pushKey(key); diff --git a/packages/json/lib/commands/OBJKEYS.ts b/packages/json/lib/commands/OBJKEYS.ts index f7e94dd4dfc..eb822cd4e2b 100644 --- a/packages/json/lib/commands/OBJKEYS.ts +++ b/packages/json/lib/commands/OBJKEYS.ts @@ -6,7 +6,6 @@ export interface JsonObjKeysOptions { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonObjKeysOptions) { parser.push('JSON.OBJKEYS'); parser.pushKey(key); diff --git a/packages/json/lib/commands/OBJLEN.ts b/packages/json/lib/commands/OBJLEN.ts index d1286a89b8c..cc630e2d693 100644 --- a/packages/json/lib/commands/OBJLEN.ts +++ b/packages/json/lib/commands/OBJLEN.ts @@ -6,7 +6,6 @@ export interface JsonObjLenOptions { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonObjLenOptions) { parser.push('JSON.OBJLEN'); parser.pushKey(key); diff --git a/packages/json/lib/commands/RESP.ts b/packages/json/lib/commands/RESP.ts index 15442d1872d..56d32c6b598 100644 --- a/packages/json/lib/commands/RESP.ts +++ b/packages/json/lib/commands/RESP.ts @@ -4,7 +4,6 @@ import { Command, RedisArgument } from "@redis/client/dist/lib/RESP/types"; type RESPReply = Array; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, path?: string) { parser.push('JSON.RESP'); parser.pushKey(key); diff --git a/packages/json/lib/commands/SET.ts b/packages/json/lib/commands/SET.ts index ff50aa67445..29c22cde891 100644 --- a/packages/json/lib/commands/SET.ts +++ b/packages/json/lib/commands/SET.ts @@ -20,7 +20,6 @@ export interface JsonSetOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/json/lib/commands/STRAPPEND.ts b/packages/json/lib/commands/STRAPPEND.ts index ccb3d6f5870..11f81fb5c18 100644 --- a/packages/json/lib/commands/STRAPPEND.ts +++ b/packages/json/lib/commands/STRAPPEND.ts @@ -7,7 +7,6 @@ export interface JsonStrAppendOptions { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, append: string, options?: JsonStrAppendOptions) { parser.push('JSON.STRAPPEND'); parser.pushKey(key); diff --git a/packages/json/lib/commands/STRLEN.ts b/packages/json/lib/commands/STRLEN.ts index 644cdf27ef7..38e1f560daf 100644 --- a/packages/json/lib/commands/STRLEN.ts +++ b/packages/json/lib/commands/STRLEN.ts @@ -6,7 +6,6 @@ export interface JsonStrLenOptions { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonStrLenOptions) { parser.push('JSON.STRLEN'); parser.pushKey(key); diff --git a/packages/json/lib/commands/TOGGLE.ts b/packages/json/lib/commands/TOGGLE.ts index 85c769729c7..9a60c31c007 100644 --- a/packages/json/lib/commands/TOGGLE.ts +++ b/packages/json/lib/commands/TOGGLE.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, NumberReply, NullReply, Command, } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument) { parser.push('JSON.TOGGLE'); parser.pushKey(key); diff --git a/packages/json/lib/commands/TYPE.ts b/packages/json/lib/commands/TYPE.ts index 1146043b2c2..d7290461164 100644 --- a/packages/json/lib/commands/TYPE.ts +++ b/packages/json/lib/commands/TYPE.ts @@ -6,7 +6,6 @@ export interface JsonTypeOptions { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonTypeOptions) { parser.push('JSON.TYPE'); parser.pushKey(key); diff --git a/packages/search/lib/commands/AGGREGATE.ts b/packages/search/lib/commands/AGGREGATE.ts index f7d90fdabcb..5e9a56727be 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -220,8 +220,6 @@ function transformAggregateReplyResp3( } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtAggregateOptions) { parser.push('FT.AGGREGATE', index, query); diff --git a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts index e8c85c6f16b..ae86025eaa4 100644 --- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts +++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts @@ -41,7 +41,6 @@ function transformAggregateWithCursorReplyResp3( } export default { - IS_READ_ONLY: AGGREGATE.IS_READ_ONLY, parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtAggregateWithCursorOptions) { AGGREGATE.parseCommand(parser, index, query, options); parser.push('WITHCURSOR'); diff --git a/packages/search/lib/commands/ALIASADD.ts b/packages/search/lib/commands/ALIASADD.ts index c35e60bed4f..e0c71cedd89 100644 --- a/packages/search/lib/commands/ALIASADD.ts +++ b/packages/search/lib/commands/ALIASADD.ts @@ -2,8 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, alias: RedisArgument, index: RedisArgument) { parser.push('FT.ALIASADD', alias, index); }, diff --git a/packages/search/lib/commands/ALIASDEL.ts b/packages/search/lib/commands/ALIASDEL.ts index 9a2dbda4b9e..bcfd289a3c9 100644 --- a/packages/search/lib/commands/ALIASDEL.ts +++ b/packages/search/lib/commands/ALIASDEL.ts @@ -2,8 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, alias: RedisArgument) { parser.push('FT.ALIASDEL', alias); }, diff --git a/packages/search/lib/commands/ALIASUPDATE.ts b/packages/search/lib/commands/ALIASUPDATE.ts index 3bd5ea92ba3..40dd6efe34f 100644 --- a/packages/search/lib/commands/ALIASUPDATE.ts +++ b/packages/search/lib/commands/ALIASUPDATE.ts @@ -2,8 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, alias: RedisArgument, index: RedisArgument) { parser.push('FT.ALIASUPDATE', alias, index); }, diff --git a/packages/search/lib/commands/ALTER.ts b/packages/search/lib/commands/ALTER.ts index 4a68817bd2c..ffe8d5e5ad0 100644 --- a/packages/search/lib/commands/ALTER.ts +++ b/packages/search/lib/commands/ALTER.ts @@ -3,8 +3,6 @@ import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/li import { RediSearchSchema, parseSchema } from './CREATE'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, schema: RediSearchSchema) { parser.push('FT.ALTER', index, 'SCHEMA', 'ADD'); parseSchema(parser, schema); diff --git a/packages/search/lib/commands/CREATE.ts b/packages/search/lib/commands/CREATE.ts index ef25f518761..4dc266000d2 100644 --- a/packages/search/lib/commands/CREATE.ts +++ b/packages/search/lib/commands/CREATE.ts @@ -424,8 +424,6 @@ export interface CreateOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, schema: RediSearchSchema, options?: CreateOptions) { parser.push('FT.CREATE', index); diff --git a/packages/search/lib/commands/CURSOR_DEL.ts b/packages/search/lib/commands/CURSOR_DEL.ts index 5f638ebb0ee..bf48db7565d 100644 --- a/packages/search/lib/commands/CURSOR_DEL.ts +++ b/packages/search/lib/commands/CURSOR_DEL.ts @@ -2,8 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { SimpleStringReply, Command, RedisArgument, NumberReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, cursorId: UnwrapReply) { parser.push('FT.CURSOR', 'DEL', index, cursorId.toString()); }, diff --git a/packages/search/lib/commands/CURSOR_READ.ts b/packages/search/lib/commands/CURSOR_READ.ts index 6df0c56f8d8..bfa8bea9f0a 100644 --- a/packages/search/lib/commands/CURSOR_READ.ts +++ b/packages/search/lib/commands/CURSOR_READ.ts @@ -7,8 +7,6 @@ export interface FtCursorReadOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, cursor: UnwrapReply, options?: FtCursorReadOptions) { parser.push('FT.CURSOR', 'READ', index, cursor.toString()); diff --git a/packages/search/lib/commands/DICTADD.ts b/packages/search/lib/commands/DICTADD.ts index 2106775f854..7498e0d7dae 100644 --- a/packages/search/lib/commands/DICTADD.ts +++ b/packages/search/lib/commands/DICTADD.ts @@ -3,8 +3,6 @@ import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, dictionary: RedisArgument, term: RedisVariadicArgument) { parser.push('FT.DICTADD', dictionary); parser.pushVariadic(term); diff --git a/packages/search/lib/commands/DICTDEL.ts b/packages/search/lib/commands/DICTDEL.ts index 988af1139e9..2cc6cf14405 100644 --- a/packages/search/lib/commands/DICTDEL.ts +++ b/packages/search/lib/commands/DICTDEL.ts @@ -3,8 +3,6 @@ import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, dictionary: RedisArgument, term: RedisVariadicArgument) { parser.push('FT.DICTDEL', dictionary); parser.pushVariadic(term); diff --git a/packages/search/lib/commands/DICTDUMP.ts b/packages/search/lib/commands/DICTDUMP.ts index 3c223442ecb..3c34df566a9 100644 --- a/packages/search/lib/commands/DICTDUMP.ts +++ b/packages/search/lib/commands/DICTDUMP.ts @@ -2,8 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, dictionary: RedisArgument) { parser.push('FT.DICTDUMP', dictionary); }, diff --git a/packages/search/lib/commands/DROPINDEX.ts b/packages/search/lib/commands/DROPINDEX.ts index 407bdd031aa..93f61116fd0 100644 --- a/packages/search/lib/commands/DROPINDEX.ts +++ b/packages/search/lib/commands/DROPINDEX.ts @@ -6,8 +6,6 @@ export interface FtDropIndexOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, options?: FtDropIndexOptions) { parser.push('FT.DROPINDEX', index); diff --git a/packages/search/lib/commands/EXPLAIN.ts b/packages/search/lib/commands/EXPLAIN.ts index 39a430f4371..a3727a75366 100644 --- a/packages/search/lib/commands/EXPLAIN.ts +++ b/packages/search/lib/commands/EXPLAIN.ts @@ -9,8 +9,6 @@ export interface FtExplainOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/EXPLAINCLI.ts b/packages/search/lib/commands/EXPLAINCLI.ts index 4ef5fba88d6..7a740a36aca 100644 --- a/packages/search/lib/commands/EXPLAINCLI.ts +++ b/packages/search/lib/commands/EXPLAINCLI.ts @@ -7,8 +7,6 @@ export interface FtExplainCLIOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/INFO.ts b/packages/search/lib/commands/INFO.ts index 4e16c071898..1409b9b9c8a 100644 --- a/packages/search/lib/commands/INFO.ts +++ b/packages/search/lib/commands/INFO.ts @@ -5,8 +5,6 @@ import { createTransformTuplesReplyFunc, transformDoubleReply } from "@redis/cli import { TuplesReply } from '@redis/client/dist/lib/RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument) { parser.push('FT.INFO', index); }, diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.ts index 9646df2b6e9..9ba060c1d37 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.ts @@ -10,8 +10,6 @@ import { } from './PROFILE_SEARCH'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, index: string, diff --git a/packages/search/lib/commands/PROFILE_SEARCH.ts b/packages/search/lib/commands/PROFILE_SEARCH.ts index 3fcb5ec3968..812f7c40379 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.ts @@ -95,8 +95,6 @@ function transformProfileSearchReplyResp3( } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index 0a560188000..644ba630f00 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -218,8 +218,6 @@ function transformSearchReplyResp3( } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSearchOptions) { parser.push('FT.SEARCH', index, query); diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.ts index 9ce2977d664..cf77688c3a8 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.ts @@ -2,8 +2,6 @@ import { Command, ReplyUnion, TypeMapping } from '@redis/client/dist/lib/RESP/ty import SEARCH, { SearchRawReply } from './SEARCH'; export default { - NOT_KEYED_COMMAND: SEARCH.NOT_KEYED_COMMAND, - IS_READ_ONLY: SEARCH.IS_READ_ONLY, parseCommand(...args: Parameters) { SEARCH.parseCommand(...args); args[0].push('NOCONTENT'); diff --git a/packages/search/lib/commands/SPELLCHECK.ts b/packages/search/lib/commands/SPELLCHECK.ts index 42e88fe538c..2b94dea6de4 100644 --- a/packages/search/lib/commands/SPELLCHECK.ts +++ b/packages/search/lib/commands/SPELLCHECK.ts @@ -65,8 +65,6 @@ function transformSpellCheckReplyResp3(rawReply: ReplyUnion): SpellCheckReply { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSpellCheckOptions) { parser.push('FT.SPELLCHECK', index, query); diff --git a/packages/search/lib/commands/SUGADD.ts b/packages/search/lib/commands/SUGADD.ts index 34e5bccb7f1..7e0ec20c40b 100644 --- a/packages/search/lib/commands/SUGADD.ts +++ b/packages/search/lib/commands/SUGADD.ts @@ -7,7 +7,6 @@ export interface FtSugAddOptions { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, string: RedisArgument, score: number, options?: FtSugAddOptions) { parser.push('FT.SUGADD'); parser.pushKey(key); diff --git a/packages/search/lib/commands/SUGDEL.ts b/packages/search/lib/commands/SUGDEL.ts index 6bc99456d2e..d258688ea46 100644 --- a/packages/search/lib/commands/SUGDEL.ts +++ b/packages/search/lib/commands/SUGDEL.ts @@ -2,7 +2,6 @@ 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: true, parseCommand(parser: CommandParser, key: RedisArgument, string: RedisArgument) { parser.push('FT.SUGDEL'); parser.pushKey(key); diff --git a/packages/search/lib/commands/SUGGET.ts b/packages/search/lib/commands/SUGGET.ts index e8a3aecdab0..24857f1e251 100644 --- a/packages/search/lib/commands/SUGGET.ts +++ b/packages/search/lib/commands/SUGGET.ts @@ -7,7 +7,6 @@ export interface FtSugGetOptions { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, prefix: RedisArgument, options?: FtSugGetOptions) { parser.push('FT.SUGGET'); parser.pushKey(key); diff --git a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts index 60bf5ee86d9..667cb726e12 100644 --- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts @@ -3,7 +3,6 @@ import { isNullReply } from '@redis/client/dist/lib/commands/generic-transformer import SUGGET from './SUGGET'; export default { - IS_READ_ONLY: SUGGET.IS_READ_ONLY, parseCommand(...args: Parameters) { SUGGET.parseCommand(...args); args[0].push('WITHPAYLOADS'); diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES.ts b/packages/search/lib/commands/SUGGET_WITHSCORES.ts index 94021033b38..4c2dc1d69b0 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES.ts @@ -8,7 +8,6 @@ type SuggestScore = { } export default { - IS_READ_ONLY: SUGGET.IS_READ_ONLY, parseCommand(...args: Parameters) { SUGGET.parseCommand(...args); args[0].push('WITHSCORES'); diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts index de829aebb22..932fd608b44 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts @@ -9,7 +9,6 @@ type SuggestScoreWithPayload = { } export default { - IS_READ_ONLY: SUGGET.IS_READ_ONLY, parseCommand(...args: Parameters) { SUGGET.parseCommand(...args); args[0].push( diff --git a/packages/search/lib/commands/SUGLEN.ts b/packages/search/lib/commands/SUGLEN.ts index a3f0fbe45ed..787be1581f8 100644 --- a/packages/search/lib/commands/SUGLEN.ts +++ b/packages/search/lib/commands/SUGLEN.ts @@ -2,7 +2,6 @@ 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: true, parseCommand(parser: CommandParser, key: RedisArgument) { parser.push('FT.SUGLEN', key); }, diff --git a/packages/search/lib/commands/SYNDUMP.ts b/packages/search/lib/commands/SYNDUMP.ts index 5f454f96fe0..3ce4171f3f6 100644 --- a/packages/search/lib/commands/SYNDUMP.ts +++ b/packages/search/lib/commands/SYNDUMP.ts @@ -2,8 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument) { parser.push('FT.SYNDUMP', index); }, diff --git a/packages/search/lib/commands/SYNUPDATE.ts b/packages/search/lib/commands/SYNUPDATE.ts index 3af735412ae..41f690a3fa5 100644 --- a/packages/search/lib/commands/SYNUPDATE.ts +++ b/packages/search/lib/commands/SYNUPDATE.ts @@ -7,8 +7,6 @@ export interface FtSynUpdateOptions { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/TAGVALS.ts b/packages/search/lib/commands/TAGVALS.ts index 0afddb247fd..f9805967a70 100644 --- a/packages/search/lib/commands/TAGVALS.ts +++ b/packages/search/lib/commands/TAGVALS.ts @@ -2,8 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, fieldName: RedisArgument) { parser.push('FT.TAGVALS', index, fieldName); }, diff --git a/packages/time-series/lib/commands/ADD.ts b/packages/time-series/lib/commands/ADD.ts index 0f254339ff9..a6e0c5b2953 100644 --- a/packages/time-series/lib/commands/ADD.ts +++ b/packages/time-series/lib/commands/ADD.ts @@ -28,7 +28,6 @@ export interface TsAddOptions { } export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/time-series/lib/commands/ALTER.ts b/packages/time-series/lib/commands/ALTER.ts index 29f99290a52..3d60ae32ab1 100644 --- a/packages/time-series/lib/commands/ALTER.ts +++ b/packages/time-series/lib/commands/ALTER.ts @@ -7,7 +7,6 @@ import { parseRetentionArgument, parseChunkSizeArgument, parseDuplicatePolicy, p export type TsAlterOptions = Pick; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, options?: TsAlterOptions) { parser.push('TS.ALTER'); parser.pushKey(key); diff --git a/packages/time-series/lib/commands/CREATE.ts b/packages/time-series/lib/commands/CREATE.ts index c499a752f23..40dd8abbcbf 100644 --- a/packages/time-series/lib/commands/CREATE.ts +++ b/packages/time-series/lib/commands/CREATE.ts @@ -23,7 +23,6 @@ export interface TsCreateOptions { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, options?: TsCreateOptions) { parser.push('TS.CREATE'); parser.pushKey(key); diff --git a/packages/time-series/lib/commands/CREATERULE.ts b/packages/time-series/lib/commands/CREATERULE.ts index a4ee28dd17b..14298c2449d 100644 --- a/packages/time-series/lib/commands/CREATERULE.ts +++ b/packages/time-series/lib/commands/CREATERULE.ts @@ -28,7 +28,6 @@ export const TIME_SERIES_AGGREGATION_TYPE = { export type TimeSeriesAggregationType = typeof TIME_SERIES_AGGREGATION_TYPE[keyof typeof TIME_SERIES_AGGREGATION_TYPE]; export default { - IS_READ_ONLY: false, parseCommand( parser: CommandParser, sourceKey: RedisArgument, diff --git a/packages/time-series/lib/commands/DECRBY.ts b/packages/time-series/lib/commands/DECRBY.ts index 8ff09d926c0..98c533eb8ab 100644 --- a/packages/time-series/lib/commands/DECRBY.ts +++ b/packages/time-series/lib/commands/DECRBY.ts @@ -2,7 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import INCRBY, { parseIncrByArguments } from './INCRBY'; export default { - IS_READ_ONLY: INCRBY.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/time-series/lib/commands/DEL.ts b/packages/time-series/lib/commands/DEL.ts index de9cadf88c9..73a57dc928c 100644 --- a/packages/time-series/lib/commands/DEL.ts +++ b/packages/time-series/lib/commands/DEL.ts @@ -3,7 +3,6 @@ import { Timestamp, transformTimestampArgument } from './helpers'; import { RedisArgument, NumberReply, Command, } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, key: RedisArgument, fromTimestamp: Timestamp, toTimestamp: Timestamp) { parser.push('TS.DEL'); parser.pushKey(key); diff --git a/packages/time-series/lib/commands/DELETERULE.ts b/packages/time-series/lib/commands/DELETERULE.ts index b4e47a0fba6..29f548d5f8d 100644 --- a/packages/time-series/lib/commands/DELETERULE.ts +++ b/packages/time-series/lib/commands/DELETERULE.ts @@ -2,7 +2,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, sourceKey: RedisArgument, destinationKey: RedisArgument) { parser.push('TS.DELETERULE'); parser.pushKeys([sourceKey, destinationKey]); diff --git a/packages/time-series/lib/commands/GET.ts b/packages/time-series/lib/commands/GET.ts index c1bb2c1c749..ca1330f8b0e 100644 --- a/packages/time-series/lib/commands/GET.ts +++ b/packages/time-series/lib/commands/GET.ts @@ -8,7 +8,6 @@ export interface TsGetOptions { export type TsGetReply = TuplesReply<[]> | TuplesReply<[NumberReply, DoubleReply]>; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: RedisArgument, options?: TsGetOptions) { parser.push('TS.GET'); parser.pushKey(key); diff --git a/packages/time-series/lib/commands/INCRBY.ts b/packages/time-series/lib/commands/INCRBY.ts index 2e40e9e5714..13e9a8973d3 100644 --- a/packages/time-series/lib/commands/INCRBY.ts +++ b/packages/time-series/lib/commands/INCRBY.ts @@ -46,7 +46,6 @@ export function parseIncrByArguments( } export default { - IS_READ_ONLY: false, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/time-series/lib/commands/INFO.ts b/packages/time-series/lib/commands/INFO.ts index 07279168d9e..f0befe5abdd 100644 --- a/packages/time-series/lib/commands/INFO.ts +++ b/packages/time-series/lib/commands/INFO.ts @@ -250,7 +250,6 @@ function transformInfoReplyResp3(reply: ReplyUnion, preserve?: unknown, typeMapp } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, key: string) { parser.push('TS.INFO'); parser.pushKey(key); diff --git a/packages/time-series/lib/commands/INFO_DEBUG.ts b/packages/time-series/lib/commands/INFO_DEBUG.ts index 13344976a38..9b3685aff12 100644 --- a/packages/time-series/lib/commands/INFO_DEBUG.ts +++ b/packages/time-series/lib/commands/INFO_DEBUG.ts @@ -71,7 +71,6 @@ function normalizeChunks(chunks: unknown): InfoDebugReply['chunks'] { } export default { - IS_READ_ONLY: INFO.IS_READ_ONLY, parseCommand(parser: CommandParser, key: string) { INFO.parseCommand(parser, key); parser.push('DEBUG'); diff --git a/packages/time-series/lib/commands/MADD.ts b/packages/time-series/lib/commands/MADD.ts index b4c91a98384..11d943c893e 100644 --- a/packages/time-series/lib/commands/MADD.ts +++ b/packages/time-series/lib/commands/MADD.ts @@ -9,7 +9,6 @@ export interface TsMAddSample { } export default { - IS_READ_ONLY: false, parseCommand(parser: CommandParser, toAdd: Array) { parser.push('TS.MADD'); diff --git a/packages/time-series/lib/commands/MGET.ts b/packages/time-series/lib/commands/MGET.ts index 1b8a9a55a90..4c2740c25e4 100644 --- a/packages/time-series/lib/commands/MGET.ts +++ b/packages/time-series/lib/commands/MGET.ts @@ -45,8 +45,6 @@ export type MGetRawReply3 = MapReply< >; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, filter: RedisVariadicArgument, options?: TsMGetOptions) { parser.push('TS.MGET'); parseLatestArgument(parser, options?.LATEST); diff --git a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts index d74d073c174..dd8c37aa957 100644 --- a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts @@ -6,7 +6,6 @@ import { parseSelectedLabelsArguments } from './helpers'; import { createTransformMGetLabelsReply } from './MGET_WITHLABELS'; export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, filter: RedisVariadicArgument, selectedLabels: RedisVariadicArgument, options?: TsMGetOptions) { parser.push('TS.MGET'); parseLatestArgument(parser, options?.LATEST); diff --git a/packages/time-series/lib/commands/MGET_WITHLABELS.ts b/packages/time-series/lib/commands/MGET_WITHLABELS.ts index 737e7236130..cda2e7bd5a2 100644 --- a/packages/time-series/lib/commands/MGET_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MGET_WITHLABELS.ts @@ -51,7 +51,6 @@ export function createTransformMGetLabelsReply() { } export default { - IS_READ_ONLY: true, parseCommand(parser: CommandParser, filter: RedisVariadicArgument, options?: TsMGetWithLabelsOptions) { parser.push('TS.MGET'); parseLatestArgument(parser, options?.LATEST); diff --git a/packages/time-series/lib/commands/MRANGE.ts b/packages/time-series/lib/commands/MRANGE.ts index b73eadd4993..a1a8eb32c98 100644 --- a/packages/time-series/lib/commands/MRANGE.ts +++ b/packages/time-series/lib/commands/MRANGE.ts @@ -49,8 +49,6 @@ export function createTransformMRangeArguments(command: RedisArgument) { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand: createTransformMRangeArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeRawReply2, _?: unknown, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts index 5c4d41c850c..ed0ae62dbd9 100644 --- a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts @@ -102,7 +102,6 @@ export function extractResp3MRangeSources(raw: TsMRangeGroupByRawMetadataReply3) } export default { - IS_READ_ONLY: true, parseCommand: createTransformMRangeGroupByArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeGroupByRawReply2, _?: unknown, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MRANGE_MULTIAGGR.ts b/packages/time-series/lib/commands/MRANGE_MULTIAGGR.ts index bdb2dc5093d..b72f4426816 100644 --- a/packages/time-series/lib/commands/MRANGE_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MRANGE_MULTIAGGR.ts @@ -56,8 +56,6 @@ export function createTransformMRangeMultiArguments(command: RedisArgument) { } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand: createTransformMRangeMultiArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeMultiRawReply2, _?: unknown, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts index addf9073b00..505f717963f 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts @@ -57,7 +57,6 @@ export function createTransformMRangeSelectedLabelsArguments(command: RedisArgum } export default { - IS_READ_ONLY: true, parseCommand: createTransformMRangeSelectedLabelsArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeSelectedLabelsRawReply2, _?: unknown, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts index 38e3a4a60d3..80923604b2a 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts @@ -50,7 +50,6 @@ export function createMRangeSelectedLabelsGroupByTransformArguments( } export default { - IS_READ_ONLY: true, parseCommand: createMRangeSelectedLabelsGroupByTransformArguments('TS.MRANGE'), transformReply: { 2: MRANGE_SELECTED_LABELS.transformReply[2], diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_MULTIAGGR.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_MULTIAGGR.ts index d95297ceba4..9c86d08784f 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_MULTIAGGR.ts @@ -64,8 +64,6 @@ export function createTransformMRangeSelectedLabelsMultiArguments(command: Redis } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand: createTransformMRangeSelectedLabelsMultiArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeSelectedLabelsMultiRawReply2, _?: unknown, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts index ddf4a6d68e4..f44c8e8bd8d 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts @@ -54,8 +54,6 @@ export function createTransformMRangeWithLabelsArguments(command: RedisArgument) } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand: createTransformMRangeWithLabelsArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeWithLabelsRawReply2, _?: unknown, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts index 134eac1ef54..c32c6fe0163 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts @@ -53,7 +53,6 @@ export function createMRangeWithLabelsGroupByTransformArguments(command: RedisAr } export default { - IS_READ_ONLY: true, parseCommand: createMRangeWithLabelsGroupByTransformArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeWithLabelsGroupByRawReply2, _?: unknown, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_MULTIAGGR.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_MULTIAGGR.ts index b1b0f4bd7a6..b134b8d1ede 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_MULTIAGGR.ts @@ -61,8 +61,6 @@ export function createTransformMRangeWithLabelsMultiArguments(command: RedisArgu } export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand: createTransformMRangeWithLabelsMultiArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeWithLabelsMultiRawReply2, _?: unknown, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MREVRANGE.ts b/packages/time-series/lib/commands/MREVRANGE.ts index 99d3123dd27..e8ab6ad6b6d 100644 --- a/packages/time-series/lib/commands/MREVRANGE.ts +++ b/packages/time-series/lib/commands/MREVRANGE.ts @@ -2,8 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE, { createTransformMRangeArguments } from './MRANGE'; export default { - NOT_KEYED_COMMAND: MRANGE.NOT_KEYED_COMMAND, - IS_READ_ONLY: MRANGE.IS_READ_ONLY, parseCommand: createTransformMRangeArguments('TS.MREVRANGE'), transformReply: MRANGE.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts index 4afcd113505..dce0ba66a13 100644 --- a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts @@ -2,7 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_GROUPBY, { createTransformMRangeGroupByArguments } from './MRANGE_GROUPBY'; export default { - IS_READ_ONLY: MRANGE_GROUPBY.IS_READ_ONLY, parseCommand: createTransformMRangeGroupByArguments('TS.MREVRANGE'), transformReply: MRANGE_GROUPBY.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_MULTIAGGR.ts b/packages/time-series/lib/commands/MREVRANGE_MULTIAGGR.ts index f0a3fde7255..4e1bccc2674 100644 --- a/packages/time-series/lib/commands/MREVRANGE_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MREVRANGE_MULTIAGGR.ts @@ -2,8 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_MULTIAGGR, { createTransformMRangeMultiArguments } from './MRANGE_MULTIAGGR'; export default { - NOT_KEYED_COMMAND: MRANGE_MULTIAGGR.NOT_KEYED_COMMAND, - IS_READ_ONLY: MRANGE_MULTIAGGR.IS_READ_ONLY, parseCommand: createTransformMRangeMultiArguments('TS.MREVRANGE'), transformReply: MRANGE_MULTIAGGR.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts index 10e00fc7a29..14a263e5146 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts @@ -2,7 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_SELECTED_LABELS, { createTransformMRangeSelectedLabelsArguments } from './MRANGE_SELECTED_LABELS'; export default { - IS_READ_ONLY: MRANGE_SELECTED_LABELS.IS_READ_ONLY, parseCommand: createTransformMRangeSelectedLabelsArguments('TS.MREVRANGE'), transformReply: MRANGE_SELECTED_LABELS.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts index b000c04c183..1670f50a732 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts @@ -2,7 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_SELECTED_LABELS_GROUPBY, { createMRangeSelectedLabelsGroupByTransformArguments } from './MRANGE_SELECTED_LABELS_GROUPBY'; export default { - IS_READ_ONLY: MRANGE_SELECTED_LABELS_GROUPBY.IS_READ_ONLY, parseCommand: createMRangeSelectedLabelsGroupByTransformArguments('TS.MREVRANGE'), transformReply: MRANGE_SELECTED_LABELS_GROUPBY.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_MULTIAGGR.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_MULTIAGGR.ts index b50adaf7828..a204130b138 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_MULTIAGGR.ts @@ -2,8 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_SELECTED_LABELS_MULTIAGGR, { createTransformMRangeSelectedLabelsMultiArguments } from './MRANGE_SELECTED_LABELS_MULTIAGGR'; export default { - NOT_KEYED_COMMAND: MRANGE_SELECTED_LABELS_MULTIAGGR.NOT_KEYED_COMMAND, - IS_READ_ONLY: MRANGE_SELECTED_LABELS_MULTIAGGR.IS_READ_ONLY, parseCommand: createTransformMRangeSelectedLabelsMultiArguments('TS.MREVRANGE'), transformReply: MRANGE_SELECTED_LABELS_MULTIAGGR.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts index 6cde143c422..b05f062a2fd 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts @@ -2,8 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_WITHLABELS, { createTransformMRangeWithLabelsArguments } from './MRANGE_WITHLABELS'; export default { - NOT_KEYED_COMMAND: MRANGE_WITHLABELS.NOT_KEYED_COMMAND, - IS_READ_ONLY: MRANGE_WITHLABELS.IS_READ_ONLY, parseCommand: createTransformMRangeWithLabelsArguments('TS.MREVRANGE'), transformReply: MRANGE_WITHLABELS.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts index 4727112b974..47dd35547f4 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts @@ -2,7 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_WITHLABELS_GROUPBY, { createMRangeWithLabelsGroupByTransformArguments } from './MRANGE_WITHLABELS_GROUPBY'; export default { - IS_READ_ONLY: MRANGE_WITHLABELS_GROUPBY.IS_READ_ONLY, parseCommand: createMRangeWithLabelsGroupByTransformArguments('TS.MREVRANGE'), transformReply: MRANGE_WITHLABELS_GROUPBY.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_MULTIAGGR.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_MULTIAGGR.ts index 3e0efb10599..5192d3474de 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_MULTIAGGR.ts @@ -2,8 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_WITHLABELS_MULTIAGGR, { createTransformMRangeWithLabelsMultiArguments } from './MRANGE_WITHLABELS_MULTIAGGR'; export default { - NOT_KEYED_COMMAND: MRANGE_WITHLABELS_MULTIAGGR.NOT_KEYED_COMMAND, - IS_READ_ONLY: MRANGE_WITHLABELS_MULTIAGGR.IS_READ_ONLY, parseCommand: createTransformMRangeWithLabelsMultiArguments('TS.MREVRANGE'), transformReply: MRANGE_WITHLABELS_MULTIAGGR.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/QUERYINDEX.ts b/packages/time-series/lib/commands/QUERYINDEX.ts index 1b53e84b7a3..9d208a9ede8 100644 --- a/packages/time-series/lib/commands/QUERYINDEX.ts +++ b/packages/time-series/lib/commands/QUERYINDEX.ts @@ -3,8 +3,6 @@ import { ArrayReply, BlobStringReply, SetReply, Command } from '@redis/client/di import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, parseCommand(parser: CommandParser, filter: RedisVariadicArgument) { parser.push('TS.QUERYINDEX'); parser.pushVariadic(filter); diff --git a/packages/time-series/lib/commands/RANGE.ts b/packages/time-series/lib/commands/RANGE.ts index c318d555a10..f66303fa32a 100644 --- a/packages/time-series/lib/commands/RANGE.ts +++ b/packages/time-series/lib/commands/RANGE.ts @@ -81,7 +81,6 @@ export function transformRangeArguments( } export default { - IS_READ_ONLY: true, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/time-series/lib/commands/RANGE_MULTIAGGR.ts b/packages/time-series/lib/commands/RANGE_MULTIAGGR.ts index 2fe7aea6906..ac5bf530129 100644 --- a/packages/time-series/lib/commands/RANGE_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/RANGE_MULTIAGGR.ts @@ -80,7 +80,6 @@ export function transformRangeMultiArguments( } export default { - IS_READ_ONLY: true, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/time-series/lib/commands/READ.ts b/packages/time-series/lib/commands/READ.ts index 9d60eae4ba3..816b93fa228 100644 --- a/packages/time-series/lib/commands/READ.ts +++ b/packages/time-series/lib/commands/READ.ts @@ -41,7 +41,6 @@ export interface TsReadOptions { } export default { - IS_READ_ONLY: true, parseCommand( parser: CommandParser, key: RedisArgument, diff --git a/packages/time-series/lib/commands/REVRANGE.ts b/packages/time-series/lib/commands/REVRANGE.ts index 238b2ce9fe7..9a71a220a30 100644 --- a/packages/time-series/lib/commands/REVRANGE.ts +++ b/packages/time-series/lib/commands/REVRANGE.ts @@ -2,7 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import RANGE, { transformRangeArguments } from './RANGE'; export default { - IS_READ_ONLY: RANGE.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; diff --git a/packages/time-series/lib/commands/REVRANGE_MULTIAGGR.ts b/packages/time-series/lib/commands/REVRANGE_MULTIAGGR.ts index e8b71806ddd..edb26cc8ebc 100644 --- a/packages/time-series/lib/commands/REVRANGE_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/REVRANGE_MULTIAGGR.ts @@ -2,7 +2,6 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import RANGE_MULTIAGGR, { transformRangeMultiArguments } from './RANGE_MULTIAGGR'; export default { - IS_READ_ONLY: RANGE_MULTIAGGR.IS_READ_ONLY, parseCommand(...args: Parameters) { const parser = args[0]; From 1d43513bd086f3dd6e9709e4e4118fe82a03ff8d Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 30 Jul 2026 14:14:11 +0300 Subject: [PATCH 03/17] feat(client): parse COMMAND key specs + policy metadata layer Parse the COMMAND key-specifications into CommandReply, add the request/response policy constants, and the static + dynamic policy resolvers that back cluster routing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dynamic-policy-resolver-factory.ts | 122 ++++++++++++ packages/client/lib/command-metadata/index.ts | 20 ++ .../command-metadata/policies-constants.ts | 154 +++++++++++++++ .../client/lib/command-metadata/predicates.ts | 81 ++++++++ .../static-metadata-resolver.ts | 97 ++++++++++ packages/client/lib/command-metadata/types.ts | 39 ++++ .../lib/commands/generic-transformers.ts | 178 +++++++++++++++++- 7 files changed, 687 insertions(+), 4 deletions(-) create mode 100644 packages/client/lib/command-metadata/dynamic-policy-resolver-factory.ts create mode 100644 packages/client/lib/command-metadata/index.ts create mode 100644 packages/client/lib/command-metadata/policies-constants.ts create mode 100644 packages/client/lib/command-metadata/predicates.ts create mode 100644 packages/client/lib/command-metadata/static-metadata-resolver.ts create mode 100644 packages/client/lib/command-metadata/types.ts diff --git a/packages/client/lib/command-metadata/dynamic-policy-resolver-factory.ts b/packages/client/lib/command-metadata/dynamic-policy-resolver-factory.ts new file mode 100644 index 00000000000..50d256ffc9f --- /dev/null +++ b/packages/client/lib/command-metadata/dynamic-policy-resolver-factory.ts @@ -0,0 +1,122 @@ +import type { CommandReply } from '../commands/generic-transformers'; +import type { CommandMetadata } from './policies-constants'; +import { REQUEST_POLICIES_WITH_DEFAULTS, defaultCommandPolicies } from './policies-constants'; +import { parseCommandName, type PolicyResolver, type ModuleMetadataRecords } from './types'; +import { StaticMetadataResolver } from './static-metadata-resolver'; + +/** + * Function type that returns command information from Redis + */ +export type CommandFetcher = () => Promise>; + +/** + * A factory for creating policy resolvers that dynamically build policies based on the Redis server's COMMAND response. + * + * This factory fetches command information from Redis and analyzes the response to determine + * appropriate routing policies for each command, returning a StaticMetadataResolver with the built policies. + */ +export class DynamicPolicyResolverFactory { + /** + * Creates a StaticMetadataResolver by fetching command information from Redis + * and building appropriate policies based on the command characteristics. + * + * @param commandFetcher Function to fetch command information from Redis + * @param fallbackResolver Optional fallback resolver to use when policies are not found + * @returns A new StaticMetadataResolver with the fetched policies + */ + static async create( + commandFetcher: CommandFetcher, + fallbackResolver?: PolicyResolver + ): Promise { + const commands = await commandFetcher(); + const policies = DynamicPolicyResolverFactory.buildModuleMetadataRecords(commands); + + return new StaticMetadataResolver(policies, fallbackResolver); + } + + /** + * Builds module->command policy records from COMMAND replies. + * + * Also used by `scripts/generate-command-metadata-data.ts` to regenerate + * `command-metadata-data.ts`, so the static data is guaranteed to match what + * this factory would derive at runtime. + */ + static buildModuleMetadataRecords(commands: Array): ModuleMetadataRecords { + const policies: ModuleMetadataRecords = {}; + + for (const command of commands) { + const parsed = parseCommandName(command.name); + + // Skip commands with invalid format (more than one dot) + if (!parsed) { + continue; + } + + const { moduleName, commandName } = parsed; + + // Initialize module if it doesn't exist + if (!policies[moduleName]) { + policies[moduleName] = {}; + } + + // Determine policies for this command + const commandPolicies = DynamicPolicyResolverFactory.#buildCommandPolicies(command); + policies[moduleName][commandName] = commandPolicies; + } + + return policies; + } + + /** + * Builds CommandMetadata for a command based on its characteristics. + * + * Priority order: + * 1. Use explicit policies from the command if available + * 2. Classify as DEFAULT_KEYLESS if keySpecification is empty + * 3. Classify as DEFAULT_KEYED if keySpecification is not empty + */ + static #buildCommandPolicies(command: CommandReply): CommandMetadata { + // Determine if command is keyless based on keySpecification + const isKeyless = command.isKeyless + + const defaults = defaultCommandPolicies(isKeyless); + + let subcommands: Record | undefined; + if(command.subcommands.length > 0) { + subcommands = {}; + for (const subcommand of command.subcommands) { + + // Subcommands are in format "parentCommand|subcommand" + const parts = subcommand.name.split("|") + if(parts.length !== 2) { + throw new Error(`Invalid subcommand name: ${subcommand.name}`); + } + const subcommandName = parts[1]; + + subcommands[subcommandName] = DynamicPolicyResolverFactory.#buildCommandPolicies(subcommand); + } + } + + const request = command.policies.request ?? defaults.request; + + return { + request, + response: command.policies.response ?? defaults.response, + isKeyless, + // Mirror the raw server signals verbatim. Derivation (replica-safety, + // CSC eligibility) is NOT precomputed here — it lives in the + // `isReplicaSafe` / `isCacheable` predicates, so the static table and a + // dynamic live-`COMMAND` resolver feed the identical algorithm. + flags: [...command.flags], + tips: command.tips.length ? command.tips : undefined, + // Only the multi_shard splitter consumes key specs. This builder also + // produces command-metadata-data.ts, so copying them unconditionally + // would pollute the generated data with specs nothing reads + // (~tripling the file). + keySpecs: request === REQUEST_POLICIES_WITH_DEFAULTS.MULTI_SHARD + ? command.keySpecs + : undefined, + subcommands + }; + } +} \ No newline at end of file diff --git a/packages/client/lib/command-metadata/index.ts b/packages/client/lib/command-metadata/index.ts new file mode 100644 index 00000000000..f5c1e80011c --- /dev/null +++ b/packages/client/lib/command-metadata/index.ts @@ -0,0 +1,20 @@ +export type { Either, PolicyResult, PolicyResolver, ModuleMetadataRecords, CommandMetadataRecords } from './types'; + +export { StaticMetadataResolver } from './static-metadata-resolver'; +export { DynamicPolicyResolverFactory, type CommandFetcher } from './dynamic-policy-resolver-factory'; + +export * from './policies-constants'; +export { isReplicaSafe, isCacheable } from './predicates'; +export { COMMAND_METADATA } from './command-metadata-data'; + +import { StaticMetadataResolver } from './static-metadata-resolver'; +import { COMMAND_METADATA } from './command-metadata-data'; + +/** + * Process-wide resolver over the generated static metadata table. The table is + * static generated data, so a single shared instance serves every client + * (standalone, cluster, sentinel, pool) — no constructor threading required. + * A future per-connection dynamic resolver built from each server's own + * `COMMAND` reply can chain to this one via the constructor's fallback. + */ +export const defaultCommandMetadata = new StaticMetadataResolver(COMMAND_METADATA); diff --git a/packages/client/lib/command-metadata/policies-constants.ts b/packages/client/lib/command-metadata/policies-constants.ts new file mode 100644 index 00000000000..5acf2504941 --- /dev/null +++ b/packages/client/lib/command-metadata/policies-constants.ts @@ -0,0 +1,154 @@ +import type { KeySpec } from '../commands/generic-transformers'; + +export const REQUEST_POLICIES_WITH_DEFAULTS = { + /** + * The client should execute the command on all nodes - masters and replicas alike. + * This tip is in-use by commands that don't accept key name arguments. + * The command operates atomically per shard. + */ + ALL_NODES: "all_nodes", + /** + * The client should execute the command on all master shards (e.g., the DBSIZE command). + * This tip is in-use by commands that don't accept key name arguments. + * The command operates atomically per shard. + */ + ALL_SHARDS: "all_shards", + /** + * The client should execute the command on several shards. + * The client should split the inputs according to the hash slots of its input key name arguments. + * For example, the command DEL {foo} {foo}1 bar should be split to DEL {foo} {foo}1 and DEL bar. + * If the keys are hashed to more than a single slot, + * the command must be split even if all the slots are managed by the same shard. + * Examples for such commands include MSET, MGET and DEL. + * However, note that SUNIONSTORE isn't considered as multi_shard because all of its keys must belong to the same hash slot. + */ + MULTI_SHARD: "multi_shard", + /** + * Indicates a non-trivial form of the client's request policy, such as the SCAN command. + */ + SPECIAL: "special", + /** + * The default behavior a client should implement for commands without the request_policy tip is as follows: + * + * 1. The command doesn't accept key name arguments: + * the client can execute the command on an arbitrary shard. + */ + DEFAULT_KEYLESS: "default-keyless", + /** + * The default behavior a client should implement for commands without the request_policy tip is as follows: + * + * 2. For commands that accept one or more key name arguments: + * the client should route the command to a single shard, + * as determined by the hash slot of the input keys. + */ + DEFAULT_KEYED: "default-keyed" +} as const; + +export type RequestPolicyWithDefaults = typeof REQUEST_POLICIES_WITH_DEFAULTS[keyof typeof REQUEST_POLICIES_WITH_DEFAULTS]; + +export const RESPONSE_POLICIES_WITH_DEFAULTS = { + /** + * The client should return success if at least one shard didn't reply with an error. + * The client should reply with the first non-error reply it obtains. + * If all shards return an error, the client can reply with any one of these. + * Example: SCRIPT KILL command that's sent to all shards. + */ + ONE_SUCCEEDED: "one_succeeded", + /** + * The client should return successfully only if there are no error replies. + * Even a single error reply should disqualify the aggregate and be returned. + * Otherwise, the client should return one of the non-error replies. + * Examples: CONFIG SET, SCRIPT FLUSH and SCRIPT LOAD commands. + */ + ALL_SUCCEEDED: "all_succeeded", + /** + * The client should return the result of a logical AND operation on all replies. + * Only applies to integer replies, usually from commands that return either 0 or 1. + * Example: SCRIPT EXISTS command returns 1 only when all shards report that a given script SHA1 sum is in their cache. + */ + AGG_LOGICAL_AND: "agg_logical_and", + /** + * The client should return the result of a logical OR operation on all replies. + * Only applies to integer replies, usually from commands that return either 0 or 1. + */ + AGG_LOGICAL_OR: "agg_logical_or", + /** + * The client should return the minimal value from the replies. + * Only applies to numerical replies. + * Example: WAIT command should return the minimal number of synchronized replicas from all shards. + */ + AGG_MIN: "agg_min", + /** + * The client should return the maximal value from the replies. + * Only applies to numerical replies. + */ + AGG_MAX: "agg_max", + /** + * The client should return the sum of replies. + * Only applies to numerical replies. + * Example: DBSIZE command. + */ + AGG_SUM: "agg_sum", + /** + * Indicates a non-trivial form of reply policy. + * Example: INFO command with complex aggregation logic. + */ + SPECIAL: "special", + /** + * The default behavior for commands without a response_policy tip that don't accept key name arguments: + * the client can aggregate all replies within a single nested data structure. + * Example: KEYS command replies should be packed in a single array in no particular order. + */ + DEFAULT_KEYLESS: "default-keyless", + /** + * The default behavior for commands without a response_policy tip that accept one or more key name arguments: + * the client needs to retain the same order of replies as the input key names. + * Example: MGET's aggregated reply should maintain key order. + */ + DEFAULT_KEYED: "default-keyed" +} as const; + +export type ResponsePolicyWithDefaults = typeof RESPONSE_POLICIES_WITH_DEFAULTS[keyof typeof RESPONSE_POLICIES_WITH_DEFAULTS]; + +/** + * The default policies for a command without request/response tips, per the + * command-tips spec: keyed → single shard by hash slot with order-preserving + * replies; keyless → arbitrary shard, replies passed through/merged. Shared + * by the dynamic resolver factory (table construction) and the cluster + * dispatch fallback for commands unknown to the resolver. + */ +export function defaultCommandPolicies(isKeyless: boolean): Pick { + return isKeyless + ? { request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, isKeyless: true } + : { request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, isKeyless: false }; +} + +export interface CommandMetadata { + readonly request: RequestPolicyWithDefaults; + readonly response: ResponsePolicyWithDefaults; + readonly subcommands?: Record; + readonly isKeyless: boolean; + /** + * COMMAND key specifications — the reconstruction recipe for splitting the + * command per slot. Only populated for `multi_shard` commands; other + * commands never split, so their entries stay lean. + */ + readonly keySpecs?: ReadonlyArray; + /** + * Raw command flags from the server `COMMAND` reply (e.g. `write`, + * `readonly`, `script`). Stored verbatim rather than pre-cooked into + * booleans so the derivation lives in one place (`isReplicaSafe` / + * `isCacheable`) and the static table + a future dynamic live-`COMMAND` + * resolver feed the identical algorithm. Optional: absent for synthesized + * fallback policies and hand-written overrides, in which case the readers + * fall back to the hardcoded `Command.IS_READ_ONLY` / `Command.CACHEABLE`. + */ + readonly flags?: ReadonlyArray; + /** + * Raw command tips from the server `COMMAND` reply, minus the + * `request_policy:` / `response_policy:` tips (already captured as + * `request` / `response`). Carries `nondeterministic_output`, `dont_cache`, + * etc. — the signals `isCacheable` consumes. + */ + readonly tips?: ReadonlyArray; +} \ No newline at end of file diff --git a/packages/client/lib/command-metadata/predicates.ts b/packages/client/lib/command-metadata/predicates.ts new file mode 100644 index 00000000000..b3dcdc86b67 --- /dev/null +++ b/packages/client/lib/command-metadata/predicates.ts @@ -0,0 +1,81 @@ +import type { CommandMetadata } from './policies-constants'; + +/** + * Whether it is safe to route a command to a replica. + * + * In node-redis `Command.IS_READ_ONLY` means, for all intents and purposes, + * "safe to send to a replica" — it is consumed only by the cluster and sentinel + * routers to choose replica vs master. + * + * Override-first: a defined `IS_READ_ONLY` (or an explicit `isReadonly` + * argument on the raw `sendCommand` path) is deliberate intent and always + * wins — per-command corrections live in the command definitions, not in the + * generated table. Only when no intent is declared does the table decide: + * + * - `write` flag → never replica-safe. This is the server's own rejection + * signal: a read-only replica rejects a command iff it carries `CMD_WRITE` + * (`processCommand`, redis/src/server.c — "-READONLY You can't write + * against a read only replica."). The `readonly` flag is deliberately NOT + * consulted: its definition is broader (ACL `@read`, key-spec RO/RW, ...) + * and not 1:1 with replica-safety. + * - `script_runner` flag (EVAL/EVALSHA/FCALL family) → not replica-safe by + * default: the server cannot statically know whether a script writes, so + * routing is master unless the script/command declares `IS_READ_ONLY` + * (`defineScript`, or the hand-set value on the `_RO` variants). + * - otherwise, keyed → replica-safe. Every keyed non-write command is a data + * read the replica can serve. + * - keyless → not replica-safe. The flagless-keyless bucket is admin, + * connection-state and pub/sub commands (SHUTDOWN, FAILOVER, HELLO, AUTH, + * MULTI, ...) where "no write flag" does not mean "sensible on a replica" + * — SHUTDOWN is accepted by a replica and shuts it down. Read-ish members + * (PING, INFO, TIME, ...) opt back in via `IS_READ_ONLY: true` overrides + * in their command definitions. + * + * Unknown commands (user scripts/functions/unknown modules miss the table) + * with no declared intent default to master — the safe choice. + */ +export function isReplicaSafe( + meta: CommandMetadata | undefined, + override: boolean | undefined +): boolean { + if (override !== undefined) return override; + if (!meta?.flags) return false; + if (meta.flags.includes('write')) return false; + if (meta.flags.includes('script_runner')) return false; + return !meta.isKeyless; +} + +/** + * Whether a command's reply is eligible for client-side caching (CSC). + * + * Override-first, like `isReplicaSafe`: a defined `Command.CACHEABLE` always + * wins (e.g. `CACHEABLE: false` on commands whose server metadata makes them + * look cacheable when they are not — TOUCH only bumps LRU/LFU and generates no + * invalidation). With no declared intent, the cross-client CSC "Command + * Eligibility" algorithm decides: a command is cacheable if all of the + * following hold — + * - no `dont_cache` tip (explicit negative override), + * - has the `readonly` flag, + * - takes at least one key-name argument (`!isKeyless`; CSC invalidation is + * key-tracking based, so keyless read-only commands like KEYS must not cache), + * - no `nondeterministic_output` tip (value nondeterminism; `*_output_order` + * is fine — HGETALL/SMEMBERS stay cacheable), + * - no `script_runner` flag (EVAL_RO/EVALSHA_RO/FCALL_RO). + * + * Unknown commands with no declared intent are not cacheable. + */ +export function isCacheable( + meta: CommandMetadata | undefined, + override: boolean | undefined +): boolean { + if (override !== undefined) return override; + if (!meta?.flags) return false; + const tips = meta.tips ?? []; + return !tips.includes('dont_cache') + && meta.flags.includes('readonly') + && !meta.isKeyless + && !tips.includes('nondeterministic_output') + // `script_runner` (Redis 8.10) marks the EVAL_RO/EVALSHA_RO/FCALL_RO family, + // which must not cache. + && !meta.flags.includes('script_runner'); +} diff --git a/packages/client/lib/command-metadata/static-metadata-resolver.ts b/packages/client/lib/command-metadata/static-metadata-resolver.ts new file mode 100644 index 00000000000..29dcaa628a8 --- /dev/null +++ b/packages/client/lib/command-metadata/static-metadata-resolver.ts @@ -0,0 +1,97 @@ +import { parseCommandName, type PolicyResult, type PolicyResolver, type ModuleMetadataRecords, type CommandMetadataRecords } from './types'; +import { COMMAND_METADATA } from './command-metadata-data'; +import { CommandIdentifier } from '../client/parser'; +import type { CommandMetadata } from './policies-constants'; + +// The rebuilt lookup tables use null-prototype objects: command names come +// off the wire, and a name like "constructor" or "__proto__" must miss the +// table instead of resolving an Object.prototype member as metadata. +const lowercaseCommandMetadata = (metadata: CommandMetadata): CommandMetadata => { + if (!metadata.subcommands) return metadata; + const subcommands: Record = Object.create(null); + for (const [name, sub] of Object.entries(metadata.subcommands)) { + subcommands[name.toLowerCase()] = lowercaseCommandMetadata(sub); + } + return { ...metadata, subcommands }; +}; + +const lowercaseModuleMetadata = (metadata: ModuleMetadataRecords): ModuleMetadataRecords => { + const out: ModuleMetadataRecords = Object.create(null); + for (const [moduleName, commands] of Object.entries(metadata)) { + const normalized: CommandMetadataRecords = Object.create(null); + for (const [commandName, entry] of Object.entries(commands)) { + normalized[commandName.toLowerCase()] = lowercaseCommandMetadata(entry); + } + out[moduleName.toLowerCase()] = normalized; + } + return out; +}; + +export class StaticMetadataResolver implements PolicyResolver { + private readonly fallbackResolver: PolicyResolver | null = null; + private readonly policies: ModuleMetadataRecords; + + constructor( + policies: ModuleMetadataRecords = COMMAND_METADATA, + fallbackResolver?: PolicyResolver + ) { + this.policies = lowercaseModuleMetadata(policies); + this.fallbackResolver = fallbackResolver || null; + } + + /** + * Convenience over `resolvePolicy` for the override-first predicate readers: + * returns the resolved metadata or `undefined` on any miss. + */ + lookup(commandIdentifier: CommandIdentifier): CommandMetadata | undefined { + const result = this.resolvePolicy(commandIdentifier); + return result.ok ? result.value : undefined; + } + + resolvePolicy(commandIdentifier: CommandIdentifier): PolicyResult { + const parsed = parseCommandName(commandIdentifier.command.toLowerCase()); + + if (!parsed) { + return { ok: false, error: 'wrong-command-or-module-name' }; + } + + const { moduleName, commandName } = parsed; + + if (!this.policies[moduleName]) { + if (this.fallbackResolver) { + return this.fallbackResolver.resolvePolicy(commandIdentifier); + } + + // For std module commands, return 'unknown-command' instead of 'unknown-module' + // to provide better UX for single-word commands + if (moduleName === 'std') { + return { ok: false, error: 'unknown-command' }; + } + return { ok: false, error: 'unknown-module' }; + } + + if (!this.policies[moduleName][commandName]) { + if (this.fallbackResolver) { + return this.fallbackResolver.resolvePolicy(commandIdentifier); + } + return { ok: false, error: 'unknown-command' }; + } + + const policy = this.policies[moduleName][commandName]; + + if (policy.subcommands && commandIdentifier.subcommand !== undefined) { + const subcommandPolicy = policy.subcommands[commandIdentifier.subcommand.toLowerCase()]; + if (subcommandPolicy) { + return { + ok: true, + value: subcommandPolicy + }; + } + } + + return { + ok: true, + value: policy + }; + } +} diff --git a/packages/client/lib/command-metadata/types.ts b/packages/client/lib/command-metadata/types.ts new file mode 100644 index 00000000000..c4f7c48aa7b --- /dev/null +++ b/packages/client/lib/command-metadata/types.ts @@ -0,0 +1,39 @@ +import { CommandIdentifier } from '../client/parser'; +import type { CommandMetadata } from './policies-constants'; + +export type Either = + | { readonly ok: true; readonly value: TOk } + | { readonly ok: false; readonly error: TError }; + +export type PolicyResult = Either; + +export interface PolicyResolver { + /** + * The response of the COMMAND command uses "." to separate the module name from the command name. + */ + resolvePolicy(commandIdentifier: CommandIdentifier): PolicyResult; + + /** + * Convenience over `resolvePolicy`: returns the resolved metadata or + * `undefined` on any miss, for the override-first predicates + * (`isReplicaSafe(resolver.lookup(id), command.IS_READ_ONLY)`). + */ + lookup(commandIdentifier: CommandIdentifier): CommandMetadata | undefined; +} + +/** + * Parses a `COMMAND`-style command name into its module/command parts: + * `"ping"` → `std.ping`, `"ft.search"` → `ft.search`. More than one dot is + * invalid in Redis → `undefined`. Callers own case normalization. + */ +export function parseCommandName(fullCommandName: string): { moduleName: string; commandName: string } | undefined { + const parts = fullCommandName.split('.'); + if (parts.length === 1) return { moduleName: 'std', commandName: fullCommandName }; + if (parts.length === 2) return { moduleName: parts[0], commandName: parts[1] }; + return undefined; +} + +export type CommandMetadataRecords = Record; +// The response of the COMMAND command uses "." to separate the module name from the command name. +// For example, "ft.search" refers to the "search" command in the "ft" module. It is important to use the same naming convention here. +export type ModuleMetadataRecords = Record; diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index 82479e6c1fb..f8c90a76a52 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -1,4 +1,5 @@ import { BasicCommandParser, CommandParser } from '../client/parser'; +import { REQUEST_POLICIES_WITH_DEFAULTS, RequestPolicyWithDefaults, RESPONSE_POLICIES_WITH_DEFAULTS, ResponsePolicyWithDefaults } from '../command-metadata/policies-constants'; import { RESP_TYPES } from '../RESP/decoder'; import { UnwrapReply, ArrayReply, BlobStringReply, BooleanReply, CommandArguments, DoubleReply, NullReply, NumberReply, RedisArgument, ReplyUnion, TuplesReply, MapReply, TypeMapping, Command } from '../RESP/types'; @@ -342,9 +343,23 @@ export type CommandRawReply = [ firstKeyIndex: number, lastKeyIndex: number, step: number, - categories: Array + categories: Array, + tips: Array, + keySpecifications: Array, + subcommands: Array ]; +export type KeySpec = { + beginSearch: + | { type: 'index'; index: number } + | { type: 'keyword'; keyword: string; startFrom: number } + | { type: 'unknown' }; + findKeys: + | { type: 'range'; lastKey: number; keyStep: number; limit: number } + | { type: 'keynum'; keyNumIdx: number; firstKey: number; keyStep: number } + | { type: 'unknown' }; +}; + export type CommandReply = { name: string, arity: number, @@ -352,13 +367,159 @@ export type CommandReply = { firstKeyIndex: number, lastKeyIndex: number, step: number, - categories: Set + categories: Set, + policies: { request: RequestPolicyWithDefaults | undefined, response: ResponsePolicyWithDefaults | undefined } + isKeyless: boolean, + /** + * True when the server tags the command with the `nondeterministic_output` + * tip (e.g. XPENDING). Distinct from `nondeterministic_output_order`, which + * is NOT captured here. Consumed to precompute `cacheable`. + */ + nondeterministicOutput: boolean, + /** + * Raw command tips, minus the `request_policy:` / `response_policy:` tips + * (captured separately in `policies`). Carries `nondeterministic_output`, + * `dont_cache`, etc. — mirrored into `CommandMetadata.tips` so CSC + * eligibility is derived from the raw server signal. + */ + tips: Array, + keySpecs: Array, + subcommands: Array }; +/** + * Normalizes one map-shaped level of a key specification to the RESP3 object + * shape. RESP3 already delivers objects; RESP2 delivers the same data as flat + * `[field, value, ...]` pair arrays. + */ +function normalizeKeySpecMap(raw: unknown): Record | undefined { + if (raw === null || typeof raw !== 'object') return undefined; + + if (!Array.isArray(raw)) return raw as Record; + + if (raw.length % 2 !== 0) return undefined; + + const normalized: Record = {}; + for (let i = 0; i < raw.length; i += 2) { + const field = raw[i]; + if (typeof field !== 'string') return undefined; + normalized[field] = raw[i + 1]; + } + return normalized; +} + +function transformNumber(raw: unknown): number | undefined { + const value = Number(raw); + return Number.isInteger(value) ? value : undefined; +} + +const UNKNOWN_KEY_SPEC_PART = { type: 'unknown' } as const; + +function transformBeginSearch(raw: unknown): KeySpec['beginSearch'] { + const beginSearch = normalizeKeySpecMap(raw); + const spec = normalizeKeySpecMap(beginSearch?.spec); + if (!beginSearch || !spec) return UNKNOWN_KEY_SPEC_PART; + + switch (beginSearch.type) { + case 'index': { + const index = transformNumber(spec.index); + if (index !== undefined) return { type: 'index', index }; + break; + } + case 'keyword': { + const startFrom = transformNumber(spec.startfrom); + if (typeof spec.keyword === 'string' && startFrom !== undefined) { + return { type: 'keyword', keyword: spec.keyword, startFrom }; + } + break; + } + } + return UNKNOWN_KEY_SPEC_PART; +} + +function transformFindKeys(raw: unknown): KeySpec['findKeys'] { + const findKeys = normalizeKeySpecMap(raw); + const spec = normalizeKeySpecMap(findKeys?.spec); + if (!findKeys || !spec) return UNKNOWN_KEY_SPEC_PART; + + switch (findKeys.type) { + case 'range': { + const lastKey = transformNumber(spec.lastkey), + keyStep = transformNumber(spec.keystep), + limit = transformNumber(spec.limit); + if (lastKey !== undefined && keyStep !== undefined && limit !== undefined) { + return { type: 'range', lastKey, keyStep, limit }; + } + break; + } + case 'keynum': { + const keyNumIdx = transformNumber(spec.keynumidx), + firstKey = transformNumber(spec.firstkey), + keyStep = transformNumber(spec.keystep); + if (keyNumIdx !== undefined && firstKey !== undefined && keyStep !== undefined) { + return { type: 'keynum', keyNumIdx, firstKey, keyStep }; + } + break; + } + } + return UNKNOWN_KEY_SPEC_PART; +} + +/** + * Parses one COMMAND key-specification entry. Unrecognized or malformed + * shapes parse to `{ type: 'unknown' }` parts instead of throwing — consumers + * (e.g. the multi_shard splitter) decide whether unknown is acceptable. + */ +export function transformKeySpec(raw: unknown): KeySpec { + const entry = normalizeKeySpecMap(raw); + return { + beginSearch: transformBeginSearch(entry?.begin_search), + findKeys: transformFindKeys(entry?.find_keys) + }; +} + export function transformCommandReply( this: void, - [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories]: CommandRawReply + // Fields 8-10 (tips, key specifications, subcommands) exist since Redis 7.0; + // default them so a shorter reply (older server, truncating proxy) parses + // instead of throwing "tips is not iterable". + [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories, tips = [], keySpecifications = [], subcommandsReply = []]: CommandRawReply ): CommandReply { + + + // Tips are free-form hints with no ordering guarantee — commands like INFO + // declare 'nondeterministic_output' before their policy tips, so scan the + // whole array instead of relying on positions. + let requestPolicy: RequestPolicyWithDefaults | undefined; + let responsePolicy: ResponsePolicyWithDefaults | undefined; + // Exact match — must NOT catch 'nondeterministic_output_order', which does + // not disqualify caching (HGETALL/SMEMBERS keep unordered but cacheable). + let nondeterministicOutput = false; + // Non-policy tips, mirrored verbatim into CommandMetadata.tips (dont_cache, + // nondeterministic_output, ...). The policy tips are captured separately. + const otherTips: Array = []; + + for (const tip of tips) { + if (tip.startsWith('request_policy:')) { + const raw = tip.slice('request_policy:'.length); + if ((Object.values(REQUEST_POLICIES_WITH_DEFAULTS) as string[]).includes(raw)) { + requestPolicy = raw as RequestPolicyWithDefaults; + } + } else if (tip.startsWith('response_policy:')) { + const raw = tip.slice('response_policy:'.length); + if ((Object.values(RESPONSE_POLICIES_WITH_DEFAULTS) as string[]).includes(raw)) { + responsePolicy = raw as ResponsePolicyWithDefaults; + } + } else { + otherTips.push(tip); + if (tip === 'nondeterministic_output') { + nondeterministicOutput = true; + } + } + } + + const subcommands = subcommandsReply.map(transformCommandReply); + return { name, arity, @@ -366,7 +527,16 @@ export function transformCommandReply( firstKeyIndex, lastKeyIndex, step, - categories: new Set(categories) + categories: new Set(categories), + policies: { + request: requestPolicy, + response: responsePolicy + }, + isKeyless: keySpecifications.length === 0, + nondeterministicOutput, + tips: otherTips, + keySpecs: keySpecifications.map(transformKeySpec), + subcommands }; } From 7bcc623c5ccba654a2668ba8daf67e447c1790f8 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 30 Jul 2026 14:14:12 +0300 Subject: [PATCH 04/17] feat(client): add multi_shard command splitter Split a multi_shard command's keys per hash slot using the server key specs as the reconstruction recipe (numkeys rewrite, options-suffix copy, single-slot fast path), recording group indices for reply reassembly. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../multi-shard-splitter.ts | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 packages/client/lib/cluster/request-response-policies/multi-shard-splitter.ts diff --git a/packages/client/lib/cluster/request-response-policies/multi-shard-splitter.ts b/packages/client/lib/cluster/request-response-policies/multi-shard-splitter.ts new file mode 100644 index 00000000000..2f3526bf905 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/multi-shard-splitter.ts @@ -0,0 +1,163 @@ +import calculateSlot from 'cluster-key-slot'; +import type { RedisArgument } from '../../RESP/types'; +import type { KeySpec } from '../../commands/generic-transformers'; + +export type SubCommand = { + args: Array; + /** + * 0-based ordinals of this sub-command's key groups in the original + * command, for order-preserving reply reassembly (e.g. MGET). + */ + groupIndices: Array; + /** + * Absolute indices into `args` of this sub-command's keys (the first arg of + * each group). Lets the caller build a sub-parser that marks keys, so core + * `_execute` routes by the sub-command's own `firstKey`. + */ + keyPositions: Array; +}; + +/** + * Splits a multi_shard command's arguments into one sub-command per hash + * slot, using the command's COMMAND key specification as the reconstruction + * recipe ("the command must be split even if all the slots are managed by + * the same shard" — but same-*slot* commands pass through unsplit). + * + * Each key group is the key plus its `keyStep - 1` trailing siblings (e.g. + * MSET's value). A sub-command is prefix + that slot's groups (in original + * relative order) + suffix; for `keynum` specs the numkeys argument in the + * prefix is rewritten to the sub-command's group count. + * + * Throws on anything it cannot split deterministically — a wrong split of a + * write command means corrupted data, so refusal beats guessing. All current + * multi_shard commands (DEL, UNLINK, EXISTS, TOUCH, MGET, MSET) declare + * exactly one supported spec. MSETEX is curated OUT of multi_shard + * (command-metadata-overrides.ts): its NX/XX condition is all-or-nothing + * across all keys and cannot be evaluated per shard — it routes default-keyed + * like MSETNX, so the keynum branch below currently has no live caller. + */ +export function splitMultiShardCommand( + args: ReadonlyArray, + keySpecs: ReadonlyArray | undefined +): Map { + const label = args.length > 0 ? args[0].toString() : ''; + + if (!keySpecs || keySpecs.length === 0) { + throw new Error(`Cannot split ${label}: command has no key specification`); + } + // TODO(multi-spec): a command whose keys are interchangeable but + // syntactically scattered (e.g. a fixed-position key plus a keyword-tail + // list) could legitimately be multi_shard with several specs, and + // multi-region reconstruction would be deterministic. No such command + // exists, and specs alone cannot distinguish that shape from linked-operand + // specs (GEORADIUS-like) where splitting is meaningless — so refuse until a + // real command motivates multi-region support. + if (keySpecs.length > 1) { + throw new Error(`Cannot split ${label}: multiple key specifications are not supported`); + } + + const { beginSearch, findKeys } = keySpecs[0]; + if (beginSearch.type !== 'index') { + throw new Error(`Cannot split ${label}: unsupported begin_search type '${beginSearch.type}'`); + } + + const start = beginSearch.index; + let keyRegionStart: number; + let keyRegionEnd: number; + let keyStep: number; + // Absolute position of the numkeys argument to rewrite per sub-command. + let keyNumIdx: number | undefined; + + switch (findKeys.type) { + case 'range': { + // All current multi_shard range specs are "until end of args"; bounded + // ranges (lastKey >= 0) and limit can be added when a command needs them. + if (findKeys.lastKey !== -1 || findKeys.limit !== 0) { + throw new Error(`Cannot split ${label}: unsupported find_keys range (lastkey ${findKeys.lastKey}, limit ${findKeys.limit})`); + } + keyStep = findKeys.keyStep; + keyRegionStart = start; + keyRegionEnd = args.length; + break; + } + case 'keynum': { + keyStep = findKeys.keyStep; + keyNumIdx = start + findKeys.keyNumIdx; + keyRegionStart = start + findKeys.firstKey; + if (keyNumIdx >= keyRegionStart) { + throw new Error(`Cannot split ${label}: numkeys argument inside the key region`); + } + const numKeys = parsePositiveInteger(args[keyNumIdx]); + if (numKeys === undefined) { + throw new Error(`Cannot split ${label}: malformed numkeys argument '${args[keyNumIdx]}'`); + } + keyRegionEnd = keyRegionStart + numKeys * keyStep; + break; + } + default: + throw new Error(`Cannot split ${label}: unsupported find_keys type '${findKeys.type}'`); + } + + if (keyStep < 1) { + throw new Error(`Cannot split ${label}: invalid keystep ${keyStep}`); + } + if (keyRegionStart < 1 || keyRegionEnd > args.length) { + throw new Error(`Cannot split ${label}: key region overruns the arguments`); + } + const regionLength = keyRegionEnd - keyRegionStart; + if (regionLength <= 0 || regionLength % keyStep !== 0) { + throw new Error(`Cannot split ${label}: key region does not align with keystep ${keyStep}`); + } + + const groupCount = regionLength / keyStep; + const slotGroups = new Map>(); + for (let group = 0; group < groupCount; group++) { + const slot = calculateSlot(args[keyRegionStart + group * keyStep]); + const groups = slotGroups.get(slot); + if (groups) { + groups.push(group); + } else { + slotGroups.set(slot, [group]); + } + } + + const subCommands = new Map(); + + // Single-slot fast path: nothing to split — pass the original command + // through untouched (also preserves single-slot atomicity). Keys keep their + // original absolute positions. + if (slotGroups.size === 1) { + const [[slot, groupIndices]] = slotGroups; + const keyPositions = groupIndices.map(group => keyRegionStart + group * keyStep); + subCommands.set(slot, { args: [...args], groupIndices, keyPositions }); + return subCommands; + } + + const suffix = args.slice(keyRegionEnd); + for (const [slot, groupIndices] of slotGroups) { + const subArgs = args.slice(0, keyRegionStart); + if (keyNumIdx !== undefined) { + subArgs[keyNumIdx] = groupIndices.length.toString(); + } + // Groups are appended in order, each keyStep wide with the key first, so + // the j-th group's key lands at keyRegionStart + j * keyStep in subArgs. + const keyPositions: Array = []; + groupIndices.forEach((group, j) => { + keyPositions.push(keyRegionStart + j * keyStep); + const groupStart = keyRegionStart + group * keyStep; + for (let i = 0; i < keyStep; i++) { + subArgs.push(args[groupStart + i]); + } + }); + subArgs.push(...suffix); + subCommands.set(slot, { args: subArgs, groupIndices, keyPositions }); + } + + return subCommands; +} + +function parsePositiveInteger(arg: RedisArgument | undefined): number | undefined { + if (arg === undefined) return undefined; + const value = Number(arg.toString()); + return Number.isInteger(value) && value > 0 ? value : undefined; +} From 27706be414baaee50b695039428f55afa2506834 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 30 Jul 2026 14:14:12 +0300 Subject: [PATCH 05/17] feat(client): cluster request/response policy dispatch engine Add _executeWithPolicies: resolve a command's policy, route it to an execution plan (pass-through, fan-out, or multi_shard split), and reduce the replies via the response policy. Includes the default-policy fallback for policy-less commands and the raw sendCommand split path. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/client/lib/client/parser.ts | 29 ++ packages/client/lib/cluster/index.ts | 340 ++++++++++++++---- .../request-response-policies/dispatch.ts | 334 +++++++++++++++++ .../generic-aggregators.ts | 153 ++++++++ .../request-response-policies/index.ts | 5 + 5 files changed, 787 insertions(+), 74 deletions(-) create mode 100644 packages/client/lib/cluster/request-response-policies/dispatch.ts create mode 100644 packages/client/lib/cluster/request-response-policies/generic-aggregators.ts create mode 100644 packages/client/lib/cluster/request-response-policies/index.ts diff --git a/packages/client/lib/client/parser.ts b/packages/client/lib/client/parser.ts index bc45f5e4651..f2a412300e9 100644 --- a/packages/client/lib/client/parser.ts +++ b/packages/client/lib/client/parser.ts @@ -33,11 +33,14 @@ export function prefixKeys(keyPrefix: RedisArgument | undefined, keys: RedisVari : [prefixKey(keyPrefix, keys)]; } +export type CommandIdentifier = { command: string, subcommand: string | undefined }; + export interface CommandParser { redisArgs: ReadonlyArray; keys: ReadonlyArray; firstKey: RedisArgument | undefined; preserve: unknown; + commandIdentifier: CommandIdentifier; push: (...arg: Array) => unknown; pushVariadic: (vals: RedisVariadicArgument) => unknown; @@ -57,6 +60,7 @@ export class BasicCommandParser implements CommandParser { #redisArgs: Array = []; #keys: Array = []; readonly #keyPrefix: RedisArgument | undefined; + #commandIdentifier: CommandIdentifier | undefined; preserve: unknown; /** @@ -90,6 +94,20 @@ export class BasicCommandParser implements CommandParser { return tmp.join('_'); } + // Memoized: read only after the command is fully parsed, and consumed up to + // three times per cluster command (policy resolution + both post-reply + // hooks) — each evaluation would otherwise re-decode Buffer args. + get commandIdentifier(): CommandIdentifier { + if (this.#commandIdentifier) return this.#commandIdentifier; + const rawCommand = this.#redisArgs[0]; + const rawSubcommand = this.#redisArgs[1]; + const command = rawCommand instanceof Buffer ? rawCommand.toString() : rawCommand; + const subcommand = rawSubcommand === undefined + ? undefined + : rawSubcommand instanceof Buffer ? rawSubcommand.toString() : rawSubcommand; + return this.#commandIdentifier = { command, subcommand }; + } + push(...arg: Array) { this.#redisArgs.push(...arg); }; @@ -137,6 +155,17 @@ export class BasicCommandParser implements CommandParser { this.#addKey(key, applyPrefix); } + /** + * Records a routing key whose value is already present in the pushed args, + * without appending it again. Used by the raw cluster `sendCommand` path, + * where the caller supplies the routing key separately from the full, + * already-assembled argument list — so `firstKey` resolves for routing while + * `redisArgs` stays an exact copy of the command sent on the wire. + */ + markRoutingKey(key: RedisArgument) { + this.#keys.push(key); + } + pushKeysLength(keys: RedisVariadicArgument) { if (Array.isArray(keys)) { this.#redisArgs.push(keys.length.toString()); diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index d9ebdedd4e5..a64eabaecc0 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -10,13 +10,18 @@ import { PubSubListener, PubSubListeners } from '../client/pub-sub'; import { ErrorReply } from '../errors'; import { RedisTcpSocketOptions } from '../client/socket'; import { ClientSideCacheConfig, PooledClientSideCacheProvider } from '../client/cache'; -import { BasicCommandParser } from '../client/parser'; +import { BasicCommandParser, CommandParser } from '../client/parser'; import { ASKING_CMD } from '../commands/ASKING'; import SingleEntryCache from '../single-entry-cache' import { publish, CHANNELS } from '../client/tracing'; import { ClientIdentity, ClientRole, generateClusterClientId } from '../client/identity'; import { DEFAULT_COMMAND_TIMEOUT } from '../defaults'; import { FieldsetRegistry } from '../himport/registry'; +import { defaultCommandMetadata, defaultCommandPolicies, isReplicaSafe, PolicyResolver, REQUEST_POLICIES_WITH_DEFAULTS, RESPONSE_POLICIES_WITH_DEFAULTS, type CommandMetadata } from '../command-metadata'; +import { REQUEST_ROUTERS, RESPONSE_REDUCERS, NUMERIC_AGG_POLICIES, remapAggregateReply } from './request-response-policies/dispatch'; +import calculateSlot from 'cluster-key-slot'; +import { finalizeFtCursor } from './request-response-policies/ft-cursor'; +import { finalizeScanCursor } from './request-response-policies/scan-cursor'; export type ClusterTopologyRefreshOnReconnectionAttemptStrategy = false | @@ -30,7 +35,6 @@ type WithCommands< [P in keyof typeof NON_STICKY_COMMANDS]: CommandSignature<(typeof NON_STICKY_COMMANDS)[P], RESP, TYPE_MAPPING>; }; - interface ClusterCommander< M extends RedisModules, F extends RedisFunctions, @@ -196,11 +200,11 @@ export default class RedisCluster< const parser = new BasicCommandParser(this._self._keyPrefix); command.parseCommand(parser, ...args); - return this._self._execute( - parser.firstKey, + return this._self._executeWithPolicies( + parser, command.IS_READ_ONLY, this._commandOptions, - (client, opts) => client._executeCommand(command, parser, opts, transformReply) + p => (client, opts) => client._executeCommand(command, p, opts, transformReply) ); }; } @@ -212,11 +216,11 @@ export default class RedisCluster< const parser = new BasicCommandParser(this._self._keyPrefix); command.parseCommand(parser, ...args); - return this._self._execute( - parser.firstKey, + return this._self._executeWithPolicies( + parser, command.IS_READ_ONLY, this._self._commandOptions, - (client, opts) => client._executeCommand(command, parser, opts, transformReply) + p => (client, opts) => client._executeCommand(command, p, opts, transformReply) ); }; } @@ -230,11 +234,11 @@ export default class RedisCluster< parser.push(...prefix); fn.parseCommand(parser, ...args); - return this._self._execute( - parser.firstKey, + return this._self._executeWithPolicies( + parser, fn.IS_READ_ONLY, this._self._commandOptions, - (client, opts) => client._executeCommand(fn, parser, opts, transformReply) + p => (client, opts) => client._executeCommand(fn, p, opts, transformReply) ); }; } @@ -248,11 +252,11 @@ export default class RedisCluster< parser.push(...prefix); script.parseCommand(parser, ...args); - return this._self._execute( - parser.firstKey, + return this._self._executeWithPolicies( + parser, script.IS_READ_ONLY, this._commandOptions, - (client, opts) => client._executeScript(script, parser, opts, transformReply) + p => (client, opts) => client._executeScript(script, p, opts, transformReply) ); }; } @@ -328,6 +332,7 @@ export default class RedisCluster< private _self = this; private _commandOptions?: ClusterCommandOptions; + private _policyResolver: PolicyResolver; /** * An array of the cluster slots, each slot contain its `master` and `replicas`. @@ -408,6 +413,10 @@ export default class RedisCluster< this.on(RESUBSCRIBE_LISTENERS_EVENT, this.resubscribeAllPubSubListeners.bind(this)); this._commandOptions = { timeout: DEFAULT_COMMAND_TIMEOUT, ...options?.commandOptions }; + + // Shared process-wide resolver over the static metadata table. Kept as an + // instance field so a future per-connection dynamic resolver can replace it. + this._policyResolver = defaultCommandMetadata; } duplicate< @@ -507,83 +516,253 @@ export default class RedisCluster< }; } - async _execute( - firstKey: RedisArgument | undefined, + /** + * Resolves the command's policies and executes it accordingly: the request + * policy picks the target clients, each target runs through the core + * `_execute` transport primitive, and the response policy aggregates the + * replies. Call sites pass a `makeFn` factory that builds the per-client + * execution closure (command, script, or raw `sendCommand`). + */ + async _executeWithPolicies( + parser: CommandParser, isReadonly: boolean | undefined, options: ClusterCommandOptions | undefined, - fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise + makeFn: (parser: CommandParser) => (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ): Promise { - const maxCommandRedirections = this._options.maxCommandRedirections ?? 16; - let { client, slotNumber } = await this._slots.getClientAndSlotNumber(firstKey, isReadonly); - let i = 0; + const policyResult = this._policyResolver.resolvePolicy(parser.commandIdentifier); + + // Commands the resolver doesn't know — user-defined custom commands, + // scripts/functions, modules absent from the policy table — have no + // request/response policy and nothing to split or aggregate. Fall back to + // the default key-routed path (single client by `firstKey`, sole reply + // passed through) rather than failing. Scripts/functions are single-slot + // by contract, so default-keyed is always correct for them. Known + // multi_shard commands that can't be split still throw from the splitter. + const policy: CommandMetadata = policyResult.ok + ? policyResult.value + : defaultCommandPolicies(parser.keys.length === 0); + + // Override-first: a defined `IS_READ_ONLY` (command definition, script, or + // the raw `sendCommand` caller argument threaded in as `isReadonly`) wins; + // otherwise the table's write/script_runner flags and keyed-ness decide + // (see `isReplicaSafe`). + const readonly = isReplicaSafe(policy, isReadonly); + + const requestPolicy = policy.request + const responsePolicy = policy.response + + // Fast path: default-keyed request + response — the overwhelming majority + // of traffic (every single-key command). Route by firstKey and pass the + // sole reply through, skipping the plan/reducer/post-reply machinery, + // which is a no-op for this shape (the hooks only concern + // FT.AGGREGATE/FT.CURSOR/SCAN and the remap only numeric aggregates — + // none of them default-keyed). + if ( + requestPolicy === REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED && + responsePolicy === RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED + ) { + return this._execute(parser, readonly, options, makeFn(parser)); + } - let myFn = fn; + // https://redis.io/docs/latest/develop/reference/command-tips + const router = REQUEST_ROUTERS[requestPolicy]; + if (!router) { + throw new Error(`Unknown request policy ${requestPolicy}`); + } + // Validated before any per-node promise is dispatched — throwing after + // would orphan in-flight rejections (unhandled-rejection noise) and run + // side effects for a reply that can never be reduced. + const reducer = RESPONSE_REDUCERS[responsePolicy]; + if (!reducer) { + throw new Error(`Unknown response policy ${responsePolicy}`); + } + // Routers are typed against the erased base cluster types (routing is + // below the typed command surface); bridge this instantiation's slots in. + const plan = await router( + this._slots as unknown as Parameters[0], + parser, + readonly, + policy.keySpecs + ); - while (true) { - try { - const opts: ClusterCommandOptions = { ...options, slotNumber }; - return await myFn(client, opts); - } catch (_err) { - const err = _err as Error; - myFn = fn; + if (plan.length === 0) { + throw new Error(`Request policy ${requestPolicy} produced no target nodes`); + } + + // Numeric aggregation must see raw numbers: strip the caller's type + // mapping from the per-node executions (a `NUMBER: String` mapping would + // feed strings into the numeric reducers) and re-apply it to the + // aggregated result below. Pass-through policies keep the mapping — their + // replies reach the caller undisturbed. + const numericAgg = NUMERIC_AGG_POLICIES.has(responsePolicy); + const requestedMapping = options?.typeMapping; + const execOptions = numericAgg && requestedMapping + ? { ...options, typeMapping: undefined } + : options; + + // Track the actually-serving client for single-target plans: after a + // MOVED/ASK redirect the reply comes from a different node than + // `plan[0].client`, and the post-reply hooks must bind cursors to the node + // that really served. Multi-target hooks are no-ops, so nothing to track. + const served: { client?: RedisClientType } = {}; + const responsePromises = plan.map(entry => { + const entryParser = entry.parser ?? parser; + // Re-narrow the opaque routed client to this cluster's instantiation. + const client = entry.client as RedisClientType | undefined; + return this._execute( + entryParser, readonly, execOptions, makeFn(entryParser), client, + plan.length === 1 ? served : undefined + ); + }); + + const positionHints = plan.map(entry => entry.groupIndices); + let reply = await (reducer(responsePromises, parser, positionHints) as Promise); + if (numericAgg) { + reply = remapAggregateReply(reply, requestedMapping); + } + + // Attribute the reply to the node that actually served it (MOVED/ASK may + // have redirected away from the plan's original target). The plan carries + // the erased base client type (routing runs below the typed surface), so + // widen this instantiation's client back at the boundary. + const servedClient = served.client as unknown as typeof plan[0]['client']; + const hookPlan = servedClient && servedClient !== plan[0].client + ? [{ ...plan[0], client: servedClient }] + : plan; + + // Sticky-cursor bookkeeping: FT.AGGREGATE/FT.CURSOR bind/rebind/evict the + // serving node and swap the server cursor id in the reply for a + // client-minted token (server ids are per-node and can collide across + // shards). Command-name gated; best-effort — on failure the caller gets + // the raw server cursor, which MISSes (with a clear error) on the next + // READ/DEL instead of silently routing to the wrong node. + try { + reply = finalizeFtCursor( + this._slots as unknown as Parameters[0], + parser, + hookPlan, + reply + ) as typeof reply; + } catch { /* cursor finalization is best-effort */ } + + // Cluster-wide SCAN: advance the scan chain and swap the per-node server + // cursor for the chain's virtual token. Command-name gated; best-effort — + // on failure the caller gets the raw server cursor, which MISSes (with a + // clear error) on the next call instead of silently iterating wrong. + try { + reply = finalizeScanCursor( + this._slots as unknown as Parameters[0], + parser, + hookPlan, + reply + ) as typeof reply; + } catch { /* scan finalization is best-effort */ } + + return reply; + } + + /** + * Core transport primitive: sends one command to one client — resolved by + * the parser's first key unless `pinnedClient` is given — with MOVED/ASK + * redirect handling. Policy-free; fan-out and aggregation live in + * `_executeWithPolicies`. + */ + async _execute( + parser: CommandParser, + isReadonly: boolean | undefined, + options: ClusterCommandOptions | undefined, + fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise, + pinnedClient?: RedisClientType, + // When given, records the client that actually served the reply — after a + // MOVED/ASK redirect that differs from the plan's original target, and the + // post-reply hooks (sticky cursor bindings) must attribute to it. + served?: { client?: RedisClientType } + ): Promise { + const maxCommandRedirections = this._options.maxCommandRedirections ?? 16; + + // The slot number travels with every attempt (commands-queue + // `slotNumber`): during an SMIGRATED maintenance event + // `extractCommandsForSlots` relocates queued commands to the destination + // node by this value. Pinned plans (default-keyed pins, multi_shard + // sub-commands) derive it from the parser's first key; keyless fan-outs + // carry none. The slot of a key never changes, so it is computed once + // even though MOVED redirects re-resolve the client. + let client: RedisClientType; + let slotNumber: number | undefined; + if (pinnedClient) { + client = pinnedClient; + slotNumber = parser.firstKey === undefined ? undefined : calculateSlot(parser.firstKey); + } else { + ({ client, slotNumber } = await this._slots.getClientAndSlotNumber(parser.firstKey, isReadonly)); + } + let i = 0; + + let myFn = fn; + + while (true) { + try { + if (served) served.client = client; + const opts: ClusterCommandOptions = { ...options, slotNumber }; + return await myFn(client, opts); + } catch (_err) { + const err = _err as Error; + myFn = fn; + + // TODO: error class + if (++i > maxCommandRedirections || !(err instanceof Error)) { + if (err instanceof Error) { + publish(CHANNELS.ERROR, () => ({ + error: err, + origin: 'cluster', + internal: false, + clientId: client._clientId, + retryCount: i, + })); + } + throw err; + } - // TODO: error class - if (++i > maxCommandRedirections || !(err instanceof Error)) { - if (err instanceof Error) { + if (err.message.startsWith('ASK')) { publish(CHANNELS.ERROR, () => ({ error: err, origin: 'cluster', - internal: false, + internal: true, clientId: client._clientId, retryCount: i, })); + const address = err.message.substring(err.message.lastIndexOf(' ') + 1); + let redirectTo = await this._slots.getMasterByAddress(address); + if (!redirectTo) { + await this._slots.rediscover(client); + redirectTo = await this._slots.getMasterByAddress(address); + } + + if (!redirectTo) { + throw new Error(`Cannot find node ${address}`); + } + + client = redirectTo; + myFn = this._handleAsk(fn); + continue; } - throw err; - } - if (err.message.startsWith('ASK')) { - publish(CHANNELS.ERROR, () => ({ - error: err, - origin: 'cluster', - internal: true, - clientId: client._clientId, - retryCount: i, - })); - const address = err.message.substring(err.message.lastIndexOf(' ') + 1); - let redirectTo = await this._slots.getMasterByAddress(address); - if (!redirectTo) { + if (err.message.startsWith('MOVED')) { + publish(CHANNELS.ERROR, () => ({ + error: err, + origin: 'cluster', + internal: true, + clientId: client._clientId, + retryCount: i, + })); await this._slots.rediscover(client); - redirectTo = await this._slots.getMasterByAddress(address); + client = (await this._slots.getClientAndSlotNumber(parser.firstKey, isReadonly)).client; + continue; } - if (!redirectTo) { - throw new Error(`Cannot find node ${address}`); - } - - client = redirectTo; - myFn = this._handleAsk(fn); - continue; - } - - if (err.message.startsWith('MOVED')) { - publish(CHANNELS.ERROR, () => ({ - error: err, - origin: 'cluster', - internal: true, - clientId: client._clientId, - retryCount: i, - })); - await this._slots.rediscover(client); - const clientAndSlot = await this._slots.getClientAndSlotNumber(firstKey, isReadonly); - client = clientAndSlot.client; - slotNumber = clientAndSlot.slotNumber; - continue; + throw err; } - - throw err; } - } } async sendCommand( @@ -599,11 +778,24 @@ export default class RedisCluster< ...this._commandOptions, ...options } - return this._self._execute( - firstKey, + + // `args` is the full command as sent on the wire (name at index 0), so it + // becomes `redisArgs` verbatim — policy resolution reads the command name + // and the multi_shard splitter's key-spec offsets line up. The caller's + // `firstKey` is marked for routing without re-appending it. + const parser = new BasicCommandParser(); + args.forEach(arg => parser.push(arg)); + if (firstKey !== undefined) parser.markRoutingKey(firstKey); + + // Raw path: no command object, so readonly-ness stays an explicit caller + // argument and the reply is returned untransformed. The closure sends the + // per-entry parser's args, so split multi_shard sub-commands each carry + // their own slot's arguments (and the unsplit case sends `args` unchanged). + return this._self._executeWithPolicies( + parser, isReadonly, opts, - (client, opts) => client.sendCommand(args, opts) + p => (client, opts) => client.sendCommand(p.redisArgs as CommandArguments, opts) ); } diff --git a/packages/client/lib/cluster/request-response-policies/dispatch.ts b/packages/client/lib/cluster/request-response-policies/dispatch.ts new file mode 100644 index 00000000000..9d885ded989 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/dispatch.ts @@ -0,0 +1,334 @@ +import { BasicCommandParser, type CommandParser } from '../../client/parser'; +import type { RedisClientType } from '../../client'; +import type { + RedisArgument, RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping +} from '../../RESP/types'; +import { RESP_TYPES } from '../../RESP/decoder'; +import type { KeySpec } from '../../commands/generic-transformers'; +import type RedisClusterSlots from '../cluster-slots'; +import { splitMultiShardCommand, type SubCommand } from './multi-shard-splitter'; +import { + aggregateLogicalAnd, + aggregateLogicalOr, + aggregateMax, + aggregateMerge, + aggregateMin, + aggregateSum +} from './generic-aggregators'; +import { + REQUEST_POLICIES_WITH_DEFAULTS, + RESPONSE_POLICIES_WITH_DEFAULTS, + type RequestPolicyWithDefaults, + type ResponsePolicyWithDefaults +} from '../../command-metadata/policies-constants'; +import { routeFtCursor, upperCommand } from './ft-cursor'; +import { routeScan } from './scan-cursor'; + +// Routing runs *below* the typed command surface: routers never inspect the +// command's M/F/S/RESP/TM parameters, they just shuffle opaque clients from +// `slots` into the plan and on to `_execute`. So these types are deliberately +// not generic — they use the base constraint types. The engine re-narrows the +// client to its own instantiation at the `_execute` boundary. +type ClusterClient = RedisClientType; +type ClusterSlots = RedisClusterSlots; + +/** + * One unit of work a request policy schedules. Pass-through policies set only + * `client` (run the original command on that node). `multi_shard` sets `parser` + * (a per-slot sub-command, routed by its own `firstKey`) and `groupIndices` + * (where its replies belong in the reassembled result). + */ +export type RoutedCommand = { + client?: ClusterClient; + parser?: CommandParser; + groupIndices?: Array; +}; + +export type RequestRouter = ( + slots: ClusterSlots, + parser: CommandParser, + isReadonly: boolean | undefined, + keySpecs: ReadonlyArray | undefined +) => Promise>; + +export type ResponseReducer = ( + responsePromises: Promise[], + parser: CommandParser, + /** + * For `multi_shard` commands, `positionHints[p]` is the original 0-based + * group ordinals carried by the p-th sub-command (plan order == promise + * order). Reducers that preserve order (e.g. default-keyed MGET) use it to + * scatter each sub-reply back into the caller's key order. `undefined` + * entries mean "not split"; the whole array is absent for non-split commands. + */ + positionHints?: Array | undefined> +) => Promise; + +// --- request routers --- + +export const routeAllNodes: RequestRouter = + async (slots) => (await slots.getAllClients()).map(client => ({ client })); + +export const routeAllShards: RequestRouter = + async (slots) => (await slots.getAllMasterClients()).map(client => ({ client })); + +/** + * Splits the command into one sub-command per hash slot (using the COMMAND key + * specs as the reconstruction recipe) and returns a plan entry per slot. Each + * entry carries its own sub-parser, so core `_execute` routes it by that + * slot's `firstKey` and handles MOVED/ASK with the sub-command's own key. + */ +export const routeMultiShard: RequestRouter = + async (_slots, parser, _isReadonly, keySpecs) => { + const subCommands = splitMultiShardCommand(parser.redisArgs, keySpecs); + return Array.from(subCommands.values(), sub => ({ + parser: buildSubParser(sub), + groupIndices: sub.groupIndices + })); + }; + +/** + * Rebuilds a `CommandParser` from a split sub-command, marking the keys at + * their known positions so `firstKey` resolves to this slot's first key. + */ +function buildSubParser(sub: SubCommand): CommandParser { + const parser = new BasicCommandParser(); + const keyPositions = new Set(sub.keyPositions); + for (let i = 0; i < sub.args.length; i++) { + const arg = sub.args[i] as RedisArgument; + if (keyPositions.has(i)) { + parser.pushKey(arg); + } else { + parser.push(arg); + } + } + return parser; +} + +// `nodeClient` connects lazily — with `minimizeConnections` a node may have +// no client until first use, and `.client!` would put `undefined` in the plan +// (breaking the post-reply hooks that attribute the reply to `plan[0].client`). +export const routeDefaultKeyless: RequestRouter = + async (slots) => [{ client: await slots.nodeClient(slots.getRandomNode()) }]; + +export const routeDefaultKeyed: RequestRouter = + async (slots, parser, isReadonly) => + [{ client: (await slots.getClientAndSlotNumber(parser.firstKey, isReadonly)).client }]; + +/** + * Uppercased command key ("RANDOMKEY", "MEMORY STATS") used both to look up a + * per-command special handler and to label warnings. `commandIdentifier` + * preserves the caller's casing, so normalize before matching. + */ +function specialKey(parser: CommandParser): string { + const { subcommand } = parser.commandIdentifier; + const c = upperCommand(parser); + return subcommand ? `${c} ${subcommand.toUpperCase()}` : c; +} + +/** + * Special-request routers. Looked up by `COMMAND SUBCOMMAND` first, then bare + * `COMMAND` — SCAN registers bare because its second argument is a cursor, + * which the naive `commandIdentifier` mistakes for a subcommand. + */ +export const SPECIAL_REQUEST_ROUTERS: Record = { + 'FT.CURSOR READ': routeFtCursor, + 'FT.CURSOR DEL': routeFtCursor, + SCAN: routeScan +}; + +/** + * Router for the `special` request policy. Commands with a dedicated handler + * (e.g. FT.CURSOR sticky routing, cluster-wide SCAN) short-circuit into + * `SPECIAL_REQUEST_ROUTERS` first. Everything else has non-trivial routing no + * generic rule captures and no handler yet: route to a single (random) node + * like a keyless command so it still works, but warn — the reply reflects + * only that one node. + */ +export const routeSpecial: RequestRouter = + async (slots, parser, isReadonly, keySpecs) => { + const handler = SPECIAL_REQUEST_ROUTERS[specialKey(parser)] + ?? SPECIAL_REQUEST_ROUTERS[parser.commandIdentifier.command.toUpperCase()]; + if (handler) return handler(slots, parser, isReadonly, keySpecs); + + console.warn( + `node-redis: no cluster routing implemented for the "special" request policy of ` + + `"${specialKey(parser)}"; routing to a single node. The reply may be incomplete.` + ); + return [{ client: await slots.nodeClient(slots.getRandomNode()) }]; + }; + +// --- response reducers --- + +/** + * `one_succeeded`: first fulfilled reply wins. When every node rejects the + * policy requires surfacing one of the shard errors (e.g. SCRIPT KILL's + * NOTBUSY), not the opaque `AggregateError` that `Promise.any` throws. + */ +export const reduceOneSucceeded = async (promises: Promise[]): Promise => { + try { + return await Promise.any(promises); + } catch (err) { + if (err instanceof AggregateError && err.errors.length > 0) throw err.errors[0]; + throw err; + } +}; + +export const reduceAllSucceeded = async (promises: Promise[]): Promise => { + const responses = await Promise.all(promises); + return responses[0]; +}; + +/** Lifts a synchronous all-replies aggregator into a response reducer. */ +const reduceWith = (aggregate: (replies: Array) => unknown) => + async (promises: Promise[]): Promise => + aggregate(await Promise.all(promises)) as T; + +export const reduceLogicalAnd = reduceWith(aggregateLogicalAnd); +export const reduceLogicalOr = reduceWith(aggregateLogicalOr); +export const reduceMin = reduceWith(aggregateMin); +export const reduceMax = reduceWith(aggregateMax); +export const reduceSum = reduceWith(aggregateSum); + +/** + * RANDOMKEY under `all_shards`: each master returns a random key from its own + * keyspace (or nil when empty). Return one of the non-nil replies at random so + * the result is a valid random key across the whole cluster and never a + * false-nil when some shard is empty but others hold keys. All shards empty → + * nil. + */ +export const reduceRandomKey = async (promises: Promise[]): Promise => { + const responses = await Promise.all(promises); + const keys = responses.filter(reply => reply != null); + if (keys.length === 0) return responses[0]; + return keys[Math.floor(Math.random() * keys.length)]; +}; + +/** + * Per-command reducers for the `special` response policy, keyed like + * `SPECIAL_REQUEST_ROUTERS` (`COMMAND SUBCOMMAND`, bare-command fallback). A + * `special` response needs command-specific merging that no generic rule + * captures; commands absent here hit `reduceSpecial`'s generic fallback. SCAN + * (response also tipped `special`) needs no entry: its plan is single-node, so + * the fallback passes the sole reply through and the cursor rewrite happens in + * `finalizeScanCursor`. + */ +export const SPECIAL_RESPONSE_REDUCERS: Record> = { + RANDOMKEY: reduceRandomKey +}; + +/** + * Entry point for the `special` response policy: dispatch to a per-command + * reducer if one exists, else fall back to the default-keyless reduction (sole + * reply as is, or a merge of a fan-out) so the command works instead of + * throwing. Warn on the fallback because the merged shape is unlikely to be + * what the command really wants. + */ +export const reduceSpecial = async (promises: Promise[], parser: CommandParser): Promise => { + const reducer = SPECIAL_RESPONSE_REDUCERS[specialKey(parser)] + ?? SPECIAL_RESPONSE_REDUCERS[parser.commandIdentifier.command.toUpperCase()]; + if (reducer) return reducer(promises, parser) as Promise; + + if (promises.length > 1) { + console.warn( + `node-redis: no cluster aggregation implemented for the "special" response policy of ` + + `"${specialKey(parser)}"; merging replies from ${promises.length} nodes. The result shape may be wrong.` + ); + } + return reduceDefaultKeyless(promises); +}; + +export const reduceDefaultKeyless = async (promises: Promise[]): Promise => { + const responses = await Promise.all(promises); + // Merging is only meaningful for fan-out replies (e.g. KEYS under + // all_shards); the single-target case must pass scalar replies through. + if (responses.length === 1) return responses[0]; + return aggregateMerge(responses) as T; +}; + +export const reduceDefaultKeyed = async ( + promises: Promise[], + _parser: CommandParser, + positionHints?: Array | undefined> +): Promise => { + const responses = await Promise.all(promises); + + // Unsplit (single-target read, or a multi_shard command that landed on one + // slot): pass the sole reply through unchanged. + if (!positionHints?.some(hint => hint !== undefined)) { + return responses[0]; + } + + // Split multi_shard (e.g. MGET across slots): each sub-reply is an array in + // its own group order; scatter element i back to its original group ordinal + // so the result matches the caller's key order regardless of slot/arrival. + const result: Array = []; + responses.forEach((reply, p) => { + const indices = positionHints[p]; + if (!indices) { + throw new Error('default-keyed reducer: split reply missing position hints'); + } + const elements = reply as Array; + indices.forEach((groupIndex, i) => { + result[groupIndex] = elements[i]; + }); + }); + return result as T; +}; + +/** + * Response policies whose reducers compute over raw numbers (scalars or + * number arrays). Per-node replies for these plans are decoded *without* the + * caller's type mapping — a `NUMBER: String` mapping would otherwise feed + * strings into the numeric aggregators and throw — and the caller's mapping + * is applied to the aggregated result instead (`remapAggregateReply`). + */ +export const NUMERIC_AGG_POLICIES: ReadonlySet = new Set([ + RESPONSE_POLICIES_WITH_DEFAULTS.AGG_SUM, + RESPONSE_POLICIES_WITH_DEFAULTS.AGG_MIN, + RESPONSE_POLICIES_WITH_DEFAULTS.AGG_MAX, + RESPONSE_POLICIES_WITH_DEFAULTS.AGG_LOGICAL_AND, + RESPONSE_POLICIES_WITH_DEFAULTS.AGG_LOGICAL_OR +]); + +/** + * Applies the caller's NUMBER type mapping to a numeric aggregate (scalar, or + * an array for the element-wise reducers like SCRIPT EXISTS), so aggregated + * fan-out replies keep the same shape a standalone client would return. + * Aggregation itself runs in JS number space, so — unlike standalone decode — + * a `NUMBER: String` mapping does not preserve integer precision above 2^53 + * (see cluster-policy-caveats.md). + */ +export function remapAggregateReply(reply: T, typeMapping: TypeMapping | undefined): T { + const map = typeMapping?.[RESP_TYPES.NUMBER]; + if (!map || map === Number) return reply; + // NUMBER maps to NumberConstructor | StringConstructor; both are callable. + const apply = map as (value: number) => unknown; + return (Array.isArray(reply) + ? reply.map(value => apply(value as number)) + : apply(reply as number)) as T; +} + +// --- registries --- + +export const REQUEST_ROUTERS = { + [REQUEST_POLICIES_WITH_DEFAULTS.ALL_NODES]: routeAllNodes, + [REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS]: routeAllShards, + [REQUEST_POLICIES_WITH_DEFAULTS.MULTI_SHARD]: routeMultiShard, + [REQUEST_POLICIES_WITH_DEFAULTS.SPECIAL]: routeSpecial, + [REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS]: routeDefaultKeyless, + [REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED]: routeDefaultKeyed +} as const satisfies Record; + +export const RESPONSE_REDUCERS = { + [RESPONSE_POLICIES_WITH_DEFAULTS.ONE_SUCCEEDED]: reduceOneSucceeded, + [RESPONSE_POLICIES_WITH_DEFAULTS.ALL_SUCCEEDED]: reduceAllSucceeded, + [RESPONSE_POLICIES_WITH_DEFAULTS.AGG_LOGICAL_AND]: reduceLogicalAnd, + [RESPONSE_POLICIES_WITH_DEFAULTS.AGG_LOGICAL_OR]: reduceLogicalOr, + [RESPONSE_POLICIES_WITH_DEFAULTS.AGG_MIN]: reduceMin, + [RESPONSE_POLICIES_WITH_DEFAULTS.AGG_MAX]: reduceMax, + [RESPONSE_POLICIES_WITH_DEFAULTS.AGG_SUM]: reduceSum, + [RESPONSE_POLICIES_WITH_DEFAULTS.SPECIAL]: reduceSpecial, + [RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS]: reduceDefaultKeyless, + [RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED]: reduceDefaultKeyed +} as const satisfies Record>; diff --git a/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts b/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts new file mode 100644 index 00000000000..d5fcedd552f --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts @@ -0,0 +1,153 @@ +/** + * Aggregates multiple arrays of numbers using logical AND operation. + * @remarks + * This implementation is specifically designed for Array> type only, + * despite the generic type parameter. It is currently used by the SCRIPT EXISTS command + * which returns an array of 0s and 1s from each shard. + * The generic type parameter T is provided for usage ergonomy, but the actual input structure + * will be validated at runtime. + */ +export const aggregateLogicalAnd = (replies: Array): T => { + if (replies.length === 0) return [] as T; + if ( + !replies.every( + (reply): reply is number[] => + Array.isArray(reply) && + reply.every((value): value is number => typeof value === 'number') + ) + ) { + throw new Error( + 'All replies must be array of numbers for logical AND aggregation' + ); + } + + const result = Array(replies[0].length).fill(1); + + for (const reply of replies) { + for (let i = 0; i < reply.length; i++) { + result[i] = result[i] && reply[i]; + } + } + + return result as T; +}; + +/** + * Aggregates multiple arrays of numbers using logical OR operation. + * @remarks + * Mirror of `aggregateLogicalAnd` for the `agg_logical_or` response policy. + * No command in the current metadata snapshot uses it, but the reducer is + * registered in `RESPONSE_REDUCERS`, so it must be correct for the first + * command a future metadata regeneration tags with it. + */ +export const aggregateLogicalOr = (replies: Array): T => { + if (replies.length === 0) return [] as T; + if ( + !replies.every( + (reply): reply is number[] => + Array.isArray(reply) && + reply.every((value): value is number => typeof value === 'number') + ) + ) { + throw new Error( + 'All replies must be array of numbers for logical OR aggregation' + ); + } + + const result = Array(replies[0].length).fill(0); + + for (const reply of replies) { + for (let i = 0; i < reply.length; i++) { + result[i] = result[i] || reply[i]; + } + } + + return result as T; +}; + +/** + * Aggregates multiple numbers by finding the minimum value. + * @remarks + * This implementation is specifically designed for Array type only, + * despite the generic type parameter. It is used by commands like WAIT + * which returns the minimal number of synchronized replicas from all shards. + * The generic type parameter T is provided for usage ergonomy, but the actual input structure + * will be validated at runtime. + */ +export const aggregateMin = (replies: Array): T => { + if (replies.length === 0) return 0 as T; + if (!replies.every((reply): reply is number => typeof reply === 'number')) { + throw new Error('All replies must be numbers for min aggregation'); + } + return Math.min(...replies) as T; +}; + +/** + * Aggregates multiple numbers by finding the maximum value. + * @remarks + * This implementation is specifically designed for Array type only, + * despite the generic type parameter. The generic type parameter T is provided + * for usage ergonomy, but the actual input structure will be validated at runtime. + */ +export const aggregateMax = (replies: Array): T => { + if (replies.length === 0) return 0 as T; + if (!replies.every((reply): reply is number => typeof reply === 'number')) { + throw new Error('All replies must be numbers for max aggregation'); + } + return Math.max(...replies) as T; +}; + +/** + * Aggregates multiple numbers by finding the sum of all values. + * @remarks + * This implementation is specifically designed for Array type only, + * despite the generic type parameter. The generic type parameter T is provided + * for usage ergonomy, but the actual input structure will be validated at runtime. + */ +export const aggregateSum = (replies: Array): T => { + if (replies.length === 0) return 0 as T; + if (!replies.every((reply): reply is number => typeof reply === 'number')) { + throw new Error('All replies must be numbers for sum aggregation'); + } + return replies.reduce((acc, reply) => acc + reply, 0) as T; +}; + + +export const aggregateMerge = (replies: Array): T => { + if(replies.length === 0) return undefined as T; + + const firstReply = replies[0] + + if(Array.isArray(firstReply)) { + const set = new Set() + for(const reply of replies) { + for(const item of reply as Array) { + set.add(item); + } + } + return Array.from(set) as T; + } + + if(firstReply instanceof Map) { + const map = new Map(); + for(const reply of replies) { + for(const [key, value] of reply as Map) { + map.set(key, value); + } + } + return map as T; + } + + // RESP3 map replies decode to plain objects under the default type + // mapping; merge them like the Map branch (last node wins per key). + if(typeof firstReply === 'object' && firstReply !== null) { + const merged: Record = {}; + for(const reply of replies) { + Object.assign(merged, reply); + } + return merged as T; + } + + throw new Error('Unsupported reply type for merge aggregation'); + +}; diff --git a/packages/client/lib/cluster/request-response-policies/index.ts b/packages/client/lib/cluster/request-response-policies/index.ts new file mode 100644 index 00000000000..4a5ef8f4a95 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/index.ts @@ -0,0 +1,5 @@ +// Cluster-only routing that *consumes* command metadata. The metadata table +// and its resolver now live in `lib/command-metadata/`; import those from there. +export * from './dispatch'; +export { splitMultiShardCommand, type SubCommand } from './multi-shard-splitter'; + From c748299f7c8da97bba0dff4da828b5c5d6af72d0 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 30 Jul 2026 14:14:12 +0300 Subject: [PATCH 06/17] feat(client): special-policy routing (SCAN, RANDOMKEY, FT.CURSOR) Implement the client-side special policies: cluster-wide SCAN behind a virtual cursor token, RANDOMKEY fan-out with a non-nil reduction, and sticky FT.CURSOR routing bound to the node that served the aggregate. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/client/lib/cluster/cluster-slots.ts | 159 +++++++++++++ .../request-response-policies/ft-cursor.ts | 225 ++++++++++++++++++ .../request-response-policies/scan-cursor.ts | 164 +++++++++++++ 3 files changed, 548 insertions(+) create mode 100644 packages/client/lib/cluster/request-response-policies/ft-cursor.ts create mode 100644 packages/client/lib/cluster/request-response-policies/scan-cursor.ts diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 0d4d479a85e..1099583cded 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -23,6 +23,47 @@ export type NodeAddressMap = { export const RESUBSCRIBE_LISTENERS_EVENT = '__resubscribeListeners' +/** + * Sticky-cursor binding: which node served a RediSearch cursor. FT.CURSOR + * READ/DEL carry no key, so hash-slot routing can't reach the coordinator that + * minted the cursor — we pin by `address` ("host:port"), the durable handle + * (clients are recreated on reconnect/topology refresh, addresses aren't). + */ +export interface CursorBinding { + address: string; + /** + * The server's real cursor id, kept as a string: FT cursor ids are uint64 + * and `Number` would lose precision above 2^53. The caller only ever sees + * the client-minted token this binding is keyed by. + */ + cursorId: string; + createdAt: number; + maxIdleMs?: number; +} + +/** + * Fallback idle TTL for the opportunistic cursor-binding sweep when the + * FT.AGGREGATE didn't declare MAXIDLE. Mirrors the RediSearch default (300s) + * so abandoned cursors don't leak the binding map (timer-free, like + * `smigratedSeqIdsSeen`). + */ +const DEFAULT_CURSOR_MAX_IDLE_MS = 300_000; + +/** + * One in-flight cluster-wide SCAN chain (see + * `request-response-policies/scan-cursor.ts`). SCAN cursors are per-node + * state, so a cluster-wide iteration walks the masters one at a time: the + * entry pins the node currently being scanned, the real server cursor to + * resume it with, and the masters already exhausted (tracked by address so a + * topology refresh mid-scan doesn't rescan or skip nodes that survived). + */ +export interface ScanCursorEntry { + address: string; + cursor: string; + visited: Set; + createdAt: number; +} + export interface Node< M extends RedisModules, F extends RedisFunctions, @@ -133,6 +174,16 @@ export default class RedisClusterSlots< return this.#himportRegistry; } smigratedSeqIdsSeen = new Set; + /** + * Per-instance sticky FT cursor bindings, keyed by the client-minted virtual + * token (the value the caller holds in place of the server's cursor id). + * Server cursor ids are minted per node and can collide across shards; + * client tokens come from one sequence and cannot. + */ + readonly cursorBindings = new Map(); + /** Per-instance cluster-wide SCAN chains, keyed by the virtual cursor token. */ + readonly scanCursors = new Map(); + #cursorTokenSeq = 0; #topologyRefreshPromise?: Promise; #isOpen = false; @@ -842,6 +893,27 @@ export default class RedisClusterSlots< } } + /** + * All node clients (masters and replicas), connecting lazily — with + * `minimizeConnections` nodes have no client until first use, and skipping + * them would silently fan commands out to a subset of the cluster. + * Excludes dedicated PubSub connections: they cannot run regular commands. + */ + getAllClients(): Promise>> { + this.#assertReady(); + + return Promise.all([ + ...this.masters.map(master => this.nodeClient(master)), + ...this.replicas.map(replica => this.nodeClient(replica)) + ]); + } + + getAllMasterClients(): Promise>> { + this.#assertReady(); + + return Promise.all(this.masters.map(master => this.nodeClient(master))); + } + async getClientAndSlotNumber( firstKey: RedisArgument | undefined, isReadonly: boolean | undefined @@ -917,6 +989,8 @@ export default class RedisClusterSlots< _randomNodeIterator?: IterableIterator>; getRandomNode() { + this.#assertReady(); + this._randomNodeIterator ??= this.#iterateAllNodes(); return this._randomNodeIterator.next().value as ShardNode; } @@ -955,6 +1029,91 @@ export default class RedisClusterSlots< return this.nodeClient(master); } + /** + * Reverse-resolve a routed client to its node address. FT.AGGREGATE is + * keyless, so the plan carries only the client; we need its address to bind + * the cursor. Clients are few per cluster, so the linear scan is negligible. + */ + nodeAddressByClient(client: RedisClientType): string | undefined { + for (const [address, node] of this.nodeByAddress) { + if (node.client === client) return address; + } + return undefined; + } + + /** + * Drop bindings idle past their MAXIDLE (or the default TTL). Opportunistic — + * runs on each `bindCursor` so there's no timer to manage (see + * `smigratedSeqIdsSeen`). Cheap: the map holds only live cursors. + */ + #sweepStaleCursors(now: number) { + for (const [key, binding] of this.cursorBindings) { + const ttl = binding.maxIdleMs ?? DEFAULT_CURSOR_MAX_IDLE_MS; + if (now - binding.createdAt > ttl) { + this.cursorBindings.delete(key); + } + } + } + + bindCursor(token: string, binding: Omit) { + const now = Date.now(); + this.#sweepStaleCursors(now); + this.cursorBindings.set(token, { ...binding, createdAt: now }); + } + + lookupCursor(token: string): CursorBinding | undefined { + return this.cursorBindings.get(token); + } + + evictCursor(token: string) { + this.cursorBindings.delete(token); + } + + /** + * Mint a fresh virtual cursor token (cluster-wide SCAN chains, sticky FT + * cursors). Tokens are what the client hands back to the caller in place of + * the per-node server cursor: opaque, non-"0", never colliding with each + * other. A plain counter keeps them valid-looking cursor values for callers + * that treat the cursor as an opaque number. + */ + mintCursorToken(): string { + return String(++this.#cursorTokenSeq); + } + + /** + * Same opportunistic, timer-free sweep as `#sweepStaleCursors`: an abandoned + * scan (caller stopped iterating mid-way) would otherwise leak its entry. + */ + #sweepStaleScanCursors(now: number) { + for (const [token, entry] of this.scanCursors) { + if (now - entry.createdAt > DEFAULT_CURSOR_MAX_IDLE_MS) { + this.scanCursors.delete(token); + } + } + } + + bindScanCursor(token: string, address: string, cursor: string, visited: Set) { + const now = Date.now(); + this.#sweepStaleScanCursors(now); + this.scanCursors.set(token, { address, cursor, visited, createdAt: now }); + } + + lookupScanCursor(token: string): ScanCursorEntry | undefined { + return this.scanCursors.get(token); + } + + evictScanCursor(token: string) { + this.scanCursors.delete(token); + } + + /** + * First master (in current topology order) whose address is not in + * `visited` — the next node a cluster-wide SCAN chain should walk. + */ + nextScanTarget(visited: ReadonlySet): string | undefined { + return this.masters.find(master => !visited.has(master.address))?.address; + } + getPubSubClient(): Promise> { this.#assertReady(); diff --git a/packages/client/lib/cluster/request-response-policies/ft-cursor.ts b/packages/client/lib/cluster/request-response-policies/ft-cursor.ts new file mode 100644 index 00000000000..745f2f82e6d --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/ft-cursor.ts @@ -0,0 +1,225 @@ +import { BasicCommandParser, type CommandParser } from '../../client/parser'; +import type { RedisArgument } from '../../RESP/types'; +import type { RequestRouter, RoutedCommand } from './dispatch'; + +// Routing/capture runs below the typed command surface (see dispatch.ts), so +// the slots handle is the erased base instantiation. `_executeWithPolicies` +// bridges its own typed slots in at the call boundary. +type ClusterSlots = Parameters[0]; + +/** RediSearch index names are case-sensitive raw wire strings; mirror them. */ +export function argToString(arg: RedisArgument): string { + return typeof arg === 'string' ? arg : arg.toString(); +} + +/** + * Uppercased wire command name for command-name gates — `commandIdentifier` + * preserves the caller's casing, so normalize before matching. + */ +export function upperCommand(parser: CommandParser): string { + return parser.commandIdentifier.command.toUpperCase(); +} + +/** + * Pull the continuation cursor out of an FT.AGGREGATE …WITHCURSOR / + * FT.CURSOR READ reply, across every reply path: + * - transformed command path → `{ total, results, cursor }` (RESP2 + RESP3), + * - raw RESP2 `sendCommand` → `[result, cursor]` (cursor at index 1), + * - raw RESP3 `sendCommand` → a map with a `cursor` key (Map or object). + * The value is returned as decoded (number, or string under a NUMBER type + * mapping) so a reply rewrite can preserve its wire type. Returns `undefined` + * when no cursor field is present (e.g. FT.CURSOR DEL). + */ +export function extractCursorValue(reply: unknown): number | string | undefined { + if (reply == null) return undefined; + + if (reply instanceof Map) { + return reply.has('cursor') ? asCursorValue(reply.get('cursor')) : undefined; + } + + if (Array.isArray(reply)) { + return asCursorValue(reply[1]); + } + + if (typeof reply === 'object' && 'cursor' in (reply as Record)) { + return asCursorValue((reply as Record).cursor); + } + + return undefined; +} + +function asCursorValue(value: unknown): number | string | undefined { + if (typeof value === 'number' || typeof value === 'string') return value; + return undefined; +} + +function isExhausted(cursor: number | string): boolean { + return cursor === 0 || cursor === '0'; +} + +/** + * Read the numeric MAXIDLE (ms) an FT.AGGREGATE …WITHCURSOR declared, if any. + * `MAXIDLE 0` means "no idle limit" server-side (clamped to the server + * default), so it maps to `undefined` — the binding falls back to the default + * client-side TTL instead of expiring instantly. + */ +function maxIdleFromAggregateArgs(redisArgs: ReadonlyArray): number | undefined { + for (let i = 0; i < redisArgs.length - 1; i++) { + if (argToString(redisArgs[i]).toUpperCase() === 'MAXIDLE') { + const n = Number(argToString(redisArgs[i + 1])); + return Number.isNaN(n) || n <= 0 ? undefined : n; + } + } + return undefined; +} + +/** + * Sticky router for FT.CURSOR READ/DEL (HLD `request_policy: special`). + * + * The cursor argument the caller holds is a client-minted virtual token, not + * the server's cursor id: server ids are minted per node and two shards can + * mint the same id for one index, so the raw id cannot identify its node. The + * token resolves through the binding map to (node address, real cursor id) — + * pin that node and rewrite the cursor argument to the real id on the wire. + * + * A MISS (token never minted here, chain already exhausted, or expired) is + * unusable by this client, so throw before any network call rather than fan + * out or guess. + */ +export const routeFtCursor: RequestRouter = async (slots, parser) => { + const { redisArgs } = parser; + + // Malformed raw command (missing index/cursor): forward to any node so the + // server returns its own arity error instead of a client-side TypeError. + if (redisArgs.length < 4) { + return [{ client: await slots.nodeClient(slots.getRandomNode()) }]; + } + + const token = argToString(redisArgs[3]); + + const binding = slots.lookupCursor(token); + if (binding) { + const client = await slots.getMasterByAddress(binding.address); + if (client) return [{ client, parser: withCursorArg(parser, binding.cursorId) }]; + + throw new Error( + `FT.CURSOR: the node serving cursor ${token} on index "${argToString(redisArgs[2])}" ` + + `has left the cluster.` + ); + } + + throw new Error( + `FT.CURSOR: unknown cursor ${token} on index "${argToString(redisArgs[2])}". ` + + `Cluster cursors are minted per client instance and expire when idle — ` + + `the cursor was not created by this client, has already been exhausted, ` + + `or has expired.` + ); +}; + +/** Copy of the FT.CURSOR parser with the cursor argument (index 3) replaced. */ +function withCursorArg(parser: CommandParser, cursorId: string): CommandParser { + const sub = new BasicCommandParser(); + const { redisArgs } = parser; + for (let i = 0; i < redisArgs.length; i++) { + sub.push(i === 3 ? cursorId : redisArgs[i] as RedisArgument); + } + return sub; +} + +/** + * Command-name-gated post-reply hook for FT.AGGREGATE / FT.CURSOR (invoked + * from `_executeWithPolicies` after the reducer, like `finalizeScanCursor`). + * Mints/rebinds/evicts the sticky binding and swaps the server cursor id in + * the reply for the client token, so the caller only ever loops on tokens. + * No-op for any other command, and for multi-target plans (cursor commands + * are single-node). Returns the (possibly rewritten) reply. + */ +export function finalizeFtCursor( + slots: ClusterSlots, + parser: CommandParser, + plan: ReadonlyArray, + reply: unknown +): unknown { + const cmd = upperCommand(parser); + + if (cmd !== 'FT.AGGREGATE' && cmd !== 'FT.CURSOR') return reply; + if (plan.length !== 1) return reply; + + const { redisArgs } = parser; + + if (cmd === 'FT.AGGREGATE') { + const cursor = extractCursorValue(reply); + // cursor 0 → exhausted in one batch, nothing to pin. + if (cursor === undefined || isExhausted(cursor)) return reply; + + const client = plan[0].client; + if (!client) return reply; + const address = slots.nodeAddressByClient(client); + if (!address) return reply; + + const token = slots.mintCursorToken(); + slots.bindCursor(token, { + address, + cursorId: argToString(cursor as RedisArgument), + maxIdleMs: maxIdleFromAggregateArgs(redisArgs) + }); + return withReplyCursor(reply, token, cursor); + } + + // FT.CURSOR READ / DEL — the caller-held token is at arg 3 (the routed + // sub-parser carries the real id; this hook receives the original parser). + const { subcommand } = parser.commandIdentifier; + const sub = subcommand === undefined ? undefined : subcommand.toUpperCase(); + const token = argToString(redisArgs[3]); + + if (sub === 'DEL') { + // Self-cleaning: evict locally regardless of the server reply. + slots.evictCursor(token); + return reply; + } + + if (sub === 'READ') { + const binding = slots.lookupCursor(token); + if (!binding) return reply; // never bound here — nothing to maintain + + const next = extractCursorValue(reply); + if (next === undefined || isExhausted(next)) { + slots.evictCursor(token); // exhausted + return reply; // the server's 0 ends the caller's loop as-is + } + + // Same node serves the continuation; refresh createdAt, keep MAXIDLE, and + // track the (usually unchanged) real id. The caller keeps its token. + slots.bindCursor(token, { + address: binding.address, + cursorId: argToString(next as RedisArgument), + maxIdleMs: binding.maxIdleMs + }); + return withReplyCursor(reply, token, next); + } + + return reply; +} + +/** + * Rebuild the reply with the virtual token in place of the server cursor id, + * preserving the reply shape and the cursor's decoded type (a string cursor — + * NUMBER: String mapping — stays a string). + */ +function withReplyCursor(reply: unknown, token: string, original: number | string): unknown { + const cursor = typeof original === 'number' ? Number(token) : token; + + if (reply instanceof Map) { + const copy = new Map(reply as Map); + copy.set('cursor', cursor); + return copy; + } + + if (Array.isArray(reply)) { + const copy = reply.slice(); + copy[1] = cursor; + return copy; + } + + return { ...(reply as Record), cursor }; +} diff --git a/packages/client/lib/cluster/request-response-policies/scan-cursor.ts b/packages/client/lib/cluster/request-response-policies/scan-cursor.ts new file mode 100644 index 00000000000..8c1f6f739b8 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/scan-cursor.ts @@ -0,0 +1,164 @@ +import { BasicCommandParser, type CommandParser } from '../../client/parser'; +import type { RedisArgument } from '../../RESP/types'; +import type { RequestRouter, RoutedCommand } from './dispatch'; +import { argToString, upperCommand } from './ft-cursor'; + +// Routing/finalization runs below the typed command surface (see dispatch.ts), +// so the slots handle is the erased base instantiation. `_executeWithPolicies` +// bridges its own typed slots in at the call boundary. +type ClusterSlots = Parameters[0]; + +/** + * Cluster-wide SCAN (server `request_policy: special` + `response_policy: + * special`). SCAN cursors are per-node state, so the cluster iteration walks + * the masters one at a time behind a *virtual* cursor: + * + * - `SCAN 0` starts a chain on the first master. + * - Each reply's server cursor is swapped for a client-minted token that maps + * back to (serving node, real cursor, masters already exhausted). The + * caller keeps its usual `cursor !== "0"` loop; the token is opaque. + * - When a node's cursor returns 0 the chain advances to the next unvisited + * master (fresh cursor 0); when every master is exhausted the caller + * finally sees "0". + * + * The visited set is tracked by node address, so a topology change mid-scan + * neither rescans a surviving node nor gets stuck on a departed one. The usual + * SCAN guarantees apply per node; keys migrating between nodes mid-iteration + * may be missed or duplicated — same caveat as every cluster-wide scan. + */ +export const routeScan: RequestRouter = async (slots, parser) => { + // Malformed raw command (missing cursor): forward to any node so the server + // returns its own arity error instead of a client-side TypeError. + if (parser.redisArgs.length < 2) { + return [{ client: await slots.nodeClient(slots.getRandomNode()) }]; + } + + const cursorArg = argToString(parser.redisArgs[1]); + + if (cursorArg === '0') { + const address = slots.nextScanTarget(EMPTY_VISITED); + if (!address) throw new Error('SCAN: no master nodes available'); + return [{ client: await pinnedMaster(slots, address) }]; + } + + const entry = slots.lookupScanCursor(cursorArg); + if (!entry) { + throw new Error( + `SCAN: unknown cursor "${cursorArg}". Cluster-wide SCAN cursors are ` + + `minted per client instance and expire when idle — restart the scan from 0.` + ); + } + return [{ + client: await pinnedMaster(slots, entry.address), + parser: withCursor(parser, entry.cursor) + }]; +}; + +const EMPTY_VISITED: ReadonlySet = new Set(); + +async function pinnedMaster(slots: ClusterSlots, address: string) { + const client = await slots.getMasterByAddress(address); + if (!client) { + throw new Error( + `SCAN: node ${address} serving this cursor has left the cluster — ` + + `restart the scan from 0.` + ); + } + return client; +} + +/** Copy of the SCAN parser with the cursor argument (index 1) replaced. */ +function withCursor(parser: CommandParser, cursor: string): CommandParser { + const sub = new BasicCommandParser(); + const { redisArgs } = parser; + for (let i = 0; i < redisArgs.length; i++) { + sub.push(i === 1 ? cursor : redisArgs[i] as RedisArgument); + } + return sub; +} + +/** + * Post-reply hook for SCAN (invoked from `_executeWithPolicies` after the + * reducer, like `finalizeFtCursor`): advances the chain state and swaps + * the server cursor in the reply for the chain's virtual token. No-op for any + * other command or a non-single-target plan. Returns the (possibly rewritten) + * reply. + */ +export function finalizeScanCursor( + slots: ClusterSlots, + parser: CommandParser, + plan: ReadonlyArray, + reply: unknown +): unknown { + if (upperCommand(parser) !== 'SCAN') return reply; + if (plan.length !== 1 || !plan[0].client) return reply; + + const serverCursor = extractScanCursor(reply); + if (serverCursor === undefined) return reply; + + const address = slots.nodeAddressByClient(plan[0].client); + if (!address) return reply; + + const callerCursor = argToString(parser.redisArgs[1]); + const chain = callerCursor === '0' ? undefined : slots.lookupScanCursor(callerCursor); + const visited = chain?.visited ?? new Set(); + + if (argToString(serverCursor) !== '0') { + // Node not exhausted: resume it next call with the real cursor. + const token = callerCursor === '0' ? slots.mintCursorToken() : callerCursor; + slots.bindScanCursor(token, address, argToString(serverCursor), visited); + return withReplyCursor(reply, token, serverCursor); + } + + // Node exhausted: advance to the next unvisited master, or finish. + visited.add(address); + const next = slots.nextScanTarget(visited); + if (!next) { + if (callerCursor !== '0') slots.evictScanCursor(callerCursor); + return reply; // server cursor is already "0" — the chain is done + } + const token = callerCursor === '0' ? slots.mintCursorToken() : callerCursor; + slots.bindScanCursor(token, next, '0', visited); + return withReplyCursor(reply, token, serverCursor); +} + +/** + * Pull the cursor out of a SCAN reply across both reply paths: + * - transformed command path → `{ cursor, keys }`, + * - raw `sendCommand` → `[cursor, keys]` (cursor at index 0). + * Returns `undefined` for anything else (unknown shape → leave the reply be). + */ +function extractScanCursor(reply: unknown): RedisArgument | undefined { + if (reply == null) return undefined; + + if (Array.isArray(reply)) { + return isCursorValue(reply[0]) ? reply[0] : undefined; + } + + if (typeof reply === 'object' && 'cursor' in (reply as Record)) { + const cursor = (reply as Record).cursor; + return isCursorValue(cursor) ? cursor : undefined; + } + + return undefined; +} + +function isCursorValue(value: unknown): value is RedisArgument { + return typeof value === 'string' || value instanceof Buffer; +} + +/** + * Rebuild the reply with the virtual token in place of the server cursor, + * preserving the reply shape and the cursor's wire type (Buffer stays Buffer + * under a Buffer type mapping). + */ +function withReplyCursor(reply: unknown, token: string, original: RedisArgument): unknown { + const cursor = original instanceof Buffer ? Buffer.from(token) : token; + + if (Array.isArray(reply)) { + const copy = reply.slice(); + copy[0] = cursor; + return copy; + } + return { ...(reply as Record), cursor }; +} From 91ea94d48cdc14d97de89af1841924aaff35960d Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 30 Jul 2026 14:14:12 +0300 Subject: [PATCH 07/17] test(client): policy unit + cluster integration specs Unit-test the splitter, resolvers, aggregators and cursor routers, and add cluster-docker integration coverage with one assertion per reachable request/response policy type. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/client/lib/client/parser.spec.ts | 31 ++ .../client/lib/cluster/cluster-slots.spec.ts | 10 +- packages/client/lib/cluster/index.spec.ts | 235 ++++++++++- .../dispatch.spec.ts | 151 +++++++ .../ft-cursor.spec.ts | 255 +++++++++++ .../ft-policies.spec.ts | 101 +++++ .../generic-aggregators.spec.ts | 51 +++ .../multi-shard-splitter.spec.ts | 198 +++++++++ .../scan-cursor.spec.ts | 204 +++++++++ .../dynamic-policy-resolver.spec.ts | 398 ++++++++++++++++++ .../lib/command-metadata/predicates.spec.ts | 95 +++++ .../static-metadata-resolver.spec.ts | 256 +++++++++++ packages/client/lib/commands/COMMAND.spec.ts | 358 +++++++++++++++- packages/client/lib/commands/MSETEX.spec.ts | 15 + .../lib/commands/generic-transformers.spec.ts | 31 +- .../lib/commands/CURSOR_READ.cluster.spec.ts | 97 +++++ packages/search/lib/test-utils.ts | 12 + 17 files changed, 2474 insertions(+), 24 deletions(-) create mode 100644 packages/client/lib/cluster/request-response-policies/dispatch.spec.ts create mode 100644 packages/client/lib/cluster/request-response-policies/ft-cursor.spec.ts create mode 100644 packages/client/lib/cluster/request-response-policies/ft-policies.spec.ts create mode 100644 packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts create mode 100644 packages/client/lib/cluster/request-response-policies/multi-shard-splitter.spec.ts create mode 100644 packages/client/lib/cluster/request-response-policies/scan-cursor.spec.ts create mode 100644 packages/client/lib/command-metadata/dynamic-policy-resolver.spec.ts create mode 100644 packages/client/lib/command-metadata/predicates.spec.ts create mode 100644 packages/client/lib/command-metadata/static-metadata-resolver.spec.ts create mode 100644 packages/search/lib/commands/CURSOR_READ.cluster.spec.ts diff --git a/packages/client/lib/client/parser.spec.ts b/packages/client/lib/client/parser.spec.ts index 4677456250e..ed6280553e5 100644 --- a/packages/client/lib/client/parser.spec.ts +++ b/packages/client/lib/client/parser.spec.ts @@ -160,4 +160,35 @@ describe('BasicCommandParser', () => { } }); }); + + describe('markRoutingKey', () => { + it('sets firstKey without appending to redisArgs', () => { + const parser = new BasicCommandParser(); + parser.push('MGET', 'k1', 'k2'); + parser.markRoutingKey('k1'); + + // redisArgs stays an exact copy of what was pushed (the wire command). + assert.deepEqual(parser.redisArgs, ['MGET', 'k1', 'k2']); + // ...but the key is registered for routing. + assert.deepEqual(parser.keys, ['k1']); + assert.equal(parser.firstKey, 'k1'); + }); + + it('leaves keys empty when never called (keyless raw command)', () => { + const parser = new BasicCommandParser(); + parser.push('PING'); + + assert.deepEqual(parser.redisArgs, ['PING']); + assert.deepEqual(parser.keys, []); + assert.equal(parser.firstKey, undefined); + }); + + it('keeps commandIdentifier pointing at the command name', () => { + const parser = new BasicCommandParser(); + parser.push('GET', 'k1'); + parser.markRoutingKey('k1'); + + assert.deepEqual(parser.commandIdentifier, { command: 'GET', subcommand: 'k1' }); + }); + }); }); diff --git a/packages/client/lib/cluster/cluster-slots.spec.ts b/packages/client/lib/cluster/cluster-slots.spec.ts index 9d03dfc723a..38d8e2e774d 100644 --- a/packages/client/lib/cluster/cluster-slots.spec.ts +++ b/packages/client/lib/cluster/cluster-slots.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import { EventEmitter } from 'node:events'; import { RedisClusterClientOptions } from './index'; import RedisClusterSlots from './cluster-slots'; +import { ClientClosedError } from '../errors'; describe('RedisClusterSlots', () => { describe('initialization', () => { @@ -45,12 +46,15 @@ describe('RedisClusterSlots', () => { }); describe('getRandomNode', ()=> { - it('should not enter infinite loop when no nodes', () => { + // getRandomNode backs the keyless/fan-out routes, so on a cluster that is + // not open it must throw the standard ClientClosedError like every other + // command path rather than returning undefined or spinning the node + // iterator. The zero-node iterator guard still protects the ready case. + it('throws ClientClosedError when the cluster is not connected', () => { const slots = new RedisClusterSlots({ rootNodes: [] }, () => true) - slots.getRandomNode() - slots.getRandomNode() + assert.throws(() => slots.getRandomNode(), ClientClosedError) }); }); }); diff --git a/packages/client/lib/cluster/index.spec.ts b/packages/client/lib/cluster/index.spec.ts index f4b94fc81b8..ede3ff5c53e 100644 --- a/packages/client/lib/cluster/index.spec.ts +++ b/packages/client/lib/cluster/index.spec.ts @@ -8,6 +8,7 @@ import { spy } from 'sinon'; import RedisClient from '../client'; import { RESP_TYPES } from '../RESP/decoder'; import calculateSlot from 'cluster-key-slot'; +import { CommandParser } from '../client/parser'; describe('Cluster command lifecycle', () => { it('rejects commands before connect', async () => { @@ -314,10 +315,240 @@ describe('Cluster', () => { assert.ok(nodeClient instanceof RedisClient); }, GLOBAL.CLUSTERS.WITH_REPLICAS); - testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => { - await assert.rejects(cluster.mGet(['a', 'b'])); + testUtils.testWithCluster('mGet splits cross-slot keys and preserves caller order', async cluster => { + // 'a' and 'b' hash to different slots — on master this rejected with + // CROSSSLOT; the multi_shard split routes each key to its shard and + // reassembles the replies in the caller's key order. + await Promise.all([cluster.set('a', 'value-a'), cluster.set('b', 'value-b')]); + assert.deepEqual(await cluster.mGet(['a', 'b']), ['value-a', 'value-b']); + assert.deepEqual( + await cluster.mGet(['b', 'missing', 'a']), + ['value-b', null, 'value-a'] + ); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('numeric aggregates honor a NUMBER type mapping', async cluster => { + const mapped = cluster.withTypeMapping({ [RESP_TYPES.NUMBER]: String }); + await Promise.all([cluster.set('a', '1'), cluster.set('b', '1')]); + + // multi_shard agg_sum: per-node replies aggregate raw, result is re-mapped. + assert.equal(await mapped.del(['a', 'b']), '2'); + // all_shards agg_sum fan-out. + assert.equal(typeof await mapped.dbSize(), 'string'); + // Unmapped client on the same cluster is untouched. + assert.equal(typeof await cluster.dbSize(), 'number'); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('cluster-wide SCAN iterates every master', async cluster => { + const expected = new Set(); + const writes: Array> = []; + for (let i = 0; i < 100; i++) { + const key = `scan-all:${i}`; + expected.add(key); + writes.push(cluster.set(key, 'v')); + } + await Promise.all(writes); + + // Low COUNT forces several iterations per node, exercising both the + // virtual-token continuation on one node and the advance between nodes. + const found = new Set(); + let cursor = '0'; + do { + const reply = await cluster.scan(cursor, { MATCH: 'scan-all:*', COUNT: 29 }); + cursor = reply.cursor; + for (const key of reply.keys) found.add(key); + } while (cursor !== '0'); + + assert.deepEqual(found, expected); }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('cluster-wide SCAN rejects a foreign cursor', async cluster => { + await assert.rejects(cluster.scan('123456'), /unknown cursor/); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('RANDOMKEY finds the key whichever shard holds it', async cluster => { + // Single key in the whole cluster: a single-node RANDOMKEY would return + // nil whenever the randomly-picked node is one of the empty masters; the + // all_shards fan-out + non-nil reduction must always find it. + await cluster.set('the-only-key', 'v'); + for (let i = 0; i < 5; i++) { + assert.equal(await cluster.randomKey(), 'the-only-key'); + } + }, GLOBAL.CLUSTERS.OPEN); + + // One end-to-end assertion per reachable request/response policy type. The + // multi_shard MGET/DEL scatter, cluster-wide SCAN and RANDOMKEY cases live + // above; FT.CURSOR (special) is covered in the search package. These fill + // the rest of the matrix from .specs/request-response-policies-spec.md. + describe('request/response policies', () => { + testUtils.testWithCluster('default-keyed SET/GET round-trips', async cluster => { + assert.equal(await cluster.set('policy:sg', 'v'), 'OK'); + assert.equal(await cluster.get('policy:sg'), 'v'); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('default-keyless ECHO returns the sole reply', async cluster => { + assert.equal(await cluster.echo('hello'), 'hello'); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('all_shards KEYS merges matches from every master', async cluster => { + await cluster.flushAll(); + const expected = new Set(['pk:a', 'pk:b', 'pk:c', 'pk:d']); + await Promise.all([...expected].map(key => cluster.set(key, 'v'))); + // default-keyless response over an all_shards fan-out: union, deduped. + assert.deepEqual(new Set(await cluster.keys('pk:*')), expected); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('all_shards FLUSHALL + PING collapse identical replies', async cluster => { + await Promise.all([cluster.set('fa:x', '1'), cluster.set('fa:y', '1')]); + // all_succeeded: every master must ACK; identical replies collapse to one. + assert.equal(await cluster.flushAll(), 'OK'); + assert.equal(await cluster.dbSize(), 0); + assert.equal(await cluster.ping(), 'PONG'); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('all_shards WAIT returns the min synced-replica count', async cluster => { + await cluster.set('wait:k', 'v'); + // agg_min: one replica per master → each shard reports >=1; the floor is 1. + assert.equal(await cluster.wait(1, 1000), 1); + }, GLOBAL.CLUSTERS.WITH_REPLICAS); + + testUtils.testWithCluster('all_shards SCRIPT EXISTS ANDs presence across masters', async cluster => { + // SCRIPT LOAD is all_nodes → the script lands on every master. + const everywhere = await cluster.scriptLoad('return 1'); + assert.deepEqual(await cluster.scriptExists(everywhere), [1]); + + // Load a second script on a single master only → agg_logical_and → 0. + const oneMaster = await cluster.nodeClient(cluster.masters[0]); + const partial = await oneMaster.scriptLoad('return 2'); + assert.deepEqual(await cluster.scriptExists(partial), [0]); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('all_nodes CONFIG SET reaches replicas too', async cluster => { + // all_nodes (not all_shards): replicas must see it directly, not via + // replication — verify each replica reports the new value. + assert.equal(await cluster.configSet({ 'maxmemory-samples': '7' }), 'OK'); + for (const replica of cluster.replicas) { + const client = await cluster.nodeClient(replica); + assert.equal((await client.configGet('maxmemory-samples'))['maxmemory-samples'], '7'); + } + }, GLOBAL.CLUSTERS.WITH_REPLICAS); + + testUtils.testWithCluster('all_nodes SLOWLOG LEN fans out and reduces with agg_sum', async cluster => { + // Raw path: resolves the `slowlog len` subcommand policy (all_nodes / + // agg_sum). After RESET (all_nodes / all_succeeded) the fan-out reduces + // to a single scalar count, not a per-node array or a throw. + await cluster.sendCommand(undefined, false, ['SLOWLOG', 'RESET']); + const len = await cluster.sendCommand(undefined, false, ['SLOWLOG', 'LEN']); + assert.equal(typeof len, 'number'); + assert.ok(len >= 0); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('multi_shard EXISTS/TOUCH/UNLINK sum counts across slots', async cluster => { + await Promise.all([cluster.set('a', '1'), cluster.set('b', '1')]); + // agg_sum over the per-slot splits. + assert.equal(await cluster.exists(['a', 'b', 'missing']), 2); + assert.equal(await cluster.touch(['a', 'b']), 2); + assert.equal(await cluster.unlink(['a', 'b']), 2); + assert.equal(await cluster.exists(['a', 'b']), 0); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('multi_shard MSET writes each pair to its own shard', async cluster => { + // all_succeeded over the split key/value pairs. + assert.equal(await cluster.mSet([['a', 'va'], ['b', 'vb']]), 'OK'); + assert.deepEqual(await cluster.mGet(['a', 'b']), ['va', 'vb']); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('multi_shard single-slot keys take the no-split fast path', async cluster => { + // Hash tags pin both keys to one slot → the splitter returns the command + // unsplit and it routes as a single atomic command to that shard. + await Promise.all([cluster.set('{t}1', 'v1'), cluster.set('{t}2', 'v2')]); + assert.deepEqual(await cluster.mGet(['{t}1', '{t}2']), ['v1', 'v2']); + assert.equal(await cluster.del(['{t}1', '{t}2']), 2); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('multi_shard follows a MOVED/ASK redirect per sub-command', async cluster => { + // 'key' hashes to slot 12539; find a second key homed on a different + // master so the mGet genuinely spans two shards. + const keyA = 'key', + slot = calculateSlot(keyA), + migrating = cluster.slots[slot].master, + importing = cluster.masters.find(master => master.id !== migrating.id)!; + + let keyB = ''; + for (const candidate of ['b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']) { + if (cluster.slots[calculateSlot(candidate)].master.id === importing.id) { + keyB = candidate; + break; + } + } + assert.ok(keyB, 'expected a key homed on the importing master'); + + await Promise.all([cluster.set(keyA, 'va'), cluster.set(keyB, 'vb')]); + + const [importingClient, migratingClient] = await Promise.all([ + cluster.nodeClient(importing), + cluster.nodeClient(migrating) + ]); + await Promise.all([ + importingClient.clusterSetSlot(slot, 'IMPORTING', migrating.id), + migratingClient.clusterSetSlot(slot, 'MIGRATING', importing.id) + ]); + await migratingClient.migrate(importing.host, importing.port, keyA, 0, 10); + + // keyA's sub-command hits the migrating node and follows ASK to importing; + // keyB's routes straight to importing. Caller order is preserved. + assert.deepEqual(await cluster.mGet([keyA, keyB]), ['va', 'vb']); + + await Promise.all([ + importingClient.clusterSetSlot(slot, 'NODE', importing.id), + migratingClient.clusterSetSlot(slot, 'NODE', importing.id) + ]); + + // Slot now permanently on importing → keyA's sub-command takes MOVED. + assert.deepEqual(await cluster.mGet([keyA, keyB]), ['va', 'vb']); + }, { + serverArguments: [], + numberOfMasters: 2 + }); + + testUtils.testWithCluster('unknown command falls back to key routing (no policy throw)', async cluster => { + // A made-up wire command is absent from the policy table → the resolver + // returns {ok:false} → the default-keyed fallback routes by the key and + // the request reaches that shard, which rejects it as unknown. Reaching + // the server proves the fallback routed instead of throwing a client-side + // policy-resolution error before any network call. + await assert.rejects( + cluster.custom.unknown('policyless:key'), + /unknown command/i + ); + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + modules: { + custom: { + unknown: { + parseCommand(parser: CommandParser, key: string) { + parser.push('DEFINITELY_NOT_A_COMMAND'); + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => unknown + } + } + } + } + }); + + testUtils.testWithCluster('raw sendCommand splits a known multi_shard command', async cluster => { + await Promise.all([cluster.set('a', 'value-a'), cluster.set('b', 'value-b')]); + // The resolver returns MGET's keySpecs for the raw args and the splitter + // works on the flat redisArgs, reassembling replies in caller order. + assert.deepEqual( + await cluster.sendCommand(undefined, true, ['MGET', 'a', 'b']), + ['value-a', 'value-b'] + ); + }, GLOBAL.CLUSTERS.OPEN); + }); + describe('minimizeConnections', () => { testUtils.testWithCluster('false', async cluster => { for (const master of cluster.masters) { diff --git a/packages/client/lib/cluster/request-response-policies/dispatch.spec.ts b/packages/client/lib/cluster/request-response-policies/dispatch.spec.ts new file mode 100644 index 00000000000..48936ec61d7 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/dispatch.spec.ts @@ -0,0 +1,151 @@ +import { strict as assert } from 'node:assert'; +import type { CommandParser } from '../../client/parser'; +import { reduceDefaultKeyed, reduceOneSucceeded, reduceRandomKey, reduceSpecial, remapAggregateReply } from './dispatch'; +import { RESP_TYPES } from '../../RESP/decoder'; +import type { TypeMapping } from '../../RESP/types'; + +// The reducer ignores the parser; a stub keeps the calls readable. +const PARSER = {} as CommandParser; + +describe('reduceRandomKey', () => { + it('returns the sole non-nil reply when other shards are empty', async () => { + const reply = await reduceRandomKey([ + Promise.resolve(null), + Promise.resolve('the-key'), + Promise.resolve(null) + ]); + assert.equal(reply, 'the-key'); + }); + + it('returns one of the non-nil replies', async () => { + const reply = await reduceRandomKey([ + Promise.resolve('a'), + Promise.resolve(null), + Promise.resolve('b') + ]); + assert.ok(reply === 'a' || reply === 'b'); + }); + + it('returns nil only when every shard is empty', async () => { + const reply = await reduceRandomKey([Promise.resolve(null), Promise.resolve(null)]); + assert.equal(reply, null); + }); + + it('is dispatched for RANDOMKEY through the special response policy', async () => { + const parser = { + commandIdentifier: { command: 'randomkey', subcommand: undefined } + } as unknown as CommandParser; + const reply = await reduceSpecial([Promise.resolve(null), Promise.resolve('k')], parser); + assert.equal(reply, 'k'); + }); +}); + +describe('reduceDefaultKeyed', () => { + it('passes the sole reply through when not split (no hints)', async () => { + const reply = await reduceDefaultKeyed([Promise.resolve(['v1', 'v2'])], PARSER); + assert.deepEqual(reply, ['v1', 'v2']); + }); + + it('passes through when hints are all undefined (single-key command)', async () => { + const reply = await reduceDefaultKeyed([Promise.resolve('v1')], PARSER, [undefined]); + assert.equal(reply, 'v1'); + }); + + it('passes through a single-slot multi_shard reply unchanged', async () => { + const reply = await reduceDefaultKeyed( + [Promise.resolve(['v0', 'v1', 'v2'])], + PARSER, + [[0, 1, 2]] + ); + assert.deepEqual(reply, ['v0', 'v1', 'v2']); + }); + + it('scatters interleaved sub-replies back into original key order (MGET A,B,A,B)', async () => { + // keys hash to A,B,A,B -> slot A holds groups [0,2], slot B holds [1,3]. + const reply = await reduceDefaultKeyed( + [ + Promise.resolve(['a0', 'a2']), + Promise.resolve(['b1', 'b3']) + ], + PARSER, + [[0, 2], [1, 3]] + ); + assert.deepEqual(reply, ['a0', 'b1', 'a2', 'b3']); + }); + + it('places each sub-reply by its hint regardless of plan order', async () => { + // slot B (groups [1]) listed before slot A (groups [0,2]). + const reply = await reduceDefaultKeyed( + [ + Promise.resolve(['b1']), + Promise.resolve(['a0', 'a2']) + ], + PARSER, + [[1], [0, 2]] + ); + assert.deepEqual(reply, ['a0', 'b1', 'a2']); + }); + + it('preserves null replies (missing keys) at their positions', async () => { + const reply = await reduceDefaultKeyed( + [ + Promise.resolve(['a0', null]), + Promise.resolve([null]) + ], + PARSER, + [[0, 2], [1]] + ); + assert.deepEqual(reply, ['a0', null, null]); + }); + + it('throws when a split reply is missing its position hint', async () => { + await assert.rejects( + reduceDefaultKeyed( + [Promise.resolve(['a0']), Promise.resolve(['b1'])], + PARSER, + [[0], undefined] + ), + /missing position hints/ + ); + }); +}); + +describe('reduceOneSucceeded', () => { + it('returns the first fulfilled reply', async () => { + const reply = await reduceOneSucceeded([ + Promise.reject(new Error('NOTBUSY No scripts in execution right now.')), + Promise.resolve('OK') + ]); + assert.equal(reply, 'OK'); + }); + + it('surfaces a shard error instead of AggregateError when every node rejects', async () => { + await assert.rejects( + reduceOneSucceeded([ + Promise.reject(new Error('NOTBUSY No scripts in execution right now.')), + Promise.reject(new Error('NOTBUSY No scripts in execution right now.')) + ]), + /NOTBUSY/ + ); + }); +}); + +describe('remapAggregateReply', () => { + const STRING_MAPPING = { [RESP_TYPES.NUMBER]: String } as TypeMapping; + + it('passes through without a mapping', () => { + assert.equal(remapAggregateReply(3, undefined), 3); + }); + + it('passes through under the identity Number mapping', () => { + assert.equal(remapAggregateReply(3, { [RESP_TYPES.NUMBER]: Number } as TypeMapping), 3); + }); + + it('maps a scalar aggregate (DEL/DBSIZE sum, WAIT min)', () => { + assert.equal(remapAggregateReply(3, STRING_MAPPING), '3'); + }); + + it('maps array aggregates element-wise (SCRIPT EXISTS logical AND)', () => { + assert.deepEqual(remapAggregateReply([1, 0, 1], STRING_MAPPING), ['1', '0', '1']); + }); +}); diff --git a/packages/client/lib/cluster/request-response-policies/ft-cursor.spec.ts b/packages/client/lib/cluster/request-response-policies/ft-cursor.spec.ts new file mode 100644 index 00000000000..3737b52e099 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/ft-cursor.spec.ts @@ -0,0 +1,255 @@ +import { strict as assert } from 'node:assert'; +import type { CommandParser } from '../../client/parser'; +import { routeFtCursor, finalizeFtCursor, extractCursorValue } from './ft-cursor'; + +/** + * Minimal stand-in for the cursor-relevant surface of `RedisClusterSlots`, + * mirroring the token-keyed binding map, the token mint and the + * address→client map so the router/finalize logic is exercised without + * spinning a cluster. + */ +class FakeSlots { + cursorBindings = new Map(); + clientsByAddress = new Map(); + #seq = 0; + + mintCursorToken() { return String(++this.#seq); } + bindCursor(token: string, binding: { address: string; cursorId: string; maxIdleMs?: number }) { + this.cursorBindings.set(token, { ...binding, createdAt: 0 }); + } + lookupCursor(token: string) { return this.cursorBindings.get(token); } + evictCursor(token: string) { this.cursorBindings.delete(token); } + async getMasterByAddress(address: string) { return this.clientsByAddress.get(address); } + getRandomNode() { return { address: this.clientsByAddress.keys().next().value as string }; } + async nodeClient(node: { address: string }) { return this.clientsByAddress.get(node.address); } + nodeAddressByClient(client: object) { + for (const [address, c] of this.clientsByAddress) if (c === client) return address; + return undefined; + } +} + +const parserOf = (...args: Array) => + ({ redisArgs: args, commandIdentifier: { command: args[0], subcommand: args[1] } }) as unknown as CommandParser; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- routers/finalize run below the typed surface +const asSlots = (s: FakeSlots) => s as any; + +describe('extractCursorValue', () => { + it('reads the transformed-path `{ cursor }` object (RESP2 + RESP3)', () => { + assert.equal(extractCursorValue({ total: 1, results: [], cursor: 42 }), 42); + }); + + it('reads raw RESP2 `[result, cursor]` at index 1', () => { + assert.equal(extractCursorValue([['result'], 7]), 7); + }); + + it('reads raw RESP3 map key `cursor`', () => { + assert.equal(extractCursorValue(new Map([['results', []], ['cursor', 9]])), 9); + }); + + it('preserves a string cursor (NUMBER: String type mapping)', () => { + assert.equal(extractCursorValue({ cursor: '12345678901234567890' }), '12345678901234567890'); + }); + + it('returns undefined when there is no cursor (e.g. FT.CURSOR DEL "OK")', () => { + assert.equal(extractCursorValue('OK'), undefined); + assert.equal(extractCursorValue(null), undefined); + }); +}); + +describe('routeFtCursor', () => { + it('pins the bound client and rewrites the token to the real cursor id', async () => { + const slots = new FakeSlots(); + const client = { id: 'node-a' }; + slots.clientsByAddress.set('127.0.0.1:7000', client); + slots.bindCursor('1', { address: '127.0.0.1:7000', cursorId: '18446744073709551615' }); + + const plan = await routeFtCursor(asSlots(slots), parserOf('FT.CURSOR', 'READ', 'idx', '1'), undefined, undefined); + assert.equal(plan.length, 1); + assert.equal(plan[0].client, client); + assert.deepEqual(plan[0].parser!.redisArgs, ['FT.CURSOR', 'READ', 'idx', '18446744073709551615']); + }); + + it('forwards a malformed FT.CURSOR (missing cursor arg) to a node for the server arity error', async () => { + const slots = new FakeSlots(); + const client = { id: 'node-a' }; + slots.clientsByAddress.set('127.0.0.1:7000', client); + + const plan = await routeFtCursor(asSlots(slots), parserOf('FT.CURSOR', 'READ', 'idx'), undefined, undefined); + assert.deepEqual(plan, [{ client }]); + }); + + it('throws on MISS (token never minted here)', async () => { + const slots = new FakeSlots(); + await assert.rejects( + routeFtCursor(asSlots(slots), parserOf('FT.CURSOR', 'READ', 'idx', '404'), undefined, undefined), + /unknown cursor 404 on index "idx"/ + ); + }); + + it('throws when the bound node is gone (getMasterByAddress → undefined)', async () => { + const slots = new FakeSlots(); + slots.bindCursor('5', { address: '127.0.0.1:9999', cursorId: '5' }); // address not in clientsByAddress + await assert.rejects( + routeFtCursor(asSlots(slots), parserOf('FT.CURSOR', 'DEL', 'idx', '5'), undefined, undefined), + /left the cluster/ + ); + }); +}); + +describe('finalizeFtCursor — FT.AGGREGATE', () => { + it('mints a token, binds it to (address, real id) and rewrites the reply (RESP2 array)', () => { + const slots = new FakeSlots(); + const client = {}; + slots.clientsByAddress.set('10.0.0.1:6379', client); + + const reply = finalizeFtCursor( + asSlots(slots), + parserOf('FT.AGGREGATE', 'idx', '*', 'WITHCURSOR'), + [{ client } as never], + [[], 55] + ); + assert.deepEqual(reply, [[], 1]); // token 1 replaces the server id, numeric type kept + assert.deepEqual(slots.lookupCursor('1'), { + address: '10.0.0.1:6379', cursorId: '55', createdAt: 0, maxIdleMs: undefined + }); + }); + + it('rewrites the transformed `{ cursor }` reply and captures MAXIDLE', () => { + const slots = new FakeSlots(); + const client = {}; + slots.clientsByAddress.set('10.0.0.1:6379', client); + + const reply = finalizeFtCursor( + asSlots(slots), + parserOf('FT.AGGREGATE', 'idx', '*', 'WITHCURSOR', 'MAXIDLE', '5000'), + [{ client } as never], + { total: 0, results: [], cursor: 88 } + ); + assert.deepEqual(reply, { total: 0, results: [], cursor: 1 }); + assert.deepEqual(slots.lookupCursor('1'), { + address: '10.0.0.1:6379', cursorId: '88', createdAt: 0, maxIdleMs: 5000 + }); + }); + + it('rewrites a raw RESP3 Map reply without mutating the original', () => { + const slots = new FakeSlots(); + const client = {}; + slots.clientsByAddress.set('10.0.0.1:6379', client); + const original = new Map([['results', []], ['cursor', 66]]); + + const reply = finalizeFtCursor( + asSlots(slots), parserOf('FT.AGGREGATE', 'idx', '*', 'WITHCURSOR'), [{ client } as never], original + ) as Map; + assert.equal(reply.get('cursor'), 1); + assert.equal(original.get('cursor'), 66); + }); + + it('treats MAXIDLE 0 as "no idle limit" (server clamps it), not a 0ms TTL', () => { + const slots = new FakeSlots(); + const client = {}; + slots.clientsByAddress.set('10.0.0.1:6379', client); + + finalizeFtCursor( + asSlots(slots), + parserOf('FT.AGGREGATE', 'idx', '*', 'WITHCURSOR', 'MAXIDLE', '0'), + [{ client } as never], + { cursor: 88 } + ); + assert.equal(slots.lookupCursor('1')!.maxIdleMs, undefined); + }); + + it('does not bind or rewrite when the aggregate exhausts in one batch (cursor 0)', () => { + const slots = new FakeSlots(); + const client = {}; + slots.clientsByAddress.set('10.0.0.1:6379', client); + + const reply = finalizeFtCursor( + asSlots(slots), parserOf('FT.AGGREGATE', 'idx', '*', 'WITHCURSOR'), [{ client } as never], { cursor: 0 } + ); + assert.deepEqual(reply, { cursor: 0 }); + assert.equal(slots.cursorBindings.size, 0); + }); +}); + +describe('finalizeFtCursor — FT.CURSOR lifecycle', () => { + const seed = (maxIdleMs?: number) => { + const slots = new FakeSlots(); + const client = {}; + slots.clientsByAddress.set('10.0.0.1:6379', client); + slots.bindCursor('7', { address: '10.0.0.1:6379', cursorId: '100', maxIdleMs }); + return { slots, client }; + }; + + it('keeps the token stable across a READ and rewrites the reply back to it', () => { + const { slots, client } = seed(); + const reply = finalizeFtCursor( + asSlots(slots), parserOf('FT.CURSOR', 'READ', 'idx', '7'), [{ client } as never], { cursor: 100 } + ); + assert.deepEqual(reply, { cursor: 7 }); + assert.deepEqual(slots.lookupCursor('7'), { + address: '10.0.0.1:6379', cursorId: '100', createdAt: 0, maxIdleMs: undefined + }); + }); + + it('tracks a changed continuation id under the same token', () => { + const { slots, client } = seed(); + const reply = finalizeFtCursor( + asSlots(slots), parserOf('FT.CURSOR', 'READ', 'idx', '7'), [{ client } as never], { cursor: 200 } + ); + assert.deepEqual(reply, { cursor: 7 }); + assert.equal(slots.lookupCursor('7')!.cursorId, '200'); + }); + + it('preserves MAXIDLE across READ rebinds', () => { + const { slots, client } = seed(600_000); + finalizeFtCursor(asSlots(slots), parserOf('FT.CURSOR', 'READ', 'idx', '7'), [{ client } as never], { cursor: 200 }); + assert.equal(slots.lookupCursor('7')!.maxIdleMs, 600_000); + }); + + it('evicts on READ → cursor 0 and passes the server 0 through (ends the loop)', () => { + const { slots, client } = seed(); + const reply = finalizeFtCursor( + asSlots(slots), parserOf('FT.CURSOR', 'READ', 'idx', '7'), [{ client } as never], { cursor: 0 } + ); + assert.deepEqual(reply, { cursor: 0 }); + assert.equal(slots.lookupCursor('7'), undefined); + }); + + it('evicts on DEL regardless of reply, then a follow-up READ MISSes', async () => { + const { slots, client } = seed(); + finalizeFtCursor(asSlots(slots), parserOf('FT.CURSOR', 'DEL', 'idx', '7'), [{ client } as never], 'OK'); + assert.equal(slots.lookupCursor('7'), undefined); + await assert.rejects(routeFtCursor(asSlots(slots), parserOf('FT.CURSOR', 'READ', 'idx', '7'), undefined, undefined)); + }); +}); + +describe('cross-shard cursor-id collision', () => { + it('two shards minting the same real id for one index get distinct tokens and route independently', async () => { + const slots = new FakeSlots(); + const clientA = { id: 'a' }, clientB = { id: 'b' }; + slots.clientsByAddress.set('a:1', clientA); + slots.clientsByAddress.set('b:1', clientB); + + // Both aggregates return server cursor 42 for the same index. + const replyA = finalizeFtCursor( + asSlots(slots), parserOf('FT.AGGREGATE', 'idx', '*', 'WITHCURSOR'), [{ client: clientA } as never], { cursor: 42 } + ) as { cursor: number }; + const replyB = finalizeFtCursor( + asSlots(slots), parserOf('FT.AGGREGATE', 'idx', '*', 'WITHCURSOR'), [{ client: clientB } as never], { cursor: 42 } + ) as { cursor: number }; + + assert.notEqual(replyA.cursor, replyB.cursor); + + const planA = await routeFtCursor( + asSlots(slots), parserOf('FT.CURSOR', 'READ', 'idx', String(replyA.cursor)), undefined, undefined + ); + const planB = await routeFtCursor( + asSlots(slots), parserOf('FT.CURSOR', 'READ', 'idx', String(replyB.cursor)), undefined, undefined + ); + assert.equal(planA[0].client, clientA); + assert.equal(planB[0].client, clientB); + assert.deepEqual(planA[0].parser!.redisArgs, ['FT.CURSOR', 'READ', 'idx', '42']); + assert.deepEqual(planB[0].parser!.redisArgs, ['FT.CURSOR', 'READ', 'idx', '42']); + }); +}); diff --git a/packages/client/lib/cluster/request-response-policies/ft-policies.spec.ts b/packages/client/lib/cluster/request-response-policies/ft-policies.spec.ts new file mode 100644 index 00000000000..b837b60af70 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/ft-policies.spec.ts @@ -0,0 +1,101 @@ +import { strict as assert } from 'node:assert'; +import { + StaticMetadataResolver, + REQUEST_POLICIES_WITH_DEFAULTS, + RESPONSE_POLICIES_WITH_DEFAULTS +} from '../../command-metadata'; + +/** + * Snapshot of the HLD "Command Routing Policy Table" — client interpretation column. + * + * `default(keyless)` and `default(hashslot)` from the HLD denote "no policy + * declared"; the client routes by the default rules. They are stored here as + * `default-keyless` / `default-keyed` to match the resolver vocabulary. + * + * `ft.cursor` carries the HLD `special` request_policy (sticky cursor): READ/DEL + * are routed to the node that served the FT.AGGREGATE that minted the cursor + * (see `ft-cursor.ts`). Its response stays `default(keyless)` — single-node + * pass-through. + */ +const KEYLESS = { + request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, + isKeyless: true +} as const; + +const KEYED = { + request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED, + isKeyless: false +} as const; + +const SPECIAL_CURSOR = { + request: REQUEST_POLICIES_WITH_DEFAULTS.SPECIAL, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, + isKeyless: true +} as const; + +const HLD_FT_TABLE: Record = { + 'FT.CREATE': KEYLESS, + 'FT.SEARCH': KEYLESS, + 'FT.AGGREGATE': KEYLESS, + 'FT.DICTADD': KEYLESS, + 'FT.DICTDEL': KEYLESS, + 'FT.DICTDUMP': KEYLESS, + 'FT.SUGLEN': KEYED, + 'FT.CURSOR': SPECIAL_CURSOR, + 'FT.SUGADD': KEYED, + 'FT.SUGGET': KEYED, + 'FT.SUGDEL': KEYED, + 'FT.SPELLCHECK': KEYLESS, + 'FT.EXPLAIN': KEYLESS, + 'FT.EXPLAINCLI': KEYLESS, + 'FT.ALIASADD': KEYLESS, + 'FT.ALIASUPDATE': KEYLESS, + 'FT.ALIASDEL': KEYLESS, + 'FT.INFO': KEYLESS, + 'FT.TAGVALS': KEYLESS, + 'FT.SYNDUMP': KEYLESS, + 'FT.SYNUPDATE': KEYLESS, + 'FT.PROFILE': KEYLESS, + 'FT.ALTER': KEYLESS, + 'FT.DROPINDEX': KEYLESS, + 'FT.DROP': KEYLESS +}; + +describe('FT.* policy table matches the HLD', () => { + const resolver = new StaticMetadataResolver(); + + for (const [command, expected] of Object.entries(HLD_FT_TABLE)) { + it(`${command} resolves to the HLD policy`, () => { + const result = resolver.resolvePolicy({ command, subcommand: undefined }); + assert.equal(result.ok, true, `expected ${command} to resolve`); + if (result.ok) { + assert.equal(result.value.request, expected.request, `${command} request`); + assert.equal(result.value.response, expected.response, `${command} response`); + assert.equal(result.value.isKeyless, expected.isKeyless, `${command} isKeyless`); + } + }); + } + + it('resolves FT.CURSOR READ/DEL subcommands to the special request policy', () => { + for (const subcommand of ['READ', 'DEL']) { + const result = resolver.resolvePolicy({ command: 'FT.CURSOR', subcommand }); + assert.equal(result.ok, true, `expected FT.CURSOR ${subcommand} to resolve`); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.SPECIAL, `FT.CURSOR ${subcommand} request`); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, `FT.CURSOR ${subcommand} response`); + } + } + }); + + it('does not expose dropped debug commands (e.g. FT._LIST)', () => { + const result = resolver.resolvePolicy({ command: 'FT._LIST', subcommand: undefined }); + assert.equal(result.ok, false); + }); + + it('does not expose stray cluster-admin commands (e.g. FT.CLUSTERSET)', () => { + const result = resolver.resolvePolicy({ command: 'FT.CLUSTERSET', subcommand: undefined }); + assert.equal(result.ok, false); + }); +}); diff --git a/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts b/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts new file mode 100644 index 00000000000..d77e5edc350 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts @@ -0,0 +1,51 @@ +import { strict as assert } from 'node:assert'; +import { aggregateLogicalAnd, aggregateLogicalOr, aggregateMerge } from './generic-aggregators'; + +describe('aggregateLogicalOr', () => { + it('ORs element-wise across shards', () => { + assert.deepEqual(aggregateLogicalOr([[1, 0, 0], [0, 0, 1]]), [1, 0, 1]); + }); + + it('returns all zeros when every shard reports zeros', () => { + assert.deepEqual(aggregateLogicalOr([[0, 0], [0, 0]]), [0, 0]); + }); + + it('returns [] for an empty replies array', () => { + assert.deepEqual(aggregateLogicalOr([]), []); + }); + + it('rejects non-numeric replies', () => { + assert.throws(() => aggregateLogicalOr([['1', '0']]), /logical OR aggregation/); + }); +}); + +describe('aggregateMerge', () => { + it('merges array replies with dedup', () => { + assert.deepEqual(aggregateMerge([['a', 'b'], ['b', 'c']]), ['a', 'b', 'c']); + }); + + it('merges Map replies (last node wins per key)', () => { + const merged = aggregateMerge>([ + new Map([['a', 1], ['b', 1]]), + new Map([['b', 2]]) + ]); + assert.deepEqual([...merged.entries()], [['a', 1], ['b', 2]]); + }); + + it('merges plain-object replies (RESP3 maps under the default type mapping)', () => { + assert.deepEqual( + aggregateMerge([{ a: 1, b: 1 }, { b: 2 }]), + { a: 1, b: 2 } + ); + }); +}); + +describe('aggregateLogicalAnd', () => { + it('ANDs element-wise across shards (SCRIPT EXISTS)', () => { + assert.deepEqual(aggregateLogicalAnd([[1, 1, 0], [1, 0, 0]]), [1, 0, 0]); + }); + + it('rejects non-numeric replies', () => { + assert.throws(() => aggregateLogicalAnd([['1']]), /logical AND aggregation/); + }); +}); diff --git a/packages/client/lib/cluster/request-response-policies/multi-shard-splitter.spec.ts b/packages/client/lib/cluster/request-response-policies/multi-shard-splitter.spec.ts new file mode 100644 index 00000000000..83edc56a577 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/multi-shard-splitter.spec.ts @@ -0,0 +1,198 @@ +import { strict as assert } from 'node:assert'; +import calculateSlot from 'cluster-key-slot'; +import type { KeySpec } from '../../commands/generic-transformers'; +import { splitMultiShardCommand } from './multi-shard-splitter'; + +// Real specs of the 7 multi_shard commands (Redis 8.8.0). +const RANGE_STEP_1: Array = [{ + beginSearch: { type: 'index', index: 1 }, + findKeys: { type: 'range', lastKey: -1, keyStep: 1, limit: 0 } +}]; +const RANGE_STEP_2: Array = [{ + beginSearch: { type: 'index', index: 1 }, + findKeys: { type: 'range', lastKey: -1, keyStep: 2, limit: 0 } +}]; +const KEYNUM_STEP_2: Array = [{ + beginSearch: { type: 'index', index: 1 }, + findKeys: { type: 'keynum', keyNumIdx: 0, firstKey: 1, keyStep: 2 } +}]; + +// Hash tags pin keys to slots: {a}* keys share a slot, {b}* keys share +// a different one. +const SLOT_A = calculateSlot('{a}1'); +const SLOT_B = calculateSlot('{b}1'); + +describe('splitMultiShardCommand', () => { + before(() => assert.notEqual(SLOT_A, SLOT_B)); + + it('single-slot fast path returns the original args', () => { + const args = ['DEL', '{a}1', '{a}2', '{a}3']; + const result = splitMultiShardCommand(args, RANGE_STEP_1); + + assert.equal(result.size, 1); + assert.deepEqual(result.get(SLOT_A), { + args: ['DEL', '{a}1', '{a}2', '{a}3'], + groupIndices: [0, 1, 2], + keyPositions: [1, 2, 3] + }); + }); + + it('splits DEL per slot (range, keystep 1)', () => { + const result = splitMultiShardCommand(['DEL', '{a}1', '{b}1', '{a}2'], RANGE_STEP_1); + + assert.equal(result.size, 2); + assert.deepEqual(result.get(SLOT_A), { + args: ['DEL', '{a}1', '{a}2'], + groupIndices: [0, 2], + keyPositions: [1, 2] + }); + assert.deepEqual(result.get(SLOT_B), { + args: ['DEL', '{b}1'], + groupIndices: [1], + keyPositions: [1] + }); + }); + + it('splits MSET keeping values with their keys (range, keystep 2)', () => { + const result = splitMultiShardCommand( + ['MSET', '{a}1', 'v1', '{b}1', 'v2', '{a}2', 'v3'], + RANGE_STEP_2 + ); + + assert.equal(result.size, 2); + assert.deepEqual(result.get(SLOT_A), { + args: ['MSET', '{a}1', 'v1', '{a}2', 'v3'], + groupIndices: [0, 2], + keyPositions: [1, 3] + }); + assert.deepEqual(result.get(SLOT_B), { + args: ['MSET', '{b}1', 'v2'], + groupIndices: [1], + keyPositions: [1] + }); + }); + + it('splits MSETEX rewriting numkeys and copying the options suffix', () => { + const result = splitMultiShardCommand( + ['MSETEX', '3', '{a}1', 'v1', '{b}1', 'v2', '{a}2', 'v3', 'NX', 'EX', '10'], + KEYNUM_STEP_2 + ); + + assert.equal(result.size, 2); + assert.deepEqual(result.get(SLOT_A), { + args: ['MSETEX', '2', '{a}1', 'v1', '{a}2', 'v3', 'NX', 'EX', '10'], + groupIndices: [0, 2], + keyPositions: [2, 4] + }); + assert.deepEqual(result.get(SLOT_B), { + args: ['MSETEX', '1', '{b}1', 'v2', 'NX', 'EX', '10'], + groupIndices: [1], + keyPositions: [2] + }); + }); + + it('keynum single-slot fast path keeps original numkeys', () => { + const args = ['MSETEX', '2', '{a}1', 'v1', '{a}2', 'v2', 'KEEPTTL']; + const result = splitMultiShardCommand(args, KEYNUM_STEP_2); + + assert.equal(result.size, 1); + assert.deepEqual(result.get(SLOT_A)?.args, args); + }); + + it('records interleaved group indices for order-preserving reassembly (MGET)', () => { + const result = splitMultiShardCommand( + ['MGET', '{a}1', '{b}1', '{a}2', '{b}2'], + RANGE_STEP_1 + ); + + assert.deepEqual(result.get(SLOT_A)?.groupIndices, [0, 2]); + assert.deepEqual(result.get(SLOT_B)?.groupIndices, [1, 3]); + }); + + it('handles Buffer keys', () => { + const result = splitMultiShardCommand( + ['MGET', Buffer.from('{a}1'), '{b}1'], + RANGE_STEP_1 + ); + + assert.equal(result.size, 2); + assert.deepEqual(result.get(SLOT_A)?.args, ['MGET', Buffer.from('{a}1')]); + }); + + describe('guardrails', () => { + const args = ['DEL', '{a}1', '{b}1']; + + it('rejects missing key specs', () => { + assert.throws(() => splitMultiShardCommand(args, undefined), /Cannot split DEL: command has no key specification/); + assert.throws(() => splitMultiShardCommand(args, []), /no key specification/); + }); + + it('rejects multiple key specs', () => { + assert.throws( + () => splitMultiShardCommand(args, [...RANGE_STEP_1, ...RANGE_STEP_1]), + /multiple key specifications/ + ); + }); + + it('rejects keyword begin_search', () => { + assert.throws( + () => splitMultiShardCommand(args, [{ + beginSearch: { type: 'keyword', keyword: 'STORE', startFrom: 1 }, + findKeys: { type: 'range', lastKey: -1, keyStep: 1, limit: 0 } + }]), + /unsupported begin_search type 'keyword'/ + ); + }); + + it('rejects unknown spec parts', () => { + assert.throws( + () => splitMultiShardCommand(args, [{ beginSearch: { type: 'unknown' }, findKeys: { type: 'unknown' } }]), + /unsupported begin_search type 'unknown'/ + ); + assert.throws( + () => splitMultiShardCommand(args, [{ beginSearch: { type: 'index', index: 1 }, findKeys: { type: 'unknown' } }]), + /unsupported find_keys type 'unknown'/ + ); + }); + + it('rejects bounded ranges', () => { + assert.throws( + () => splitMultiShardCommand(['GET', '{a}1'], [{ + beginSearch: { type: 'index', index: 1 }, + findKeys: { type: 'range', lastKey: 0, keyStep: 1, limit: 0 } + }]), + /unsupported find_keys range/ + ); + }); + + it('rejects malformed numkeys', () => { + for (const numkeys of ['abc', '-1', '0', '2.5', '']) { + assert.throws( + () => splitMultiShardCommand(['MSETEX', numkeys, '{a}1', 'v1'], KEYNUM_STEP_2), + /malformed numkeys/ + ); + } + }); + + it('rejects a key region overrunning the args (numkeys too large)', () => { + assert.throws( + () => splitMultiShardCommand(['MSETEX', '3', '{a}1', 'v1'], KEYNUM_STEP_2), + /key region overruns/ + ); + }); + + it('rejects a key region misaligned with keystep (MSET missing value)', () => { + assert.throws( + () => splitMultiShardCommand(['MSET', '{a}1', 'v1', '{b}1'], RANGE_STEP_2), + /does not align with keystep/ + ); + }); + + it('rejects an empty key region', () => { + assert.throws( + () => splitMultiShardCommand(['DEL'], RANGE_STEP_1), + /key region/ + ); + }); + }); +}); diff --git a/packages/client/lib/cluster/request-response-policies/scan-cursor.spec.ts b/packages/client/lib/cluster/request-response-policies/scan-cursor.spec.ts new file mode 100644 index 00000000000..d37a0de64a8 --- /dev/null +++ b/packages/client/lib/cluster/request-response-policies/scan-cursor.spec.ts @@ -0,0 +1,204 @@ +import { strict as assert } from 'node:assert'; +import type { CommandParser } from '../../client/parser'; +import { routeScan, finalizeScanCursor } from './scan-cursor'; + +/** + * Minimal stand-in for the scan-chain surface of `RedisClusterSlots` (see + * ft-cursor.spec.ts for the same pattern), with an ordered master list so the + * node-advance logic is exercised without spinning a cluster. + */ +class FakeSlots { + scanCursors = new Map; createdAt: number }>(); + clientsByAddress = new Map(); + masterOrder: Array = []; + #seq = 0; + + mintCursorToken() { return String(++this.#seq); } + bindScanCursor(token: string, address: string, cursor: string, visited: Set) { + this.scanCursors.set(token, { address, cursor, visited, createdAt: 0 }); + } + lookupScanCursor(token: string) { return this.scanCursors.get(token); } + evictScanCursor(token: string) { this.scanCursors.delete(token); } + nextScanTarget(visited: ReadonlySet) { + return this.masterOrder.find(address => !visited.has(address)); + } + async getMasterByAddress(address: string) { return this.clientsByAddress.get(address); } + nodeAddressByClient(client: object) { + for (const [address, c] of this.clientsByAddress) if (c === client) return address; + return undefined; + } + getRandomNode() { return { address: this.masterOrder[0] }; } + async nodeClient(node: { address: string }) { return this.clientsByAddress.get(node.address); } + + addMaster(address: string) { + const client = { id: address }; + this.masterOrder.push(address); + this.clientsByAddress.set(address, client); + return client; + } +} + +const parserOf = (...args: Array) => + ({ redisArgs: args, commandIdentifier: { command: args[0], subcommand: args[1] } }) as unknown as CommandParser; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- routers/finalizers run below the typed surface +const asSlots = (s: FakeSlots) => s as any; + +describe('routeScan', () => { + it('forwards a malformed SCAN (no cursor arg) to a node for the server arity error', async () => { + const slots = new FakeSlots(); + const a = slots.addMaster('a:1'); + + const plan = await routeScan(asSlots(slots), parserOf('SCAN'), undefined, undefined); + assert.deepEqual(plan, [{ client: a }]); + }); + + it('SCAN 0 starts on the first master', async () => { + const slots = new FakeSlots(); + const a = slots.addMaster('a:1'); + slots.addMaster('b:1'); + + const plan = await routeScan(asSlots(slots), parserOf('SCAN', '0'), undefined, undefined); + assert.deepEqual(plan, [{ client: a }]); + }); + + it('throws when there are no masters', async () => { + const slots = new FakeSlots(); + await assert.rejects( + routeScan(asSlots(slots), parserOf('SCAN', '0'), undefined, undefined), + /no master nodes available/ + ); + }); + + it('routes a known token to its bound node with the real cursor substituted', async () => { + const slots = new FakeSlots(); + slots.addMaster('a:1'); + const b = slots.addMaster('b:1'); + slots.bindScanCursor('7', 'b:1', '42', new Set(['a:1'])); + + const plan = await routeScan( + asSlots(slots), parserOf('SCAN', '7', 'MATCH', 'user:*', 'COUNT', '100'), undefined, undefined + ); + assert.equal(plan.length, 1); + assert.equal(plan[0].client, b); + assert.deepEqual(plan[0].parser!.redisArgs, ['SCAN', '42', 'MATCH', 'user:*', 'COUNT', '100']); + }); + + it('throws on an unknown cursor token', async () => { + const slots = new FakeSlots(); + slots.addMaster('a:1'); + await assert.rejects( + routeScan(asSlots(slots), parserOf('SCAN', '999'), undefined, undefined), + /unknown cursor "999".*restart the scan from 0/s + ); + }); + + it('throws when the bound node has left the cluster', async () => { + const slots = new FakeSlots(); + slots.addMaster('a:1'); + slots.bindScanCursor('7', 'gone:1', '42', new Set()); + await assert.rejects( + routeScan(asSlots(slots), parserOf('SCAN', '7'), undefined, undefined), + /left the cluster/ + ); + }); +}); + +describe('finalizeScanCursor', () => { + it('walks a full two-master chain end to end (typed `{ cursor, keys }` reply)', () => { + const slots = new FakeSlots(); + const a = slots.addMaster('a:1'); + const b = slots.addMaster('b:1'); + + // SCAN 0 on node a → mid-node cursor: token minted, reply cursor swapped. + let reply = finalizeScanCursor( + asSlots(slots), parserOf('SCAN', '0'), [{ client: a }], { cursor: '5', keys: ['k1'] } + ) as { cursor: string; keys: Array }; + assert.equal(reply.cursor, '1'); + assert.deepEqual(reply.keys, ['k1']); + assert.deepEqual(slots.lookupScanCursor('1'), { address: 'a:1', cursor: '5', visited: new Set(), createdAt: 0 }); + + // node a exhausts → chain advances to node b with a fresh cursor 0. + reply = finalizeScanCursor( + asSlots(slots), parserOf('SCAN', '1'), [{ client: a }], { cursor: '0', keys: ['k2'] } + ) as { cursor: string; keys: Array }; + assert.equal(reply.cursor, '1'); + assert.deepEqual(slots.lookupScanCursor('1'), { address: 'b:1', cursor: '0', visited: new Set(['a:1']), createdAt: 0 }); + + // node b exhausts, no unvisited masters left → caller sees "0", entry evicted. + reply = finalizeScanCursor( + asSlots(slots), parserOf('SCAN', '1'), [{ client: b }], { cursor: '0', keys: ['k3'] } + ) as { cursor: string; keys: Array }; + assert.equal(reply.cursor, '0'); + assert.equal(slots.scanCursors.size, 0); + }); + + it('finishes immediately on a single-master cluster (no token minted)', () => { + const slots = new FakeSlots(); + const a = slots.addMaster('a:1'); + + const reply = finalizeScanCursor( + asSlots(slots), parserOf('SCAN', '0'), [{ client: a }], { cursor: '0', keys: [] } + ) as { cursor: string }; + assert.equal(reply.cursor, '0'); + assert.equal(slots.scanCursors.size, 0); + }); + + it('rewrites the raw `[cursor, keys]` reply shape at index 0', () => { + const slots = new FakeSlots(); + const a = slots.addMaster('a:1'); + slots.addMaster('b:1'); + + const reply = finalizeScanCursor( + asSlots(slots), parserOf('SCAN', '0'), [{ client: a }], ['17', ['k1', 'k2']] + ) as [string, Array]; + assert.deepEqual(reply, ['1', ['k1', 'k2']]); + assert.equal(slots.lookupScanCursor('1')!.cursor, '17'); + }); + + it('keeps the cursor a Buffer under a Buffer type mapping', () => { + const slots = new FakeSlots(); + const a = slots.addMaster('a:1'); + slots.addMaster('b:1'); + + const reply = finalizeScanCursor( + asSlots(slots), parserOf('SCAN', '0'), [{ client: a }], { cursor: Buffer.from('17'), keys: [] } + ) as { cursor: Buffer }; + assert.ok(reply.cursor instanceof Buffer); + assert.equal(reply.cursor.toString(), '1'); + }); + + it('ignores non-SCAN commands and unknown reply shapes', () => { + const slots = new FakeSlots(); + const a = slots.addMaster('a:1'); + + const getReply = 'value'; + assert.equal(finalizeScanCursor(asSlots(slots), parserOf('GET', 'k'), [{ client: a }], getReply), getReply); + + const weird = { notACursor: true }; + assert.equal(finalizeScanCursor(asSlots(slots), parserOf('SCAN', '0'), [{ client: a }], weird), weird); + assert.equal(slots.scanCursors.size, 0); + }); + + it('leaves the reply untouched when the serving node cannot be resolved', () => { + const slots = new FakeSlots(); + slots.addMaster('a:1'); + const stranger = {}; + + const reply = { cursor: '5', keys: [] }; + assert.equal(finalizeScanCursor(asSlots(slots), parserOf('SCAN', '0'), [{ client: stranger }], reply), reply); + assert.equal(slots.scanCursors.size, 0); + }); + + it('two interleaved scans keep independent chains', () => { + const slots = new FakeSlots(); + const a = slots.addMaster('a:1'); + slots.addMaster('b:1'); + + const r1 = finalizeScanCursor(asSlots(slots), parserOf('SCAN', '0'), [{ client: a }], { cursor: '5', keys: [] }) as { cursor: string }; + const r2 = finalizeScanCursor(asSlots(slots), parserOf('SCAN', '0'), [{ client: a }], { cursor: '9', keys: [] }) as { cursor: string }; + assert.notEqual(r1.cursor, r2.cursor); + assert.equal(slots.lookupScanCursor(r1.cursor)!.cursor, '5'); + assert.equal(slots.lookupScanCursor(r2.cursor)!.cursor, '9'); + }); +}); diff --git a/packages/client/lib/command-metadata/dynamic-policy-resolver.spec.ts b/packages/client/lib/command-metadata/dynamic-policy-resolver.spec.ts new file mode 100644 index 00000000000..bf560a4fc0f --- /dev/null +++ b/packages/client/lib/command-metadata/dynamic-policy-resolver.spec.ts @@ -0,0 +1,398 @@ +import { strict as assert } from 'node:assert'; +import type { CommandReply } from '../commands/generic-transformers'; +import { DynamicPolicyResolverFactory, type CommandFetcher, StaticMetadataResolver, REQUEST_POLICIES_WITH_DEFAULTS, RESPONSE_POLICIES_WITH_DEFAULTS } from '.'; +import testUtils, { GLOBAL } from '../test-utils'; + + +const createMockCommandFetcher = (commands: Array): CommandFetcher => async () => commands; + +describe('DynamicPolicyResolverFactory', () => { + + describe('create', () => { + it('should create StaticMetadataResolver with empty policies', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + assert.ok(resolver instanceof StaticMetadataResolver); + }); + + it('should create StaticMetadataResolver with fallback', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const fallbackResolver = new StaticMetadataResolver({ + std: { + ping: { + request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, + isKeyless: true + } + } + }); + + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher, fallbackResolver); + assert.ok(resolver instanceof StaticMetadataResolver); + + const result = resolver.resolvePolicy({ command: 'ping', subcommand: undefined }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + } + }); + }); + + describe('create with commands', () => { + it('should classify keyless commands correctly', async () => { + const mockCommands: Array = [ + { + name: 'ping', + arity: -1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + policies: { request: undefined, response: undefined }, + isKeyless: true, + tips: [], + keySpecs: [], + subcommands: [] + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy({ command: 'ping', subcommand: undefined }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + } + }); + + it('should classify keyed commands correctly', async () => { + const mockCommands: Array = [ + { + name: 'get', + arity: 2, + flags: new Set(), + firstKeyIndex: 1, + lastKeyIndex: 1, + step: 1, + categories: new Set(), + policies: { request: undefined, response: undefined }, + isKeyless: false, + tips: [], + keySpecs: [], + subcommands: [] + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy({ command: 'get', subcommand: undefined }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + } + }); + + it('should use explicit policies when available', async () => { + const mockCommands: Array = [ + { + name: 'dbsize', + arity: 1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + policies: { request: 'all_shards', response: 'agg_sum' }, + isKeyless: true, + tips: [], + keySpecs: [], + subcommands: [] + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy({ command: 'dbsize', subcommand: undefined }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, 'all_shards'); + assert.equal(result.value.response, 'agg_sum'); + } + }); + + it('should carry keySpecs through for multi_shard commands only', async () => { + const msetKeySpecs = [{ + beginSearch: { type: 'index', index: 1 }, + findKeys: { type: 'range', lastKey: -1, keyStep: 2, limit: 0 } + }] as const; + const mockCommands: Array = [ + { + name: 'mset', + arity: -3, + flags: new Set(), + firstKeyIndex: 1, + lastKeyIndex: -1, + step: 2, + categories: new Set(), + policies: { request: 'multi_shard', response: 'all_succeeded' }, + isKeyless: false, + tips: [], + keySpecs: [...msetKeySpecs], + subcommands: [] + }, + { + name: 'get', + arity: 2, + flags: new Set(), + firstKeyIndex: 1, + lastKeyIndex: 1, + step: 1, + categories: new Set(), + policies: { request: undefined, response: undefined }, + isKeyless: false, + tips: [], + keySpecs: [{ + beginSearch: { type: 'index', index: 1 }, + findKeys: { type: 'range', lastKey: 0, keyStep: 1, limit: 0 } + }], + subcommands: [] + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const msetResult = resolver.resolvePolicy({ command: 'mset', subcommand: undefined }); + assert.equal(msetResult.ok, true); + if (msetResult.ok) { + assert.deepEqual(msetResult.value.keySpecs, msetKeySpecs); + } + + // non-multi_shard commands never split — no keySpecs on their entries + const getResult = resolver.resolvePolicy({ command: 'get', subcommand: undefined }); + assert.equal(getResult.ok, true); + if (getResult.ok) { + assert.equal(getResult.value.keySpecs, undefined); + } + }); + + it('should handle module commands correctly', async () => { + const mockCommands: Array = [ + { + name: 'ft.search', + arity: -2, + flags: new Set(), + firstKeyIndex: 1, + lastKeyIndex: 1, + step: 1, + categories: new Set(), + policies: { request: 'all_shards', response: 'special' }, + isKeyless: false, + tips: [], + keySpecs: [], + subcommands: [] + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy({ command: 'ft.search', subcommand: undefined }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, 'all_shards'); + assert.equal(result.value.response, 'special'); + } + }); + + it('should handle valid module commands', async () => { + const mockCommands: Array = [ + { + name: 'json.get', + arity: 1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + policies: { request: undefined, response: undefined }, + isKeyless: true, + tips: [], + keySpecs: [], + subcommands: [] + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy({ command: 'json.get', subcommand: undefined }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + } + }); + }); + + describe('resolvePolicy', () => { + it('should work with created resolver', async () => { + const mockCommands: Array = [ + { + name: 'test', + arity: 1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + policies: { request: undefined, response: undefined }, + isKeyless: true, + tips: [], + keySpecs: [], + subcommands: [] + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy({ command: 'test', subcommand: undefined }); + assert.equal(result.ok, true); + }); + + it('should handle unknown commands', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy({ command: 'unknown', subcommand: undefined }); + assert.equal(result.ok, false); + assert.equal(result.error, 'unknown-command'); + }); + + it('should handle unknown modules', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy({ command: 'unknown.command', subcommand: undefined }); + assert.equal(result.ok, false); + assert.equal(result.error, 'unknown-module'); + }); + + it('should handle invalid command format', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + const result = resolver.resolvePolicy({ command: 'too.many.dots.here', subcommand: undefined }); + assert.equal(result.ok, false); + assert.equal(result.error, 'wrong-command-or-module-name'); + }); + }); + + describe('edge cases', () => { + it('should handle commands with partial policies', async () => { + const mockCommands: Array = [ + { + name: 'partial-request', + arity: 1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + policies: { request: 'all_nodes', response: undefined }, + isKeyless: false, + tips: [], + keySpecs: [], + subcommands: [] + }, + { + name: 'partial-response', + arity: 1, + flags: new Set(), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set(), + policies: { request: undefined, response: 'agg_sum' }, + isKeyless: true, + tips: [], + keySpecs: [], + subcommands: [] + } + ]; + + const mockCommandFetcher = createMockCommandFetcher(mockCommands); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + + // Command with only request policy should fall back to defaults + let result = resolver.resolvePolicy({ command: 'partial-request', subcommand: undefined }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.ALL_NODES); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + } + + // Command with only response policy should fall back to defaults + result = resolver.resolvePolicy({ command: 'partial-response', subcommand: undefined }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.AGG_SUM); + } + }); + + it('should handle empty command list', async () => { + const mockCommandFetcher = createMockCommandFetcher([]); + const resolver = await DynamicPolicyResolverFactory.create(mockCommandFetcher); + assert.ok(resolver instanceof StaticMetadataResolver); + + const result = resolver.resolvePolicy({ command: 'any-command', subcommand: undefined }); + assert.equal(result.ok, false); + assert.equal(result.error, 'unknown-command'); + }); + }); + + describe('integration tests', () => { + testUtils.testWithClient('should work with real Redis client', async client => { + const resolver = await DynamicPolicyResolverFactory.create(() => client.command()); + assert.ok(resolver instanceof StaticMetadataResolver); + + // Test that ping command is classified as keyless + const pingResult = resolver.resolvePolicy({ command: 'ping', subcommand: undefined }); + if (pingResult.ok) { + assert.equal(pingResult.value.request, REQUEST_POLICIES_WITH_DEFAULTS.ALL_SHARDS); + assert.equal(pingResult.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.ALL_SUCCEEDED); + } else { + assert.fail('Expected pingResult.ok to be true'); + } + + // Test that get command is classified as keyed + const getResult = resolver.resolvePolicy({ command: 'get', subcommand: undefined }); + if (getResult.ok) { + assert.equal(getResult.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(getResult.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + } else { + assert.fail('Expected getResult.ok to be true'); + } + + // Test that dbsize command uses explicit policies if available + const dbsizeResult = resolver.resolvePolicy({ command: 'dbsize', subcommand: undefined }); + + if (dbsizeResult.ok) { + assert.ok( + dbsizeResult.value.request === 'all_shards' && dbsizeResult.value.response === 'agg_sum' + ); + } else { + assert.fail('Expected dbsizeResult.ok to be true'); + } + }, GLOBAL.SERVERS.OPEN); + }); +}); diff --git a/packages/client/lib/command-metadata/predicates.spec.ts b/packages/client/lib/command-metadata/predicates.spec.ts new file mode 100644 index 00000000000..b92a133e81d --- /dev/null +++ b/packages/client/lib/command-metadata/predicates.spec.ts @@ -0,0 +1,95 @@ +import { strict as assert } from 'node:assert'; +import { defaultCommandMetadata, isReplicaSafe, isCacheable } from '.'; +import type { CommandIdentifier } from '../client/parser'; + +// Mirrors the readers in cluster/index.ts, sentinel/utils.ts and +// client/index.ts: a defined override (`Command.IS_READ_ONLY` / +// `Command.CACHEABLE`, or the raw `sendCommand` isReadonly argument) always +// wins; only undeclared intent derives from the server flags/tips. +const id = (command: string, subcommand?: string): CommandIdentifier => ({ command, subcommand }); +const replicaSafe = (i: CommandIdentifier, override?: boolean) => + isReplicaSafe(defaultCommandMetadata.lookup(i), override); +const cacheable = (i: CommandIdentifier, override?: boolean) => + isCacheable(defaultCommandMetadata.lookup(i), override); + +describe('predicates (override-first)', () => { + describe('isReplicaSafe', () => { + it('keyed non-write derives replica-safe', () => { + assert.equal(replicaSafe(id('get')), true); + assert.equal(replicaSafe(id('json.get')), true); + }); + + it('write flag derives master-only', () => { + assert.equal(replicaSafe(id('mset')), false); + }); + + it('explicit override wins in both directions (read-your-writes pin, deliberate replica opt-in)', () => { + assert.equal(replicaSafe(id('get'), false), false); + assert.equal(replicaSafe(id('shutdown'), true), true); + }); + + it('script_runner defaults to master; declared intent routes the script', () => { + // The server cannot know whether a script writes, so EVAL/EVALSHA/FCALL + // carry no write flag — routing must come from the declared intent + // (defineScript IS_READ_ONLY, or the hand-set value on the _RO variants). + assert.equal(replicaSafe(id('eval')), false); + assert.equal(replicaSafe(id('evalsha')), false); + assert.equal(replicaSafe(id('fcall')), false); + assert.equal(replicaSafe(id('evalsha'), true), true); + assert.equal(replicaSafe(id('evalsha'), false), false); + assert.equal(replicaSafe(id('eval_ro'), true), true); + }); + + it('keyless flagless (admin/connection) defaults to master', () => { + assert.equal(replicaSafe(id('shutdown')), false); + assert.equal(replicaSafe(id('failover')), false); + assert.equal(replicaSafe(id('hello')), false); + }); + + it('keyless read-ish commands opt back in via their command-definition override', () => { + // PING.ts et al. hand-set IS_READ_ONLY: true (restored per the + // readonly-discrepancies audit); the predicate itself derives false. + assert.equal(replicaSafe(id('ping')), false); + assert.equal(replicaSafe(id('ping'), true), true); + }); + + it('keyless readonly-flagged is still not replica-routed by derivation (fan-out policies route it)', () => { + assert.equal(replicaSafe(id('keys')), false); + }); + + it('unknown command (user script/function): declared intent or master', () => { + assert.equal(defaultCommandMetadata.lookup(id('definitelynotacommand')), undefined); + assert.equal(replicaSafe(id('definitelynotacommand')), false); + assert.equal(replicaSafe(id('definitelynotacommand'), true), true); + assert.equal(replicaSafe(id('definitelynotacommand'), false), false); + }); + }); + + describe('isCacheable', () => { + it('keyed readonly deterministic: cacheable', () => { + assert.equal(cacheable(id('get')), true); + }); + + it('explicit override wins (TOUCH: readonly + keyed but only bumps LRU/LFU, no invalidation)', () => { + assert.equal(cacheable(id('touch'), false), false); + assert.equal(cacheable(id('xpending'), true), true); + }); + + it('nondeterministic_output derives not cacheable (XPENDING)', () => { + assert.equal(cacheable(id('xpending')), false); + }); + + it('keyless readonly: not cacheable (KEYS/RANDOMKEY are key-invalidation-unsafe)', () => { + assert.equal(cacheable(id('keys')), false); + }); + + it('script_runner: not cacheable (EVAL_RO family)', () => { + assert.equal(cacheable(id('eval_ro')), false); + }); + + it('unknown command: declared intent or not cacheable', () => { + assert.equal(cacheable(id('definitelynotacommand')), false); + assert.equal(cacheable(id('definitelynotacommand'), true), true); + }); + }); +}); diff --git a/packages/client/lib/command-metadata/static-metadata-resolver.spec.ts b/packages/client/lib/command-metadata/static-metadata-resolver.spec.ts new file mode 100644 index 00000000000..b53a8dce785 --- /dev/null +++ b/packages/client/lib/command-metadata/static-metadata-resolver.spec.ts @@ -0,0 +1,256 @@ +import { strict as assert } from 'node:assert'; +import { + StaticMetadataResolver, + REQUEST_POLICIES_WITH_DEFAULTS, + RESPONSE_POLICIES_WITH_DEFAULTS +} from '.'; + +describe('StaticMetadataResolver', () => { + const resolver = new StaticMetadataResolver(); + + describe('subcommand detection', () => { + it('FT.SEARCH: second arg is an index name, not a subcommand', () => { + const result = resolver.resolvePolicy({ command: 'FT.SEARCH', subcommand: 'my-index' }); + assert.equal(result.ok, true); + if (result.ok) { + // FT.SEARCH has no subcommands declared → falls back to parent policy + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + } + }); + + it('MEMORY USAGE: USAGE is a declared subcommand → uses subcommand policy', () => { + const result = resolver.resolvePolicy({ command: 'MEMORY', subcommand: 'USAGE' }); + assert.equal(result.ok, true); + if (result.ok) { + // MEMORY default is keyless, MEMORY USAGE is keyed + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(result.value.isKeyless, false); + } + }); + + it('GET key: second arg is a key, not a subcommand', () => { + const result = resolver.resolvePolicy({ command: 'GET', subcommand: 'foo' }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + } + }); + + it('COMMAND INFO: INFO is a declared subcommand', () => { + const result = resolver.resolvePolicy({ command: 'COMMAND', subcommand: 'INFO' }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.isKeyless, true); + } + }); + + it('FT.SUGADD: keyed module command, no subcommand declared', () => { + const result = resolver.resolvePolicy({ command: 'FT.SUGADD', subcommand: 'mydict' }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(result.value.isKeyless, false); + } + }); + + it('undefined subcommand on a command with declared subcommands → parent policy', () => { + const result = resolver.resolvePolicy({ command: 'MEMORY', subcommand: undefined }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + } + }); + }); + + describe('casing', () => { + it('module name uppercase resolves same as lowercase', () => { + const a = resolver.resolvePolicy({ command: 'FT.SEARCH', subcommand: undefined }); + const b = resolver.resolvePolicy({ command: 'ft.search', subcommand: undefined }); + assert.deepEqual(a, b); + }); + + it('subcommand uppercase resolves same as lowercase', () => { + const a = resolver.resolvePolicy({ command: 'MEMORY', subcommand: 'USAGE' }); + const b = resolver.resolvePolicy({ command: 'memory', subcommand: 'usage' }); + assert.deepEqual(a, b); + }); + }); + + describe('server-`special` commands reverted to default-keyless (no HLD client recipe)', () => { + const REVERTED: Array<{ command: string; subcommand?: string }> = [ + { command: 'INFO' }, + { command: 'MEMORY', subcommand: 'DOCTOR' }, + { command: 'MEMORY', subcommand: 'MALLOC-STATS' }, + { command: 'MEMORY', subcommand: 'STATS' }, + { command: 'LATENCY', subcommand: 'DOCTOR' }, + { command: 'LATENCY', subcommand: 'GRAPH' }, + { command: 'LATENCY', subcommand: 'HISTOGRAM' }, + { command: 'LATENCY', subcommand: 'HISTORY' }, + { command: 'LATENCY', subcommand: 'LATEST' }, + { command: 'FUNCTION', subcommand: 'STATS' }, + { command: 'HOTKEYS', subcommand: 'GET' }, + { command: 'HOTKEYS', subcommand: 'RESET' }, + { command: 'HOTKEYS', subcommand: 'START' }, + { command: 'HOTKEYS', subcommand: 'STOP' } + ]; + + for (const { command, subcommand } of REVERTED) { + const label = subcommand ? `${command} ${subcommand}` : command; + it(`${label} → default-keyless/default-keyless`, () => { + const result = resolver.resolvePolicy({ command, subcommand }); + assert.equal(result.ok, true, `expected ${label} to resolve`); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(result.value.isKeyless, true); + } + }); + } + + it('sibling subcommands keep their server-derived policies (deep-merge)', () => { + const purge = resolver.resolvePolicy({ command: 'MEMORY', subcommand: 'PURGE' }); + assert.equal(purge.ok, true); + if (purge.ok) { + assert.equal(purge.value.request, 'all_shards'); + assert.equal(purge.value.response, 'all_succeeded'); + } + + const usage = resolver.resolvePolicy({ command: 'MEMORY', subcommand: 'USAGE' }); + assert.equal(usage.ok, true); + if (usage.ok) { + assert.equal(usage.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(usage.value.isKeyless, false); + } + + const latencyReset = resolver.resolvePolicy({ command: 'LATENCY', subcommand: 'RESET' }); + assert.equal(latencyReset.ok, true); + if (latencyReset.ok) { + assert.equal(latencyReset.value.request, 'all_nodes'); + assert.equal(latencyReset.value.response, 'agg_sum'); + } + + const hotkeysHelp = resolver.resolvePolicy({ command: 'HOTKEYS', subcommand: 'HELP' }); + assert.equal(hotkeysHelp.ok, true); + if (hotkeysHelp.ok) { + assert.equal(hotkeysHelp.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + assert.equal(hotkeysHelp.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + } + }); + + it('FT.CURSOR READ/DEL stay special (the sole HLD client recipe)', () => { + for (const subcommand of ['READ', 'DEL']) { + const result = resolver.resolvePolicy({ command: 'FT.CURSOR', subcommand }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.SPECIAL); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS); + } + } + }); + + it('HIMPORT session subcommands: all_shards with pinned response reducers', () => { + // The server tips request_policy:all_shards and NO response policy; the pinned + // reducers replace default-keyless, whose aggregateMerge throws on scalar fan-out + // replies (simple-string OK / integers) whenever ≥2 masters reply. + const expected: Array<{ subcommand: string; response: string }> = [ + { subcommand: 'PREPARE', response: 'all_succeeded' }, + { subcommand: 'DISCARD', response: 'agg_max' }, + { subcommand: 'DISCARDALL', response: 'agg_max' } + ]; + for (const { subcommand, response } of expected) { + const result = resolver.resolvePolicy({ command: 'HIMPORT', subcommand }); + assert.equal(result.ok, true, `expected HIMPORT ${subcommand} to resolve`); + if (result.ok) { + assert.equal(result.value.request, 'all_shards', subcommand); + assert.equal(result.value.response, response, subcommand); + assert.equal(result.value.isKeyless, true, subcommand); + } + } + }); + + it('HIMPORT SET: default keyed routing (key at position 2)', () => { + const result = resolver.resolvePolicy({ command: 'HIMPORT', subcommand: 'SET' }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED); + assert.equal(result.value.isKeyless, false); + } + }); + + it('SCAN keeps the server special/special (cluster-wide scan chain)', () => { + const result = resolver.resolvePolicy({ command: 'SCAN', subcommand: '0' }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, REQUEST_POLICIES_WITH_DEFAULTS.SPECIAL); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.SPECIAL); + } + }); + + it('RANDOMKEY keeps the server all_shards/special (random non-nil reply)', () => { + const result = resolver.resolvePolicy({ command: 'RANDOMKEY', subcommand: undefined }); + assert.equal(result.ok, true); + if (result.ok) { + assert.equal(result.value.request, 'all_shards'); + assert.equal(result.value.response, RESPONSE_POLICIES_WITH_DEFAULTS.SPECIAL); + } + }); + }); + + describe('errors', () => { + it('unknown command in std module', () => { + const r = resolver.resolvePolicy({ command: 'definitelynotacommand', subcommand: undefined }); + assert.equal(r.ok, false); + if (!r.ok) assert.equal(r.error, 'unknown-command'); + }); + + it('unknown module', () => { + const r = resolver.resolvePolicy({ command: 'fakemodule.something', subcommand: undefined }); + assert.equal(r.ok, false); + if (!r.ok) assert.equal(r.error, 'unknown-module'); + }); + + it('too many dots', () => { + const r = resolver.resolvePolicy({ command: 'a.b.c', subcommand: undefined }); + assert.equal(r.ok, false); + if (!r.ok) assert.equal(r.error, 'wrong-command-or-module-name'); + }); + + it('Object.prototype member names miss the table instead of resolving', () => { + for (const name of ['constructor', '__proto__', 'hasOwnProperty', 'toString']) { + const r = resolver.resolvePolicy({ command: name, subcommand: undefined }); + assert.equal(r.ok, false, name); + if (!r.ok) assert.equal(r.error, 'unknown-command', name); + } + // Unknown subcommand named like a prototype member falls back to the + // container's policy instead of resolving a Function as metadata. + const r = resolver.resolvePolicy({ command: 'config', subcommand: 'constructor' }); + assert.equal(r.ok, true); + if (r.ok) assert.equal(typeof r.value, 'object'); + }); + }); + + describe('fallback', () => { + it('falls back to the constructor-provided resolver on unknown command', () => { + const fallback = new StaticMetadataResolver({ + std: { + customping: { + request: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, + response: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS, + isKeyless: true + } + } + }); + const chained = new StaticMetadataResolver(undefined, fallback); + const r = chained.resolvePolicy({ command: 'customping', subcommand: undefined }); + assert.equal(r.ok, true); + }); + }); +}); diff --git a/packages/client/lib/commands/COMMAND.spec.ts b/packages/client/lib/commands/COMMAND.spec.ts index 860ffc30685..aa132f97520 100644 --- a/packages/client/lib/commands/COMMAND.spec.ts +++ b/packages/client/lib/commands/COMMAND.spec.ts @@ -1,17 +1,341 @@ -// import { strict as assert } from 'node:assert'; -// import testUtils, { GLOBAL } from '../test-utils'; -// import { transformArguments } from './COMMAND'; -// import { assertPingCommand } from './COMMAND_INFO.spec'; - -// describe('COMMAND', () => { -// it('transformArguments', () => { -// assert.deepEqual( -// transformArguments(), -// ['COMMAND'] -// ); -// }); - -// testUtils.testWithClient('client.command', async client => { -// assertPingCommand((await client.command()).find(command => command.name === 'ping')); -// }, GLOBAL.SERVERS.OPEN); -// }); +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs, transformCommandReply, transformKeySpec, CommandFlags, CommandCategories, CommandRawReply } from './generic-transformers'; +import COMMAND from './COMMAND'; + +describe('COMMAND', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(COMMAND), + ['COMMAND'] + ); + }); + + describe('transformCommandReply', () => { + const testCases = [ + { + name: 'without policies', + input: ['ping', -1, [CommandFlags.STALE], 0, 0, 0, [CommandCategories.FAST], [], [], []] satisfies CommandRawReply, + expected: { + name: 'ping', + arity: -1, + flags: new Set([CommandFlags.STALE]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([CommandCategories.FAST]), + policies: { request: undefined, response: undefined }, + isKeyless: true, + nondeterministicOutput: false, + tips: [], + keySpecs: [], + subcommands: [] + } + }, + { + name: 'pre-7.0 shape (no tips/key-specs/subcommands fields)', + input: ['ping', -1, [CommandFlags.STALE], 0, 0, 0, [CommandCategories.FAST]] as unknown as CommandRawReply, + expected: { + name: 'ping', + arity: -1, + flags: new Set([CommandFlags.STALE]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([CommandCategories.FAST]), + policies: { request: undefined, response: undefined }, + isKeyless: true, + nondeterministicOutput: false, + tips: [], + keySpecs: [], + subcommands: [] + } + }, + { + name: 'with valid policies', + input: ['dbsize', 1, [], 0, 0, 0, [], ['request_policy:all_shards', 'response_policy:agg_sum'], [], []] satisfies CommandRawReply, + expected: { + name: 'dbsize', + arity: 1, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: 'all_shards', response: 'agg_sum' }, + isKeyless: true, + nondeterministicOutput: false, + tips: [], + keySpecs: [], + subcommands: [] + } + }, + { + name: 'with invalid policies', + input: ['test', 0, [], 0, 0, 0, [], ['request_policy:invalid', 'response_policy:invalid'], ['some key specification'], []] satisfies CommandRawReply, + expected: { + name: 'test', + arity: 0, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: undefined, response: undefined }, + isKeyless: false, + nondeterministicOutput: false, + // invalid policy tips are still recognized as policy tips and dropped + tips: [], + keySpecs: [{ beginSearch: { type: 'unknown' }, findKeys: { type: 'unknown' } }], + subcommands: [] + } + }, + { + name: 'with request policy only', + input: ['test', 0, [], 0, 0, 0, [], ['request_policy:all_nodes'], ['some key specification'], []] satisfies CommandRawReply, + expected: { + name: 'test', + arity: 0, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: 'all_nodes', response: undefined }, + isKeyless: false, + nondeterministicOutput: false, + tips: [], + keySpecs: [{ beginSearch: { type: 'unknown' }, findKeys: { type: 'unknown' } }], + subcommands: [] + } + }, + { + name: 'with response policy only', + input: ['test', 0, [], 0, 0, 0, [], ['', 'response_policy:agg_max'], [], []] satisfies CommandRawReply, + expected: { + name: 'test', + arity: 0, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: undefined, response: 'agg_max' }, + isKeyless: true, + nondeterministicOutput: false, + // the leading '' is a non-policy tip and is mirrored verbatim + tips: [''], + keySpecs: [], + subcommands: [] + } + }, + { + // INFO declares tips in this order on a live server; the parser must not + // depend on policy tips being first + name: 'with policies after a non-policy tip', + input: ['info', -1, [], 0, 0, 0, [], ['nondeterministic_output', 'request_policy:all_shards', 'response_policy:special'], [], []] satisfies CommandRawReply, + expected: { + name: 'info', + arity: -1, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: 'all_shards', response: 'special' }, + isKeyless: true, + nondeterministicOutput: true, + tips: ['nondeterministic_output'], + keySpecs: [], + subcommands: [] + } + }, + { + // CLUSTER SLOT-STATS shape: non-policy tip first, request policy only + name: 'with request policy after a non-policy tip', + input: ['test', 0, [], 0, 0, 0, [], ['nondeterministic_output', 'request_policy:all_shards'], [], []] satisfies CommandRawReply, + expected: { + name: 'test', + arity: 0, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: 'all_shards', response: undefined }, + isKeyless: true, + nondeterministicOutput: true, + tips: ['nondeterministic_output'], + keySpecs: [], + subcommands: [] + } + }, + { + // KEYS shape: policy first, trailing non-policy tip must not be + // misread as a response policy + name: 'with non-policy tip after request policy', + input: ['keys', 2, [], 0, 0, 0, [], ['request_policy:all_shards', 'nondeterministic_output'], [], []] satisfies CommandRawReply, + expected: { + name: 'keys', + arity: 2, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: 'all_shards', response: undefined }, + isKeyless: true, + nondeterministicOutput: true, + tips: ['nondeterministic_output'], + keySpecs: [], + subcommands: [] + } + }, + { + name: 'with only non-policy tips', + input: ['test', 0, [], 0, 0, 0, [], ['nondeterministic_output', 'nondeterministic_output_order'], [], []] satisfies CommandRawReply, + expected: { + name: 'test', + arity: 0, + flags: new Set([]), + firstKeyIndex: 0, + lastKeyIndex: 0, + step: 0, + categories: new Set([]), + policies: { request: undefined, response: undefined }, + isKeyless: true, + nondeterministicOutput: true, + tips: ['nondeterministic_output', 'nondeterministic_output_order'], + keySpecs: [], + subcommands: [] + } + } + ]; + + testCases.forEach(testCase => { + it(testCase.name, () => { + assert.deepEqual( + transformCommandReply(testCase.input), + testCase.expected + ); + }); + }); + }); + + describe('transformKeySpec', () => { + // Shapes captured from a live Redis 8.8.0 COMMAND INFO reply: RESP3 + // delivers nested objects, RESP2 the same data as flat field-value pair + // arrays. Both must parse to the same (RESP3-like) result. + const testCases = [ + { + name: 'range (MSET)', + resp3: { + flags: ['OW', 'update'], + begin_search: { type: 'index', spec: { index: 1 } }, + find_keys: { type: 'range', spec: { lastkey: -1, keystep: 2, limit: 0 } } + }, + resp2: [ + 'flags', ['OW', 'update'], + 'begin_search', ['type', 'index', 'spec', ['index', 1]], + 'find_keys', ['type', 'range', 'spec', ['lastkey', -1, 'keystep', 2, 'limit', 0]] + ], + expected: { + beginSearch: { type: 'index', index: 1 }, + findKeys: { type: 'range', lastKey: -1, keyStep: 2, limit: 0 } + } + }, + { + name: 'keynum (MSETEX)', + resp3: { + flags: ['OW', 'update'], + begin_search: { type: 'index', spec: { index: 1 } }, + find_keys: { type: 'keynum', spec: { keynumidx: 0, firstkey: 1, keystep: 2 } } + }, + resp2: [ + 'flags', ['OW', 'update'], + 'begin_search', ['type', 'index', 'spec', ['index', 1]], + 'find_keys', ['type', 'keynum', 'spec', ['keynumidx', 0, 'firstkey', 1, 'keystep', 2]] + ], + expected: { + beginSearch: { type: 'index', index: 1 }, + findKeys: { type: 'keynum', keyNumIdx: 0, firstKey: 1, keyStep: 2 } + } + }, + { + name: 'keyword (GEORADIUS STORE)', + resp3: { + flags: ['OW', 'update'], + begin_search: { type: 'keyword', spec: { keyword: 'STORE', startfrom: 6 } }, + find_keys: { type: 'range', spec: { lastkey: 0, keystep: 1, limit: 0 } } + }, + resp2: [ + 'flags', ['OW', 'update'], + 'begin_search', ['type', 'keyword', 'spec', ['keyword', 'STORE', 'startfrom', 6]], + 'find_keys', ['type', 'range', 'spec', ['lastkey', 0, 'keystep', 1, 'limit', 0]] + ], + expected: { + beginSearch: { type: 'keyword', keyword: 'STORE', startFrom: 6 }, + findKeys: { type: 'range', lastKey: 0, keyStep: 1, limit: 0 } + } + }, + { + name: 'unrecognized types', + resp3: { + begin_search: { type: 'future-type', spec: { whatever: 1 } }, + find_keys: { type: 'future-type', spec: { whatever: 1 } } + }, + resp2: [ + 'begin_search', ['type', 'future-type', 'spec', ['whatever', 1]], + 'find_keys', ['type', 'future-type', 'spec', ['whatever', 1]] + ], + expected: { + beginSearch: { type: 'unknown' }, + findKeys: { type: 'unknown' } + } + } + ]; + + testCases.forEach(testCase => { + it(`${testCase.name} - RESP3 shape`, () => { + assert.deepEqual(transformKeySpec(testCase.resp3), testCase.expected); + }); + + it(`${testCase.name} - RESP2 shape`, () => { + assert.deepEqual(transformKeySpec(testCase.resp2), testCase.expected); + }); + }); + + it('malformed entries parse to unknown instead of throwing', () => { + const unknown = { beginSearch: { type: 'unknown' }, findKeys: { type: 'unknown' } }; + assert.deepEqual(transformKeySpec('some key specification'), unknown); + assert.deepEqual(transformKeySpec(null), unknown); + assert.deepEqual(transformKeySpec(['odd', 'pair', 'array']), unknown); + assert.deepEqual(transformKeySpec({ + begin_search: { type: 'index', spec: { index: 'not-a-number' } }, + find_keys: { type: 'range', spec: { lastkey: -1 } } + }), unknown); + }); + }); + + testUtils.testWithClient('client.command', async client => { + const commands = await client.command(); + + const result = commands.find(command => command.name === 'dbsize'); + assert.equal(result?.name, 'dbsize'); + assert.equal(result?.arity, 1); + assert.equal(result?.policies?.request, 'all_shards'); + assert.equal(result?.policies?.response, 'agg_sum'); + + // INFO declares 'nondeterministic_output' before its policy tips — + // regression guard for positional tips parsing + const info = commands.find(command => command.name === 'info'); + assert.equal(info?.policies?.request, 'all_shards'); + assert.equal(info?.policies?.response, 'special'); + + const mset = commands.find(command => command.name === 'mset'); + assert.deepEqual(mset?.keySpecs, [{ + beginSearch: { type: 'index', index: 1 }, + findKeys: { type: 'range', lastKey: -1, keyStep: 2, limit: 0 } + }]); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/MSETEX.spec.ts b/packages/client/lib/commands/MSETEX.spec.ts index 20eea82136f..1cd723ab9b5 100644 --- a/packages/client/lib/commands/MSETEX.spec.ts +++ b/packages/client/lib/commands/MSETEX.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from "node:assert"; import testUtils, { GLOBAL } from "../test-utils"; import MSETEX, { ExpirationMode, SetMode } from "./MSETEX"; import { parseArgs } from "./generic-transformers"; +import { defaultCommandMetadata } from "../command-metadata"; describe("MSETEX", () => { describe("transformArguments", () => { @@ -271,6 +272,20 @@ describe("MSETEX", () => { } ); + // MSETEX is curated out of multi_shard (NX/XX is all-or-nothing across ALL + // keys and cannot be evaluated per shard): the cluster must route it to the + // first key's node as a single command, never split it per slot. Assert the + // routing policy directly — server-independent, so it holds on every version + // (the previous cross-slot-CROSSSLOT check depended on 8.8+ server + // enforcement and spuriously failed on older servers). + it("is routed single-node (default-keyed), never multi_shard", () => { + const meta = defaultCommandMetadata.lookup({ + command: "MSETEX", + subcommand: undefined, + }); + assert.equal(meta?.request, "default-keyed"); + }); + testUtils.testAll( "mSetEx with NX", async (client) => { diff --git a/packages/client/lib/commands/generic-transformers.spec.ts b/packages/client/lib/commands/generic-transformers.spec.ts index 328140c3422..c9cb0e706e1 100644 --- a/packages/client/lib/commands/generic-transformers.spec.ts +++ b/packages/client/lib/commands/generic-transformers.spec.ts @@ -706,7 +706,10 @@ describe('Generic Transformers', () => { 0, 0, 0, - [CommandCategories.FAST, CommandCategories.CONNECTION] + [CommandCategories.FAST, CommandCategories.CONNECTION], + [], + [], + [] ]), { name: 'ping', @@ -715,11 +718,35 @@ describe('Generic Transformers', () => { firstKeyIndex: 0, lastKeyIndex: 0, step: 0, - categories: new Set([CommandCategories.FAST, CommandCategories.CONNECTION]) + categories: new Set([CommandCategories.FAST, CommandCategories.CONNECTION]), + policies: { request: undefined, response: undefined }, + isKeyless: true, + nondeterministicOutput: false, + tips: [], + keySpecs: [], + subcommands: [] } ); }); + it('transformCommandReply captures non-policy tips (dont_cache) and separates policy tips', () => { + const reply = transformCommandReply([ + 'touch', + -2, + [CommandFlags.READONLY, CommandFlags.FAST], + 1, + -1, + 1, + [], + ['request_policy:multi_shard', 'dont_cache', 'nondeterministic_output'], + [], + [] + ]); + assert.deepEqual(reply.tips, ['dont_cache', 'nondeterministic_output']); + assert.equal(reply.nondeterministicOutput, true); + assert.equal(reply.policies.request, 'multi_shard'); + }); + describe('pushSlotRangesArguments', () => { it('single range', () => { assert.deepEqual( diff --git a/packages/search/lib/commands/CURSOR_READ.cluster.spec.ts b/packages/search/lib/commands/CURSOR_READ.cluster.spec.ts new file mode 100644 index 00000000000..537939d749e --- /dev/null +++ b/packages/search/lib/commands/CURSOR_READ.cluster.spec.ts @@ -0,0 +1,97 @@ +import { strict as assert } from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; +import testUtils, { GLOBAL } from '../test-utils'; + +/** + * Cluster-mode sticky-cursor routing. FT.CURSOR READ/DEL carry no key, so + * without the client-side binding they'd hash-route to an arbitrary node and + * the server would reject the unknown cursor. These tests prove the binding: + * pagination completes against the coordinator that minted the cursor, DEL + * evicts it, and bindings are per client instance. + */ +describe('FT.CURSOR sticky routing (cluster)', () => { + const DOC_COUNT = 40; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test helper over the dynamic cluster surface + async function seedIndex(cluster: any) { + await cluster.ft.create('idx', { n: 'NUMERIC' }); + const writes = []; + for (let i = 0; i < DOC_COUNT; i++) { + writes.push(cluster.hSet(`doc:${i}`, { n: i })); + } + await Promise.all(writes); + } + + // FT.CREATE lands on a single node and its index definition propagates to the + // other shards asynchronously; an aggregate that routes to a shard which + // hasn't caught up yet is rejected with "No such index" (flaky on slower + // servers). Retry the aggregate — the cluster client follows MOVED and + // re-picks a node each attempt — until the index is live everywhere. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test helper over the dynamic cluster surface + async function openCursor(cluster: any) { + for (let attempt = 0; ; attempt++) { + try { + return await cluster.ft.aggregateWithCursor('idx', '*', { COUNT: 5, LOAD: '@n' }); + } catch (err) { + if (attempt >= 100 || !/no such index/i.test(String((err as Error)?.message ?? err))) throw err; + await setTimeout(50); + } + } + } + + testUtils.testWithCluster('paginates a WITHCURSOR aggregate to completion (every page hits the same coordinator)', async cluster => { + await seedIndex(cluster); + + const first = await openCursor(cluster); + assert.notEqual(first.cursor, 0, 'COUNT 5 over 40 docs should leave a live cursor'); + + let cursor = first.cursor; + let pages = 0; + // FT.CURSOR READ is keyless: without sticky routing it would hash to a + // random node and the server would reject the cursor as unknown. Draining + // the cursor to 0 without that error proves every READ reached the node + // that minted it. How many rows the aggregate ultimately yields depends on + // the server's cross-shard search coordination (which varies by version), + // so we assert the client routing contract here, not the total row count. + while (cursor !== 0) { + const page = await cluster.ft.cursorRead('idx', cursor); + cursor = page.cursor; + pages++; + } + + assert.ok(pages >= 1, 'at least one sticky FT.CURSOR READ must drain the cursor'); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('FT.CURSOR DEL evicts the binding, so a later READ throws a client-side MISS', async cluster => { + await seedIndex(cluster); + + const { cursor } = await openCursor(cluster); + assert.notEqual(cursor, 0); + + await cluster.ft.cursorDel('idx', cursor); + await assert.rejects( + cluster.ft.cursorRead('idx', cursor), + /unknown cursor/, + 'READ after DEL should MISS before any network call' + ); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('a cursor created on one client instance is unusable on another (per-instance bindings)', async cluster => { + await seedIndex(cluster); + + const { cursor } = await openCursor(cluster); + assert.notEqual(cursor, 0); + + const other = cluster.duplicate(); + await other.connect(); + try { + await assert.rejects( + other.ft.cursorRead('idx', cursor), + /unknown cursor/, + 'a second client has no binding for the first client\'s cursor' + ); + } finally { + other.destroy(); + } + }, GLOBAL.CLUSTERS.OPEN); +}); diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index bb64e1d20ff..b6da213fd61 100644 --- a/packages/search/lib/test-utils.ts +++ b/packages/search/lib/test-utils.ts @@ -33,5 +33,17 @@ export const GLOBAL = { } } } + }, + CLUSTERS: { + OPEN: { + numberOfMasters: 3, + serverArguments: [], + clusterConfiguration: { + RESP: 3 as const, + modules: { + ft: RediSearch + } + } + } } }; From b77195e9b5969d9dbb565d315c5d2b7757d498a3 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Thu, 30 Jul 2026 22:45:36 +0300 Subject: [PATCH 08/17] fix(client): aggregate array replies element-wise for agg_min/agg_max MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WAITAOF is tagged all_shards/agg_min but its reply is an array ([numlocal, numreplicas]); the scalar aggregateMin threw "All replies must be numbers", breaking every raw cluster fan-out. Fold array replies per-position — matching the server's AGG_MIN/AGG_MAX semantics and the existing element-wise logical AND/OR aggregators — via a shared helper; scalar aggregation is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../generic-aggregators.spec.ts | 39 +++++++++++- .../generic-aggregators.ts | 61 ++++++++++++++++--- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts b/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts index d77e5edc350..d9863e60a08 100644 --- a/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts +++ b/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts @@ -1,5 +1,5 @@ import { strict as assert } from 'node:assert'; -import { aggregateLogicalAnd, aggregateLogicalOr, aggregateMerge } from './generic-aggregators'; +import { aggregateLogicalAnd, aggregateLogicalOr, aggregateMerge, aggregateMin, aggregateMax } from './generic-aggregators'; describe('aggregateLogicalOr', () => { it('ORs element-wise across shards', () => { @@ -49,3 +49,40 @@ describe('aggregateLogicalAnd', () => { assert.throws(() => aggregateLogicalAnd([['1']]), /logical AND aggregation/); }); }); + +describe('aggregateMin', () => { + it('takes the minimum of scalar replies (WAIT)', () => { + assert.equal(aggregateMin([2, 0, 1]), 0); + }); + + it('takes the element-wise minimum of array replies (WAITAOF)', () => { + // each shard: [numlocal, numreplicas] + assert.deepEqual(aggregateMin([[1, 3], [1, 0], [0, 2]]), [0, 0]); + }); + + it('returns 0 for an empty replies array', () => { + assert.equal(aggregateMin([]), 0); + }); + + it('rejects non-numeric scalar replies', () => { + assert.throws(() => aggregateMin(['1', '2']), /numbers for min aggregation/); + }); + + it('rejects array replies of unequal length', () => { + assert.throws(() => aggregateMin([[1, 2], [1]]), /number arrays of equal length for min/); + }); +}); + +describe('aggregateMax', () => { + it('takes the maximum of scalar replies', () => { + assert.equal(aggregateMax([2, 0, 5]), 5); + }); + + it('takes the element-wise maximum of array replies', () => { + assert.deepEqual(aggregateMax([[1, 3], [1, 0], [0, 2]]), [1, 3]); + }); + + it('rejects array replies with a non-numeric element', () => { + assert.throws(() => aggregateMax([[1, '2']]), /number arrays of equal length for max/); + }); +}); diff --git a/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts b/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts index d5fcedd552f..55026b10f5f 100644 --- a/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts +++ b/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts @@ -66,16 +66,54 @@ export const aggregateLogicalOr = (replies: Array): T => { }; /** - * Aggregates multiple numbers by finding the minimum value. + * Per-position reduce over array replies. Validates that every reply is a + * number array of the same length, then folds column-wise with `reduce`. + * Shared by the array path of `aggregateMin`/`aggregateMax` so element-wise + * aggregation matches the server's AGG_MIN/AGG_MAX semantics for commands + * whose reply is an array (e.g. WAITAOF's `[numlocal, numreplicas]`). + */ +const aggregateElementwise = ( + replies: Array, + reduce: (a: number, b: number) => number, + label: string +): Array => { + const length = (replies[0] as Array).length; + if ( + !replies.every( + (reply): reply is number[] => + Array.isArray(reply) && + reply.length === length && + reply.every((value): value is number => typeof value === 'number') + ) + ) { + throw new Error( + `All replies must be number arrays of equal length for ${label} aggregation` + ); + } + + const result = (replies[0] as number[]).slice(); + for (let r = 1; r < replies.length; r++) { + const reply = replies[r] as number[]; + for (let i = 0; i < length; i++) { + result[i] = reduce(result[i], reply[i]); + } + } + return result; +}; + +/** + * Aggregates shard replies by taking the minimum value. * @remarks - * This implementation is specifically designed for Array type only, - * despite the generic type parameter. It is used by commands like WAIT - * which returns the minimal number of synchronized replicas from all shards. - * The generic type parameter T is provided for usage ergonomy, but the actual input structure - * will be validated at runtime. + * Scalar replies (e.g. WAIT, the minimal number of synchronized replicas) fold + * to a single minimum; array replies (e.g. WAITAOF's `[numlocal, numreplicas]`) + * fold element-wise, matching the server's AGG_MIN semantics. Input structure + * is validated at runtime; the generic `T` is for call-site ergonomy only. */ export const aggregateMin = (replies: Array): T => { if (replies.length === 0) return 0 as T; + if (Array.isArray(replies[0])) { + return aggregateElementwise(replies, Math.min, 'min') as T; + } if (!replies.every((reply): reply is number => typeof reply === 'number')) { throw new Error('All replies must be numbers for min aggregation'); } @@ -83,14 +121,17 @@ export const aggregateMin = (replies: Array): T => { }; /** - * Aggregates multiple numbers by finding the maximum value. + * Aggregates shard replies by taking the maximum value. * @remarks - * This implementation is specifically designed for Array type only, - * despite the generic type parameter. The generic type parameter T is provided - * for usage ergonomy, but the actual input structure will be validated at runtime. + * Mirrors {@link aggregateMin}: scalar replies fold to a single maximum, array + * replies fold element-wise (AGG_MAX semantics). Input structure is validated + * at runtime; the generic `T` is for call-site ergonomy only. */ export const aggregateMax = (replies: Array): T => { if (replies.length === 0) return 0 as T; + if (Array.isArray(replies[0])) { + return aggregateElementwise(replies, Math.max, 'max') as T; + } if (!replies.every((reply): reply is number => typeof reply === 'number')) { throw new Error('All replies must be numbers for max aggregation'); } From 50fa140ef3a521bae4095bbd2c8116177143d8d7 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 31 Jul 2026 12:14:05 +0300 Subject: [PATCH 09/17] fix(client): pin TOUCH master-only (write-effect despite readonly flag) The server flags TOUCH `[readonly, fast]` (no `write`), so the derived isReplicaSafe would route it to a replica. TOUCH only bumps LRU/LFU access metadata that drives eviction on the master, so it must stay master-only. Restore the explicit IS_READ_ONLY: false override (dropped when overrides were replaced by metadata derivation). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/client/lib/commands/TOUCH.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/client/lib/commands/TOUCH.ts b/packages/client/lib/commands/TOUCH.ts index 10f27e33ab9..c0966a3375b 100644 --- a/packages/client/lib/commands/TOUCH.ts +++ b/packages/client/lib/commands/TOUCH.ts @@ -3,6 +3,16 @@ import { NumberReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; export default { + // The server tags TOUCH with flags `[readonly, fast]` — no `write` flag, + // because it does not modify any value. So the metadata-derived + // `isReplicaSafe` (keyed, not `write`, not `script_runner` ⇒ replica-safe) + // would route it to a replica. That is wrong: TOUCH's only effect is bumping + // each key's LRU/LFU access metadata, which drives eviction decisions on the + // MASTER. Run on a replica it mutates replica-local counters and returns a + // count while leaving the master's eviction state untouched. It is the one + // "readonly-flagged but write-effect" command, so pin it master-only + // explicitly (the override wins over the derived value). + IS_READ_ONLY: false, CACHEABLE: false, parseCommand(parser: CommandParser, key: RedisVariadicArgument) { parser.push('TOUCH'); From 699b988d5af6bbef03204b56504a2efdd29533b8 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 31 Jul 2026 14:51:24 +0300 Subject: [PATCH 10/17] fix(client): restore IS_READ_ONLY on keyless reads for replica routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The metadata-derived isReplicaSafe returns false for every keyless command (it cannot tell a keyless read from keyless admin like SHUTDOWN), so keyless reads that master routed to replicas now go master-only in sentinel. Restore the explicit IS_READ_ONLY: true override on the keyless data reads — DBSIZE, KEYS, SCAN, RANDOMKEY, the RediSearch reads (FT.SEARCH/SPELLCHECK/EXPLAIN/ EXPLAINCLI/INFO/DICTDUMP/TAGVALS/SYNDUMP/PROFILE) and the time-series reads (TS.MGET/MRANGE/MREVRANGE/QUERYINDEX) — matching what master declared. Each is a non-mutating read over replicated data. FT.AGGREGATE is deliberately left master-only (WITHCURSOR mints server-side cursor state), as on master. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/client/lib/commands/DBSIZE.ts | 4 ++++ packages/client/lib/commands/KEYS.ts | 4 ++++ packages/client/lib/commands/RANDOMKEY.ts | 4 ++++ packages/client/lib/commands/SCAN.ts | 4 ++++ packages/search/lib/commands/DICTDUMP.ts | 4 ++++ packages/search/lib/commands/EXPLAIN.ts | 4 ++++ packages/search/lib/commands/EXPLAINCLI.ts | 4 ++++ packages/search/lib/commands/INFO.ts | 4 ++++ packages/search/lib/commands/PROFILE_AGGREGATE.ts | 4 ++++ packages/search/lib/commands/PROFILE_SEARCH.ts | 4 ++++ packages/search/lib/commands/SEARCH.ts | 4 ++++ packages/search/lib/commands/SPELLCHECK.ts | 4 ++++ packages/search/lib/commands/SYNDUMP.ts | 4 ++++ packages/search/lib/commands/TAGVALS.ts | 4 ++++ packages/time-series/lib/commands/MGET.ts | 4 ++++ packages/time-series/lib/commands/MRANGE.ts | 4 ++++ packages/time-series/lib/commands/MREVRANGE.ts | 4 ++++ packages/time-series/lib/commands/QUERYINDEX.ts | 4 ++++ 18 files changed, 72 insertions(+) diff --git a/packages/client/lib/commands/DBSIZE.ts b/packages/client/lib/commands/DBSIZE.ts index 330507894b0..d7baec4bd05 100644 --- a/packages/client/lib/commands/DBSIZE.ts +++ b/packages/client/lib/commands/DBSIZE.ts @@ -2,6 +2,10 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('DBSIZE'); }, diff --git a/packages/client/lib/commands/KEYS.ts b/packages/client/lib/commands/KEYS.ts index 57ac442a6c5..38e1ce3c25e 100644 --- a/packages/client/lib/commands/KEYS.ts +++ b/packages/client/lib/commands/KEYS.ts @@ -2,6 +2,10 @@ import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser, pattern: RedisArgument) { parser.push('KEYS', pattern); }, diff --git a/packages/client/lib/commands/RANDOMKEY.ts b/packages/client/lib/commands/RANDOMKEY.ts index adb25f6610c..0021b1217aa 100644 --- a/packages/client/lib/commands/RANDOMKEY.ts +++ b/packages/client/lib/commands/RANDOMKEY.ts @@ -2,6 +2,10 @@ import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser) { parser.push('RANDOMKEY'); }, diff --git a/packages/client/lib/commands/SCAN.ts b/packages/client/lib/commands/SCAN.ts index 3a86cf9a138..ec7254ba24a 100644 --- a/packages/client/lib/commands/SCAN.ts +++ b/packages/client/lib/commands/SCAN.ts @@ -70,6 +70,10 @@ export interface ScanOptions extends ScanCommonOptions { } export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser, cursor: RedisArgument, options?: ScanOptions) { parser.push('SCAN'); parseScanArguments(parser, cursor, options); diff --git a/packages/search/lib/commands/DICTDUMP.ts b/packages/search/lib/commands/DICTDUMP.ts index 3c34df566a9..ab38e0b80a3 100644 --- a/packages/search/lib/commands/DICTDUMP.ts +++ b/packages/search/lib/commands/DICTDUMP.ts @@ -2,6 +2,10 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser, dictionary: RedisArgument) { parser.push('FT.DICTDUMP', dictionary); }, diff --git a/packages/search/lib/commands/EXPLAIN.ts b/packages/search/lib/commands/EXPLAIN.ts index a3727a75366..90ab3449af1 100644 --- a/packages/search/lib/commands/EXPLAIN.ts +++ b/packages/search/lib/commands/EXPLAIN.ts @@ -9,6 +9,10 @@ export interface FtExplainOptions { } export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/EXPLAINCLI.ts b/packages/search/lib/commands/EXPLAINCLI.ts index 7a740a36aca..16405cac6cf 100644 --- a/packages/search/lib/commands/EXPLAINCLI.ts +++ b/packages/search/lib/commands/EXPLAINCLI.ts @@ -7,6 +7,10 @@ export interface FtExplainCLIOptions { } export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/INFO.ts b/packages/search/lib/commands/INFO.ts index 1409b9b9c8a..89611064844 100644 --- a/packages/search/lib/commands/INFO.ts +++ b/packages/search/lib/commands/INFO.ts @@ -5,6 +5,10 @@ import { createTransformTuplesReplyFunc, transformDoubleReply } from "@redis/cli import { TuplesReply } from '@redis/client/dist/lib/RESP/types'; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument) { parser.push('FT.INFO', index); }, diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.ts index 9ba060c1d37..890d9edef5e 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.ts @@ -10,6 +10,10 @@ import { } from './PROFILE_SEARCH'; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand( parser: CommandParser, index: string, diff --git a/packages/search/lib/commands/PROFILE_SEARCH.ts b/packages/search/lib/commands/PROFILE_SEARCH.ts index 812f7c40379..78a44314e89 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.ts @@ -95,6 +95,10 @@ function transformProfileSearchReplyResp3( } export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand( parser: CommandParser, index: RedisArgument, diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index 644ba630f00..adc9fd19498 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -218,6 +218,10 @@ function transformSearchReplyResp3( } export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSearchOptions) { parser.push('FT.SEARCH', index, query); diff --git a/packages/search/lib/commands/SPELLCHECK.ts b/packages/search/lib/commands/SPELLCHECK.ts index 2b94dea6de4..4fa440f7db8 100644 --- a/packages/search/lib/commands/SPELLCHECK.ts +++ b/packages/search/lib/commands/SPELLCHECK.ts @@ -65,6 +65,10 @@ function transformSpellCheckReplyResp3(rawReply: ReplyUnion): SpellCheckReply { } export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSpellCheckOptions) { parser.push('FT.SPELLCHECK', index, query); diff --git a/packages/search/lib/commands/SYNDUMP.ts b/packages/search/lib/commands/SYNDUMP.ts index 3ce4171f3f6..fbb0db248eb 100644 --- a/packages/search/lib/commands/SYNDUMP.ts +++ b/packages/search/lib/commands/SYNDUMP.ts @@ -2,6 +2,10 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument) { parser.push('FT.SYNDUMP', index); }, diff --git a/packages/search/lib/commands/TAGVALS.ts b/packages/search/lib/commands/TAGVALS.ts index f9805967a70..67a64a9fa3b 100644 --- a/packages/search/lib/commands/TAGVALS.ts +++ b/packages/search/lib/commands/TAGVALS.ts @@ -2,6 +2,10 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser, index: RedisArgument, fieldName: RedisArgument) { parser.push('FT.TAGVALS', index, fieldName); }, diff --git a/packages/time-series/lib/commands/MGET.ts b/packages/time-series/lib/commands/MGET.ts index 4c2740c25e4..da851882a80 100644 --- a/packages/time-series/lib/commands/MGET.ts +++ b/packages/time-series/lib/commands/MGET.ts @@ -45,6 +45,10 @@ export type MGetRawReply3 = MapReply< >; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser, filter: RedisVariadicArgument, options?: TsMGetOptions) { parser.push('TS.MGET'); parseLatestArgument(parser, options?.LATEST); diff --git a/packages/time-series/lib/commands/MRANGE.ts b/packages/time-series/lib/commands/MRANGE.ts index a1a8eb32c98..7e55000054d 100644 --- a/packages/time-series/lib/commands/MRANGE.ts +++ b/packages/time-series/lib/commands/MRANGE.ts @@ -49,6 +49,10 @@ export function createTransformMRangeArguments(command: RedisArgument) { } export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand: createTransformMRangeArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeRawReply2, _?: unknown, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MREVRANGE.ts b/packages/time-series/lib/commands/MREVRANGE.ts index e8ab6ad6b6d..65969f29d3c 100644 --- a/packages/time-series/lib/commands/MREVRANGE.ts +++ b/packages/time-series/lib/commands/MREVRANGE.ts @@ -2,6 +2,10 @@ import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE, { createTransformMRangeArguments } from './MRANGE'; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand: createTransformMRangeArguments('TS.MREVRANGE'), transformReply: MRANGE.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/QUERYINDEX.ts b/packages/time-series/lib/commands/QUERYINDEX.ts index 9d208a9ede8..ace1bdfc262 100644 --- a/packages/time-series/lib/commands/QUERYINDEX.ts +++ b/packages/time-series/lib/commands/QUERYINDEX.ts @@ -3,6 +3,10 @@ import { ArrayReply, BlobStringReply, SetReply, Command } from '@redis/client/di import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { + // Keyless read: replica-safe, but the metadata-derived isReplicaSafe + // returns false for keyless commands, so opt in explicitly (restores the + // pre-derivation master behavior). + IS_READ_ONLY: true, parseCommand(parser: CommandParser, filter: RedisVariadicArgument) { parser.push('TS.QUERYINDEX'); parser.pushVariadic(filter); From aa9fa83fa240488c089b8e9840e7d12336de4059 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 31 Jul 2026 15:08:37 +0300 Subject: [PATCH 11/17] fix(client): derive replica-safety for pipelined commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cluster and sentinel MULTI/pipeline builders passed command.IS_READ_ONLY raw, bypassing the metadata resolver the standalone path uses. After the overrides were dropped in favor of derivation, a read-only command in a pipeline (e.g. GET) passed `undefined` and routed to the master even though the same command standalone reaches a replica. Route the std and module builders through isReplicaSafe(lookup(id), command.IS_READ_ONLY) — override first, else server metadata — memoized per command method like the standalone path. Function/script builders keep their explicit IS_READ_ONLY, unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/client/lib/cluster/multi-command.ts | 13 ++++++++++--- packages/client/lib/sentinel/multi-commands.ts | 11 +++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/client/lib/cluster/multi-command.ts b/packages/client/lib/cluster/multi-command.ts index ea389d818b0..6a228da6745 100644 --- a/packages/client/lib/cluster/multi-command.ts +++ b/packages/client/lib/cluster/multi-command.ts @@ -4,6 +4,7 @@ import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, Commande import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander'; import { BasicCommandParser, prefixKey } from '../client/parser'; import { Tail } from '../commands/generic-transformers'; +import { isReplicaSafe, defaultCommandMetadata } from '../command-metadata'; type CommandSignature< REPLIES extends Array, @@ -95,6 +96,9 @@ export type ClusterMultiExecute = ( export default class RedisClusterMultiCommand { static #createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + // Derive replica-safety once per command (override-first, else server + // metadata) so read-only commands in a pipeline route like standalone ones. + let replicaSafe: boolean | undefined; return function (this: RedisClusterMultiCommand, ...args: Array) { const parser = new BasicCommandParser(this.#keyPrefix); @@ -103,10 +107,11 @@ export default class RedisClusterMultiCommand { const redisArgs: CommandArguments = parser.redisArgs; redisArgs.preserve = parser.preserve; const firstKey = parser.firstKey; - + replicaSafe ??= isReplicaSafe(defaultCommandMetadata.lookup(parser.commandIdentifier), command.IS_READ_ONLY); + return this.addCommand( firstKey, - command.IS_READ_ONLY, + replicaSafe, redisArgs, transformReply ); @@ -115,6 +120,7 @@ export default class RedisClusterMultiCommand { static #createModuleCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + let replicaSafe: boolean | undefined; return function (this: { _self: RedisClusterMultiCommand }, ...args: Array) { const parser = new BasicCommandParser(this._self.#keyPrefix); @@ -123,10 +129,11 @@ export default class RedisClusterMultiCommand { const redisArgs: CommandArguments = parser.redisArgs; redisArgs.preserve = parser.preserve; const firstKey = parser.firstKey; + replicaSafe ??= isReplicaSafe(defaultCommandMetadata.lookup(parser.commandIdentifier), command.IS_READ_ONLY); return this._self.addCommand( firstKey, - command.IS_READ_ONLY, + replicaSafe, redisArgs, transformReply ); diff --git a/packages/client/lib/sentinel/multi-commands.ts b/packages/client/lib/sentinel/multi-commands.ts index 1a707733c5a..60ce3edf780 100644 --- a/packages/client/lib/sentinel/multi-commands.ts +++ b/packages/client/lib/sentinel/multi-commands.ts @@ -5,6 +5,7 @@ import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../com import { RedisSentinelType } from './types'; import { BasicCommandParser } from '../client/parser'; import { Tail } from '../commands/generic-transformers'; +import { isReplicaSafe, defaultCommandMetadata } from '../command-metadata'; type CommandSignature< REPLIES extends Array, @@ -91,6 +92,9 @@ export type RedisSentinelMultiCommandType< export default class RedisSentinelMultiCommand { private static _createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + // Derive replica-safety once per command (override-first, else server + // metadata) so read-only commands in a pipeline route like standalone ones. + let replicaSafe: boolean | undefined; return function (this: RedisSentinelMultiCommand, ...args: Array) { const parser = new BasicCommandParser(this.#sentinel._keyPrefix); @@ -98,9 +102,10 @@ export default class RedisSentinelMultiCommand { const redisArgs: CommandArguments = parser.redisArgs; redisArgs.preserve = parser.preserve; + replicaSafe ??= isReplicaSafe(defaultCommandMetadata.lookup(parser.commandIdentifier), command.IS_READ_ONLY); return this.addCommand( - command.IS_READ_ONLY, + replicaSafe, redisArgs, transformReply ); @@ -109,6 +114,7 @@ export default class RedisSentinelMultiCommand { private static _createModuleCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + let replicaSafe: boolean | undefined; return function (this: { _self: RedisSentinelMultiCommand }, ...args: Array) { const parser = new BasicCommandParser(this._self.#sentinel._keyPrefix); @@ -116,9 +122,10 @@ export default class RedisSentinelMultiCommand { const redisArgs: CommandArguments = parser.redisArgs; redisArgs.preserve = parser.preserve; + replicaSafe ??= isReplicaSafe(defaultCommandMetadata.lookup(parser.commandIdentifier), command.IS_READ_ONLY); return this._self.addCommand( - command.IS_READ_ONLY, + replicaSafe, redisArgs, transformReply ); From e2cd3d6630343ef8a630a854f6563b1c7be261ca Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 31 Jul 2026 15:34:50 +0300 Subject: [PATCH 12/17] fix(client): clamp logical AND/OR aggregation to 0/1 `&&`/`||` return the operand, not a boolean, so a future agg_logical_and/or command returning non-binary numbers would leak values (1 && 2 === 2). Clamp each element to 0/1. Correct today for SCRIPT EXISTS (0/1 inputs); this future-proofs the registered reducers. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../request-response-policies/generic-aggregators.spec.ts | 8 ++++++++ .../request-response-policies/generic-aggregators.ts | 8 ++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts b/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts index d9863e60a08..9783f8aa414 100644 --- a/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts +++ b/packages/client/lib/cluster/request-response-policies/generic-aggregators.spec.ts @@ -10,6 +10,10 @@ describe('aggregateLogicalOr', () => { assert.deepEqual(aggregateLogicalOr([[0, 0], [0, 0]]), [0, 0]); }); + it('clamps non-binary numbers to 0/1', () => { + assert.deepEqual(aggregateLogicalOr([[0, 0], [2, 0]]), [1, 0]); + }); + it('returns [] for an empty replies array', () => { assert.deepEqual(aggregateLogicalOr([]), []); }); @@ -45,6 +49,10 @@ describe('aggregateLogicalAnd', () => { assert.deepEqual(aggregateLogicalAnd([[1, 1, 0], [1, 0, 0]]), [1, 0, 0]); }); + it('clamps non-binary numbers to 0/1', () => { + assert.deepEqual(aggregateLogicalAnd([[1, 2], [1, 3]]), [1, 1]); + }); + it('rejects non-numeric replies', () => { assert.throws(() => aggregateLogicalAnd([['1']]), /logical AND aggregation/); }); diff --git a/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts b/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts index 55026b10f5f..a3e4681844d 100644 --- a/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts +++ b/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts @@ -25,7 +25,9 @@ export const aggregateLogicalAnd = (replies: Array): T => { for (const reply of replies) { for (let i = 0; i < reply.length; i++) { - result[i] = result[i] && reply[i]; + // clamp to 0/1: `&&` returns the operand, so non-binary replies would + // otherwise leak through (e.g. 1 && 2 === 2). + result[i] = result[i] && reply[i] ? 1 : 0; } } @@ -58,7 +60,9 @@ export const aggregateLogicalOr = (replies: Array): T => { for (const reply of replies) { for (let i = 0; i < reply.length; i++) { - result[i] = result[i] || reply[i]; + // clamp to 0/1: `||` returns the operand, so non-binary replies would + // otherwise leak through (e.g. 0 || 3 === 3). + result[i] = result[i] || reply[i] ? 1 : 0; } } From c2f7a2be8c42dd366b4247761af048b1e9fc0760 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 31 Jul 2026 15:35:53 +0300 Subject: [PATCH 13/17] style(client): terminate policy assignments with semicolons Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/client/lib/cluster/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index a64eabaecc0..9a79e421142 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -548,8 +548,8 @@ export default class RedisCluster< // (see `isReplicaSafe`). const readonly = isReplicaSafe(policy, isReadonly); - const requestPolicy = policy.request - const responsePolicy = policy.response + const requestPolicy = policy.request; + const responsePolicy = policy.response; // Fast path: default-keyed request + response — the overwhelming majority // of traffic (every single-key command). Route by firstKey and pass the From 2a74e5858290caea5a6e942ba50dc91228166989 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 31 Jul 2026 16:55:45 +0300 Subject: [PATCH 14/17] test(search): match 8.x index-not-found wording in cursor retry The FT.CURSOR cluster tests retry the initial aggregate until the freshly created index has propagated to the coordinator, but the retry predicate only matched the 7.x "No such index" wording. On 8.x the server returns "SEARCH_INDEX_NOT_FOUND Index not found", which slipped past the predicate and rethrew on the first racy attempt (intermittent CI failures). Broaden the predicate to match both wordings. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../search/lib/commands/CURSOR_READ.cluster.spec.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/search/lib/commands/CURSOR_READ.cluster.spec.ts b/packages/search/lib/commands/CURSOR_READ.cluster.spec.ts index 537939d749e..e88eccb8b49 100644 --- a/packages/search/lib/commands/CURSOR_READ.cluster.spec.ts +++ b/packages/search/lib/commands/CURSOR_READ.cluster.spec.ts @@ -24,16 +24,20 @@ describe('FT.CURSOR sticky routing (cluster)', () => { // FT.CREATE lands on a single node and its index definition propagates to the // other shards asynchronously; an aggregate that routes to a shard which - // hasn't caught up yet is rejected with "No such index" (flaky on slower - // servers). Retry the aggregate — the cluster client follows MOVED and - // re-picks a node each attempt — until the index is live everywhere. + // hasn't caught up yet rejects the aggregate because the index is not there + // yet (flaky on slower servers). Retry the aggregate — the cluster client + // follows MOVED and re-picks a node each attempt — until the index is live + // everywhere. The "not found" wording differs by server version: 7.x says + // "No such index", 8.x says "SEARCH_INDEX_NOT_FOUND Index not found", so + // match both. + const INDEX_NOT_READY = /no such index|index not found/i; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test helper over the dynamic cluster surface async function openCursor(cluster: any) { for (let attempt = 0; ; attempt++) { try { return await cluster.ft.aggregateWithCursor('idx', '*', { COUNT: 5, LOAD: '@n' }); } catch (err) { - if (attempt >= 100 || !/no such index/i.test(String((err as Error)?.message ?? err))) throw err; + if (attempt >= 100 || !INDEX_NOT_READY.test(String((err as Error)?.message ?? err))) throw err; await setTimeout(50); } } From 86128d2ae490d9d11067934b44d78f057e9353ed Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 31 Jul 2026 22:19:52 +0300 Subject: [PATCH 15/17] fix(client): connect fan-out nodes per-entry so one dead node can't fail the route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit routeAllShards/routeAllNodes awaited getAllMasterClients()/getAllClients(), which Promise.all over lazy node connects — a single failed connect rejected the entire route before any reducer ran, so a one_succeeded command like SCRIPT KILL failed even when another shard was reachable. Return the fan-out target nodes without connecting (getAllMasterNodes/getAllNodes) and defer each node's connect into its own per-entry promise via a getClient thunk, resolved inside the plan's per-entry execution. A connect failure now rejects only that entry, so the reducer (Promise.any for one_succeeded, Promise.all for the all-must-succeed policies) sees the reachable shards. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/client/lib/cluster/cluster-slots.ts | 20 ++++++++++++------- packages/client/lib/cluster/index.ts | 9 +++++++-- .../request-response-policies/dispatch.ts | 8 ++++++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 1099583cded..77cdb344255 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -899,19 +899,25 @@ export default class RedisClusterSlots< * them would silently fan commands out to a subset of the cluster. * Excludes dedicated PubSub connections: they cannot run regular commands. */ - getAllClients(): Promise>> { + /** + * All fan-out target nodes (masters + replicas), WITHOUT connecting. The + * caller connects each node lazily in its own per-node promise so a single + * failed connect rejects only that node's execution — letting reducers such + * as `one_succeeded` still see the reachable shards — instead of a `Promise.all` + * over the connects failing the whole route up front. Excludes the dedicated + * PubSub connection (not in `masters`/`replicas`). + */ + getAllNodes() { this.#assertReady(); - return Promise.all([ - ...this.masters.map(master => this.nodeClient(master)), - ...this.replicas.map(replica => this.nodeClient(replica)) - ]); + return [...this.masters, ...this.replicas]; } - getAllMasterClients(): Promise>> { + /** Master fan-out target nodes, WITHOUT connecting (see {@link getAllNodes}). */ + getAllMasterNodes() { this.#assertReady(); - return Promise.all(this.masters.map(master => this.nodeClient(master))); + return this.masters; } async getClientAndSlotNumber( diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index 9a79e421142..5467381aad0 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -605,10 +605,15 @@ export default class RedisCluster< // `plan[0].client`, and the post-reply hooks must bind cursors to the node // that really served. Multi-target hooks are no-ops, so nothing to track. const served: { client?: RedisClientType } = {}; - const responsePromises = plan.map(entry => { + const responsePromises = plan.map(async entry => { const entryParser = entry.parser ?? parser; // Re-narrow the opaque routed client to this cluster's instantiation. - const client = entry.client as RedisClientType | undefined; + // Fan-out entries carry a `getClient` thunk instead of a resolved client, + // so each node's lazy connect runs in its own promise: a single failed + // connect rejects only this entry (reducers such as one_succeeded still + // see the reachable shards) rather than failing the whole route. + const client = (entry.client ?? (entry.getClient ? await entry.getClient() : undefined)) as + RedisClientType | undefined; return this._execute( entryParser, readonly, execOptions, makeFn(entryParser), client, plan.length === 1 ? served : undefined diff --git a/packages/client/lib/cluster/request-response-policies/dispatch.ts b/packages/client/lib/cluster/request-response-policies/dispatch.ts index 9d885ded989..89e474a2a8e 100644 --- a/packages/client/lib/cluster/request-response-policies/dispatch.ts +++ b/packages/client/lib/cluster/request-response-policies/dispatch.ts @@ -40,6 +40,10 @@ type ClusterSlots = RedisClusterSlots Promise; parser?: CommandParser; groupIndices?: Array; }; @@ -67,10 +71,10 @@ export type ResponseReducer = ( // --- request routers --- export const routeAllNodes: RequestRouter = - async (slots) => (await slots.getAllClients()).map(client => ({ client })); + async (slots) => slots.getAllNodes().map(node => ({ getClient: () => slots.nodeClient(node) })); export const routeAllShards: RequestRouter = - async (slots) => (await slots.getAllMasterClients()).map(client => ({ client })); + async (slots) => slots.getAllMasterNodes().map(node => ({ getClient: () => slots.nodeClient(node) })); /** * Splits the command into one sub-command per hash slot (using the COMMAND key From fa9cdf208147886160714a863b73eaa630a8eb5e Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 31 Jul 2026 22:32:06 +0300 Subject: [PATCH 16/17] fix(client): exclude blocking commands from CSC eligibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A blocking read (XREAD ... BLOCK) can return null on timeout; cached, that reply would be replayed instead of blocking for new data. Eligibility is per-command so the non-blocking form can't be carved out — add a `blocking` flag exclusion to isCacheable. Verified against a live COMMAND reply (447 commands, 130 cacheable): XREAD is the only command this flips. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../client/lib/command-metadata/predicates.spec.ts | 4 ++++ packages/client/lib/command-metadata/predicates.ts | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/command-metadata/predicates.spec.ts b/packages/client/lib/command-metadata/predicates.spec.ts index b92a133e81d..0b9bf4b2bef 100644 --- a/packages/client/lib/command-metadata/predicates.spec.ts +++ b/packages/client/lib/command-metadata/predicates.spec.ts @@ -87,6 +87,10 @@ describe('predicates (override-first)', () => { assert.equal(cacheable(id('eval_ro')), false); }); + it('blocking: not cacheable (XREAD ... BLOCK could cache a timeout reply)', () => { + assert.equal(cacheable(id('xread')), false); + }); + it('unknown command: declared intent or not cacheable', () => { assert.equal(cacheable(id('definitelynotacommand')), false); assert.equal(cacheable(id('definitelynotacommand'), true), true); diff --git a/packages/client/lib/command-metadata/predicates.ts b/packages/client/lib/command-metadata/predicates.ts index b3dcdc86b67..f7b8956cd7e 100644 --- a/packages/client/lib/command-metadata/predicates.ts +++ b/packages/client/lib/command-metadata/predicates.ts @@ -60,7 +60,11 @@ export function isReplicaSafe( * key-tracking based, so keyless read-only commands like KEYS must not cache), * - no `nondeterministic_output` tip (value nondeterminism; `*_output_order` * is fine — HGETALL/SMEMBERS stay cacheable), - * - no `script_runner` flag (EVAL_RO/EVALSHA_RO/FCALL_RO). + * - no `script_runner` flag (EVAL_RO/EVALSHA_RO/FCALL_RO), + * - no `blocking` flag: a blocking read (`XREAD … BLOCK`) can return null on + * timeout, which would be cached and replayed instead of blocking for new + * data; eligibility is per-command, so the non-blocking form can't be + * carved out — the only command this excludes is XREAD. * * Unknown commands with no declared intent are not cacheable. */ @@ -77,5 +81,8 @@ export function isCacheable( && !tips.includes('nondeterministic_output') // `script_runner` (Redis 8.10) marks the EVAL_RO/EVALSHA_RO/FCALL_RO family, // which must not cache. - && !meta.flags.includes('script_runner'); + && !meta.flags.includes('script_runner') + // `blocking` reads (XREAD … BLOCK) could cache a timeout reply and replay + // it instead of blocking for new data. + && !meta.flags.includes('blocking'); } From a28d8ec06da798679c3aaea2692a87469fbca58f Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Fri, 31 Jul 2026 22:34:07 +0300 Subject: [PATCH 17/17] docs(client): drop stale getAllClients docblock left above getAllNodes Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/client/lib/cluster/cluster-slots.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 77cdb344255..5f82fd1189e 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -893,12 +893,6 @@ export default class RedisClusterSlots< } } - /** - * All node clients (masters and replicas), connecting lazily — with - * `minimizeConnections` nodes have no client until first use, and skipping - * them would silently fan commands out to a subset of the cluster. - * Excludes dedicated PubSub connections: they cannot run regular commands. - */ /** * All fan-out target nodes (masters + replicas), WITHOUT connecting. The * caller connects each node lazily in its own per-node promise so a single