1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-04 15:02:09 +03:00

Add support for redis functions (#2020)

* fix #1906 - implement BITFIELD_RO

* initial support for redis functions

* fix test utils

* redis functions commands and tests

* upgrade deps

* fix "Property 'uninstall' does not exist on type 'SinonFakeTimers'"

* upgrade dockers version

* Merge branch 'master' of github.com:redis/node-redis into functions

* fix FUNCTION LIST WITHCODE and FUNCTION STATS

* upgrade deps

* set minimum version for FCALL and FCALL_RO

* fix FUNCTION LOAD

* FUNCTION LOAD

* fix FUNCTION LOAD & FUNCTION LIST & FUNCTION LOAD WITHCODE

* fix FUNCTION_LIST_WITHCODE test
This commit is contained in:
Leibale Eidelman
2022-04-25 09:09:23 -04:00
committed by GitHub
parent 23b65133c9
commit 11c6c24881
51 changed files with 1406 additions and 324 deletions

View File

@@ -3,7 +3,7 @@ import RedisCluster from './lib/cluster';
export { RedisClientType, RedisClientOptions } from './lib/client';
export { RedisModules, RedisScripts } from './lib/commands';
export { RedisModules, RedisFunctions, RedisScripts } from './lib/commands';
export const createClient = RedisClient.create;

View File

@@ -62,6 +62,15 @@ import * as ECHO from '../commands/ECHO';
import * as FAILOVER from '../commands/FAILOVER';
import * as FLUSHALL from '../commands/FLUSHALL';
import * as FLUSHDB from '../commands/FLUSHDB';
import * as FUNCTION_DELETE from '../commands/FUNCTION_DELETE';
import * as FUNCTION_DUMP from '../commands/FUNCTION_DUMP';
import * as FUNCTION_FLUSH from '../commands/FUNCTION_FLUSH';
import * as FUNCTION_KILL from '../commands/FUNCTION_KILL';
import * as FUNCTION_LIST_WITHCODE from '../commands/FUNCTION_LIST_WITHCODE';
import * as FUNCTION_LIST from '../commands/FUNCTION_LIST';
import * as FUNCTION_LOAD from '../commands/FUNCTION_LOAD';
import * as FUNCTION_RESTORE from '../commands/FUNCTION_RESTORE';
import * as FUNCTION_STATS from '../commands/FUNCTION_STATS';
import * as HELLO from '../commands/HELLO';
import * as INFO from '../commands/INFO';
import * as KEYS from '../commands/KEYS';
@@ -228,6 +237,24 @@ export default {
flushAll: FLUSHALL,
FLUSHDB,
flushDb: FLUSHDB,
FUNCTION_DELETE,
functionDelete: FUNCTION_DELETE,
FUNCTION_DUMP,
functionDump: FUNCTION_DUMP,
FUNCTION_FLUSH,
functionFlush: FUNCTION_FLUSH,
FUNCTION_KILL,
functionKill: FUNCTION_KILL,
FUNCTION_LIST_WITHCODE,
functionListWithCode: FUNCTION_LIST_WITHCODE,
FUNCTION_LIST,
functionList: FUNCTION_LIST,
FUNCTION_LOAD,
functionLoad: FUNCTION_LOAD,
FUNCTION_RESTORE,
functionRestore: FUNCTION_RESTORE,
FUNCTION_STATS,
functionStats: FUNCTION_STATS,
HELLO,
hello: HELLO,
INFO,

View File

@@ -2,7 +2,7 @@ import { strict as assert } from 'assert';
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
import RedisClient, { RedisClientType } from '.';
import { RedisClientMultiCommandType } from './multi-command';
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands';
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisFunctions, RedisScripts } from '../commands';
import { AbortError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
import { defineScript } from '../lua-script';
import { spy } from 'sinon';
@@ -10,16 +10,42 @@ import { once } from 'events';
import { ClientKillFilters } from '../commands/CLIENT_KILL';
export const SQUARE_SCRIPT = defineScript({
NUMBER_OF_KEYS: 0,
SCRIPT: 'return ARGV[1] * ARGV[1];',
NUMBER_OF_KEYS: 0,
transformArguments(number: number): Array<string> {
return [number.toString()];
},
transformReply(reply: number): number {
return reply;
}
});
export const MATH_FUNCTION = {
name: 'math',
engine: 'LUA',
code: `#!LUA name=math
redis.register_function{
function_name = "square",
callback = function(keys, args) return args[1] * args[1] end,
flags = { "no-writes" }
}`,
library: {
square: {
NAME: 'square',
NUMBER_OF_KEYS: 0,
transformArguments(number: number): Array<string> {
return [number.toString()];
}
}
}
};
export async function loadMathFunction(
client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>
): Promise<void> {
await client.functionLoad(
MATH_FUNCTION.code,
{ REPLACE: true }
);
}
describe('Client', () => {
describe('parseURL', () => {
it('redis://user:secret@localhost:6379/0', () => {
@@ -115,7 +141,14 @@ describe('Client', () => {
});
describe('legacyMode', () => {
function sendCommandAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, args: RedisCommandArguments): Promise<RedisCommandRawReply> {
function sendCommandAsync<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(
client: RedisClientType<M, F, S>,
args: RedisCommandArguments
): Promise<RedisCommandRawReply> {
return new Promise((resolve, reject) => {
(client as any).sendCommand(args, (err: Error | undefined, reply: RedisCommandRawReply) => {
if (err) return reject(err);
@@ -159,7 +192,14 @@ describe('Client', () => {
}
});
function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: Array<any>): Promise<RedisCommandRawReply> {
function setAsync<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(
client: RedisClientType<M, F, S>,
...args: Array<any>
): Promise<RedisCommandRawReply> {
return new Promise((resolve, reject) => {
(client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
if (err) return reject(err);
@@ -205,7 +245,11 @@ describe('Client', () => {
}
});
function multiExecAsync<M extends RedisModules, S extends RedisScripts>(multi: RedisClientMultiCommandType<M, S>): Promise<Array<RedisCommandRawReply>> {
function multiExecAsync<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(multi: RedisClientMultiCommandType<M, F, S>): Promise<Array<RedisCommandRawReply>> {
return new Promise((resolve, reject) => {
(multi as any).exec((err: Error | undefined, replies: Array<RedisCommandRawReply>) => {
if (err) return reject(err);
@@ -439,25 +483,44 @@ describe('Client', () => {
}
});
const module = {
echo: {
transformArguments(message: string): Array<string> {
return ['ECHO', message];
},
transformReply(reply: string): string {
return reply;
}
}
};
testUtils.testWithClient('modules', async client => {
// assert.equal(
// await client.module.echo('message'),
// 'message'
// );
assert.equal(
await client.module.echo('message'),
'message'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
modules: {
module: {
echo: {
transformArguments(message: string): Array<string> {
return ['ECHO', message];
},
transformReply(reply: string): string {
return reply;
}
}
}
module
}
}
});
testUtils.testWithClient('functions', async client => {
await loadMathFunction(client);
assert.equal(
await client.math.square(2),
4
);
}, {
...GLOBAL.SERVERS.OPEN,
minimumDockerVersion: [7, 0],
clientOptions: {
functions: {
math: MATH_FUNCTION.library
}
}
});
@@ -468,9 +531,13 @@ describe('Client', () => {
assert.ok(id !== isolatedId);
}, GLOBAL.SERVERS.OPEN);
async function killClient<M extends RedisModules, S extends RedisScripts>(
client: RedisClientType<M, S>,
errorClient: RedisClientType<M, S> = client
async function killClient<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(
client: RedisClientType<M, F, S>,
errorClient: RedisClientType<M, F, S> = client
): Promise<void> {
const onceErrorPromise = once(errorClient, 'error');
await client.sendCommand(['QUIT']);
@@ -684,7 +751,9 @@ describe('Client', () => {
try {
await assert.doesNotReject(Promise.all([
subscriber.subscribe('channel', () => {}),
subscriber.subscribe('channel', () => {
// noop
}),
publisher.publish('channel', 'message')
]));
} finally {

View File

@@ -1,5 +1,5 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString } from '../commands';
import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket';
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
@@ -9,16 +9,17 @@ import { CommandOptions, commandOptions, isCommandOptions } from '../command-opt
import { ScanOptions, ZMember } from '../commands/generic-transformers';
import { ScanCommandOptions } from '../commands/SCAN';
import { HScanTuple } from '../commands/HSCAN';
import { extendWithCommands, extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
import { attachCommands, attachExtensions, fCallArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
import { ClientClosedError, DisconnectsClientError } from '../errors';
import { URL } from 'url';
import { TcpSocketConnectOpts } from 'net';
export interface RedisClientOptions<
M extends RedisModules = Record<string, never>,
S extends RedisScripts = Record<string, never>
> extends RedisPlugins<M, S> {
M extends RedisModules = RedisModules,
F extends RedisFunctions = RedisFunctions,
S extends RedisScripts = RedisScripts
> extends RedisExtensions<M, F, S> {
url?: string;
socket?: RedisSocketOptions;
username?: string;
@@ -32,58 +33,37 @@ export interface RedisClientOptions<
isolationPoolOptions?: PoolOptions;
}
type ConvertArgumentType<Type, ToType> =
Type extends RedisCommandArgument ? (
Type extends (string & ToType) ? Type : ToType
) : (
Type extends Set<infer Member> ? Set<ConvertArgumentType<Member, ToType>> : (
Type extends Map<infer Key, infer Value> ? Map<Key, ConvertArgumentType<Value, ToType>> : (
Type extends Array<infer Member> ? Array<ConvertArgumentType<Member, ToType>> : (
Type extends Date ? Type : (
Type extends Record<keyof any, any> ? {
[Property in keyof Type]: ConvertArgumentType<Type[Property], ToType>
} : Type
)
)
)
)
);
export type RedisClientCommandSignature<
Command extends RedisCommand,
Params extends Array<unknown> = Parameters<Command['transformArguments']>
> = <Options extends CommandOptions<ClientCommandOptions>>(
...args: Params | [options: Options, ...rest: Params]
) => Promise<
ConvertArgumentType<
RedisCommandReply<Command>,
Options['returnBuffers'] extends true ? Buffer : string
>
>;
type WithCommands = {
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
[P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>;
};
export type ExcludeMappedString<S> = string extends S ? never : S;
export type WithModules<M extends RedisModules> = {
[P in keyof M as ExcludeMappedString<P>]: {
[C in keyof M[P] as ExcludeMappedString<C>]: RedisClientCommandSignature<M[P][C]>;
[C in keyof M[P] as ExcludeMappedString<C>]: RedisCommandSignature<M[P][C]>;
};
};
export type WithFunctions<F extends RedisFunctions> = {
[P in keyof F as ExcludeMappedString<P>]: {
[FF in keyof F[P] as ExcludeMappedString<FF>]: RedisCommandSignature<F[P][FF]>;
};
};
export type WithScripts<S extends RedisScripts> = {
[P in keyof S as ExcludeMappedString<P>]: RedisClientCommandSignature<S[P]>;
[P in keyof S as ExcludeMappedString<P>]: RedisCommandSignature<S[P]>;
};
export type RedisClientType<
M extends RedisModules = Record<string, never>,
F extends RedisFunctions = Record<string, never>,
S extends RedisScripts = Record<string, never>
> = RedisClient<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
> = RedisClient<M, F, S> & WithCommands & WithModules<M> & WithFunctions<F> & WithScripts<S>;
export type InstantiableRedisClient<M extends RedisModules, S extends RedisScripts> =
new (options?: RedisClientOptions<M, S>) => RedisClientType<M, S>;
export type InstantiableRedisClient<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = new (options?: RedisClientOptions<M, F, S>) => RedisClientType<M, F, S>;
export interface ClientCommandOptions extends QueueCommandOptions {
isolated?: boolean;
@@ -91,30 +71,44 @@ export interface ClientCommandOptions extends QueueCommandOptions {
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
export default class RedisClient<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> extends EventEmitter {
static commandOptions<T extends ClientCommandOptions>(options: T): CommandOptions<T> {
return commandOptions(options);
}
commandOptions = RedisClient.commandOptions;
static extend<M extends RedisModules, S extends RedisScripts>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
const Client = <any>extendWithModulesAndScripts({
static extend<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(extensions?: RedisExtensions<M, F, S>): InstantiableRedisClient<M, F, S> {
const Client = attachExtensions({
BaseClass: RedisClient,
modules: plugins?.modules,
modulesCommandsExecutor: RedisClient.prototype.commandsExecutor,
scripts: plugins?.scripts,
scriptsExecutor: RedisClient.prototype.scriptsExecutor
modulesExecutor: RedisClient.prototype.commandsExecutor,
modules: extensions?.modules,
functionsExecutor: RedisClient.prototype.functionsExecuter,
functions: extensions?.functions,
scriptsExecutor: RedisClient.prototype.scriptsExecuter,
scripts: extensions?.scripts
});
if (Client !== RedisClient) {
Client.prototype.Multi = RedisClientMultiCommand.extend(plugins);
Client.prototype.Multi = RedisClientMultiCommand.extend(extensions);
}
return Client;
}
static create<M extends RedisModules, S extends RedisScripts>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
static create<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(options?: RedisClientOptions<M, F, S>): RedisClientType<M, F, S> {
return new (RedisClient.extend(options))(options);
}
@@ -157,14 +151,14 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
return parsed;
}
readonly #options?: RedisClientOptions<M, S>;
readonly #queue: RedisCommandsQueue;
readonly #options?: RedisClientOptions<M, F, S>;
readonly #socket: RedisSocket;
readonly #isolationPool: Pool<RedisClientType<M, S>>;
readonly #queue: RedisCommandsQueue;
readonly #isolationPool: Pool<RedisClientType<M, F, S>>;
readonly #v4: Record<string, any> = {};
#selectedDB = 0;
get options(): RedisClientOptions<M, S> | undefined {
get options(): RedisClientOptions<M, F, S> | undefined {
return this.#options;
}
@@ -180,7 +174,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
return this.#v4;
}
constructor(options?: RedisClientOptions<M, S>) {
constructor(options?: RedisClientOptions<M, F, S>) {
super();
this.#options = this.#initiateOptions(options);
this.#queue = this.#initiateQueue();
@@ -198,7 +192,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
this.#legacyMode();
}
#initiateOptions(options?: RedisClientOptions<M, S>): RedisClientOptions<M, S> | undefined {
#initiateOptions(options?: RedisClientOptions<M, F, S>): RedisClientOptions<M, F, S> | undefined {
if (options?.url) {
const parsed = RedisClient.parseURL(options.url);
if (options.socket) {
@@ -350,7 +344,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
(...args: Array<unknown>): void => (this as any).sendCommand(name, ...args);
}
duplicate(overrides?: Partial<RedisClientOptions<M, S>>): RedisClientType<M, S> {
duplicate(overrides?: Partial<RedisClientOptions<M, F, S>>): RedisClientType<M, F, S> {
return new (Object.getPrototypeOf(this).constructor)({
...this.#options,
...overrides
@@ -361,9 +355,11 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
await this.#socket.connect();
}
async commandsExecutor(command: RedisCommand, args: Array<unknown>): Promise<RedisCommandReply<typeof command>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
async commandsExecutor<C extends RedisCommand>(
command: C,
args: Array<unknown>
): Promise<RedisCommandReply<C>> {
const { args: redisArgs, options } = transformCommandArguments(command, args);
return transformCommandReply(
command,
await this.#sendCommand(redisArgs, options),
@@ -371,12 +367,18 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
);
}
sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions): Promise<T> {
sendCommand<T = RedisCommandRawReply>(
args: RedisCommandArguments,
options?: ClientCommandOptions
): Promise<T> {
return this.#sendCommand(args, options);
}
// using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
#sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions): Promise<T> {
#sendCommand<T = RedisCommandRawReply>(
args: RedisCommandArguments,
options?: ClientCommandOptions
): Promise<T> {
if (!this.#socket.isOpen) {
return Promise.reject(new ClientClosedError());
}
@@ -395,9 +397,34 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
return promise;
}
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
async functionsExecuter<F extends RedisFunction>(
fn: F,
args: Array<unknown>
): Promise<RedisCommandReply<F>> {
const { args: redisArgs, options } = transformCommandArguments(fn, args);
return transformCommandReply(
fn,
await this.executeFunction(fn, redisArgs, options),
redisArgs.preserve
);
}
executeFunction(
fn: RedisFunction,
args: RedisCommandArguments,
options?: ClientCommandOptions
): Promise<RedisCommandRawReply> {
return this.#sendCommand(
fCallArguments(fn, args),
options
);
}
async scriptsExecuter<S extends RedisScript>(
script: S,
args: Array<unknown>
): Promise<RedisCommandReply<S>> {
const { args: redisArgs, options } = transformCommandArguments(script, args);
return transformCommandReply(
script,
await this.executeScript(script, redisArgs, options),
@@ -405,25 +432,29 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
);
}
async executeScript(script: RedisScript, args: RedisCommandArguments, options?: ClientCommandOptions): Promise<RedisCommandReply<typeof script>> {
async executeScript(
script: RedisScript,
args: RedisCommandArguments,
options?: ClientCommandOptions
): Promise<RedisCommandRawReply> {
const redisArgs: RedisCommandArguments = ['EVALSHA', script.SHA1];
if (script.NUMBER_OF_KEYS !== undefined) {
redisArgs.push(script.NUMBER_OF_KEYS.toString());
}
redisArgs.push(...args);
try {
return await this.#sendCommand([
'EVALSHA',
script.SHA1,
script.NUMBER_OF_KEYS.toString(),
...args
], options);
return await this.#sendCommand(redisArgs, options);
} catch (err: any) {
if (!err?.message?.startsWith?.('NOSCRIPT')) {
throw err;
}
return await this.#sendCommand([
'EVAL',
script.SCRIPT,
script.NUMBER_OF_KEYS.toString(),
...args
], options);
redisArgs[0] = 'EVAL';
redisArgs[1] = script.SCRIPT;
return this.#sendCommand(redisArgs, options);
}
}
@@ -558,11 +589,11 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
}
}
executeIsolated<T>(fn: (client: RedisClientType<M, S>) => T | Promise<T>): Promise<T> {
executeIsolated<T>(fn: (client: RedisClientType<M, F, S>) => T | Promise<T>): Promise<T> {
return this.#isolationPool.use(fn);
}
multi(): RedisClientMultiCommandType<M, S> {
multi(): RedisClientMultiCommandType<M, F, S> {
return new (this as any).Multi(
this.multiExecutor.bind(this),
this.#options?.legacyMode
@@ -639,7 +670,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
}
}
extendWithCommands({
attachCommands({
BaseClass: RedisClient,
commands: COMMANDS,
executor: RedisClient.prototype.commandsExecutor

View File

@@ -1,28 +1,63 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction } from '../commands';
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
import { extendWithCommands, extendWithModulesAndScripts, transformLegacyCommandArguments } from '../commander';
import { ExcludeMappedString } from '.';
import { attachCommands, attachExtensions, transformLegacyCommandArguments } from '../commander';
type RedisClientMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
(...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, S>;
type CommandSignature<
C extends RedisCommand,
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = (...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, F, S>;
type WithCommands<M extends RedisModules, S extends RedisScripts> = {
[P in keyof typeof COMMANDS]: RedisClientMultiCommandSignature<(typeof COMMANDS)[P], M, S>;
type WithCommands<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], M, F, S>;
};
type WithModules<M extends RedisModules, S extends RedisScripts> = {
type WithModules<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof M as ExcludeMappedString<P>]: {
[C in keyof M[P] as ExcludeMappedString<C>]: RedisClientMultiCommandSignature<M[P][C], M, S>;
[C in keyof M[P] as ExcludeMappedString<C>]: CommandSignature<M[P][C], M, F, S>;
};
};
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
[P in keyof S as ExcludeMappedString<P>]: RedisClientMultiCommandSignature<S[P], M, S>;
type WithFunctions<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof F as ExcludeMappedString<P>]: {
[FF in keyof F[P] as ExcludeMappedString<FF>]: CommandSignature<F[P][FF], M, F, S>;
};
};
export type RedisClientMultiCommandType<M extends RedisModules, S extends RedisScripts> =
RedisClientMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
type WithScripts<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof S as ExcludeMappedString<P>]: CommandSignature<S[P], M, F, S>;
};
export type RedisClientMultiCommandType<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = RedisClientMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
type InstantiableRedisMultiCommand<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = new (...args: ConstructorParameters<typeof RedisClientMultiCommand>) => RedisClientMultiCommandType<M, F, S>;
export type RedisClientMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
@@ -30,15 +65,19 @@ export default class RedisClientMultiCommand {
readonly #multi = new RedisMultiCommand();
readonly #executor: RedisClientMultiExecutor;
static extend<M extends RedisModules, S extends RedisScripts>(
plugins?: RedisPlugins<M, S>
): new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisClientMultiCommandType<M, S> {
return <any>extendWithModulesAndScripts({
static extend<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(extensions?: RedisExtensions<M, F, S>): InstantiableRedisMultiCommand<M, F, S> {
return attachExtensions({
BaseClass: RedisClientMultiCommand,
modules: plugins?.modules,
modulesCommandsExecutor: RedisClientMultiCommand.prototype.commandsExecutor,
scripts: plugins?.scripts,
scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor
modulesExecutor: RedisClientMultiCommand.prototype.commandsExecutor,
modules: extensions?.modules,
functionsExecutor: RedisClientMultiCommand.prototype.functionsExecutor,
functions: extensions?.functions,
scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor,
scripts: extensions?.scripts
});
}
@@ -102,6 +141,11 @@ export default class RedisClientMultiCommand {
return this;
}
functionsExecutor(fn: RedisFunction, args: Array<unknown>): this {
this.#multi.addFunction(fn, args);
return this;
}
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
this.#multi.addScript(script, args);
return this;
@@ -123,15 +167,13 @@ export default class RedisClientMultiCommand {
EXEC = this.exec;
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
if (!this.#multi.queue.length) return [];
return this.#multi.transformReplies(
await this.#executor(this.#multi.queue)
);
}
}
extendWithCommands({
attachCommands({
BaseClass: RedisClientMultiCommand,
commands: COMMANDS,
executor: RedisClientMultiCommand.prototype.commandsExecutor

View File

@@ -1,7 +1,7 @@
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES';
import { RedisClusterClientOptions, RedisClusterOptions } from '.';
import { RedisCommandArgument, RedisModules, RedisScripts } from '../commands';
import { RedisCommandArgument, RedisFunctions, RedisModules, RedisScripts } from '../commands';
import { RootNodesUnavailableError } from '../errors';
// We need to use 'require', because it's not possible with Typescript to import
@@ -9,9 +9,13 @@ import { RootNodesUnavailableError } from '../errors';
// set to true.
const calculateSlot = require('cluster-key-slot');
export interface ClusterNode<M extends RedisModules, S extends RedisScripts> {
export interface ClusterNode<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> {
id: string;
client: RedisClientType<M, S>;
client: RedisClientType<M, F, S>;
}
interface NodeAddress {
@@ -23,22 +27,30 @@ export type NodeAddressMap = {
[address: string]: NodeAddress;
} | ((address: string) => NodeAddress | undefined);
interface SlotNodes<M extends RedisModules, S extends RedisScripts> {
master: ClusterNode<M, S>;
replicas: Array<ClusterNode<M, S>>;
clientIterator: IterableIterator<RedisClientType<M, S>> | undefined;
interface SlotNodes<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> {
master: ClusterNode<M, F, S>;
replicas: Array<ClusterNode<M, F, S>>;
clientIterator: IterableIterator<RedisClientType<M, F, S>> | undefined;
}
type OnError = (err: unknown) => void;
export default class RedisClusterSlots<M extends RedisModules, S extends RedisScripts> {
readonly #options: RedisClusterOptions<M, S>;
readonly #Client: InstantiableRedisClient<M, S>;
export default class RedisClusterSlots<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> {
readonly #options: RedisClusterOptions<M, F, S>;
readonly #Client: InstantiableRedisClient<M, F, S>;
readonly #onError: OnError;
readonly #nodeByAddress = new Map<string, ClusterNode<M, S>>();
readonly #slots: Array<SlotNodes<M, S>> = [];
readonly #nodeByAddress = new Map<string, ClusterNode<M, F, S>>();
readonly #slots: Array<SlotNodes<M, F, S>> = [];
constructor(options: RedisClusterOptions<M, S>, onError: OnError) {
constructor(options: RedisClusterOptions<M, F, S>, onError: OnError) {
this.#options = options;
this.#Client = RedisClient.extend(options);
this.#onError = onError;
@@ -72,7 +84,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
#runningRediscoverPromise?: Promise<void>;
async rediscover(startWith: RedisClientType<M, S>): Promise<void> {
async rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
if (!this.#runningRediscoverPromise) {
this.#runningRediscoverPromise = this.#rediscover(startWith)
.finally(() => this.#runningRediscoverPromise = undefined);
@@ -81,7 +93,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
return this.#runningRediscoverPromise;
}
async #rediscover(startWith: RedisClientType<M, S>): Promise<void> {
async #rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
if (await this.#discoverNodes(startWith.options)) return;
for (const { client } of this.#nodeByAddress.values()) {
@@ -137,7 +149,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
};
}
#initiateClient(options?: RedisClusterClientOptions): RedisClientType<M, S> {
#initiateClient(options?: RedisClusterClientOptions): RedisClientType<M, F, S> {
return new this.#Client(this.#clientOptionsDefaults(options))
.on('error', this.#onError);
}
@@ -152,7 +164,12 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
}
}
#initiateClientForNode(nodeData: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set<string>, promises: Array<Promise<void>>): ClusterNode<M, S> {
#initiateClientForNode(
nodeData: RedisClusterMasterNode | RedisClusterReplicaNode,
readonly: boolean,
clientsInUse: Set<string>,
promises: Array<Promise<void>>
): ClusterNode<M, F, S> {
const address = `${nodeData.host}:${nodeData.port}`;
clientsInUse.add(address);
@@ -175,11 +192,11 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
return node;
}
getSlotMaster(slot: number): ClusterNode<M, S> {
getSlotMaster(slot: number): ClusterNode<M, F, S> {
return this.#slots[slot].master;
}
*#slotClientIterator(slotNumber: number): IterableIterator<RedisClientType<M, S>> {
*#slotClientIterator(slotNumber: number): IterableIterator<RedisClientType<M, F, S>> {
const slot = this.#slots[slotNumber];
yield slot.master.client;
@@ -188,7 +205,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
}
}
#getSlotClient(slotNumber: number): RedisClientType<M, S> {
#getSlotClient(slotNumber: number): RedisClientType<M, F, S> {
const slot = this.#slots[slotNumber];
if (!slot.clientIterator) {
slot.clientIterator = this.#slotClientIterator(slotNumber);
@@ -203,9 +220,9 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
return value;
}
#randomClientIterator?: IterableIterator<ClusterNode<M, S>>;
#randomClientIterator?: IterableIterator<ClusterNode<M, F, S>>;
#getRandomClient(): RedisClientType<M, S> {
#getRandomClient(): RedisClientType<M, F, S> {
if (!this.#nodeByAddress.size) {
throw new Error('Cluster is not connected');
}
@@ -223,7 +240,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
return value.client;
}
getClient(firstKey?: RedisCommandArgument, isReadonly?: boolean): RedisClientType<M, S> {
getClient(firstKey?: RedisCommandArgument, isReadonly?: boolean): RedisClientType<M, F, S> {
if (!firstKey) {
return this.#getRandomClient();
}
@@ -236,7 +253,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
return this.#getSlotClient(slot);
}
getMasters(): Array<ClusterNode<M, S>> {
getMasters(): Array<ClusterNode<M, F, S>> {
const masters = [];
for (const node of this.#nodeByAddress.values()) {
if (node.client.options?.readonly) continue;
@@ -247,7 +264,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
return masters;
}
getNodeByAddress(address: string): ClusterNode<M, S> | undefined {
getNodeByAddress(address: string): ClusterNode<M, F, S> | undefined {
const mappedAddress = this.#getNodeAddress(address);
return this.#nodeByAddress.get(
mappedAddress ? `${mappedAddress.host}:${mappedAddress.port}` : address
@@ -262,7 +279,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
return this.#destroy(client => client.disconnect());
}
async #destroy(fn: (client: RedisClientType<M, S>) => Promise<unknown>): Promise<void> {
async #destroy(fn: (client: RedisClientType<M, F, S>) => Promise<unknown>): Promise<void> {
const promises = [];
for (const { client } of this.#nodeByAddress.values()) {
promises.push(fn(client));

View File

@@ -18,12 +18,16 @@ import * as DECR from '../commands/DECR';
import * as DECRBY from '../commands/DECRBY';
import * as DEL from '../commands/DEL';
import * as DUMP from '../commands/DUMP';
import * as EVAL_RO from '../commands/EVAL_RO';
import * as EVAL from '../commands/EVAL';
import * as EVALSHA_RO from '../commands/EVALSHA_RO';
import * as EVALSHA from '../commands/EVALSHA';
import * as EXISTS from '../commands/EXISTS';
import * as EXPIRE from '../commands/EXPIRE';
import * as EXPIREAT from '../commands/EXPIREAT';
import * as EXPIRETIME from '../commands/EXPIRETIME';
import * as FCALL_RO from '../commands/FCALL_RO';
import * as FCALL from '../commands/FCALL';
import * as GEOADD from '../commands/GEOADD';
import * as GEODIST from '../commands/GEODIST';
import * as GEOHASH from '../commands/GEOHASH';
@@ -230,10 +234,14 @@ export default {
del: DEL,
DUMP,
dump: DUMP,
EVAL_RO,
evalRo: EVAL_RO,
EVAL,
eval: EVAL,
EVALSHA,
evalSha: EVALSHA,
EVALSHA_RO,
evalShaRo: EVALSHA_RO,
EXISTS,
exists: EXISTS,
EXPIRE,
@@ -242,6 +250,10 @@ export default {
expireAt: EXPIREAT,
EXPIRETIME,
expireTime: EXPIRETIME,
FCALL_RO,
fCallRo: FCALL_RO,
FCALL,
fCall: FCALL,
GEOADD,
geoAdd: GEOADD,
GEODIST,

View File

@@ -1,18 +1,22 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
import { ClientCommandOptions, RedisClientCommandSignature, RedisClientOptions, RedisClientType, WithModules, WithScripts } from '../client';
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, RedisFunction } from '../commands';
import { ClientCommandOptions, RedisClientOptions, RedisClientType, WithFunctions, WithModules, WithScripts } from '../client';
import RedisClusterSlots, { ClusterNode, NodeAddressMap } from './cluster-slots';
import { extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, extendWithCommands } from '../commander';
import { attachExtensions, transformCommandReply, attachCommands, transformCommandArguments } from '../commander';
import { EventEmitter } from 'events';
import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
import RedisClusterMultiCommand, { InstantiableRedisClusterMultiCommandType, RedisClusterMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command';
export type RedisClusterClientOptions = Omit<RedisClientOptions, 'modules' | 'scripts'>;
export type RedisClusterClientOptions = Omit<
RedisClientOptions,
'modules' | 'functions' | 'scripts' | 'database'
>;
export interface RedisClusterOptions<
M extends RedisModules = Record<string, never>,
F extends RedisFunctions = Record<string, never>,
S extends RedisScripts = Record<string, never>
> extends RedisPlugins<M, S> {
> extends RedisExtensions<M, F, S> {
rootNodes: Array<RedisClusterClientOptions>;
defaults?: Partial<RedisClusterClientOptions>;
useReplicas?: boolean;
@@ -21,16 +25,25 @@ export interface RedisClusterOptions<
}
type WithCommands = {
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
[P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>;
};
export type RedisClusterType<
M extends RedisModules = Record<string, never>,
F extends RedisFunctions = Record<string, never>,
S extends RedisScripts = Record<string, never>
> = RedisCluster<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
> = RedisCluster<M, F, S> & WithCommands & WithModules<M> & WithFunctions<F> & WithScripts<S>;
export default class RedisCluster<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
static extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: RedisCommandArguments): RedisCommandArgument | undefined {
export default class RedisCluster<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> extends EventEmitter {
static extractFirstKey(
command: RedisCommand,
originalArgs: Array<unknown>,
redisArgs: RedisCommandArguments
): RedisCommandArgument | undefined {
if (command.FIRST_KEY_INDEX === undefined) {
return undefined;
} else if (typeof command.FIRST_KEY_INDEX === 'number') {
@@ -40,21 +53,27 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
return command.FIRST_KEY_INDEX(...originalArgs);
}
static create<M extends RedisModules, S extends RedisScripts>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
return new (<any>extendWithModulesAndScripts({
static create<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(options?: RedisClusterOptions<M, F, S>): RedisClusterType<M, F, S> {
return new (attachExtensions({
BaseClass: RedisCluster,
modulesExecutor: RedisCluster.prototype.commandsExecutor,
modules: options?.modules,
modulesCommandsExecutor: RedisCluster.prototype.commandsExecutor,
scripts: options?.scripts,
scriptsExecutor: RedisCluster.prototype.scriptsExecutor
functionsExecutor: RedisCluster.prototype.functionsExecutor,
functions: options?.functions,
scriptsExecutor: RedisCluster.prototype.scriptsExecutor,
scripts: options?.scripts
}))(options);
}
readonly #options: RedisClusterOptions<M, S>;
readonly #slots: RedisClusterSlots<M, S>;
readonly #Multi: new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<M, S>;
readonly #options: RedisClusterOptions<M, F, S>;
readonly #slots: RedisClusterSlots<M, F, S>;
readonly #Multi: InstantiableRedisClusterMultiCommandType<M, F, S>;
constructor(options: RedisClusterOptions<M, S>) {
constructor(options: RedisClusterOptions<M, F, S>) {
super();
this.#options = options;
@@ -62,7 +81,7 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
this.#Multi = RedisClusterMultiCommand.extend(options);
}
duplicate(overrides?: Partial<RedisClusterOptions<M, S>>): RedisClusterType<M, S> {
duplicate(overrides?: Partial<RedisClusterOptions<M, F, S>>): RedisClusterType<M, F, S> {
return new (Object.getPrototypeOf(this).constructor)({
...this.#options,
...overrides
@@ -73,9 +92,11 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
return this.#slots.connect();
}
async commandsExecutor(command: RedisCommand, args: Array<unknown>): Promise<RedisCommandReply<typeof command>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
async commandsExecutor<C extends RedisCommand>(
command: C,
args: Array<unknown>
): Promise<RedisCommandReply<C>> {
const { args: redisArgs, options } = transformCommandArguments(command, args);
return transformCommandReply(
command,
await this.sendCommand(
@@ -101,9 +122,38 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
);
}
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
async functionsExecutor<F extends RedisFunction>(
fn: F,
args: Array<unknown>
): Promise<RedisCommandReply<F>> {
const { args: redisArgs, options } = transformCommandArguments(fn, args);
return transformCommandReply(
fn,
await this.executeFunction(
fn,
args,
redisArgs,
options
),
redisArgs.preserve
);
}
async executeFunction(
fn: RedisFunction,
originalArgs: Array<unknown>,
redisArgs: RedisCommandArguments,
options?: ClientCommandOptions
): Promise<RedisCommandRawReply> {
return this.#execute(
RedisCluster.extractFirstKey(fn, originalArgs, redisArgs),
fn.IS_READ_ONLY,
client => client.executeFunction(fn, redisArgs, options)
);
}
async scriptsExecutor<S extends RedisScript>(script: S, args: Array<unknown>): Promise<RedisCommandReply<S>> {
const { args: redisArgs, options } = transformCommandArguments(script, args);
return transformCommandReply(
script,
await this.executeScript(
@@ -121,7 +171,7 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
originalArgs: Array<unknown>,
redisArgs: RedisCommandArguments,
options?: ClientCommandOptions
): Promise<RedisCommandReply<typeof script>> {
): Promise<RedisCommandRawReply> {
return this.#execute(
RedisCluster.extractFirstKey(script, originalArgs, redisArgs),
script.IS_READ_ONLY,
@@ -132,7 +182,7 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
async #execute<Reply>(
firstKey: RedisCommandArgument | undefined,
isReadonly: boolean | undefined,
executor: (client: RedisClientType<M, S>) => Promise<Reply>
executor: (client: RedisClientType<M, F, S>) => Promise<Reply>
): Promise<Reply> {
const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16;
let client = this.#slots.getClient(firstKey, isReadonly);
@@ -171,7 +221,7 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
}
}
multi(routing?: RedisCommandArgument): RedisClusterMultiCommandType<M, S> {
multi(routing?: RedisCommandArgument): RedisClusterMultiCommandType<M, F, S> {
return new this.#Multi(
(commands: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => {
return this.#execute(
@@ -184,11 +234,11 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
);
}
getMasters(): Array<ClusterNode<M, S>> {
getMasters(): Array<ClusterNode<M, F, S>> {
return this.#slots.getMasters();
}
getSlotMaster(slot: number): ClusterNode<M, S> {
getSlotMaster(slot: number): ClusterNode<M, F, S> {
return this.#slots.getSlotMaster(slot);
}
@@ -201,7 +251,7 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
}
}
extendWithCommands({
attachCommands({
BaseClass: RedisCluster,
commands: COMMANDS,
executor: RedisCluster.prototype.commandsExecutor

View File

@@ -1,29 +1,63 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction } from '../commands';
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
import { extendWithCommands, extendWithModulesAndScripts } from '../commander';
import { attachCommands, attachExtensions } from '../commander';
import RedisCluster from '.';
import { ExcludeMappedString } from '../client';
type RedisClusterMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
(...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<M, S>;
type RedisClusterMultiCommandSignature<
C extends RedisCommand,
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = (...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<M, F, S>;
type WithCommands<M extends RedisModules, S extends RedisScripts> = {
[P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, S>
type WithCommands<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, F, S>;
};
type WithModules<M extends RedisModules, S extends RedisScripts> = {
type WithModules<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof M as ExcludeMappedString<P>]: {
[C in keyof M[P] as ExcludeMappedString<C>]: RedisClusterMultiCommandSignature<M[P][C], M, S>;
[C in keyof M[P] as ExcludeMappedString<C>]: RedisClusterMultiCommandSignature<M[P][C], M, F, S>;
};
};
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
[P in keyof S as ExcludeMappedString<P>]: RedisClusterMultiCommandSignature<S[P], M, S>
type WithFunctions<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof F as ExcludeMappedString<P>]: {
[FF in keyof F[P] as ExcludeMappedString<FF>]: RedisClusterMultiCommandSignature<F[P][FF], M, F, S>;
};
};
export type RedisClusterMultiCommandType<M extends RedisModules, S extends RedisScripts> =
RedisClusterMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
type WithScripts<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof S as ExcludeMappedString<P>]: RedisClusterMultiCommandSignature<S[P], M, F, S>;
};
export type RedisClusterMultiCommandType<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = RedisClusterMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
export type InstantiableRedisClusterMultiCommandType<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<M, F, S>;
export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
@@ -32,15 +66,19 @@ export default class RedisClusterMultiCommand {
readonly #executor: RedisClusterMultiExecutor;
#firstKey: RedisCommandArgument | undefined;
static extend<M extends RedisModules, S extends RedisScripts>(
plugins?: RedisPlugins<M, S>
): new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisClusterMultiCommandType<M, S> {
return <any>extendWithModulesAndScripts({
static extend<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(extensions?: RedisExtensions<M, F, S>): InstantiableRedisClusterMultiCommandType<M, F, S> {
return attachExtensions({
BaseClass: RedisClusterMultiCommand,
modules: plugins?.modules,
modulesCommandsExecutor: RedisClusterMultiCommand.prototype.commandsExecutor,
scripts: plugins?.scripts,
scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor
modulesExecutor: RedisClusterMultiCommand.prototype.commandsExecutor,
modules: extensions?.modules,
functionsExecutor: RedisClusterMultiCommand.prototype.functionsExecutor,
functions: extensions?.functions,
scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor,
scripts: extensions?.scripts
});
}
@@ -51,15 +89,8 @@ export default class RedisClusterMultiCommand {
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
const transformedArguments = command.transformArguments(...args);
if (!this.#firstKey) {
this.#firstKey = RedisCluster.extractFirstKey(command, args, transformedArguments);
}
return this.addCommand(
undefined,
transformedArguments,
command.transformReply
);
this.#firstKey ??= RedisCluster.extractFirstKey(command, args, transformedArguments);
return this.addCommand(undefined, transformedArguments, command.transformReply);
}
addCommand(
@@ -67,21 +98,21 @@ export default class RedisClusterMultiCommand {
args: RedisCommandArguments,
transformReply?: RedisCommand['transformReply']
): this {
if (!this.#firstKey) {
this.#firstKey = firstKey;
}
this.#firstKey ??= firstKey;
this.#multi.addCommand(args, transformReply);
return this;
}
functionsExecutor(fn: RedisFunction, args: Array<unknown>): this {
const transformedArguments = this.#multi.addFunction(fn, args);
this.#firstKey ??= RedisCluster.extractFirstKey(fn, args, transformedArguments);
return this;
}
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
const transformedArguments = this.#multi.addScript(script, args);
if (!this.#firstKey) {
this.#firstKey = RedisCluster.extractFirstKey(script, args, transformedArguments);
}
return this.addCommand(undefined, transformedArguments);
this.#firstKey ??= RedisCluster.extractFirstKey(script, args, transformedArguments);
return this;
}
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
@@ -106,7 +137,7 @@ export default class RedisClusterMultiCommand {
}
}
extendWithCommands({
attachCommands({
BaseClass: RedisClusterMultiCommand,
commands: COMMANDS,
executor: RedisClusterMultiCommand.prototype.commandsExecutor

View File

@@ -1,16 +1,23 @@
import { CommandOptions, isCommandOptions } from './command-options';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisCommands, RedisModules, RedisScript, RedisScripts } from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts } from './commands';
type Instantiable<T = any> = new(...args: Array<any>) => T;
type Instantiable<T = any> = new (...args: Array<any>) => T;
interface ExtendWithCommandsConfig<T extends Instantiable> {
BaseClass: T;
commands: RedisCommands;
executor(command: RedisCommand, args: Array<unknown>): unknown;
type CommandsExecutor<C extends RedisCommand = RedisCommand> =
(command: C, args: Array<unknown>) => unknown;
interface AttachCommandsConfig<C extends RedisCommand> {
BaseClass: Instantiable;
commands: Record<string, C>;
executor: CommandsExecutor<C>;
}
export function extendWithCommands<T extends Instantiable>({ BaseClass, commands, executor }: ExtendWithCommandsConfig<T>): void {
export function attachCommands<C extends RedisCommand>({
BaseClass,
commands,
executor
}: AttachCommandsConfig<C>): void {
for (const [name, command] of Object.entries(commands)) {
BaseClass.prototype[name] = function (...args: Array<unknown>): unknown {
return executor.call(this, command, args);
@@ -18,56 +25,82 @@ export function extendWithCommands<T extends Instantiable>({ BaseClass, commands
}
}
interface ExtendWithModulesAndScriptsConfig<T extends Instantiable> {
interface AttachExtensionsConfig<T extends Instantiable = Instantiable> {
BaseClass: T;
modulesExecutor: CommandsExecutor;
modules?: RedisModules;
modulesCommandsExecutor(this: InstanceType<T>, command: RedisCommand, args: Array<unknown>): unknown;
functionsExecutor: CommandsExecutor<RedisFunction>;
functions?: RedisFunctions;
scriptsExecutor: CommandsExecutor<RedisScript>;
scripts?: RedisScripts;
scriptsExecutor(this: InstanceType<T>, script: RedisScript, args: Array<unknown>): unknown;
}
export function extendWithModulesAndScripts<T extends Instantiable>(config: ExtendWithModulesAndScriptsConfig<T>): T {
let Commander: T | undefined;
export function attachExtensions(config: AttachExtensionsConfig): any {
let Commander;
if (config.modules) {
Commander = class extends config.BaseClass {
constructor(...args: Array<any>) {
super(...args);
Commander = attachWithNamespaces({
BaseClass: config.BaseClass,
namespaces: config.modules,
executor: config.modulesExecutor
});
}
for (const module of Object.keys(config.modules!)) {
this[module] = new this[module](this);
}
}
};
for (const [moduleName, module] of Object.entries(config.modules)) {
Commander.prototype[moduleName] = class {
readonly self: T;
constructor(self: InstanceType<T>) {
this.self = self;
}
};
for (const [commandName, command] of Object.entries(module)) {
Commander.prototype[moduleName].prototype[commandName] = function (...args: Array<unknown>): unknown {
return config.modulesCommandsExecutor.call(this.self, command, args);
};
}
}
if (config.functions) {
Commander = attachWithNamespaces({
BaseClass: Commander ?? config.BaseClass,
namespaces: config.functions,
executor: config.functionsExecutor
});
}
if (config.scripts) {
Commander ??= class extends config.BaseClass {};
attachCommands({
BaseClass: Commander,
commands: config.scripts,
executor: config.scriptsExecutor
});
}
for (const [name, script] of Object.entries(config.scripts)) {
Commander.prototype[name] = function (...args: Array<unknown>): unknown {
return config.scriptsExecutor.call(this, script, args);
return Commander ?? config.BaseClass;
}
interface AttachWithNamespacesConfig<C extends RedisCommand> {
BaseClass: Instantiable;
namespaces: Record<string, Record<string, C>>;
executor: CommandsExecutor<C>;
}
function attachWithNamespaces<C extends RedisCommand>({
BaseClass,
namespaces,
executor
}: AttachWithNamespacesConfig<C>): any {
const Commander = class extends BaseClass {
constructor(...args: Array<any>) {
super(...args);
for (const namespace of Object.keys(namespaces)) {
this[namespace] = Object.create(this[namespace], {
self: {
value: this
}
});
}
}
};
for (const [namespace, commands] of Object.entries(namespaces)) {
Commander.prototype[namespace] = {};
for (const [name, command] of Object.entries(commands)) {
Commander.prototype[namespace][name] = function (...args: Array<unknown>): unknown {
return executor.call(this.self, command, args);
};
}
}
return (Commander ?? config.BaseClass) as any;
return Commander;
}
export function transformCommandArguments<T>(
@@ -93,14 +126,29 @@ export function transformLegacyCommandArguments(args: Array<any>): Array<any> {
return args.flat().map(x => x?.toString?.());
}
export function transformCommandReply(
command: RedisCommand,
rawReply: RedisCommandRawReply,
export function transformCommandReply<C extends RedisCommand>(
command: C,
rawReply: unknown,
preserved: unknown
): RedisCommandReply<typeof command> {
): RedisCommandReply<C> {
if (!command.transformReply) {
return rawReply;
return rawReply as RedisCommandReply<C>;
}
return command.transformReply(rawReply, preserved);
}
export function fCallArguments(fn: RedisFunction, args: RedisCommandArguments): RedisCommandArguments {
const actualArgs: RedisCommandArguments = [
fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL',
fn.NAME
];
if (fn.NUMBER_OF_KEYS !== undefined) {
actualArgs.push(fn.NUMBER_OF_KEYS.toString());
}
actualArgs.push(...args);
return actualArgs;
}

View File

@@ -1,4 +1,6 @@
import { EvalOptions, pushEvalArguments } from './generic-transformers';
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
export function transformArguments(script: string, options?: EvalOptions): Array<string> {
return pushEvalArguments(['EVAL', script], options);

View File

@@ -1,4 +1,6 @@
import { EvalOptions, pushEvalArguments } from './generic-transformers';
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
export function transformArguments(sha1: string, options?: EvalOptions): Array<string> {
return pushEvalArguments(['EVALSHA', sha1], options);

View File

@@ -0,0 +1,17 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './EVALSHA_RO';
describe('EVALSHA_RO', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('sha1', {
keys: ['key'],
arguments: ['argument']
}),
['EVALSHA_RO', 'sha1', '1', 'key', 'argument']
);
});
});

View File

@@ -0,0 +1,9 @@
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
export const IS_READ_ONLY = true;
export function transformArguments(sha1: string, options?: EvalOptions): Array<string> {
return pushEvalArguments(['EVALSHA_RO', sha1], options);
}

View File

@@ -0,0 +1,31 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './EVAL_RO';
describe('EVAL_RO', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('return KEYS[1] + ARGV[1]', {
keys: ['key'],
arguments: ['argument']
}),
['EVAL_RO', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument']
);
});
testUtils.testWithClient('client.evalRo', async client => {
assert.equal(
await client.evalRo('return 1'),
1
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithCluster('cluster.evalRo', async cluster => {
assert.equal(
await cluster.evalRo('return 1'),
1
);
}, GLOBAL.CLUSTERS.OPEN);
});

View File

@@ -0,0 +1,9 @@
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
export const IS_READ_ONLY = true;
export function transformArguments(script: string, options?: EvalOptions): Array<string> {
return pushEvalArguments(['EVAL_RO', script], options);
}

View File

@@ -0,0 +1,29 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
import { transformArguments } from './FCALL';
describe('FCALL', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('function', {
keys: ['key'],
arguments: ['argument']
}),
['FCALL', 'function', '1', 'key', 'argument']
);
});
testUtils.testWithClient('client.fCall', async client => {
await loadMathFunction(client);
assert.equal(
await client.fCall(MATH_FUNCTION.library.square.NAME, {
arguments: ['2']
}),
4
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,7 @@
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
export function transformArguments(fn: string, options?: EvalOptions): Array<string> {
return pushEvalArguments(['FCALL', fn], options);
}

View File

@@ -0,0 +1,29 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
import { transformArguments } from './FCALL_RO';
describe('FCALL_RO', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('function', {
keys: ['key'],
arguments: ['argument']
}),
['FCALL_RO', 'function', '1', 'key', 'argument']
);
});
testUtils.testWithClient('client.fCallRo', async client => {
await loadMathFunction(client);
assert.equal(
await client.fCallRo(MATH_FUNCTION.library.square.NAME, {
arguments: ['2']
}),
4
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,9 @@
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
export const IS_READ_ONLY = true;
export function transformArguments(fn: string, options?: EvalOptions): Array<string> {
return pushEvalArguments(['FCALL_RO', fn], options);
}

View File

@@ -0,0 +1,24 @@
import { strict as assert } from 'assert';
import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './FUNCTION_DELETE';
describe('FUNCTION DELETE', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('library'),
['FUNCTION', 'DELETE', 'library']
);
});
testUtils.testWithClient('client.functionDelete', async client => {
await loadMathFunction(client);
assert.equal(
await client.functionDelete(MATH_FUNCTION.name),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,7 @@
import { RedisCommandArguments } from '.';
export function transformArguments(library: string): RedisCommandArguments {
return ['FUNCTION', 'DELETE', library];
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,21 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './FUNCTION_DUMP';
describe('FUNCTION DUMP', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['FUNCTION', 'DUMP']
);
});
testUtils.testWithClient('client.functionDump', async client => {
assert.equal(
typeof await client.functionDump(),
'string'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,7 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
export function transformArguments(): RedisCommandArguments {
return ['FUNCTION', 'DUMP'];
}
export declare function transformReply(): RedisCommandArgument;

View File

@@ -0,0 +1,30 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './FUNCTION_FLUSH';
describe('FUNCTION FLUSH', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['FUNCTION', 'FLUSH']
);
});
it('with mode', () => {
assert.deepEqual(
transformArguments('SYNC'),
['FUNCTION', 'FLUSH', 'SYNC']
);
});
});
testUtils.testWithClient('client.functionFlush', async client => {
assert.equal(
await client.functionFlush(),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,13 @@
import { RedisCommandArguments } from '.';
export function transformArguments(mode?: 'ASYNC' | 'SYNC'): RedisCommandArguments {
const args = ['FUNCTION', 'FLUSH'];
if (mode) {
args.push(mode);
}
return args;
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './FUNCTION_KILL';
describe('FUNCTION KILL', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['FUNCTION', 'KILL']
);
});
});

View File

@@ -0,0 +1,7 @@
import { RedisCommandArguments } from '.';
export function transformArguments(): RedisCommandArguments {
return ['FUNCTION', 'KILL'];
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,41 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
import { transformArguments } from './FUNCTION_LIST';
describe('FUNCTION LIST', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['FUNCTION', 'LIST']
);
});
it('with pattern', () => {
assert.deepEqual(
transformArguments('patter*'),
['FUNCTION', 'LIST', 'patter*']
);
});
});
testUtils.testWithClient('client.functionList', async client => {
await loadMathFunction(client);
assert.deepEqual(
await client.functionList(),
[{
libraryName: MATH_FUNCTION.name,
engine: MATH_FUNCTION.engine,
functions: [{
name: MATH_FUNCTION.library.square.NAME,
description: null,
flags: ['no-writes']
}]
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,16 @@
import { RedisCommandArguments } from '.';
import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers';
export function transformArguments(pattern?: string): RedisCommandArguments {
const args = ['FUNCTION', 'LIST'];
if (pattern) {
args.push(pattern);
}
return args;
}
export function transformReply(reply: Array<FunctionListRawItemReply>): Array<FunctionListItemReply> {
return reply.map(transformFunctionListItemReply);
}

View File

@@ -0,0 +1,42 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
import { transformArguments } from './FUNCTION_LIST_WITHCODE';
describe('FUNCTION LIST WITHCODE', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['FUNCTION', 'LIST', 'WITHCODE']
);
});
it('with pattern', () => {
assert.deepEqual(
transformArguments('patter*'),
['FUNCTION', 'LIST', 'patter*', 'WITHCODE']
);
});
});
testUtils.testWithClient('client.functionListWithCode', async client => {
await loadMathFunction(client);
assert.deepEqual(
await client.functionListWithCode(),
[{
libraryName: MATH_FUNCTION.name,
engine: MATH_FUNCTION.engine,
functions: [{
name: MATH_FUNCTION.library.square.NAME,
description: null,
flags: ['no-writes']
}],
libraryCode: MATH_FUNCTION.code
}]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,26 @@
import { RedisCommandArguments } from '.';
import { transformArguments as transformFunctionListArguments } from './FUNCTION_LIST';
import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers';
export function transformArguments(pattern?: string): RedisCommandArguments {
const args = transformFunctionListArguments(pattern);
args.push('WITHCODE');
return args;
}
type FunctionListWithCodeRawItemReply = [
...FunctionListRawItemReply,
'library_code',
string
];
interface FunctionListWithCodeItemReply extends FunctionListItemReply {
libraryCode: string;
}
export function transformReply(reply: Array<FunctionListWithCodeRawItemReply>): Array<FunctionListWithCodeItemReply> {
return reply.map(library => ({
...transformFunctionListItemReply(library as unknown as FunctionListRawItemReply),
libraryCode: library[7]
}));
}

View File

@@ -0,0 +1,36 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { MATH_FUNCTION } from '../client/index.spec';
import { transformArguments } from './FUNCTION_LOAD';
describe('FUNCTION LOAD', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments( 'code'),
['FUNCTION', 'LOAD', 'code']
);
});
it('with REPLACE', () => {
assert.deepEqual(
transformArguments('code', {
REPLACE: true
}),
['FUNCTION', 'LOAD', 'REPLACE', 'code']
);
});
});
testUtils.testWithClient('client.functionLoad', async client => {
assert.equal(
await client.functionLoad(
MATH_FUNCTION.code,
{ REPLACE: true }
),
MATH_FUNCTION.name
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,22 @@
import { RedisCommandArguments } from '.';
interface FunctionLoadOptions {
REPLACE?: boolean;
}
export function transformArguments(
code: string,
options?: FunctionLoadOptions
): RedisCommandArguments {
const args = ['FUNCTION', 'LOAD'];
if (options?.REPLACE) {
args.push('REPLACE');
}
args.push(code);
return args;
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,37 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './FUNCTION_RESTORE';
describe('FUNCTION RESTORE', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments('dump'),
['FUNCTION', 'RESTORE', 'dump']
);
});
it('with mode', () => {
assert.deepEqual(
transformArguments('dump', 'APPEND'),
['FUNCTION', 'RESTORE', 'dump', 'APPEND']
);
});
});
testUtils.testWithClient('client.functionRestore', async client => {
assert.equal(
await client.functionRestore(
await client.functionDump(
client.commandOptions({
returnBuffers: true
})
),
'FLUSH'
),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,16 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
export function transformArguments(
dump: RedisCommandArgument,
mode?: 'FLUSH' | 'APPEND' | 'REPLACE'
): RedisCommandArguments {
const args = ['FUNCTION', 'RESTORE', dump];
if (mode) {
args.push(mode);
}
return args;
}
export declare function transformReply(): 'OK';

View File

@@ -0,0 +1,25 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './FUNCTION_STATS';
describe('FUNCTION STATS', () => {
testUtils.isVersionGreaterThanHook([7, 0]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['FUNCTION', 'STATS']
);
});
testUtils.testWithClient('client.functionStats', async client => {
const stats = await client.functionStats();
assert.equal(stats.runningScript, null);
assert.equal(typeof stats.engines, 'object');
for (const [engine, { librariesCount, functionsCount }] of Object.entries(stats.engines)) {
assert.equal(typeof engine, 'string');
assert.equal(typeof librariesCount, 'number');
assert.equal(typeof functionsCount, 'number');
}
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,56 @@
import { RedisCommandArguments } from '.';
export function transformArguments(): RedisCommandArguments {
return ['FUNCTION', 'STATS'];
}
type FunctionStatsRawReply = [
'running_script',
null | [
'name',
string,
'command',
string,
'duration_ms',
number
],
'engines',
Array<any> // "flat tuples" (there is no way to type that)
// ...[string, [
// 'libraries_count',
// number,
// 'functions_count',
// number
// ]]
];
interface FunctionStatsReply {
runningScript: null | {
name: string;
command: string;
durationMs: number;
};
engines: Record<string, {
librariesCount: number;
functionsCount: number;
}>;
}
export function transformReply(reply: FunctionStatsRawReply): FunctionStatsReply {
const engines = Object.create(null);
for (let i = 0; i < reply[3].length; i++) {
engines[reply[3][i]] = {
librariesCount: reply[3][++i][1],
functionsCount: reply[3][i][3]
};
}
return {
runningScript: reply[1] === null ? null : {
name: reply[1][1],
command: reply[1][3],
durationMs: reply[1][5]
},
engines
};
}

View File

@@ -348,6 +348,10 @@ export interface EvalOptions {
arguments?: Array<string>;
}
export function evalFirstKeyIndex(options?: EvalOptions): string | undefined {
return options?.keys?.[0];
}
export function pushEvalArguments(args: Array<string>, options?: EvalOptions): Array<string> {
if (options?.keys) {
args.push(
@@ -491,6 +495,51 @@ export function transformCommandReply(
};
}
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<RedisFunctionFlags>
]>
];
export interface FunctionListItemReply {
libraryName: string;
engine: string;
functions: Array<{
name: string;
description: string | null;
flags: Array<RedisFunctionFlags>;
}>;
}
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 SortOptions {
BY?: string;
LIMIT?: {

View File

@@ -1,22 +1,51 @@
import { ClientCommandOptions } from '../client';
import { CommandOptions } from '../command-options';
import { RedisScriptConfig, SHA1 } from '../lua-script';
// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface RedisCommandRawReplyArray extends Array<RedisCommandRawReply> {}
export type RedisCommandRawReply = string | number | Buffer | null | undefined | RedisCommandRawReplyArray;
export type RedisCommandRawReply = string | number | Buffer | null | undefined | Array<RedisCommandRawReply>;
export type RedisCommandArgument = string | Buffer;
export type RedisCommandArguments = Array<RedisCommandArgument> & { preserve?: unknown };
export interface RedisCommand {
FIRST_KEY_INDEX?: number | ((...args: Array<any>) => RedisCommandArgument);
FIRST_KEY_INDEX?: number | ((...args: Array<any>) => RedisCommandArgument | undefined);
IS_READ_ONLY?: boolean;
transformArguments(this: void, ...args: Array<any>): RedisCommandArguments;
transformReply?(this: void, reply: any, preserved?: any): any;
}
export type RedisCommandReply<C extends RedisCommand> = C['transformReply'] extends (...args: any) => infer T ? T : RedisCommandRawReply;
export type RedisCommandReply<C extends RedisCommand> =
C['transformReply'] extends (...args: any) => infer T ? T : RedisCommandRawReply;
export type ConvertArgumentType<Type, ToType> =
Type extends RedisCommandArgument ? (
Type extends (string & ToType) ? Type : ToType
) : (
Type extends Set<infer Member> ? Set<ConvertArgumentType<Member, ToType>> : (
Type extends Map<infer Key, infer Value> ? Map<Key, ConvertArgumentType<Value, ToType>> : (
Type extends Array<infer Member> ? Array<ConvertArgumentType<Member, ToType>> : (
Type extends Date ? Type : (
Type extends Record<PropertyKey, any> ? {
[Property in keyof Type]: ConvertArgumentType<Type[Property], ToType>
} : Type
)
)
)
)
);
export type RedisCommandSignature<
Command extends RedisCommand,
Params extends Array<unknown> = Parameters<Command['transformArguments']>
> = <Options extends CommandOptions<ClientCommandOptions>>(
...args: Params | [options: Options, ...rest: Params]
) => Promise<
ConvertArgumentType<
RedisCommandReply<Command>,
Options['returnBuffers'] extends true ? Buffer : string
>
>;
export interface RedisCommands {
[command: string]: RedisCommand;
@@ -30,13 +59,33 @@ export interface RedisModules {
[module: string]: RedisModule;
}
export interface RedisFunction extends RedisCommand {
NAME: string;
NUMBER_OF_KEYS?: number;
}
export interface RedisFunctionLibrary {
[fn: string]: RedisFunction;
}
export interface RedisFunctions {
[library: string]: RedisFunctionLibrary;
}
export type RedisScript = RedisScriptConfig & SHA1;
export interface RedisScripts {
[script: string]: RedisScript;
}
export interface RedisPlugins<M extends RedisModules, S extends RedisScripts> {
export interface RedisExtensions<
M extends RedisModules = RedisModules,
F extends RedisFunctions = RedisFunctions,
S extends RedisScripts = RedisScripts
> {
modules?: M;
functions?: F;
scripts?: S;
}
export type ExcludeMappedString<S> = string extends S ? never : S;

View File

@@ -3,7 +3,7 @@ import { RedisCommand } from './commands';
export interface RedisScriptConfig extends RedisCommand {
SCRIPT: string;
NUMBER_OF_KEYS: number;
NUMBER_OF_KEYS?: number;
}
export interface SHA1 {

View File

@@ -1,4 +1,5 @@
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisScript } from './commands';
import { fCallArguments } from './commander';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunction, RedisScript } from './commands';
import { WatchError } from './errors';
export interface RedisMultiQueuedCommand {
@@ -22,6 +23,18 @@ export default class RedisMultiCommand {
});
}
addFunction(fn: RedisFunction, args: Array<unknown>): RedisCommandArguments {
const transformedArguments = fCallArguments(
fn,
fn.transformArguments(...args)
);
this.queue.push({
args: transformedArguments,
transformReply: fn.transformReply
});
return transformedArguments;
}
addScript(script: RedisScript, args: Array<unknown>): RedisCommandArguments {
const transformedArguments: RedisCommandArguments = [];
if (this.scriptsInUse.has(script.SHA1)) {
@@ -37,7 +50,9 @@ export default class RedisMultiCommand {
);
}
transformedArguments.push(script.NUMBER_OF_KEYS.toString());
if (script.NUMBER_OF_KEYS !== undefined) {
transformedArguments.push(script.NUMBER_OF_KEYS.toString());
}
const scriptArguments = script.transformArguments(...args);
transformedArguments.push(...scriptArguments);

View File

@@ -3,7 +3,7 @@ import { SinonSpy } from 'sinon';
import { promiseTimeout } from './utils';
export default new TestUtils({
defaultDockerVersion: '6.2',
defaultDockerVersion: '7.0-rc3',
dockerImageName: 'redis',
dockerImageVersionArgument: 'redis-version'
});