import { BasicCommandParser, CommandParser } from '../client/parser'; import { RESP_TYPES } from '../RESP/decoder'; import { UnwrapReply, ArrayReply, BlobStringReply, BooleanReply, CommandArguments, DoubleReply, NullReply, NumberReply, RedisArgument, TuplesReply, MapReply, TypeMapping, Command } from '../RESP/types'; export function isNullReply(reply: unknown): reply is NullReply { return reply === null; } export function isArrayReply(reply: unknown): reply is ArrayReply { return Array.isArray(reply); } export const transformBooleanReply = { 2: (reply: NumberReply<0 | 1>) => reply as unknown as UnwrapReply === 1, 3: undefined as unknown as () => BooleanReply }; export const transformBooleanArrayReply = { 2: (reply: ArrayReply>) => { return (reply as unknown as UnwrapReply).map(transformBooleanReply[2]); }, 3: undefined as unknown as () => ArrayReply }; export type BitValue = 0 | 1; export function transformDoubleArgument(num: number): string { switch (num) { case Infinity: return '+inf'; case -Infinity: return '-inf'; default: return num.toString(); } } export function transformStringDoubleArgument(num: RedisArgument | number): RedisArgument { if (typeof num !== 'number') return num; return transformDoubleArgument(num); } export const transformDoubleReply = { 2: (reply: BlobStringReply, preserve?: any, typeMapping?: TypeMapping): DoubleReply => { const double = typeMapping ? typeMapping[RESP_TYPES.DOUBLE] : undefined; switch (double) { case String: { return reply as unknown as DoubleReply; } default: { let ret: number; switch (reply.toString()) { case 'inf': case '+inf': ret = Infinity; case '-inf': ret = -Infinity; case 'nan': ret = NaN; default: ret = Number(reply); } return ret as unknown as DoubleReply; } } }, 3: undefined as unknown as () => DoubleReply }; export function createTransformDoubleReplyResp2Func(preserve?: any, typeMapping?: TypeMapping) { return (reply: BlobStringReply) => { return transformDoubleReply[2](reply, preserve, typeMapping); } } export const transformDoubleArrayReply = { 2: (reply: Array, preserve?: any, typeMapping?: TypeMapping) => { return reply.map(createTransformDoubleReplyResp2Func(preserve, typeMapping)); }, 3: undefined as unknown as () => ArrayReply } export function createTransformNullableDoubleReplyResp2Func(preserve?: any, typeMapping?: TypeMapping) { return (reply: BlobStringReply | NullReply) => { return transformNullableDoubleReply[2](reply, preserve, typeMapping); } } export const transformNullableDoubleReply = { 2: (reply: BlobStringReply | NullReply, preserve?: any, typeMapping?: TypeMapping) => { if (reply === null) return null; return transformDoubleReply[2](reply as BlobStringReply, preserve, typeMapping); }, 3: undefined as unknown as () => DoubleReply | NullReply }; export interface Stringable { toString(): string; } export function transformTuplesToMap( reply: UnwrapReply>, func: (elem: any) => T, ) { const message = Object.create(null); for (let i = 0; i < reply.length; i+= 2) { message[reply[i].toString()] = func(reply[i + 1]); } return message; } export function createTransformTuplesReplyFunc(preserve?: any, typeMapping?: TypeMapping) { return (reply: ArrayReply) => { return transformTuplesReply(reply, preserve, typeMapping); }; } export function transformTuplesReply( reply: ArrayReply, preserve?: any, typeMapping?: TypeMapping ): MapReply { const mapType = typeMapping ? typeMapping[RESP_TYPES.MAP] : undefined; const inferred = reply as unknown as UnwrapReply switch (mapType) { case Array: { return reply as unknown as MapReply; } case Map: { const ret = new Map; for (let i = 0; i < inferred.length; i += 2) { ret.set(inferred[i].toString(), inferred[i + 1] as any); } return ret as unknown as MapReply;; } default: { const ret: Record = Object.create(null); for (let i = 0; i < inferred.length; i += 2) { ret[inferred[i].toString()] = inferred[i + 1] as any; } return ret as unknown as MapReply;; } } } export interface SortedSetMember { value: RedisArgument; score: number; } export type SortedSetSide = 'MIN' | 'MAX'; export const transformSortedSetReply = { 2: (reply: ArrayReply, preserve?: any, typeMapping?: TypeMapping) => { const inferred = reply as unknown as UnwrapReply, members = []; for (let i = 0; i < inferred.length; i += 2) { members.push({ value: inferred[i], score: transformDoubleReply[2](inferred[i + 1], preserve, typeMapping) }); } return members; }, 3: (reply: ArrayReply>) => { return (reply as unknown as UnwrapReply).map(member => { const [value, score] = member as unknown as UnwrapReply; return { value, score }; }); } } export type ListSide = 'LEFT' | 'RIGHT'; export function transformEXAT(EXAT: number | Date): string { return (typeof EXAT === 'number' ? EXAT : Math.floor(EXAT.getTime() / 1000)).toString(); } export function transformPXAT(PXAT: number | Date): string { return (typeof PXAT === 'number' ? PXAT : PXAT.getTime()).toString(); } export interface EvalOptions { keys?: Array; arguments?: Array; } export function evalFirstKeyIndex(options?: EvalOptions): string | undefined { return options?.keys?.[0]; } export function pushEvalArguments(args: Array, options?: EvalOptions): Array { if (options?.keys) { args.push( options.keys.length.toString(), ...options.keys ); } else { args.push('0'); } if (options?.arguments) { args.push(...options.arguments); } return args; } export function pushVariadicArguments(args: CommandArguments, value: RedisVariadicArgument): CommandArguments { if (Array.isArray(value)) { // https://github.com/redis/node-redis/pull/2160 args = args.concat(value); } else { args.push(value); } return args; } export function pushVariadicNumberArguments( args: CommandArguments, value: number | Array ): CommandArguments { if (Array.isArray(value)) { for (const item of value) { args.push(item.toString()); } } else { args.push(value.toString()); } return args; } export type RedisVariadicArgument = RedisArgument | Array; export function pushVariadicArgument( args: Array, value: RedisVariadicArgument ): CommandArguments { if (Array.isArray(value)) { args.push(value.length.toString(), ...value); } else { args.push('1', value); } return args; } export function parseOptionalVariadicArgument( parser: CommandParser, name: RedisArgument, value?: RedisVariadicArgument ) { if (value === undefined) return; parser.push(name); parser.pushVariadicWithLength(value); } export enum CommandFlags { WRITE = 'write', // command may result in modifications READONLY = 'readonly', // command will never modify keys DENYOOM = 'denyoom', // reject command if currently out of memory ADMIN = 'admin', // server admin command PUBSUB = 'pubsub', // pubsub-related command NOSCRIPT = 'noscript', // deny this command from scripts RANDOM = 'random', // command has random results, dangerous for scripts SORT_FOR_SCRIPT = 'sort_for_script', // if called from script, sort output LOADING = 'loading', // allow command while database is loading STALE = 'stale', // allow command while replica has stale data SKIP_MONITOR = 'skip_monitor', // do not show this command in MONITOR ASKING = 'asking', // cluster related - accept even if importing FAST = 'fast', // command operates in constant or log(N) time. Used for latency monitoring. MOVABLEKEYS = 'movablekeys' // keys have no pre-determined position. You must discover keys yourself. } export enum CommandCategories { KEYSPACE = '@keyspace', READ = '@read', WRITE = '@write', SET = '@set', SORTEDSET = '@sortedset', LIST = '@list', HASH = '@hash', STRING = '@string', BITMAP = '@bitmap', HYPERLOGLOG = '@hyperloglog', GEO = '@geo', STREAM = '@stream', PUBSUB = '@pubsub', ADMIN = '@admin', FAST = '@fast', SLOW = '@slow', BLOCKING = '@blocking', DANGEROUS = '@dangerous', CONNECTION = '@connection', TRANSACTION = '@transaction', SCRIPTING = '@scripting' } export type CommandRawReply = [ name: string, arity: number, flags: Array, firstKeyIndex: number, lastKeyIndex: number, step: number, categories: Array ]; export type CommandReply = { name: string, arity: number, flags: Set, firstKeyIndex: number, lastKeyIndex: number, step: number, categories: Set }; export function transformCommandReply( this: void, [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories]: CommandRawReply ): CommandReply { return { name, arity, flags: new Set(flags), firstKeyIndex, lastKeyIndex, step, categories: new Set(categories) }; } export enum RedisFunctionFlags { NO_WRITES = 'no-writes', ALLOW_OOM = 'allow-oom', ALLOW_STALE = 'allow-stale', NO_CLUSTER = 'no-cluster' } export type FunctionListRawItemReply = [ 'library_name', string, 'engine', string, 'functions', Array<[ 'name', string, 'description', string | null, 'flags', Array ]> ]; export interface FunctionListItemReply { libraryName: string; engine: string; functions: Array<{ name: string; description: string | null; flags: Array; }>; } export function transformFunctionListItemReply(reply: FunctionListRawItemReply): FunctionListItemReply { return { libraryName: reply[1], engine: reply[3], functions: reply[5].map(fn => ({ name: fn[1], description: fn[3], flags: fn[5] })) }; } export interface SlotRange { start: number; end: number; } function parseSlotRangeArguments( parser: CommandParser, range: SlotRange ): void { parser.push( range.start.toString(), range.end.toString() ); } export function parseSlotRangesArguments( parser: CommandParser, ranges: SlotRange | Array ) { if (Array.isArray(ranges)) { for (const range of ranges) { parseSlotRangeArguments(parser, range); } } else { parseSlotRangeArguments(parser, ranges); } } export type RawRangeReply = [ start: number, end: number ]; export interface RangeReply { start: number; end: number; } export function transformRangeReply([start, end]: RawRangeReply): RangeReply { return { start, end }; } export type ZKeyAndWeight = { key: RedisArgument; weight: number; }; export type ZVariadicKeys = T | [T, ...Array]; export type ZKeys = ZVariadicKeys | ZVariadicKeys; export function parseZKeysArguments( parser: CommandParser, keys: ZKeys ) { if (Array.isArray(keys)) { parser.push(keys.length.toString()); if (keys.length) { if (isPlainKeys(keys)) { parser.pushKeys(keys); } else { for (let i = 0; i < keys.length; i++) { parser.pushKey(keys[i].key) } parser.push('WEIGHTS'); for (let i = 0; i < keys.length; i++) { parser.push(transformDoubleArgument(keys[i].weight)); } } } } else { parser.push('1'); if (isPlainKey(keys)) { parser.pushKey(keys); } else { parser.pushKey(keys.key); parser.push('WEIGHTS', transformDoubleArgument(keys.weight)); } } } function isPlainKey(key: RedisArgument | ZKeyAndWeight): key is RedisArgument { return typeof key === 'string' || key instanceof Buffer; } function isPlainKeys(keys: Array | Array): keys is Array { return isPlainKey(keys[0]); } export type Tail = T extends [infer Head, ...infer Tail] ? Tail : never; /** * @deprecated */ export function parseArgs(command: Command, ...args: Array): CommandArguments { const parser = new BasicCommandParser(); command.parseCommand!(parser, ...args); const redisArgs: CommandArguments = parser.redisArgs; if (parser.preserve) { redisArgs.preserve = parser.preserve; } return redisArgs; } export type StreamMessageRawReply = TuplesReply<[ id: BlobStringReply, message: ArrayReply ]>; export type StreamMessageReply = { id: BlobStringReply, message: MapReply, }; export function transformStreamMessageReply(typeMapping: TypeMapping | undefined, reply: StreamMessageRawReply): StreamMessageReply { const [ id, message ] = reply as unknown as UnwrapReply; return { id: id, message: transformTuplesReply(message, undefined, typeMapping) }; } export function transformStreamMessageNullReply(typeMapping: TypeMapping | undefined, reply: StreamMessageRawReply | NullReply) { return isNullReply(reply) ? reply : transformStreamMessageReply(typeMapping, reply); } export type StreamMessagesReply = Array; export type StreamsMessagesReply = Array<{ name: BlobStringReply | string; messages: StreamMessagesReply; }> | null; export function transformStreamMessagesReply( r: ArrayReply, typeMapping?: TypeMapping ): StreamMessagesReply { const reply = r as unknown as UnwrapReply; return reply.map(transformStreamMessageReply.bind(undefined, typeMapping)); } type StreamMessagesRawReply = TuplesReply<[name: BlobStringReply, ArrayReply]>; type StreamsMessagesRawReply2 = ArrayReply; export function transformStreamsMessagesReplyResp2( reply: UnwrapReply, preserve?: any, typeMapping?: TypeMapping ): StreamsMessagesReply | NullReply { // FUTURE: resposne type if resp3 was working, reverting to old v4 for now //: MapReply | NullReply { if (reply === null) return null as unknown as NullReply; switch (typeMapping? typeMapping[RESP_TYPES.MAP] : undefined) { /* FUTURE: a response type for when resp3 is working properly case Map: { const ret = new Map(); for (let i=0; i < reply.length; i++) { const stream = reply[i] as unknown as UnwrapReply; const name = stream[0]; const rawMessages = stream[1]; ret.set(name.toString(), transformStreamMessagesReply(rawMessages, typeMapping)); } return ret as unknown as MapReply; } case Array: { const ret: Array = []; for (let i=0; i < reply.length; i++) { const stream = reply[i] as unknown as UnwrapReply; const name = stream[0]; const rawMessages = stream[1]; ret.push(name); ret.push(transformStreamMessagesReply(rawMessages, typeMapping)); } return ret as unknown as MapReply; } default: { const ret: Record = Object.create(null); for (let i=0; i < reply.length; i++) { const stream = reply[i] as unknown as UnwrapReply; const name = stream[0] as unknown as UnwrapReply; const rawMessages = stream[1]; ret[name.toString()] = transformStreamMessagesReply(rawMessages); } return ret as unknown as MapReply; } */ // V4 compatible response type default: { const ret: StreamsMessagesReply = []; for (let i=0; i < reply.length; i++) { const stream = reply[i] as unknown as UnwrapReply; ret.push({ name: stream[0], messages: transformStreamMessagesReply(stream[1]) }); } return ret; } } } type StreamsMessagesRawReply3 = MapReply>; export function transformStreamsMessagesReplyResp3(reply: UnwrapReply): MapReply | NullReply { if (reply === null) return null as unknown as NullReply; if (reply instanceof Map) { const ret = new Map(); for (const [n, rawMessages] of reply) { const name = n as unknown as UnwrapReply; ret.set(name.toString(), transformStreamMessagesReply(rawMessages)); } return ret as unknown as MapReply } else if (reply instanceof Array) { const ret = []; for (let i=0; i < reply.length; i += 2) { const name = reply[i] as BlobStringReply; const rawMessages = reply[i+1] as ArrayReply; ret.push(name); ret.push(transformStreamMessagesReply(rawMessages)); } return ret as unknown as MapReply } else { const ret = Object.create(null); for (const [name, rawMessages] of Object.entries(reply)) { ret[name] = transformStreamMessagesReply(rawMessages); } return ret as unknown as MapReply } }