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 9e491c9c396..d8644a3d3ad 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 { ClientClosedError, ClientOfflineError, DisconnectsClientError, WatchError } from '../errors'; import { URL } from 'node:url'; import { TcpSocketConnectOpts } from 'node:net'; @@ -25,9 +26,35 @@ import { ClientMetricsHandle, ClientRegistry } from '../opentelemetry'; import { ClientIdentity, ClientRole, generateClientId } from './identity'; import { trace, sanitizeArgs, publish, CHANNELS, type CommandTraceContext } from './tracing'; import { DEFAULT_COMMAND_TIMEOUT } from '../defaults'; +import { FieldsetRegistry, PreparedFieldsets } from '../himport/registry'; +import HIMPORT_DISCARD from '../commands/HIMPORT_DISCARD'; +import HIMPORT_DISCARDALL from '../commands/HIMPORT_DISCARDALL'; +import HIMPORT_PREPARE from '../commands/HIMPORT_PREPARE'; +import HIMPORT_SET from '../commands/HIMPORT_SET'; const noop = () => {}; +const HIMPORT_SESSION_SUBCOMMANDS = new Set(['PREPARE', 'DISCARD', 'DISCARDALL']); + +/** + * MULTI/pipeline stores raw args only, so the HIMPORT transparency hook never sees these + * commands — a PREPARE/DISCARD executed that way would silently diverge the client registry + * from server state (a fieldset the registry doesn't know about, or a discarded one it + * would resurrect via lazy prepare). Rejected client-side at the exec funnel, which covers + * both the typed multi methods and raw `multi.addCommand(...)`. HIMPORT SET stays allowed: + * it mutates no registry state (the fieldset must already exist on the carrying connection). + */ +function assertNoHimportSessionCommands(commands: Array) { + for (const { args } of commands) { + if (String(args[0]).toUpperCase() !== 'HIMPORT') continue; + if (HIMPORT_SESSION_SUBCOMMANDS.has(String(args[1]).toUpperCase())) { + throw new Error( + 'HIMPORT PREPARE/DISCARD/DISCARDALL are not supported inside MULTI/pipeline; call them on the client before the transaction' + ); + } + } +} + export interface RedisClientOptions< M extends RedisModules = RedisModules, F extends RedisFunctions = RedisFunctions, @@ -161,6 +188,14 @@ export interface RedisClientOptions< * ``` */ clientSideCache?: ClientSideCacheProvider | ClientSideCacheConfig; + /** + * @internal + * Shared HIMPORT fieldset registry. Pool/cluster/sentinel construct one instance and inject + * it into every client they own so a fieldset registered on the logical client can be + * transparently re-prepared on any physical connection; `duplicate()` injects the parent's. + * Not a user-facing option — when omitted the client owns a fresh registry. + */ + himportRegistry?: FieldsetRegistry; /** * If set to true, disables sending client identifier (user-agent like message) to the redis server */ @@ -549,6 +584,10 @@ export default class RedisClient< #dirtyWatch?: string; #watchEpoch?: number; #clientSideCache?: ClientSideCacheProvider; + // What the user registered on the logical client (shared across pooled/cluster/sentinel + // clients via the himportRegistry option) vs. what THIS socket's server session holds. + #himportRegistry: FieldsetRegistry; + #preparedFieldsets = new PreparedFieldsets(); #credentialsSubscription: Disposable | null = null; // Flag used to pause writing to the socket during maintenance windows. // When true, prevents new commands from being written while waiting for: @@ -688,6 +727,7 @@ export default class RedisClient< this.#queue = this.#initiateQueue(this.#clientIdentity.id); this.#socket = this.#initiateSocket(this.#clientIdentity.id); + this.#himportRegistry = this.#options.himportRegistry ?? new FieldsetRegistry(); this.#registerForMetrics(); @@ -981,6 +1021,10 @@ export default class RedisClient< .on('error', err => { this.emit('error', err); this.#clientSideCache?.onError(); + // Session state died with the socket. Cleared HERE (not on re-ready): a hImportSet + // issued between disconnect and re-ready would otherwise see a live-looking entry, + // skip the PREPARE injection, and replay bare onto the new socket's empty session. + this.#preparedFieldsets.clear(); if (this.#socket.isOpen && !this.#options.disableOfflineQueue) { this.#queue.flushWaitingForReply(err); } else { @@ -1141,6 +1185,9 @@ export default class RedisClient< >(overrides?: Partial>) { return new (Object.getPrototypeOf(this).constructor)({ ...this._self.#options, + // The options spread only carries the registry if the user originally passed one — + // inject the live instance explicitly: a duplicate SHARES the parent's registrations. + himportRegistry: this._self.#himportRegistry, commandOptions: this._commandOptions, ...overrides }) as RedisClientType<_M, _F, _S, _RESP, _TYPE_MAPPING>; @@ -1175,6 +1222,9 @@ export default class RedisClient< if(this._self.#socket) { this._self._ejectSocket().destroy(); } + // A swapped-in socket carries a fresh server session with no other observable signal + // on this client — the explicit wipe is the only fieldset-invalidation mechanism here. + this._self.#preparedFieldsets.clear(); this._self.#socket = socket; this._self.#attachListeners(this._self.#socket); } @@ -1225,23 +1275,217 @@ export default class RedisClient< commandOptions: CommandOptions | undefined, transformReply: TransformReply | undefined, ) { - const csc = this._self.#clientSideCache; - const defaultTypeMapping = this._self.#options.commandOptions === commandOptions || - (this._self.#options.commandOptions?.typeMapping === commandOptions?.typeMapping); + if ( + command === HIMPORT_SET || command === HIMPORT_PREPARE || + command === HIMPORT_DISCARD || command === HIMPORT_DISCARDALL + ) { + return this._self.#executeHimport(this, command, parser, commandOptions, transformReply); + } + const csc = this._self.#clientSideCache; 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(); + // 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); - const finalReply = transformReply ? transformReply(reply, parser.preserve, commandOptions?.typeMapping) : reply; + // 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); - publish(CHANNELS.COMMAND_REPLY, () => ({ args: sanitizeArgs(parser.redisArgs), reply: finalReply, clientId: this._self._clientId })); + 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; - return finalReply; + publish(CHANNELS.COMMAND_REPLY, () => ({ args: sanitizeArgs(parser.redisArgs), reply: finalReply, clientId: this._self._clientId })); + + return finalReply; + } + + /** + * HIMPORT transparency layer (design: ../himport/registry.ts module doc). Runs instead of + * the normal `_executeCommand` tail for the four HIMPORT commands and keeps this + * connection's server session coherent with the client-level fieldset registry: + * + * 1. Reconcile — replays DISCARDs this session missed (it was serving other commands when + * the user discarded on another connection). + * 2. Lazy prepare — pipelines a PREPARE in front of a SET when this session lacks the + * fieldset or holds a stale field-list version. No await in between: the server + * processes them in arrival order on one socket. + * 3. Registry-based replies — DISCARD/DISCARDALL resolve with the registry mutation result + * (`1`/count of registrations removed), not the per-session server reply, so the answer + * is deterministic across pools and clusters. + * 4. Retry-once — a SET failing with `no such fieldset` for a *registered* fieldset means + * this session lost its state through a path the client did not observe; re-prepare on + * this same connection and retry a single time. + * + * Raw `sendCommand(['HIMPORT', ...])` bypasses this layer by construction. + */ + async #executeHimport( + // The proxy-aware caller (`duplicate`d command-options carriers derive from the client), + // needed for `sendCommand` merging and `_commandOptions`; `this` inside is always _self. + client: RedisClient, + command: Command, + parser: CommandParser, + commandOptions: CommandOptions | undefined, + transformReply: TransformReply | undefined, + retried = false + ): Promise { + const registry = this.#himportRegistry; + const prepared = this.#preparedFieldsets; + const args = parser.redisArgs; + // The injected commands must ride with the main command's effective queue placement: + // same chainId (ASK-redirect chains flush per chain) and same asap-ness (asap unshifts + // to the queue front — a non-asap injection would let an asap SET jump its own PREPARE). + const effectiveAsap = commandOptions?.asap ?? client._commandOptions?.asap; + const effectiveChainId = commandOptions?.chainId ?? client._commandOptions?.chainId; + const injectOpts: CommandOptions = { asap: effectiveAsap, chainId: effectiveChainId }; + + // Commands to reach the wire BEFORE the main command, in this order. Each carries the + // rollback undoing its optimistic bookkeeping if the server rejects it. + const prelude: Array<{ args: Array, rollback: () => void }> = []; + + // -- Reconcile: replay discards this session may still be holding. A DISCARD must + // precede a same-name SET on the wire, or the SET would write through the discarded + // template; hence prelude, not fire-and-forget. + if (prepared.syncedDiscardCount < registry.discardCount) { + const countAtInjection = registry.discardCount; + const pending = registry.pendingDiscards(prepared.names()); + // Every session name is pending → one DISCARDALL wipes the session in a single + // command. Safe precisely because nothing this session holds is worth keeping. + const collapseToDiscardAll = pending.size > 0 && pending.size === prepared.size; + const rollbackVersions = new Map(); + for (const name of pending) { + rollbackVersions.set(name, prepared.get(name)!); + prepared.delete(name); + } + prepared.syncedDiscardCount = countAtInjection; + // Failed injected DISCARD → the fieldset is still alive on this session with no + // client-side trace; restore the trace and knock the synced count back so the next + // HIMPORT command re-reconciles. Guarded so a newer PREPARE is never clobbered. + const rollback = (names: Iterable) => { + for (const name of names) { + const version = rollbackVersions.get(name); + if (version !== undefined && prepared.get(name) === undefined) { + prepared.set(name, version); + } + } + prepared.syncedDiscardCount = Math.min(prepared.syncedDiscardCount, countAtInjection - 1); + }; + if (collapseToDiscardAll) { + prelude.push({ args: ['HIMPORT', 'DISCARDALL'], rollback: () => rollback(pending) }); + } else { + for (const name of pending) { + prelude.push({ args: ['HIMPORT', 'DISCARD', name], rollback: () => rollback([name]) }); + } + } } + + // -- Per-command registry bookkeeping, optimistic (before any reply) so concurrent + // same-tick commands see the final state and don't double-prepare (NF.2). + let userPrepare: { name: string, version: number } | undefined; + let registryReply: number | undefined; + let setName: string | undefined; + + if (command === HIMPORT_PREPARE) { + const name = String(args[2]); + registry.set(name, args.slice(3)); + const version = registry.get(name)!.version; + userPrepare = { name, version }; + prepared.set(name, version); + } else if (command === HIMPORT_SET) { + // args layout: [HIMPORT, SET, key, fieldset, ...values] — index 2 is the (possibly + // keyPrefix-affected) key, index 3 is the fieldset name. + const name = String(args[3]); + setName = name; + const entry = registry.get(name); + if (entry !== undefined) { + const sessionVersion = prepared.get(name); + if (sessionVersion === undefined || sessionVersion < entry.version) { + prepared.set(name, entry.version); + prelude.push({ + args: ['HIMPORT', 'PREPARE', name, ...entry.fields], + // Injected-PREPARE failures are often transient (LOADING, failover) — roll back + // only the session claim, never the registration, or a valid fieldset would + // permanently lose lazy re-prepare. + rollback: () => { + if (prepared.get(name) === entry.version) prepared.delete(name); + } + }); + } + } + // Name absent from the registry → send as-is; the server's `no such fieldset` is + // authoritative and must not be masked. + } else if (command === HIMPORT_DISCARD) { + const name = String(args[2]); + registryReply = registry.discard(name) ? 1 : 0; + prepared.delete(name); + } else { + registryReply = registry.discardAll(); + prepared.clear(); + } + + // -- Enqueue. Everything below runs in one synchronous tick, so the prelude and the + // main command flush to the socket in a single write (the HLD-blessed pipelining). + const send = () => client.sendCommand(parser.redisArgs, commandOptions); + let mainPromise: Promise; + if (effectiveAsap) { + // asap unshifts, so consecutive front-insertions reverse: enqueue the main command + // first, then the prelude back-to-front — the wire sees prelude order, then main. + mainPromise = send(); + for (let i = prelude.length - 1; i >= 0; i--) { + const injection = prelude[i]; + client.sendCommand(injection.args, injectOpts).catch(injection.rollback); + } + } else { + for (const injection of prelude) { + client.sendCommand(injection.args, injectOpts).catch(injection.rollback); + } + mainPromise = send(); + } + + let reply: unknown; + try { + reply = await mainPromise; + } catch (err) { + if (command === HIMPORT_PREPARE && userPrepare !== undefined) { + // A rejected user PREPARE (e.g. duplicate field name) must not leave a registration + // that lazy prepare would replay forever. Version-guarded: a newer successful + // PREPARE must not be clobbered. Deleting through `discard()` also bumps + // discardCount, so sessions still holding an OLDER field list for this name + // reconcile it away instead of silently serving stale SETs. + const { name, version } = userPrepare; + if (prepared.get(name) === version) prepared.delete(name); + if (registry.get(name)?.version === version) registry.discard(name); + } else if ( + command === HIMPORT_SET && setName !== undefined && !retried && + (err as Error)?.message?.includes?.('no such fieldset') && + registry.get(setName) !== undefined + ) { + // Recover-and-retry-once (HLD NF.4): the session claim lied — state was lost through + // a path the client did not observe. Blind retries on another connection would fail + // identically; re-prepare HERE and retry a single time. + prepared.delete(setName); + return this.#executeHimport(client, command, parser, commandOptions, transformReply, true); + } + throw err; + } + + const finalReply = registryReply !== undefined + ? registryReply + : transformReply ? transformReply(reply, parser.preserve, commandOptions?.typeMapping) : reply; + + publish(CHANNELS.COMMAND_REPLY, () => ({ args: sanitizeArgs(parser.redisArgs), reply: finalReply, clientId: this._clientId })); + + return finalReply; } /** @@ -1510,6 +1754,8 @@ export default class RedisClient< commands: Array, selectedDB?: number ) { + assertNoHimportSessionCommands(commands); + if (!this._self.#socket.isOpen) { return Promise.reject(new ClientClosedError()); } @@ -1566,6 +1812,8 @@ export default class RedisClient< commands: Array, selectedDB?: number ) { + assertNoHimportSessionCommands(commands); + const dirtyWatch = this._self.#dirtyWatch; this._self.#dirtyWatch = undefined; const watchEpoch = this._self.#watchEpoch; @@ -1754,6 +2002,12 @@ export default class RedisClient< * Reset the client to its default state (i.e. stop PubSub, stop monitoring, select default DB, etc.) */ async reset() { + // RESET wipes the server session's fieldsets. Cleared at entry, not after the + // round-trip: RESET queues at the #toWrite tail, so a concurrent hImportSet whose hook + // runs before a post-completion clear would see a live entry and queue a bare SET + // BEHIND the RESET — onto the wiped session. With the entry-time clear it re-injects + // PREPARE, which queues behind RESET too, in the correct order. + this._self.#preparedFieldsets.clear(); const chainId = Symbol('Reset Chain'), promises = [this._self.#queue.reset(chainId)], selectedDB = this._self.#options?.database ?? 0; 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/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/client/pool.ts b/packages/client/lib/client/pool.ts index 34942bcca4a..dac53f71d3c 100644 --- a/packages/client/lib/client/pool.ts +++ b/packages/client/lib/client/pool.ts @@ -14,6 +14,7 @@ import SingleEntryCache from '../single-entry-cache'; import { MULTI_MODE, MultiMode } from '../multi-command'; import { publish, CHANNELS } from './tracing'; import { ClientIdentity, ClientRole, generateClientId } from './identity'; +import { FieldsetRegistry } from '../himport/registry'; export interface RedisPoolOptions { /** @@ -324,6 +325,12 @@ export class RedisClientPool< } } + // One fieldset registry for the whole pool: a fieldset registered through any borrowed + // connection must be transparently re-preparable on every other pooled connection. + // Deliberately NOT inherited when the pool is created from an existing client + // (createPool spreads the client's options, which never carry an instance). + clientOptions = { ...clientOptions, himportRegistry: new FieldsetRegistry() }; + // Capture the key prefix for the pool's own parser construction, then strip it from // the pooled clients' options: the pool builds the (already prefixed) parser and hands // it to a pooled client, which never re-parses — so inner clients must not re-prefix. diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index a3d69688162..18c32c58a40 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -10,6 +10,7 @@ import { BasicPooledClientSideCache, PooledClientSideCacheProvider } from '../cl import { SMIGRATED_EVENT, SMigratedEvent, dbgMaintenance } from '../client/enterprise-maintenance-manager'; import { ClientRole } from '../client/identity'; import ClusterReconnectionTracker from './cluster-reconnection-tracker'; +import { FieldsetRegistry } from '../himport/registry'; interface NodeAddress { host: string; @@ -22,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, @@ -120,7 +162,23 @@ export default class RedisClusterSlots< readonly nodeByAddress = new Map | ShardNode>(); pubSubNode?: PubSubNode; clientSideCache?: PooledClientSideCacheProvider; + /** + * One fieldset registry for the whole cluster: HIMPORT PREPARE fans out to all masters + * via the policy layer, and every node client (including MOVED/rediscovered nodes and + * SMIGRATED destinations) must lazily re-prepare from the same registrations. + */ + readonly #himportRegistry = new FieldsetRegistry(); 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; @@ -593,6 +651,7 @@ export default class RedisClusterSlots< let wasReady = false; const client = this.#clientFactory( this.#clientOptionsDefaults({ clientSideCache: this.clientSideCache, + himportRegistry: this.#himportRegistry, RESP: this.#options.RESP, socket, readonly, @@ -818,6 +877,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 @@ -893,6 +973,8 @@ export default class RedisClusterSlots< _randomNodeIterator?: IterableIterator>; getRandomNode() { + this.#assertReady(); + this._randomNodeIterator ??= this.#iterateAllNodes(); return this._randomNodeIterator.next().value as ShardNode; } @@ -931,6 +1013,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/index.spec.ts b/packages/client/lib/cluster/index.spec.ts index 6de2a8a623e..cb7775ba844 100644 --- a/packages/client/lib/cluster/index.spec.ts +++ b/packages/client/lib/cluster/index.spec.ts @@ -314,8 +314,65 @@ 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); describe('minimizeConnections', () => { diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index 43e82153847..b027ed6aecf 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -10,12 +10,17 @@ 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 { 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 | @@ -29,7 +34,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, @@ -187,11 +191,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) ); }; } @@ -203,11 +207,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) ); }; } @@ -221,11 +225,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) ); }; } @@ -239,11 +243,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) ); }; } @@ -319,6 +323,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`. @@ -399,6 +404,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< @@ -492,83 +501,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( @@ -584,11 +763,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.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/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/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-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/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/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'; + 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/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; +} 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/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 }; +} 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/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/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/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.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/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.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/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/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.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/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/HIMPORT_DISCARD.spec.ts b/packages/client/lib/commands/HIMPORT_DISCARD.spec.ts new file mode 100644 index 00000000000..a71145a163e --- /dev/null +++ b/packages/client/lib/commands/HIMPORT_DISCARD.spec.ts @@ -0,0 +1,36 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import HIMPORT_DISCARD from './HIMPORT_DISCARD'; +import { parseArgs } from './generic-transformers'; + +describe('HIMPORT DISCARD', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(HIMPORT_DISCARD, 'fieldset'), + ['HIMPORT', 'DISCARD', 'fieldset'] + ); + }); + }); + + describe('behavior', () => { + testUtils.isVersionGreaterThanHook([8, 10]); + + testUtils.testAll('hImportDiscard', async client => { + await client.hImportPrepare('fieldset', ['f1', 'f2']); + + assert.equal( + await client.hImportDiscard('fieldset'), + 1 + ); + + assert.equal( + await client.hImportDiscard('fieldset'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + }); +}); diff --git a/packages/client/lib/commands/HIMPORT_DISCARD.ts b/packages/client/lib/commands/HIMPORT_DISCARD.ts new file mode 100644 index 00000000000..3559c9e9e37 --- /dev/null +++ b/packages/client/lib/commands/HIMPORT_DISCARD.ts @@ -0,0 +1,9 @@ +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; + +export default { + parseCommand(parser: CommandParser, fieldset: string) { + parser.push('HIMPORT', 'DISCARD', fieldset); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HIMPORT_DISCARDALL.spec.ts b/packages/client/lib/commands/HIMPORT_DISCARDALL.spec.ts new file mode 100644 index 00000000000..56d9455a0f5 --- /dev/null +++ b/packages/client/lib/commands/HIMPORT_DISCARDALL.spec.ts @@ -0,0 +1,36 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import HIMPORT_DISCARDALL from './HIMPORT_DISCARDALL'; +import { parseArgs } from './generic-transformers'; + +describe('HIMPORT DISCARDALL', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(HIMPORT_DISCARDALL), + ['HIMPORT', 'DISCARDALL'] + ); + }); + }); + + describe('behavior', () => { + testUtils.isVersionGreaterThanHook([8, 10]); + + testUtils.testAll('hImportDiscardAll', async client => { + assert.equal( + await client.hImportDiscardAll(), + 0 + ); + + await client.hImportPrepare('fieldset', ['f1', 'f2']); + + assert.equal( + await client.hImportDiscardAll(), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + }); +}); diff --git a/packages/client/lib/commands/HIMPORT_DISCARDALL.ts b/packages/client/lib/commands/HIMPORT_DISCARDALL.ts new file mode 100644 index 00000000000..fa658a57368 --- /dev/null +++ b/packages/client/lib/commands/HIMPORT_DISCARDALL.ts @@ -0,0 +1,9 @@ +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; + +export default { + parseCommand(parser: CommandParser) { + parser.push('HIMPORT', 'DISCARDALL'); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HIMPORT_PREPARE.spec.ts b/packages/client/lib/commands/HIMPORT_PREPARE.spec.ts new file mode 100644 index 00000000000..1d19143b191 --- /dev/null +++ b/packages/client/lib/commands/HIMPORT_PREPARE.spec.ts @@ -0,0 +1,50 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import HIMPORT_PREPARE from './HIMPORT_PREPARE'; +import { parseArgs } from './generic-transformers'; + +describe('HIMPORT PREPARE', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(HIMPORT_PREPARE, 'fieldset', 'field'), + ['HIMPORT', 'PREPARE', 'fieldset', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + parseArgs(HIMPORT_PREPARE, 'fieldset', ['f1', 'f2']), + ['HIMPORT', 'PREPARE', 'fieldset', 'f1', 'f2'] + ); + }); + + it('preserves caller field order', () => { + assert.deepEqual( + parseArgs(HIMPORT_PREPARE, 'fieldset', ['c', 'a', 'b']), + ['HIMPORT', 'PREPARE', 'fieldset', 'c', 'a', 'b'] + ); + }); + }); + + describe('behavior', () => { + testUtils.isVersionGreaterThanHook([8, 10]); + + testUtils.testAll('hImportPrepare', async client => { + assert.equal( + await client.hImportPrepare('fieldset', ['f1', 'f2']), + 'OK' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + testUtils.testWithClient('rejects duplicate field names', async client => { + await assert.rejects( + client.hImportPrepare('fieldset', ['f1', 'f1']), + /duplicate field name/ + ); + }, GLOBAL.SERVERS.OPEN); + }); +}); diff --git a/packages/client/lib/commands/HIMPORT_PREPARE.ts b/packages/client/lib/commands/HIMPORT_PREPARE.ts new file mode 100644 index 00000000000..babfefd584f --- /dev/null +++ b/packages/client/lib/commands/HIMPORT_PREPARE.ts @@ -0,0 +1,11 @@ +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + parseCommand(parser: CommandParser, fieldset: string, fields: RedisVariadicArgument) { + parser.push('HIMPORT', 'PREPARE', fieldset); + parser.pushVariadic(fields); + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/HIMPORT_SET.spec.ts b/packages/client/lib/commands/HIMPORT_SET.spec.ts new file mode 100644 index 00000000000..20b5d7d5dd6 --- /dev/null +++ b/packages/client/lib/commands/HIMPORT_SET.spec.ts @@ -0,0 +1,63 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import HIMPORT_SET from './HIMPORT_SET'; +import { parseArgs } from './generic-transformers'; + +describe('HIMPORT SET', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + parseArgs(HIMPORT_SET, 'key', 'fieldset', 'value'), + ['HIMPORT', 'SET', 'key', 'fieldset', 'value'] + ); + }); + + it('array', () => { + assert.deepEqual( + parseArgs(HIMPORT_SET, 'key', 'fieldset', ['v1', 'v2']), + ['HIMPORT', 'SET', 'key', 'fieldset', 'v1', 'v2'] + ); + }); + }); + + describe('behavior', () => { + testUtils.isVersionGreaterThanHook([8, 10]); + + testUtils.testAll('hImportSet roundtrip', async client => { + await client.hImportPrepare('fieldset', ['f1', 'f2']); + + assert.equal( + await client.hImportSet('key', 'fieldset', ['v1', 'v2']), + 'OK' + ); + + // enumeration order is canonicalized server-side — assert content, not order + assert.deepEqual( + await client.hGetAll('key'), + { f1: 'v1', f2: 'v2' } + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + testUtils.testWithClient('rejects on value count mismatch', async client => { + await client.hImportPrepare('fieldset', ['f1', 'f2']); + + await assert.rejects( + client.hImportSet('key', 'fieldset', ['v1']), + /value count/ + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('rejects with WRONGTYPE on non-hash key', async client => { + await client.set('key', 'string'); + await client.hImportPrepare('fieldset', ['f1']); + + await assert.rejects( + client.hImportSet('key', 'fieldset', ['v1']), + /WRONGTYPE/ + ); + }, GLOBAL.SERVERS.OPEN); + }); +}); diff --git a/packages/client/lib/commands/HIMPORT_SET.ts b/packages/client/lib/commands/HIMPORT_SET.ts new file mode 100644 index 00000000000..b0c37eae62a --- /dev/null +++ b/packages/client/lib/commands/HIMPORT_SET.ts @@ -0,0 +1,13 @@ +import { CommandParser } from '../client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +export default { + parseCommand(parser: CommandParser, key: RedisArgument, fieldset: string, values: RedisVariadicArgument) { + parser.push('HIMPORT', 'SET'); + parser.pushKey(key); + parser.push(fieldset); + parser.pushVariadic(values); + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; 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/MSETEX.spec.ts b/packages/client/lib/commands/MSETEX.spec.ts index 20eea82136f..4892b423bc2 100644 --- a/packages/client/lib/commands/MSETEX.spec.ts +++ b/packages/client/lib/commands/MSETEX.spec.ts @@ -271,6 +271,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) — it routes to the first key's + // node like MSETNX, so the server rejects cross-slot key sets itself. + testUtils.testWithCluster( + "mSetEx cross-slot keys are rejected by the server", + async (cluster) => { + await assert.rejects( + cluster.mSetEx(["a", "value1", "b", "value2"]), + /CROSSSLOT/ + ); + }, + { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] } + ); + testUtils.testAll( "mSetEx with NX", async (client) => { 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/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/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 }; } diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 3ae4f4485ff..0ca0efb92c0 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -162,6 +162,10 @@ import HGET from './HGET'; import HGETALL from './HGETALL'; import HGETDEL from './HGETDEL'; import HGETEX from './HGETEX'; +import HIMPORT_DISCARD from './HIMPORT_DISCARD'; +import HIMPORT_DISCARDALL from './HIMPORT_DISCARDALL'; +import HIMPORT_PREPARE from './HIMPORT_PREPARE'; +import HIMPORT_SET from './HIMPORT_SET'; import HINCRBY from './HINCRBY'; import HINCRBYFLOAT from './HINCRBYFLOAT'; import HKEYS from './HKEYS'; @@ -2347,6 +2351,116 @@ export default { * @param options - Options for setting expiration */ hGetEx: HGETEX, + /** + * + * @experimental + * + * Removes a fieldset registration from the client and discards it from the server + * connection session. The reply is registry-based: `1` if the fieldset was registered on + * this client and removed, `0` otherwise. + * @param fieldset - Name of the fieldset to discard + * @see https://redis.io/commands/himport-discard/ + * @since 8.10 + */ + HIMPORT_DISCARD, + /** + * + * @experimental + * + * Removes a fieldset registration from the client and discards it from the server + * connection session. The reply is registry-based: `1` if the fieldset was registered on + * this client and removed, `0` otherwise. + * @param fieldset - Name of the fieldset to discard + * @see https://redis.io/commands/himport-discard/ + * @since 8.10 + */ + hImportDiscard: HIMPORT_DISCARD, + /** + * + * @experimental + * + * Removes all fieldset registrations from the client and discards them from the server + * connection session. The reply is registry-based: the number of fieldsets that were + * registered on this client and removed. + * @see https://redis.io/commands/himport-discardall/ + * @since 8.10 + */ + HIMPORT_DISCARDALL, + /** + * + * @experimental + * + * Removes all fieldset registrations from the client and discards them from the server + * connection session. The reply is registry-based: the number of fieldsets that were + * registered on this client and removed. + * @see https://redis.io/commands/himport-discardall/ + * @since 8.10 + */ + hImportDiscardAll: HIMPORT_DISCARDALL, + /** + * + * @experimental + * + * Registers an ordered list of field names under a fieldset name for use by subsequent + * HIMPORT SET calls. The client keeps the registration and transparently re-prepares + * connections as needed (reconnects, pool growth, cluster topology changes). Raw + * `sendCommand(['HIMPORT', ...])` and raw `sendCommand(['RESET'])` bypass this managed + * layer. `duplicate()` shares the parent's registrations; `createPool()` does not. + * Inside MULTI, PREPARE/DISCARD/DISCARDALL are rejected client-side; HIMPORT SET is + * allowed if the fieldset was prepared on that connection beforehand. + * @param fieldset - Name to register the field list under + * @param fields - One or more hash field names; values in later HIMPORT SET calls map + * positionally to this order + * @see https://redis.io/commands/himport-prepare/ + * @since 8.10 + */ + HIMPORT_PREPARE, + /** + * + * @experimental + * + * Registers an ordered list of field names under a fieldset name for use by subsequent + * HIMPORT SET calls. The client keeps the registration and transparently re-prepares + * connections as needed (reconnects, pool growth, cluster topology changes). Raw + * `sendCommand(['HIMPORT', ...])` and raw `sendCommand(['RESET'])` bypass this managed + * layer. `duplicate()` shares the parent's registrations; `createPool()` does not. + * Inside MULTI, PREPARE/DISCARD/DISCARDALL are rejected client-side; HIMPORT SET is + * allowed if the fieldset was prepared on that connection beforehand. + * @param fieldset - Name to register the field list under + * @param fields - One or more hash field names; values in later HIMPORT SET calls map + * positionally to this order + * @see https://redis.io/commands/himport-prepare/ + * @since 8.10 + */ + hImportPrepare: HIMPORT_PREPARE, + /** + * + * @experimental + * + * Creates or fully replaces the hash at key using the field list of a prepared fieldset. + * Values map positionally to the fields given to HIMPORT PREPARE for that fieldset. Hash + * field enumeration order is not guaranteed to match the prepare order. + * @param key - Hash key to create or replace + * @param fieldset - Name of a fieldset registered with HIMPORT PREPARE + * @param values - Values, one per prepared field, in the prepared order + * @see https://redis.io/commands/himport-set/ + * @since 8.10 + */ + HIMPORT_SET, + /** + * + * @experimental + * + * Creates or fully replaces the hash at key using the field list of a prepared fieldset. + * Values map positionally to the fields given to HIMPORT PREPARE for that fieldset. Hash + * field enumeration order is not guaranteed to match the prepare order. + * @param key - Hash key to create or replace + * @param fieldset - Name of a fieldset registered with HIMPORT PREPARE + * @param values - Values, one per prepared field, in the prepared order + * @see https://redis.io/commands/himport-set/ + * @since 8.10 + */ + hImportSet: HIMPORT_SET, /** * Increments the integer value of a field in a hash by the given number * @param key - Key of the hash diff --git a/packages/client/lib/himport/registry.spec.ts b/packages/client/lib/himport/registry.spec.ts new file mode 100644 index 00000000000..eafce9f1fe8 --- /dev/null +++ b/packages/client/lib/himport/registry.spec.ts @@ -0,0 +1,178 @@ +import { strict as assert } from 'node:assert'; +import { FieldsetRegistry, PreparedFieldsets } from './registry'; + +describe('FieldsetRegistry', () => { + describe('set', () => { + it('is idempotent: same fields keep the same version', () => { + const registry = new FieldsetRegistry(); + registry.set('fs', ['a', 'b']); + const version = registry.get('fs')!.version; + + registry.set('fs', ['a', 'b']); + assert.equal(registry.get('fs')!.version, version); + }); + + it('bumps the version on a changed field list', () => { + const registry = new FieldsetRegistry(); + registry.set('fs', ['a', 'b']); + const version = registry.get('fs')!.version; + + registry.set('fs', ['a', 'c']); + assert.ok(registry.get('fs')!.version > version); + }); + + it('field order matters — reordered fields are a new version', () => { + const registry = new FieldsetRegistry(); + registry.set('fs', ['a', 'b']); + const version = registry.get('fs')!.version; + + registry.set('fs', ['b', 'a']); + assert.ok(registry.get('fs')!.version > version); + }); + + it('versions stay monotonic across discard + re-prepare', () => { + const registry = new FieldsetRegistry(); + registry.set('fs', ['a', 'b']); + const version = registry.get('fs')!.version; + + registry.discard('fs'); + registry.set('fs', ['a', 'b']); + assert.ok(registry.get('fs')!.version > version); + }); + + it('compares fields byte-wise: Buffer and string with identical bytes are equal', () => { + const registry = new FieldsetRegistry(); + registry.set('fs', ['a', Buffer.from('b')]); + const version = registry.get('fs')!.version; + + registry.set('fs', [Buffer.from('a'), 'b']); + assert.equal(registry.get('fs')!.version, version); + }); + + it('stores a copy — mutating the caller array does not affect the registry', () => { + const registry = new FieldsetRegistry(); + const fields = ['a', 'b']; + registry.set('fs', fields); + + fields[1] = 'mutated'; + assert.deepEqual(registry.get('fs')!.fields, ['a', 'b']); + }); + + it('accepts empty string as a fieldset name', () => { + const registry = new FieldsetRegistry(); + registry.set('', ['a']); + assert.ok(registry.get('') !== undefined); + }); + }); + + describe('discard', () => { + it('returns true and bumps discardCount for a registered name', () => { + const registry = new FieldsetRegistry(); + registry.set('fs', ['a']); + + assert.equal(registry.discard('fs'), true); + assert.equal(registry.discardCount, 1); + assert.equal(registry.get('fs'), undefined); + }); + + it('no-op discard returns false and does not bump discardCount', () => { + const registry = new FieldsetRegistry(); + + assert.equal(registry.discard('missing'), false); + assert.equal(registry.discardCount, 0); + }); + + it('repeated discards bump discardCount once', () => { + const registry = new FieldsetRegistry(); + registry.set('fs', ['a']); + + registry.discard('fs'); + registry.discard('fs'); + registry.discard('fs'); + assert.equal(registry.discardCount, 1); + }); + }); + + describe('discardAll', () => { + it('returns the number of removed registrations with a single discardCount bump', () => { + const registry = new FieldsetRegistry(); + registry.set('fs1', ['a']); + registry.set('fs2', ['b']); + + assert.equal(registry.discardAll(), 2); + assert.equal(registry.discardCount, 1); + assert.equal(registry.get('fs1'), undefined); + }); + + it('no-op on an empty registry: returns 0, no bump', () => { + const registry = new FieldsetRegistry(); + + assert.equal(registry.discardAll(), 0); + assert.equal(registry.discardCount, 0); + }); + }); + + describe('pendingDiscards', () => { + it('returns exactly the session-only names', () => { + const registry = new FieldsetRegistry(); + registry.set('kept', ['a']); + registry.set('discarded', ['b']); + registry.discard('discarded'); + + assert.deepEqual( + registry.pendingDiscards(new Set(['kept', 'discarded'])), + new Set(['discarded']) + ); + }); + + it('a discarded and re-registered name is not pending', () => { + const registry = new FieldsetRegistry(); + registry.set('fs', ['a']); + registry.discard('fs'); + registry.set('fs', ['b']); + + assert.deepEqual( + registry.pendingDiscards(new Set(['fs'])), + new Set() + ); + }); + }); +}); + +describe('PreparedFieldsets', () => { + it('tracks name → version', () => { + const prepared = new PreparedFieldsets(); + prepared.set('fs', 3); + + assert.equal(prepared.get('fs'), 3); + assert.equal(prepared.get('missing'), undefined); + }); + + it('names() returns a snapshot of tracked names', () => { + const prepared = new PreparedFieldsets(); + prepared.set('fs1', 1); + prepared.set('fs2', 2); + + assert.deepEqual(prepared.names(), new Set(['fs1', 'fs2'])); + }); + + it('clear() wipes entries but keeps syncedDiscardCount', () => { + const prepared = new PreparedFieldsets(); + prepared.set('fs', 1); + prepared.syncedDiscardCount = 5; + + prepared.clear(); + assert.equal(prepared.size, 0); + assert.equal(prepared.syncedDiscardCount, 5); + }); + + it('delete() removes a single entry', () => { + const prepared = new PreparedFieldsets(); + prepared.set('fs1', 1); + prepared.set('fs2', 2); + + assert.equal(prepared.delete('fs1'), true); + assert.equal(prepared.delete('fs1'), false); + assert.deepEqual(prepared.names(), new Set(['fs2'])); + }); +}); diff --git a/packages/client/lib/himport/registry.ts b/packages/client/lib/himport/registry.ts new file mode 100644 index 00000000000..f2154c7ffcb --- /dev/null +++ b/packages/client/lib/himport/registry.ts @@ -0,0 +1,198 @@ +import { RedisArgument } from '../RESP/types'; + +/** + * Client-side bookkeeping for the HIMPORT command family (hinted hash templates, Redis 8.10). + * + * Server-side fieldsets are session state attached to one physical connection: they die on + * disconnect and RESET, and no other connection can see them. The client therefore keeps two + * layers of state: + * + * - {@link FieldsetRegistry} — one per logical client (standalone client, pool, cluster, + * sentinel), shared by every `RedisClient` the logical client owns. It records what the + * user *registered* (`hImportPrepare`) and is the replay source for transparently + * re-preparing connections. + * - {@link PreparedFieldsets} — one per `RedisClient`, never shared. It mirrors what that + * client's *current socket session* actually holds, so the lazy-prepare hook can decide + * whether an `HIMPORT SET` needs a `PREPARE` pipelined in front of it. + * + * TODO(himport-multi) — auto-prepare inside MULTI/pipeline. The multi queue stores raw args + * only (`multi-command.ts`) — no `Command` identity survives. Recipe when picked up: in + * `_executeMulti` sniff HIMPORT SET raw args (Buffer-tolerant compare of args[0]/args[1]) and + * inject PREPAREs under the same `chainId` BEFORE the `['MULTI']` (fieldsets are + * session-level — the PREPARE need not be inside the transaction); keep injected promises out + * of the positional result mapping (`transformReplies` uses only the last reply); same + * treatment in `_executePipeline`. Needs aborted-EXEC registry bookkeeping thought through. + * Until then: inside MULTI the fieldset must already exist on that connection (prepare + * beforehand outside the MULTI) — documented. The session-subcommand reject guard already + * scans queued commands at the same funnel; when this TODO is picked up, revisit whether that + * reject should become a registry-mirroring sniff instead. + */ + +export interface Fieldset { + /** + * The field names exactly as the caller passed them to `hImportPrepare` — never deduped, + * sorted, or reordered (the server pairs SET values to this order positionally). Holds a + * sliced copy, never an alias of the caller's array; mutating a Buffer's *contents* in + * place still aliases — conventional Node.js behavior. + */ + fields: Array; + /** + * Staleness token from the registry-wide monotonic counter. A connection whose + * `PreparedFieldsets` entry holds a lower number must re-PREPARE before its next SET. + */ + version: number; +} + +/** + * What the user registered on the logical client: fieldset name → ordered field list. + * Single source of truth for lazy re-prepare; also the source of the registry-based + * DISCARD/DISCARDALL replies. Mutated only by the per-connection command hook — safe because + * `set` is idempotent (cluster fan-out runs the hook once per master against this same + * instance) and `discard`/`discardAll` are effective-only. + */ +export class FieldsetRegistry { + /** + * Fieldset name → registration. Map keys are `String()`-coerced names (a Buffer key would + * be identity-compared by `Map` and poison lookups); values hold sliced copies, never + * aliases of caller arrays. + */ + #fieldsets = new Map(); + + /** + * Source of `Fieldset.version`. Registry-wide and never reset — a per-entry counter + * restarting at 0 after discard + re-prepare would make a stale connection entry compare + * as fresh, skip the re-PREPARE, and silently bind values against the old field list. + */ + #versionCounter = 0; + + /** + * Counts EFFECTIVE discards only (a discard of an unknown name does not bump), so no-op + * discards never force fleet-wide reconciles and a cluster fan-out of one user DISCARD + * bumps exactly once (the first hook run deletes, the rest early-return). Connections + * compare their `syncedDiscardCount` snapshot against this to detect pending discards. + */ + discardCount = 0; + + /** + * Idempotent upsert: registering the same name with a deep-equal field list keeps the + * existing version (cluster fan-out and per-worker startup prepares must not churn + * versions); a new name or a changed field list gets the next counter value. + */ + set(name: string, fields: Array): void { + const key = String(name); + const existing = this.#fieldsets.get(key); + if (existing !== undefined && fieldsEqual(existing.fields, fields)) return; + this.#fieldsets.set(key, { + fields: fields.slice(), + version: ++this.#versionCounter + }); + } + + get(name: string): Fieldset | undefined { + return this.#fieldsets.get(String(name)); + } + + /** + * Returns `true` iff the name was registered — this boolean IS the user-facing + * `hImportDiscard` reply (registry-based, not the server session's). + */ + discard(name: string): boolean { + const removed = this.#fieldsets.delete(String(name)); + if (removed) this.discardCount++; + return removed; + } + + /** + * Returns the number of registrations removed — this count IS the user-facing + * `hImportDiscardAll` reply. + */ + discardAll(): number { + const removed = this.#fieldsets.size; + if (removed > 0) { + this.#fieldsets.clear(); + this.discardCount++; + } + return removed; + } + + /** + * Names a connection's session still holds but the registry no longer does — i.e. the + * DISCARDs that connection must replay to catch up. Names that were discarded and + * re-registered are NOT returned: the version-gated lazy PREPARE (a silent server-side + * replace) covers them without a DISCARD. + */ + pendingDiscards(sessionNames: Set): Set { + const pending = new Set(); + for (const name of sessionNames) { + if (!this.#fieldsets.has(name)) pending.add(name); + } + return pending; + } +} + +/** + * What one `RedisClient`'s current socket session holds. Every entry is a CLAIM about + * server-side session state, not client state: it is tied to the socket's lifetime, so the + * client wipes this whole object on socket error, `reset()`, and socket replacement. A claim + * that survives an unenumerated loss path lies — the `no such fieldset` recover-and-retry + * net heals that at the cost of one extra round trip. + */ +export class PreparedFieldsets { + /** + * Fieldset name → the {@link Fieldset.version} this session was prepared with. A missing + * entry or a lower version means the next dependent command must pipeline a PREPARE first. + */ + #versions = new Map(); + + /** + * Snapshot of `FieldsetRegistry.discardCount` this connection has reconciled up to. + * `syncedDiscardCount < registry.discardCount` means discards happened that this session + * may still hold — reconcile before the next HIMPORT command. Knocked back when an + * injected DISCARD fails, so the reconcile re-runs. + */ + syncedDiscardCount = 0; + + get(name: string): number | undefined { + return this.#versions.get(name); + } + + set(name: string, version: number): void { + this.#versions.set(name, version); + } + + delete(name: string): boolean { + return this.#versions.delete(name); + } + + names(): Set { + return new Set(this.#versions.keys()); + } + + get size(): number { + return this.#versions.size; + } + + clear(): void { + this.#versions.clear(); + } +} + +/** + * Element-wise, byte-level equality: a `Buffer` and a `string` with identical bytes are + * equal. Byte-level matters because the idempotency decision must match what the wire would + * carry, not the JS representation. + */ +function fieldsEqual(a: Array, b: Array): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + const x = a[i], y = b[i]; + if (typeof x === 'string' && typeof y === 'string') { + if (x !== y) return false; + } else { + const xb = typeof x === 'string' ? Buffer.from(x) : x; + const yb = typeof y === 'string' ? Buffer.from(y) : y; + if (!xb.equals(yb)) return false; + } + } + return true; +} diff --git a/packages/client/lib/himport/transparency.spec.ts b/packages/client/lib/himport/transparency.spec.ts new file mode 100644 index 00000000000..2ea9a8138b8 --- /dev/null +++ b/packages/client/lib/himport/transparency.spec.ts @@ -0,0 +1,209 @@ +import { strict as assert } from 'node:assert'; +import { once } from 'node:events'; +import testUtils, { GLOBAL } from '../test-utils'; + +/** + * Integration tests for the HIMPORT transparency layer (`#executeHimport` + + * `FieldsetRegistry`/`PreparedFieldsets`): lazy prepare, lazy discard reconcile, + * registry-based replies, and the recover-and-retry-once net. + */ +describe('HIMPORT transparency layer', () => { + testUtils.isVersionGreaterThanHook([8, 10]); + + // Force a disconnect and wait for the socket error — the next command reconnects. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async function killClient(client: any): Promise { + const onceErrorPromise = once(client, 'error'); + await client.sendCommand(['QUIT']); + await Promise.all([ + onceErrorPromise, + assert.rejects(client.ping()) + ]); + } + + testUtils.testWithClient('lazily re-prepares after a reconnect', async client => { + await client.hImportPrepare('fs', ['f1', 'f2']); + await killClient(client); + + // The new session has no fieldsets; the hook must pipeline a PREPARE in front. + assert.equal(await client.hImportSet('key', 'fs', ['v1', 'v2']), 'OK'); + assert.deepEqual(await client.hGetAll('key'), { f1: 'v1', f2: 'v2' }); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('re-prepares when the field list changes (version staleness)', async client => { + await client.hImportPrepare('fs', ['old1', 'old2']); + await client.hImportSet('key1', 'fs', ['v1', 'v2']); + + await client.hImportPrepare('fs', ['new1', 'new2']); + await client.hImportSet('key2', 'fs', ['v1', 'v2']); + + assert.deepEqual(await client.hGetAll('key2'), { new1: 'v1', new2: 'v2' }); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('lazily re-prepares after a typed reset()', async client => { + await client.hImportPrepare('fs', ['f1']); + await client.reset(); + + assert.equal(await client.hImportSet('key', 'fs', ['v1']), 'OK'); + assert.deepEqual(await client.hGetAll('key'), { f1: 'v1' }); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('discard stops lazy prepare; the server error propagates', async client => { + await client.hImportPrepare('fs', ['f1']); + assert.equal(await client.hImportDiscard('fs'), 1); + + await assert.rejects( + client.hImportSet('key', 'fs', ['v1']), + /no such fieldset/ + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('asap SET reaches the wire behind its injected PREPARE', async client => { + await client.hImportPrepare('fs', ['f1']); + // Fresh socket, empty session — the injection must unshift IN FRONT of the asap SET; + // same-order enqueue would put the SET first and fail with `no such fieldset`. + await killClient(client); + + assert.equal(await client.asap().hImportSet('key', 'fs', ['v1']), 'OK'); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('rejected user PREPARE removes the registration', async client => { + await assert.rejects( + client.hImportPrepare('fs', ['dup', 'dup']), + /duplicate field name/ + ); + + // No lazy-prepare retry loop: the registration is gone, the server error propagates. + await assert.rejects( + client.hImportSet('key', 'fs', ['v1', 'v2']), + /no such fieldset/ + ); + + // A corrected PREPARE registers fresh. + await client.hImportPrepare('fs', ['f1', 'f2']); + assert.equal(await client.hImportSet('key', 'fs', ['v1', 'v2']), 'OK'); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('retries once when the session lost its state unobserved', async client => { + await client.hImportPrepare('fs', ['f1']); + await client.hImportSet('key1', 'fs', ['v1']); + + // Raw RESET bypasses the typed reset() wipe: the server session is gone but the + // client still claims the fieldset is prepared — exactly the lie the + // recover-and-retry-once net exists to absorb. + await client.sendCommand(['RESET']); + + assert.equal(await client.hImportSet('key2', 'fs', ['v1']), 'OK'); + // hGet, not hGetAll: raw RESET also reverted the connection protocol to RESP2, so + // map-shaped replies would decode flat; a bulk string is identical in both protocols. + assert.equal(await client.hGet('key2', 'f1'), 'v1'); + }, GLOBAL.SERVERS.OPEN); + + describe('MULTI/pipeline guard', () => { + testUtils.testWithClient('rejects session subcommands inside MULTI', async client => { + for (const build of [ + () => client.multi().hImportPrepare('fs', ['f1']), + () => client.multi().hImportDiscard('fs'), + () => client.multi().hImportDiscardAll(), + () => client.multi().addCommand(['HIMPORT', 'PREPARE', 'fs', 'f1']) + ]) { + await assert.rejects( + build().exec(), + /not supported inside MULTI\/pipeline/ + ); + } + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('allows hImportSet inside MULTI after an out-of-band prepare', async client => { + await client.hImportPrepare('fs', ['f1']); + // Same connection: prepare above already prepared this session. + const replies = await client.multi() + .hImportSet('key', 'fs', ['v1']) + .exec(); + + assert.deepEqual(replies, ['OK']); + assert.deepEqual(await client.hGetAll('key'), { f1: 'v1' }); + }, GLOBAL.SERVERS.OPEN); + }); + + describe('pool', () => { + testUtils.testWithClientPool('each borrowed connection lazily self-prepares', async pool => { + await pool.hImportPrepare('fs', ['f1', 'f2']); + + // Concurrent SETs spread across pooled connections; each one must transparently + // prepare its own session on first use. + const keys = ['key1', 'key2', 'key3', 'key4', 'key5', 'key6']; + const replies = await Promise.all( + keys.map(key => pool.hImportSet(key, 'fs', ['v1', 'v2'])) + ); + assert.deepEqual(replies, keys.map(() => 'OK')); + + assert.deepEqual(await pool.hGetAll('key6'), { f1: 'v1', f2: 'v2' }); + }, { + ...GLOBAL.SERVERS.OPEN, + poolOptions: { minimum: 3 } + }); + + testUtils.testWithClientPool('re-prepare with changed fields reaches every connection', async pool => { + await pool.hImportPrepare('fs', ['old1', 'old2']); + await Promise.all( + ['a1', 'a2', 'a3', 'a4'].map(key => pool.hImportSet(key, 'fs', ['v1', 'v2'])) + ); + + await pool.hImportPrepare('fs', ['new1', 'new2']); + const keys = ['b1', 'b2', 'b3', 'b4', 'b5', 'b6']; + await Promise.all(keys.map(key => pool.hImportSet(key, 'fs', ['v1', 'v2']))); + + for (const key of keys) { + assert.deepEqual(await pool.hGetAll(key), { new1: 'v1', new2: 'v2' }); + } + }, { + ...GLOBAL.SERVERS.OPEN, + poolOptions: { minimum: 3 } + }); + + testUtils.testWithClientPool('discard is coherent across connections (lazy reconcile)', async pool => { + await pool.hImportPrepare('fs', ['f1', 'f2']); + // Spread lazy prepares across connections so several sessions hold the fieldset. + await Promise.all( + ['a1', 'a2', 'a3', 'a4', 'a5', 'a6'].map(key => pool.hImportSet(key, 'fs', ['v1', 'v2'])) + ); + + // Registry-based reply: 1 regardless of which connection the DISCARD borrows. + assert.equal(await pool.hImportDiscard('fs'), 1); + assert.equal(await pool.hImportDiscard('fs'), 0); + + // No stale-SET success on ANY connection: sessions still holding the fieldset must + // reconcile the discard before the SET reaches the wire. + for (const key of ['c1', 'c2', 'c3', 'c4', 'c5', 'c6']) { + await assert.rejects( + pool.hImportSet(key, 'fs', ['v1', 'v2']), + /no such fieldset/ + ); + } + }, { + ...GLOBAL.SERVERS.OPEN, + poolOptions: { minimum: 3 } + }); + }); + + describe('cluster', () => { + testUtils.testWithCluster('prepare fans out; SETs route by slot; discardAll is registry-based', async cluster => { + // Pins the §7 response-policy fix: without the all_succeeded override the fan-out + // reply reduction throws 'Unsupported reply type for merge aggregation'. + assert.equal(await cluster.hImportPrepare('fs', ['f1', 'f2']), 'OK'); + + // Keys hashing to different slots — each SET routes to its own master. + const keys = ['key:{1}', 'key:{2}', 'key:{3}', 'key:{4}', 'key:{5}']; + const replies = await Promise.all( + keys.map(key => cluster.hImportSet(key, 'fs', ['v1', 'v2'])) + ); + assert.deepEqual(replies, keys.map(() => 'OK')); + assert.deepEqual(await cluster.hGetAll('key:{3}'), { f1: 'v1', f2: 'v2' }); + + // Registry-based reply: exactly 1 registered fieldset removed — not a per-master + // sum, not a session count. + assert.equal(await cluster.hImportDiscardAll(), 1); + }, GLOBAL.CLUSTERS.OPEN); + }); +}); diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index 92e89f838d9..4bf145bc880 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -20,6 +20,7 @@ import { TcpNetConnectOpts } from 'node:net'; import { RedisTcpSocketOptions } from '../client/socket'; import { BasicPooledClientSideCache, PooledClientSideCacheProvider } from '../client/cache'; import { ClientIdentity, ClientRole, generateClientId } from '../client/identity'; +import { FieldsetRegistry } from '../himport/registry'; import { DEFAULT_COMMAND_TIMEOUT } from '../defaults'; import { ScanOptions } from '../commands/SCAN'; @@ -862,6 +863,10 @@ export class RedisSentinelInternal< if (this.#nodeClientOptions.url !== undefined) { throw new Error("invalid nodeClientOptions for Sentinel"); } + // One fieldset registry across master/replica node clients: fieldsets registered before + // a failover must be transparently re-preparable on the promoted master's connections. + // (Sentinel-monitor clients use #sentinelClientOptions and never run HIMPORT.) + this.#nodeClientOptions.himportRegistry = new FieldsetRegistry(); if (options.clientSideCache) { if (options.clientSideCache instanceof PooledClientSideCacheProvider) { 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/client/package.json b/packages/client/package.json index fb39cf4f481..b58355bad01 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; +}); 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 84629328b24..2a98c9dc58e 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -211,8 +211,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 5cad7bc6237..bbdc31ac740 100644 --- a/packages/search/lib/commands/CREATE.ts +++ b/packages/search/lib/commands/CREATE.ts @@ -409,8 +409,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.cluster.spec.ts b/packages/search/lib/commands/CURSOR_READ.cluster.spec.ts new file mode 100644 index 00000000000..517772c248a --- /dev/null +++ b/packages/search/lib/commands/CURSOR_READ.cluster.spec.ts @@ -0,0 +1,75 @@ +import { strict as assert } from 'node:assert'; +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); + } + + testUtils.testWithCluster('paginates a WITHCURSOR aggregate to completion (every page hits the same coordinator)', async cluster => { + await seedIndex(cluster); + + const first = await cluster.ft.aggregateWithCursor('idx', '*', { COUNT: 5, LOAD: '@n' }); + assert.notEqual(first.cursor, 0, 'COUNT 5 over 40 docs should leave a live cursor'); + + let rows = first.results.length; + let cursor = first.cursor; + // Each READ is keyless: if routing weren't sticky it would land on a random + // node and throw "Cursor not found". Completing the loop proves stickiness. + while (cursor !== 0) { + const page = await cluster.ft.cursorRead('idx', cursor); + rows += page.results.length; + cursor = page.cursor; + } + + assert.equal(rows, DOC_COUNT, 'all pages assembled without an unknown-cursor error'); + }, 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 cluster.ft.aggregateWithCursor('idx', '*', { COUNT: 5, LOAD: '@n' }); + 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 cluster.ft.aggregateWithCursor('idx', '*', { COUNT: 5, LOAD: '@n' }); + 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/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 8f8da9d9bcb..8ddc008e61e 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -215,8 +215,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 635bf3b0530..2a97dd8cef4 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/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 + } + } + } } }; 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 fd99fc0969d..92bec374dbd 100644 --- a/packages/time-series/lib/commands/MRANGE.ts +++ b/packages/time-series/lib/commands/MRANGE.ts @@ -47,8 +47,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 470816030d7..0ead923c7f1 100644 --- a/packages/time-series/lib/commands/MRANGE_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MRANGE_MULTIAGGR.ts @@ -53,8 +53,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 353d23b5b7e..5c3c9113f9f 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts @@ -55,7 +55,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 78676db5119..f66cfdc2b97 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_MULTIAGGR.ts @@ -61,8 +61,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 925b7c4fb73..5cac099c1d0 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts @@ -52,8 +52,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 0192e9c97c8..fd43c612cc2 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_MULTIAGGR.ts @@ -58,8 +58,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 43c1357f88f..e8758c22038 100644 --- a/packages/time-series/lib/commands/RANGE.ts +++ b/packages/time-series/lib/commands/RANGE.ts @@ -67,7 +67,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 dc32ff94684..858c14dd80b 100644 --- a/packages/time-series/lib/commands/RANGE_MULTIAGGR.ts +++ b/packages/time-series/lib/commands/RANGE_MULTIAGGR.ts @@ -66,7 +66,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];