import COMMANDS from './commands'; import { RedisCommand, RedisModules } from './commands'; import { ClientCommandOptions, RedisClientType, WithPlugins } from './client'; import { RedisSocketOptions } from './socket'; import RedisClusterSlots from './cluster-slots'; import { RedisLuaScript, RedisLuaScripts } from './lua-script'; import { commandOptions, CommandOptions, isCommandOptions } from './command-options'; export interface RedisClusterOptions { rootNodes: Array; modules?: M; scripts?: S; useReplicas?: boolean; maxCommandRedirections?: number; } export type RedisClusterType = WithPlugins & RedisCluster; export default class RedisCluster { static defineCommand(on: any, name: string, command: RedisCommand): void { on[name] = async function (...args: Array): Promise { const options = isCommandOptions(args[0]) && args[0]; return command.transformReply( await this.sendCommand( command, command.transformArguments(...(options ? args.slice(1) : args)), options ) ); }; } static create(options: RedisClusterOptions): RedisClusterType { return new RedisCluster(options); } static commandOptions(options: ClientCommandOptions): CommandOptions { return commandOptions(options); } readonly #options: RedisClusterOptions; readonly #slots: RedisClusterSlots; constructor(options: RedisClusterOptions) { this.#options = options; this.#slots = new RedisClusterSlots(options); this.#initiateModules(); this.#initiateScripts(); } #initiateModules(): void { if (!this.#options.modules) return; for (const m of this.#options.modules) { for (const [name, command] of Object.entries(m)) { RedisCluster.defineCommand(this, name, command); } } } #initiateScripts(): void { if (!this.#options.scripts) return; for (const [name, script] of Object.entries(this.#options.scripts)) { (this as any)[name] = async function (...args: Parameters): Promise> { const options = isCommandOptions(args[0]) && args[0]; return script.transformReply( await this.executeScript( script, script.transformArguments(...(options ? args.slice(1) : args)), options ) ); }; } } async connect(): Promise { return this.#slots.connect(); } async sendCommand(command: C, args: Array, options?: ClientCommandOptions, redirections: number = 0): Promise> { const client = this.#getClient(command, args); try { return await client.sendCommand(args, options); } catch (err) { if (await this.#handleCommandError(err, client, redirections)) { return this.sendCommand(command, args, options, redirections + 1); } throw err; } } async executeScript(script: S, args: Array, options?: ClientCommandOptions, redirections: number = 0): Promise> { const client = this.#getClient(script, args); try { return await client.executeScript(script, args, options); } catch (err) { if (await this.#handleCommandError(err, client, redirections)) { return this.executeScript(script, args, options, redirections + 1); } throw err; } } #getClient(commandOrScript: RedisCommand | RedisLuaScript, args: Array): RedisClientType { return this.#slots.getClient( commandOrScript.FIRST_KEY_INDEX ? args[commandOrScript.FIRST_KEY_INDEX] : undefined, commandOrScript.IS_READ_ONLY ); } async #handleCommandError(err: Error, client: RedisClientType, redirections: number = 0): Promise { if (redirections < (this.#options.maxCommandRedirections ?? 16)) { throw err; } if (err.message.startsWith('ASK')) { // TODO } else if (err.message.startsWith('MOVED')) { await this.#slots.discover(client); } throw err; } getMasters(): Array> { return this.#slots.getMasters(); } disconnect(): Promise { return this.#slots.disconnect(); } } for (const [name, command] of Object.entries(COMMANDS)) { RedisCluster.defineCommand(RedisCluster.prototype, name, command); }