import COMMANDS from './commands/client'; import { RedisCommand, RedisModules, RedisReply } from './commands'; import RedisCommandsQueue from './commands-queue'; import { RedisLuaScript, RedisLuaScripts } from './lua-script'; type RedisMultiCommandSignature = (...args: Parameters) => RedisMultiCommandType; type WithCommands = { [P in keyof typeof COMMANDS]: RedisMultiCommandSignature<(typeof COMMANDS)[P], M, S> }; type WithModules = { [P in keyof M[number]]: RedisMultiCommandSignature }; type WithScripts = { [P in keyof S]: RedisMultiCommandSignature }; export type RedisMultiCommandType = RedisMultiCommand & WithCommands & WithModules & WithScripts; export interface MultiQueuedCommand { encodedCommand: string; transformReply?: RedisCommand['transformReply']; } export type RedisMultiExecutor = (queue: Array, chainId: Symbol) => Promise>; export default class RedisMultiCommand { static defineCommand(on: any, name: string, command: RedisCommand): void { on[name] = function (...args: Parameters) { return this.addCommand(command.transformArguments(...args), command.transformReply); }; } static defineLuaScript(on: any, name: string, script: RedisLuaScript): void { on[name] = function (...args: Array) { let evalArgs; if (this.#scriptsInUse.has(name)) { evalArgs = [ 'EVALSHA', script.SHA ]; } else { this.#scriptsInUse.add(name); evalArgs = [ 'EVAL', script.SCRIPT ]; } return this.addCommand( [ ...evalArgs, script.NUMBER_OF_KEYS, ...script.transformArguments(...args) ], script.transformReply ); }; } static create(executor: RedisMultiExecutor, modules?: M, scripts?: S): RedisMultiCommandType { return new RedisMultiCommand(executor, modules, scripts); } readonly #executor: RedisMultiExecutor; readonly #queue: Array = []; readonly #scriptsInUse = new Set(); constructor(executor: RedisMultiExecutor, modules?: RedisModules, scripts?: RedisLuaScripts) { this.#executor = executor; this.#initiateModules(modules); this.#initiateScripts(scripts); } #initiateModules(modules?: RedisModules): void { if (!modules) return; for (const m of modules) { for (const [name, command] of Object.entries(m)) { RedisMultiCommand.defineCommand(this, name, command); } } } #initiateScripts(scripts?: RedisLuaScripts): void { if (!scripts) return; for (const [name, script] of Object.entries(scripts)) { RedisMultiCommand.defineLuaScript(this, name, script); } } addCommand(args: Array, transformReply?: RedisCommand['transformReply']): this { this.#queue.push({ encodedCommand: RedisCommandsQueue.encodeCommand(args), transformReply }); return this; } async exec(): Promise> { 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.map((reply, i) => { const { transformReply } = queue[i + 1]; return transformReply ? transformReply(reply) : reply; }); }; } for (const [name, command] of Object.entries(COMMANDS)) { RedisMultiCommand.defineCommand(RedisMultiCommand.prototype, name, command); }