import COMMANDS, { TransformArgumentsReply } from './commands'; import { RedisCommand, RedisModules, RedisReply } from './commands'; import RedisCommandsQueue from './commands-queue'; import { RedisLuaScripts } from './lua-script'; import { RedisClientOptions } from './client'; type RedisMultiCommandSignature = (...args: Parameters) => RedisMultiCommandType; type WithCommands = { [P in keyof typeof COMMANDS]: RedisMultiCommandSignature<(typeof COMMANDS)[P], M, S> }; type WithModules = { [P in keyof M]: { [C in keyof M[P]]: RedisMultiCommandSignature; }; }; type WithScripts = { [P in keyof S]: RedisMultiCommandSignature }; export type RedisMultiCommandType = RedisMultiCommand & WithCommands & WithModules & WithScripts; export interface MultiQueuedCommand { encodedCommand: string; preservedArguments?: unknown; transformReply?: RedisCommand['transformReply']; } export type RedisMultiExecutor = (queue: Array, chainId?: symbol) => Promise>; export default class RedisMultiCommand { static create(executor: RedisMultiExecutor, clientOptions?: RedisClientOptions): RedisMultiCommandType { return new RedisMultiCommand(executor, clientOptions); } readonly #executor: RedisMultiExecutor; readonly #clientOptions: RedisClientOptions | undefined; readonly #queue: Array = []; readonly #scriptsInUse = new Set(); readonly #v4: Record = {}; get v4(): Record { if (!this.#clientOptions?.legacyMode) { throw new Error('client is not in "legacy mode"'); } return this.#v4; } constructor(executor: RedisMultiExecutor, clientOptions?: RedisClientOptions) { this.#executor = executor; this.#clientOptions = clientOptions; this.#initiateModules(); this.#initiateScripts(); this.#legacyMode(); } #initiateModules(): void { if (!this.#clientOptions?.modules) return; for (const [moduleName, commands] of Object.entries(this.#clientOptions.modules)) { const module: { [P in keyof typeof commands]: RedisMultiCommandSignature<(typeof commands)[P], M, S> } = {}; for (const [commandName, command] of Object.entries(commands)) { module[commandName] = (...args) => this.executeCommand(command, args); } (this as any)[moduleName] = module; } } #initiateScripts(): void { if (!this.#clientOptions?.scripts) return; for (const [name, script] of Object.entries(this.#clientOptions.scripts)) { (this as any)[name] = function (...args: Array) { const transformedArgs: TransformArgumentsReply = []; if (this.#scriptsInUse.has(name)) { transformedArgs.push( 'EVALSHA', script.SHA ); } else { this.#scriptsInUse.add(name); transformedArgs.push( 'EVAL', script.SCRIPT ); } transformedArgs.push(script.NUMBER_OF_KEYS.toString()); const scriptArgs = script.transformArguments(...args); transformedArgs.push(...scriptArgs); transformedArgs.preserve = scriptArgs.preserve; return this.addCommand( transformedArgs, script.transformReply ); }; } } #legacyMode(): void { if (!this.#clientOptions?.legacyMode) return; this.#v4.exec = this.exec.bind(this); (this as any).exec = (callback?: (err: Error | null, replies?: Array) => unknown): void => { this.#v4.exec() .then((reply: Array) => { if (!callback) return; callback(null, reply); }) .catch((err: Error) => { if (!callback) { // this.emit('error', err); return; } callback(err); }); }; for (const name of Object.keys(COMMANDS)) { this.#defineLegacyCommand(name); } if (this.#clientOptions?.modules) { for (const [module, commands] of Object.entries(this.#clientOptions.modules)) { for (const name of Object.keys(commands)) { this.#v4[module] = {}; this.#defineLegacyCommand(name, module); } } } if (this.#clientOptions.scripts) { for (const name of Object.keys(this.#clientOptions.scripts)) { this.#defineLegacyCommand(name); } } } #defineLegacyCommand(name: string, moduleName?: string): void { const handler = (...args: Array): RedisMultiCommandType => { return this.addCommand([ name, ...args.flat() as Array ]); }; if (moduleName) { this.#v4[moduleName][name] = (this as any)[moduleName][name]; (this as any)[moduleName][name] = handler; } else { this.#v4[name] = (this as any)[name].bind(this); (this as any)[name] = handler; } } addCommand(args: TransformArgumentsReply, transformReply?: RedisCommand['transformReply']): RedisMultiCommandType { this.#queue.push({ encodedCommand: RedisCommandsQueue.encodeCommand(args), preservedArguments: args.preserve, transformReply }); return this; } executeCommand(command: RedisCommand, args: Array): RedisMultiCommandType { return this.addCommand( command.transformArguments(...args), command.transformReply ); } async exec(execAsPipeline = false): Promise> { if (execAsPipeline) { return this.execAsPipeline(); } else if (!this.#queue.length) { return []; } const queue = this.#queue.splice(0); queue.unshift({ encodedCommand: RedisCommandsQueue.encodeCommand(['MULTI']) }); queue.push({ encodedCommand: RedisCommandsQueue.encodeCommand(['EXEC']) }); const rawReplies = await this.#executor(queue, Symbol('[RedisMultiCommand] Chain ID')); return (rawReplies[rawReplies.length - 1]! as Array).map((reply, i) => { const { transformReply, preservedArguments } = queue[i + 1]; return transformReply ? transformReply(reply, preservedArguments) : reply; }); } async execAsPipeline(): Promise> { if (!this.#queue.length) { return []; } return await this.#executor(this.#queue.splice(0)); } } for (const [name, command] of Object.entries(COMMANDS)) { (RedisMultiCommand.prototype as any)[name] = function (...args: Array): RedisMultiCommand { return this.executeCommand(command, args); }; }