1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-07 13:22:56 +03:00
This commit is contained in:
Leibale
2023-06-20 20:04:05 -04:00
parent f22879dffa
commit 4894c26458
35 changed files with 848 additions and 662 deletions

View File

@@ -161,9 +161,15 @@ Some command arguments/replies have changed to align more closely to data types
- `HELLO`: `protover` moved from the options object to it's own argument, `auth` -> `AUTH`, `clientName` -> `SETNAME` - `HELLO`: `protover` moved from the options object to it's own argument, `auth` -> `AUTH`, `clientName` -> `SETNAME`
- `MODULE LIST`: `version` -> `ver` [^map-keys] - `MODULE LIST`: `version` -> `ver` [^map-keys]
- `MEMORY STATS`: [^map-keys] - `MEMORY STATS`: [^map-keys]
- `CLIENT TRACKINGINFO`: `flags` in RESP2 - `Set<string>` -> `Array<string>` (to match RESP3 default type mapping)
- `CLUSETER SETSLOT`: `ClusterSlotStates` -> `CLUSTER_SLOT_STATES` [^enum-to-constants]
- `FUNCTION RESTORE`: the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
- `CLUSTER RESET`: the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
[^enum-to-constants]: TODO [^enum-to-constants]: TODO
[^boolean-to-number]: TODO [^boolean-to-number]: TODO
[^map-keys]: [TODO](https://github.com/redis/node-redis/discussions/2506) [^map-keys]: [TODO](https://github.com/redis/node-redis/discussions/2506)
[^future-proofing]: TODO

View File

@@ -1,4 +1,4 @@
export { RedisModules, RedisFunctions, RedisScripts, RespVersions } from './lib/RESP/types'; export { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, CommandPolicies } from './lib/RESP/types';
export { RESP_TYPES } from './lib/RESP/decoder'; export { RESP_TYPES } from './lib/RESP/decoder';
export { VerbatimString } from './lib/RESP/verbatim-string'; export { VerbatimString } from './lib/RESP/verbatim-string';
export { defineScript } from './lib/lua-script'; export { defineScript } from './lib/lua-script';

View File

@@ -6,11 +6,14 @@ import { ChannelListeners, PubSub, PubSubCommand, PubSubListener, PubSubType, Pu
import { AbortError, ErrorReply } from '../errors'; import { AbortError, ErrorReply } from '../errors';
import { EventEmitter } from 'stream'; import { EventEmitter } from 'stream';
export interface CommandOptions { export interface CommandOptions<T = TypeMapping> {
chainId?: symbol; chainId?: symbol;
asap?: boolean; asap?: boolean;
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
typeMapping?: TypeMapping; /**
* Maps bettween RESP and JavaScript types
*/
typeMapping?: T;
} }
export interface CommandWaitingToBeSent extends CommandWaitingForReply { export interface CommandWaitingToBeSent extends CommandWaitingForReply {

View File

@@ -122,10 +122,10 @@ describe('Client', () => {
client.connect() client.connect()
]); ]);
const promise = once(client, 'end'); await Promise.all([
console.log('listen to end', client.listeners('end')); once(client, 'end'),
client.close(); client.close()
await promise; ]);
}, { }, {
...GLOBAL.SERVERS.OPEN, ...GLOBAL.SERVERS.OPEN,
disableClientSetup: true disableClientSetup: true
@@ -335,9 +335,7 @@ describe('Client', () => {
}); });
testUtils.testWithClient('duplicate should reuse command options', async client => { testUtils.testWithClient('duplicate should reuse command options', async client => {
const duplicate = client.withTypeMapping({ const duplicate = client.duplicate();
[RESP_TYPES.SIMPLE_STRING]: Buffer
}).duplicate();
await duplicate.connect(); await duplicate.connect();
@@ -351,6 +349,13 @@ describe('Client', () => {
} }
}, { }, {
...GLOBAL.SERVERS.OPEN, ...GLOBAL.SERVERS.OPEN,
clientOptions: {
commandOptions: {
typeMapping: {
[RESP_TYPES.SIMPLE_STRING]: Buffer
}
}
},
disableClientSetup: true, disableClientSetup: true,
}); });

View File

@@ -11,18 +11,27 @@ import { Command, CommandArguments, CommandSignature, TypeMapping, CommanderConf
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command'; import { RedisMultiQueuedCommand } from '../multi-command';
import HELLO, { HelloOptions } from '../commands/HELLO'; import HELLO, { HelloOptions } from '../commands/HELLO';
import { ReplyWithTypeMapping, CommandReply } from '../RESP/types';
import { ScanOptions, ScanCommonOptions } from '../commands/SCAN'; import { ScanOptions, ScanCommonOptions } from '../commands/SCAN';
import { RedisLegacyClient, RedisLegacyClientType } from './legacy-mode'; import { RedisLegacyClient, RedisLegacyClientType } from './legacy-mode';
// import { RedisClientPool } from './pool'; // import { RedisClientPool } from './pool';
interface ClientCommander<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
> extends CommanderConfig<M, F, S, RESP>{
commandOptions?: CommandOptions<TYPE_MAPPING>;
}
export interface RedisClientOptions< export interface RedisClientOptions<
M extends RedisModules = RedisModules, M extends RedisModules = RedisModules,
F extends RedisFunctions = RedisFunctions, F extends RedisFunctions = RedisFunctions,
S extends RedisScripts = RedisScripts, S extends RedisScripts = RedisScripts,
RESP extends RespVersions = RespVersions, RESP extends RespVersions = RespVersions,
TYPE_MAPPING extends TypeMapping = TypeMapping TYPE_MAPPING extends TypeMapping = TypeMapping
> extends CommanderConfig<M, F, S, RESP>, TypeMappingOption<TYPE_MAPPING> { > extends ClientCommander<M, F, S, RESP, TYPE_MAPPING> {
/** /**
* `redis[s]://[[username][:password]@][host][:port][/db-number]` * `redis[s]://[[username][:password]@][host][:port][/db-number]`
* See [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details * See [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details
@@ -68,13 +77,6 @@ export interface RedisClientOptions<
pingInterval?: number; pingInterval?: number;
} }
export interface TypeMappingOption<TYPE_MAPPING extends TypeMapping> {
/**
* Maps bettwen RESP types to JavaScript types
*/
typeMapping?: TYPE_MAPPING;
}
type WithCommands< type WithCommands<
RESP extends RespVersions, RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping TYPE_MAPPING extends TypeMapping
@@ -134,7 +136,7 @@ export type RedisClientType<
RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING> RedisClientExtensions<M, F, S, RESP, TYPE_MAPPING>
); );
type ProxyClient = RedisClient<any, any, any, any, any> & { commandOptions?: CommandOptions }; type ProxyClient = RedisClient<any, any, any, any, any>;
type NamespaceProxyClient = { self: ProxyClient }; type NamespaceProxyClient = { self: ProxyClient };
@@ -153,7 +155,7 @@ export default class RedisClient<
const transformReply = getTransformReply(command, resp); const transformReply = getTransformReply(command, resp);
return async function (this: ProxyClient, ...args: Array<unknown>) { return async function (this: ProxyClient, ...args: Array<unknown>) {
const redisArgs = command.transformArguments(...args), const redisArgs = command.transformArguments(...args),
reply = await this.sendCommand(redisArgs, this.commandOptions); reply = await this.sendCommand(redisArgs, this._commandOptions);
return transformReply ? return transformReply ?
transformReply(reply, redisArgs.preserve) : transformReply(reply, redisArgs.preserve) :
reply; reply;
@@ -164,7 +166,7 @@ export default class RedisClient<
const transformReply = getTransformReply(command, resp); const transformReply = getTransformReply(command, resp);
return async function (this: NamespaceProxyClient, ...args: Array<unknown>) { return async function (this: NamespaceProxyClient, ...args: Array<unknown>) {
const redisArgs = command.transformArguments(...args), const redisArgs = command.transformArguments(...args),
reply = await this.self.sendCommand(redisArgs, this.self.commandOptions); reply = await this.self.sendCommand(redisArgs, this.self._commandOptions);
return transformReply ? return transformReply ?
transformReply(reply, redisArgs.preserve) : transformReply(reply, redisArgs.preserve) :
reply; reply;
@@ -178,7 +180,7 @@ export default class RedisClient<
const fnArgs = fn.transformArguments(...args), const fnArgs = fn.transformArguments(...args),
reply = await this.self.sendCommand( reply = await this.self.sendCommand(
prefix.concat(fnArgs), prefix.concat(fnArgs),
this.self.commandOptions this.self._commandOptions
); );
return transformReply ? return transformReply ?
transformReply(reply, fnArgs.preserve) : transformReply(reply, fnArgs.preserve) :
@@ -192,12 +194,12 @@ export default class RedisClient<
return async function (this: ProxyClient, ...args: Array<unknown>) { return async function (this: ProxyClient, ...args: Array<unknown>) {
const scriptArgs = script.transformArguments(...args), const scriptArgs = script.transformArguments(...args),
redisArgs = prefix.concat(scriptArgs), redisArgs = prefix.concat(scriptArgs),
reply = await this.sendCommand(redisArgs, this.commandOptions).catch((err: unknown) => { reply = await this.sendCommand(redisArgs, this._commandOptions).catch((err: unknown) => {
if (!(err as Error)?.message?.startsWith?.('NOSCRIPT')) throw err; if (!(err as Error)?.message?.startsWith?.('NOSCRIPT')) throw err;
redisArgs[0] = 'EVAL'; redisArgs[0] = 'EVAL';
redisArgs[1] = script.SCRIPT; redisArgs[1] = script.SCRIPT;
return this.sendCommand(redisArgs, this.commandOptions); return this.sendCommand(redisArgs, this._commandOptions);
}); });
return transformReply ? return transformReply ?
transformReply(reply, scriptArgs.preserve) : transformReply(reply, scriptArgs.preserve) :
@@ -211,7 +213,7 @@ export default class RedisClient<
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2, RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {} TYPE_MAPPING extends TypeMapping = {}
>(config?: CommanderConfig<M, F, S, RESP> & TypeMappingOption<TYPE_MAPPING>) { >(config?: ClientCommander<M, F, S, RESP, TYPE_MAPPING>) {
const Client = attachConfig({ const Client = attachConfig({
BaseClass: RedisClient, BaseClass: RedisClient,
commands: COMMANDS, commands: COMMANDS,
@@ -282,10 +284,11 @@ export default class RedisClient<
self = this; self = this;
private readonly _options?: RedisClientOptions<M, F, S, RESP>; private readonly _options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>;
private readonly _socket: RedisSocket; private readonly _socket: RedisSocket;
private readonly _queue: RedisCommandsQueue; private readonly _queue: RedisCommandsQueue;
private _selectedDB = 0; private _selectedDB = 0;
private _commandOptions?: CommandOptions<TYPE_MAPPING>;
get options(): RedisClientOptions<M, F, S, RESP> | undefined { get options(): RedisClientOptions<M, F, S, RESP> | undefined {
return this._options; return this._options;
@@ -303,14 +306,14 @@ export default class RedisClient<
return this._queue.isPubSubActive; return this._queue.isPubSubActive;
} }
constructor(options?: RedisClientOptions<M, F, S, RESP>) { constructor(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>) {
super(); super();
this._options = this._initiateOptions(options); this._options = this._initiateOptions(options);
this._queue = this._initiateQueue(); this._queue = this._initiateQueue();
this._socket = this._initiateSocket(); this._socket = this._initiateSocket();
} }
private _initiateOptions(options?: RedisClientOptions<M, F, S, RESP>): RedisClientOptions<M, F, S, RESP> | undefined { private _initiateOptions(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>): RedisClientOptions<M, F, S, RESP, TYPE_MAPPING> | undefined {
if (options?.url) { if (options?.url) {
const parsed = RedisClient.parseURL(options.url); const parsed = RedisClient.parseURL(options.url);
if (options.socket) { if (options.socket) {
@@ -324,10 +327,8 @@ export default class RedisClient<
this._selectedDB = options.database; this._selectedDB = options.database;
} }
if (options?.typeMapping) { if (options?.commandOptions) {
(this as unknown as ProxyClient).commandOptions = { this._commandOptions = options.commandOptions;
typeMapping: options.typeMapping
};
} }
return options; return options;
@@ -462,15 +463,18 @@ export default class RedisClient<
}, this._options.pingInterval); }, this._options.pingInterval);
} }
withCommandOptions<T extends CommandOptions>(options: T) { withCommandOptions<
OPTIONS extends CommandOptions<TYPE_MAPPING>,
TYPE_MAPPING extends TypeMapping
>(options: OPTIONS) {
const proxy = Object.create(this.self); const proxy = Object.create(this.self);
proxy.commandOptions = options; proxy._commandOptions = options;
return proxy as RedisClientType< return proxy as RedisClientType<
M, M,
F, F,
S, S,
RESP, RESP,
T['typeMapping'] extends TypeMapping ? T['typeMapping'] : {} TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}
>; >;
} }
@@ -482,8 +486,8 @@ export default class RedisClient<
value: V value: V
) { ) {
const proxy = Object.create(this.self); const proxy = Object.create(this.self);
proxy.commandOptions = Object.create((this as unknown as ProxyClient).commandOptions ?? null); proxy._commandOptions = Object.create(this._commandOptions ?? null);
proxy.commandOptions[key] = value; proxy._commandOptions[key] = value;
return proxy as RedisClientType< return proxy as RedisClientType<
M, M,
F, F,
@@ -539,17 +543,11 @@ export default class RedisClient<
_RESP extends RespVersions = RESP, _RESP extends RespVersions = RESP,
_TYPE_MAPPING extends TypeMapping = TYPE_MAPPING _TYPE_MAPPING extends TypeMapping = TYPE_MAPPING
>(overrides?: Partial<RedisClientOptions<_M, _F, _S, _RESP, _TYPE_MAPPING>>) { >(overrides?: Partial<RedisClientOptions<_M, _F, _S, _RESP, _TYPE_MAPPING>>) {
const client = new (Object.getPrototypeOf(this).constructor)({ return new (Object.getPrototypeOf(this).constructor)({
...this._options, ...this._options,
commandOptions: this._commandOptions,
...overrides ...overrides
}) as RedisClientType<_M, _F, _S, _RESP, _TYPE_MAPPING>; }) as RedisClientType<_M, _F, _S, _RESP, _TYPE_MAPPING>;
const { commandOptions } = this as ProxyClient;
if (commandOptions) {
return client.withCommandOptions(commandOptions);
}
return client;
} }
connect() { connect() {
@@ -732,7 +730,7 @@ export default class RedisClient<
const promise = Promise.all( const promise = Promise.all(
commands.map(({ args }) => this._queue.addCommand(args, { commands.map(({ args }) => this._queue.addCommand(args, {
typeMapping: (this as ProxyClient).commandOptions?.typeMapping typeMapping: this._commandOptions?.typeMapping
})) }))
); );
this._scheduleWrite(); this._scheduleWrite();
@@ -750,7 +748,7 @@ export default class RedisClient<
return Promise.reject(new ClientClosedError()); return Promise.reject(new ClientClosedError());
} }
const typeMapping = (this as ProxyClient).commandOptions?.typeMapping, const typeMapping = this._commandOptions?.typeMapping,
chainId = Symbol('MULTI Chain'), chainId = Symbol('MULTI Chain'),
promises = [ promises = [
this._queue.addCommand(['MULTI'], { chainId }), this._queue.addCommand(['MULTI'], { chainId }),

View File

@@ -4,84 +4,84 @@ import { once } from 'events';
import RedisSocket, { RedisSocketOptions } from './socket'; import RedisSocket, { RedisSocketOptions } from './socket';
describe('Socket', () => { describe('Socket', () => {
function createSocket(options: RedisSocketOptions): RedisSocket { function createSocket(options: RedisSocketOptions): RedisSocket {
const socket = new RedisSocket( const socket = new RedisSocket(
() => Promise.resolve(), () => Promise.resolve(),
options options
); );
socket.on('error', () => { socket.on('error', () => {
// ignore errors // ignore errors
});
return socket;
}
describe('reconnectStrategy', () => {
it('false', async () => {
const socket = createSocket({
host: 'error',
connectTimeout: 1,
reconnectStrategy: false
});
await assert.rejects(socket.connect());
assert.equal(socket.isOpen, false);
});
it('0', async () => {
const socket = createSocket({
host: 'error',
connectTimeout: 1,
reconnectStrategy: 0
});
socket.connect();
await once(socket, 'error');
assert.equal(socket.isOpen, true);
assert.equal(socket.isReady, false);
socket.disconnect();
assert.equal(socket.isOpen, false);
});
it('custom strategy', async () => {
const numberOfRetries = 3;
const reconnectStrategy = spy((retries: number) => {
assert.equal(retries + 1, reconnectStrategy.callCount);
if (retries === numberOfRetries) return new Error(`${numberOfRetries}`);
return 0;
});
const socket = createSocket({
host: 'error',
connectTimeout: 1,
reconnectStrategy
});
await assert.rejects(socket.connect(), {
message: `${numberOfRetries}`
});
assert.equal(socket.isOpen, false);
});
it('should handle errors', async () => {
const socket = createSocket({
host: 'error',
connectTimeout: 1,
reconnectStrategy(retries: number) {
if (retries === 1) return new Error('done');
throw new Error();
}
});
await assert.rejects(socket.connect());
assert.equal(socket.isOpen, false);
});
}); });
return socket;
}
describe('reconnectStrategy', () => {
it('false', async () => {
const socket = createSocket({
host: 'error',
connectTimeout: 1,
reconnectStrategy: false
});
await assert.rejects(socket.connect());
assert.equal(socket.isOpen, false);
});
it('0', async () => {
const socket = createSocket({
host: 'error',
connectTimeout: 1,
reconnectStrategy: 0
});
socket.connect();
await once(socket, 'error');
assert.equal(socket.isOpen, true);
assert.equal(socket.isReady, false);
socket.destroy();
assert.equal(socket.isOpen, false);
});
it('custom strategy', async () => {
const numberOfRetries = 3;
const reconnectStrategy = spy((retries: number) => {
assert.equal(retries + 1, reconnectStrategy.callCount);
if (retries === numberOfRetries) return new Error(`${numberOfRetries}`);
return 0;
});
const socket = createSocket({
host: 'error',
connectTimeout: 1,
reconnectStrategy
});
await assert.rejects(socket.connect(), {
message: `${numberOfRetries}`
});
assert.equal(socket.isOpen, false);
});
it('should handle errors', async () => {
const socket = createSocket({
host: 'error',
connectTimeout: 1,
reconnectStrategy(retries: number) {
if (retries === 1) return new Error('done');
throw new Error();
}
});
await assert.rejects(socket.connect());
assert.equal(socket.isOpen, false);
});
});
}); });

View File

@@ -10,17 +10,30 @@ import { RedisMultiQueuedCommand } from '../multi-command';
import { PubSubListener } from '../client/pub-sub'; import { PubSubListener } from '../client/pub-sub';
import { ErrorReply } from '../errors'; import { ErrorReply } from '../errors';
interface ClusterCommander<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping,
POLICIES extends CommandPolicies
> extends CommanderConfig<M, F, S, RESP>{
commandOptions?: ClusterCommandOptions<TYPE_MAPPING, POLICIES>;
}
export type RedisClusterClientOptions = Omit< export type RedisClusterClientOptions = Omit<
RedisClientOptions, RedisClientOptions,
'modules' | 'functions' | 'scripts' | 'database' | 'RESP' keyof ClusterCommander<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, CommandPolicies>
>; >;
export interface RedisClusterOptions< export interface RedisClusterOptions<
M extends RedisModules = RedisModules, M extends RedisModules = RedisModules,
F extends RedisFunctions = RedisFunctions, F extends RedisFunctions = RedisFunctions,
S extends RedisScripts = RedisScripts, S extends RedisScripts = RedisScripts,
RESP extends RespVersions = RespVersions RESP extends RespVersions = RespVersions,
> extends CommanderConfig<M, F, S, RESP> { TYPE_MAPPING extends TypeMapping = TypeMapping,
POLICIES extends CommandPolicies = CommandPolicies
> extends ClusterCommander<M, F, S, RESP, TYPE_MAPPING, POLICIES> {
/** /**
* Should contain details for some of the cluster nodes that the client will use to discover * Should contain details for some of the cluster nodes that the client will use to discover
* the "cluster topology". We recommend including details for at least 3 nodes here. * the "cluster topology". We recommend including details for at least 3 nodes here.
@@ -70,11 +83,14 @@ export type RedisClusterType<
> = RedisCluster<M, F, S, RESP, TYPE_MAPPING, POLICIES> & WithCommands<RESP, TYPE_MAPPING, POLICIES>; > = RedisCluster<M, F, S, RESP, TYPE_MAPPING, POLICIES> & WithCommands<RESP, TYPE_MAPPING, POLICIES>;
// & WithModules<M> & WithFunctions<F> & WithScripts<S> // & WithModules<M> & WithFunctions<F> & WithScripts<S>
export interface ClusterCommandOptions extends CommandOptions { export interface ClusterCommandOptions<
policies?: CommandPolicies; TYPE_MAPPING extends TypeMapping = TypeMapping,
POLICIES extends CommandPolicies = CommandPolicies
> extends CommandOptions<TYPE_MAPPING> {
policies?: POLICIES;
} }
type ProxyCluster = RedisCluster<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, CommandPolicies> & { commandOptions?: ClusterCommandOptions }; type ProxyCluster = RedisCluster<any, any, any, any, any, any>;
type NamespaceProxyCluster = { self: ProxyCluster }; type NamespaceProxyCluster = { self: ProxyCluster };
@@ -113,7 +129,7 @@ export default class RedisCluster<
firstKey, firstKey,
command.IS_READ_ONLY, command.IS_READ_ONLY,
redisArgs, redisArgs,
this.commandOptions, this._commandOptions,
command.POLICIES command.POLICIES
); );
@@ -136,7 +152,7 @@ export default class RedisCluster<
firstKey, firstKey,
command.IS_READ_ONLY, command.IS_READ_ONLY,
redisArgs, redisArgs,
this.self.commandOptions, this.self._commandOptions,
command.POLICIES command.POLICIES
); );
@@ -161,7 +177,7 @@ export default class RedisCluster<
firstKey, firstKey,
fn.IS_READ_ONLY, fn.IS_READ_ONLY,
redisArgs, redisArgs,
this.self.commandOptions, this.self._commandOptions,
fn.POLICIES fn.POLICIES
); );
@@ -186,7 +202,7 @@ export default class RedisCluster<
firstKey, firstKey,
script.IS_READ_ONLY, script.IS_READ_ONLY,
redisArgs, redisArgs,
this.commandOptions, this._commandOptions,
script.POLICIES script.POLICIES
); );
@@ -200,8 +216,10 @@ export default class RedisCluster<
M extends RedisModules = {}, M extends RedisModules = {},
F extends RedisFunctions = {}, F extends RedisFunctions = {},
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2 RESP extends RespVersions = 2,
>(config?: CommanderConfig<M, F, S, RESP>) { TYPE_MAPPING extends TypeMapping = {},
POLICIES extends CommandPolicies = {}
>(config?: ClusterCommander<M, F, S, RESP, TYPE_MAPPING, POLICIES>) {
const Cluster = attachConfig({ const Cluster = attachConfig({
BaseClass: RedisCluster, BaseClass: RedisCluster,
commands: COMMANDS, commands: COMMANDS,
@@ -217,7 +235,7 @@ export default class RedisCluster<
return (options?: Omit<RedisClusterOptions, keyof Exclude<typeof config, undefined>>) => { return (options?: Omit<RedisClusterOptions, keyof Exclude<typeof config, undefined>>) => {
// returning a proxy of the client to prevent the namespaces.self to leak between proxies // returning a proxy of the client to prevent the namespaces.self to leak between proxies
// namespaces will be bootstraped on first access per proxy // namespaces will be bootstraped on first access per proxy
return Object.create(new Cluster(options)) as RedisClusterType<M, F, S, RESP>; return Object.create(new Cluster(options)) as RedisClusterType<M, F, S, RESP, TYPE_MAPPING, POLICIES>;
}; };
} }
@@ -225,15 +243,19 @@ export default class RedisCluster<
M extends RedisModules = {}, M extends RedisModules = {},
F extends RedisFunctions = {}, F extends RedisFunctions = {},
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2 RESP extends RespVersions = 2,
>(options?: RedisClusterOptions<M, F, S, RESP>) { TYPE_MAPPING extends TypeMapping = {},
POLICIES extends CommandPolicies = {}
>(options?: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>) {
return RedisCluster.factory(options)(options); return RedisCluster.factory(options)(options);
} }
private readonly _options: RedisClusterOptions<M, F, S, RESP>; private readonly _options: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>;
private readonly _slots: RedisClusterSlots<M, F, S, RESP>; private readonly _slots: RedisClusterSlots<M, F, S, RESP>;
private _commandOptions?: ClusterCommandOptions<TYPE_MAPPING, POLICIES>;
/** /**
* An array of the cluster slots, each slot contain its `master` and `replicas`. * An array of the cluster slots, each slot contain its `master` and `replicas`.
* Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica). * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica).
@@ -285,34 +307,49 @@ export default class RedisCluster<
return this._slots.isOpen; return this._slots.isOpen;
} }
constructor(options: RedisClusterOptions<M, F, S, RESP>) { constructor(options: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>) {
super(); super();
this._options = options; this._options = options;
this._slots = new RedisClusterSlots(options, this.emit.bind(this)); this._slots = new RedisClusterSlots(options, this.emit.bind(this));
if (options?.commandOptions) {
this._commandOptions = options.commandOptions;
}
} }
duplicate(overrides?: Partial<RedisClusterOptions<M, F, S>>): RedisClusterType<M, F, S> { duplicate<
_M extends RedisModules = M,
_F extends RedisFunctions = F,
_S extends RedisScripts = S,
_RESP extends RespVersions = RESP,
_TYPE_MAPPING extends TypeMapping = TYPE_MAPPING
>(overrides?: Partial<RedisClusterOptions<_M, _F, _S, _RESP, _TYPE_MAPPING>>) {
return new (Object.getPrototypeOf(this).constructor)({ return new (Object.getPrototypeOf(this).constructor)({
...this._options, ...this._options,
commandOptions: this._commandOptions,
...overrides ...overrides
}); }) as RedisClusterType<_M, _F, _S, _RESP, _TYPE_MAPPING>;
} }
connect() { connect() {
return this._slots.connect(); return this._slots.connect();
} }
withCommandOptions<T extends ClusterCommandOptions>(options: T) { withCommandOptions<
OPTIONS extends ClusterCommandOptions<TYPE_MAPPING, CommandPolicies>,
TYPE_MAPPING extends TypeMapping,
POLICIES extends CommandPolicies
>(options: OPTIONS) {
const proxy = Object.create(this); const proxy = Object.create(this);
proxy.commandOptions = options; proxy._commandOptions = options;
return proxy as RedisClusterType< return proxy as RedisClusterType<
M, M,
F, F,
S, S,
RESP, RESP,
T['typeMapping'] extends TypeMapping ? T['typeMapping'] : {}, TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {},
T['policies'] extends CommandPolicies ? T['policies'] : {} POLICIES extends CommandPolicies ? POLICIES : {}
>; >;
} }
@@ -324,8 +361,8 @@ export default class RedisCluster<
value: V value: V
) { ) {
const proxy = Object.create(this); const proxy = Object.create(this);
proxy.commandOptions = Object.create((this as unknown as ProxyCluster).commandOptions ?? null); proxy._commandOptions = Object.create(this._commandOptions ?? null);
proxy.commandOptions[key] = value; proxy._commandOptions[key] = value;
return proxy as RedisClusterType< return proxy as RedisClusterType<
M, M,
F, F,

View File

@@ -22,32 +22,29 @@ describe('ACL LOG', () => {
}); });
testUtils.testWithClient('client.aclLog', async client => { testUtils.testWithClient('client.aclLog', async client => {
// make sure to create at least one log // make sure to create one log
await Promise.all([ await assert.rejects(
client.aclSetUser('test', 'on +@all'),
client.auth({ client.auth({
username: 'test', username: 'incorrect',
password: 'test' password: 'incorrect'
}),
client.auth({
username: 'default',
password: ''
}) })
]); );
const logs = await client.aclLog(); const logs = await client.aclLog();
assert.ok(Array.isArray(logs)); assert.ok(Array.isArray(logs));
for (const log of logs) { for (const log of logs) {
assert.equal(typeof log.count, 'number'); assert.equal(typeof log.count, 'number');
assert.equal(typeof log.timestamp, 'number'); assert.equal(typeof log.reason, 'string');
assert.equal(typeof log.context, 'string');
assert.equal(typeof log.object, 'string');
assert.equal(typeof log.username, 'string'); assert.equal(typeof log.username, 'string');
assert.equal(typeof log.clientId, 'string'); assert.equal(typeof log['age-seconds'], 'number');
assert.equal(typeof log.command, 'string'); assert.equal(typeof log['client-info'], 'string');
assert.equal(typeof log.args, 'string'); if (testUtils.isVersionGreaterThan([7, 2])) {
assert.equal(typeof log.key, 'string'); assert.equal(typeof log['entry-id'], 'number');
assert.equal(typeof log.result, 'number'); assert.equal(typeof log['timestamp-created'], 'number');
assert.equal(typeof log.duration, 'number'); assert.equal(typeof log['timestamp-last-updated'], 'number');
}
} }
}, GLOBAL.SERVERS.OPEN); }, GLOBAL.SERVERS.OPEN);
}); });

View File

@@ -1,4 +1,4 @@
import { Resp2Reply } from '../RESP/types'; import { DoubleReply, Resp2Reply } from '../RESP/types';
import { ArrayReply, BlobStringReply, Command, NumberReply, TuplesToMapReply } from '../RESP/types'; import { ArrayReply, BlobStringReply, Command, NumberReply, TuplesToMapReply } from '../RESP/types';
export type AclLogReply = ArrayReply<TuplesToMapReply<[ export type AclLogReply = ArrayReply<TuplesToMapReply<[
@@ -7,8 +7,14 @@ export type AclLogReply = ArrayReply<TuplesToMapReply<[
[BlobStringReply<'context'>, BlobStringReply], [BlobStringReply<'context'>, BlobStringReply],
[BlobStringReply<'object'>, BlobStringReply], [BlobStringReply<'object'>, BlobStringReply],
[BlobStringReply<'username'>, BlobStringReply], [BlobStringReply<'username'>, BlobStringReply],
[BlobStringReply<'age-seconds'>, BlobStringReply], [BlobStringReply<'age-seconds'>, DoubleReply],
[BlobStringReply<'client-info'>, BlobStringReply] [BlobStringReply<'client-info'>, BlobStringReply],
/** added in 7.0 */
[BlobStringReply<'entry-id'>, NumberReply],
/** added in 7.0 */
[BlobStringReply<'timestamp-created'>, NumberReply],
/** added in 7.0 */
[BlobStringReply<'timestamp-last-updated'>, NumberReply]
]>>; ]>>;
export default { export default {
@@ -24,15 +30,18 @@ export default {
return args; return args;
}, },
transformReply: { transformReply: {
2: (reply: Resp2Reply<AclLogReply>) => ({ 2: (reply: Resp2Reply<AclLogReply>) => reply.map(item => ({
count: Number(reply[1]), count: item[1],
reason: reply[3], reason: item[3],
context: reply[5], context: item[5],
object: reply[7], object: item[7],
username: reply[9], username: item[9],
'age-seconds': Number(reply[11]), 'age-seconds': Number(item[11]),
'client-info': reply[13] 'client-info': item[13],
}), 'entry-id': item[15],
'timestamp-created': item[17],
'timestamp-last-updated': item[19]
})),
3: undefined as unknown as () => AclLogReply 3: undefined as unknown as () => AclLogReply
} }
} as const satisfies Command; } as const satisfies Command;

View File

@@ -4,7 +4,7 @@ export default {
FIRST_KEY_INDEX: undefined, FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true, IS_READ_ONLY: true,
transformArguments() { transformArguments() {
return ['ACL', 'USERS']; return ['ACL', 'WHOAMI'];
}, },
transformReply: undefined as unknown as () => BlobStringReply transformReply: undefined as unknown as () => BlobStringReply
} as const satisfies Command; } as const satisfies Command;

View File

@@ -1,101 +1,101 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils'; import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CLIENT_TRACKING'; import CLIENT_TRACKING from './CLIENT_TRACKING';
describe('CLIENT TRACKING', () => { describe('CLIENT TRACKING', () => {
testUtils.isVersionGreaterThanHook([6]); testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => { describe('transformArguments', () => {
describe('true', () => { describe('true', () => {
it('simple', () => { it('simple', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(true), CLIENT_TRACKING.transformArguments(true),
['CLIENT', 'TRACKING', 'ON'] ['CLIENT', 'TRACKING', 'ON']
); );
}); });
it('with REDIRECT', () => { it('with REDIRECT', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(true, { CLIENT_TRACKING.transformArguments(true, {
REDIRECT: 1 REDIRECT: 1
}), }),
['CLIENT', 'TRACKING', 'ON', 'REDIRECT', '1'] ['CLIENT', 'TRACKING', 'ON', 'REDIRECT', '1']
); );
}); });
describe('with BCAST', () => { describe('with BCAST', () => {
it('simple', () => { it('simple', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(true, { CLIENT_TRACKING.transformArguments(true, {
BCAST: true BCAST: true
}), }),
['CLIENT', 'TRACKING', 'ON', 'BCAST'] ['CLIENT', 'TRACKING', 'ON', 'BCAST']
); );
});
describe('with PREFIX', () => {
it('string', () => {
assert.deepEqual(
transformArguments(true, {
BCAST: true,
PREFIX: 'prefix'
}),
['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', 'prefix']
);
});
it('array', () => {
assert.deepEqual(
transformArguments(true, {
BCAST: true,
PREFIX: ['1', '2']
}),
['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', '1', 'PREFIX', '2']
);
});
});
});
it('with OPTIN', () => {
assert.deepEqual(
transformArguments(true, {
OPTIN: true
}),
['CLIENT', 'TRACKING', 'ON', 'OPTIN']
);
});
it('with OPTOUT', () => {
assert.deepEqual(
transformArguments(true, {
OPTOUT: true
}),
['CLIENT', 'TRACKING', 'ON', 'OPTOUT']
);
});
it('with NOLOOP', () => {
assert.deepEqual(
transformArguments(true, {
NOLOOP: true
}),
['CLIENT', 'TRACKING', 'ON', 'NOLOOP']
);
});
}); });
it('false', () => { describe('with PREFIX', () => {
it('string', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(false), CLIENT_TRACKING.transformArguments(true, {
['CLIENT', 'TRACKING', 'OFF'] BCAST: true,
PREFIX: 'prefix'
}),
['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', 'prefix']
); );
});
it('array', () => {
assert.deepEqual(
CLIENT_TRACKING.transformArguments(true, {
BCAST: true,
PREFIX: ['1', '2']
}),
['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', '1', 'PREFIX', '2']
);
});
}); });
});
it('with OPTIN', () => {
assert.deepEqual(
CLIENT_TRACKING.transformArguments(true, {
OPTIN: true
}),
['CLIENT', 'TRACKING', 'ON', 'OPTIN']
);
});
it('with OPTOUT', () => {
assert.deepEqual(
CLIENT_TRACKING.transformArguments(true, {
OPTOUT: true
}),
['CLIENT', 'TRACKING', 'ON', 'OPTOUT']
);
});
it('with NOLOOP', () => {
assert.deepEqual(
CLIENT_TRACKING.transformArguments(true, {
NOLOOP: true
}),
['CLIENT', 'TRACKING', 'ON', 'NOLOOP']
);
});
}); });
testUtils.testWithClient('client.clientTracking', async client => { it('false', () => {
assert.equal( assert.deepEqual(
await client.clientTracking(false), CLIENT_TRACKING.transformArguments(false),
'OK' ['CLIENT', 'TRACKING', 'OFF']
); );
}, GLOBAL.SERVERS.OPEN); });
});
testUtils.testWithClient('client.clientTracking', async client => {
assert.equal(
await client.clientTracking(false),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
}); });

View File

@@ -1,83 +1,86 @@
// import { RedisCommandArgument, RedisCommandArguments } from '.'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
import { RedisVariadicArgument } from './generic-transformers';
interface CommonOptions {
REDIRECT?: number;
NOLOOP?: boolean;
}
// interface CommonOptions { interface BroadcastOptions {
// REDIRECT?: number; BCAST?: boolean;
// NOLOOP?: boolean; PREFIX?: RedisVariadicArgument;
// } }
// interface BroadcastOptions { interface OptInOptions {
// BCAST?: boolean; OPTIN?: boolean;
// PREFIX?: RedisCommandArgument | Array<RedisCommandArgument>; }
// }
// interface OptInOptions { interface OptOutOptions {
// OPTIN?: boolean; OPTOUT?: boolean;
// } }
// interface OptOutOptions { type ClientTrackingOptions = CommonOptions & (
// OPTOUT?: boolean; BroadcastOptions |
// } OptInOptions |
OptOutOptions
);
// type ClientTrackingOptions = CommonOptions & ( export default {
// BroadcastOptions | FIRST_KEY_INDEX: undefined,
// OptInOptions | IS_READ_ONLY: true,
// OptOutOptions transformArguments<M extends boolean>(
// ); mode: M,
options?: M extends true ? ClientTrackingOptions : never
) {
const args: Array<RedisArgument> = [
'CLIENT',
'TRACKING',
mode ? 'ON' : 'OFF'
];
// export function transformArguments<M extends boolean>( if (mode) {
// mode: M, if (options?.REDIRECT) {
// options?: M extends true ? ClientTrackingOptions : undefined args.push(
// ): RedisCommandArguments { 'REDIRECT',
// const args: RedisCommandArguments = [ options.REDIRECT.toString()
// 'CLIENT', );
// 'TRACKING', }
// mode ? 'ON' : 'OFF'
// ];
// if (mode) { if (isBroadcast(options)) {
// if (options?.REDIRECT) { args.push('BCAST');
// args.push(
// 'REDIRECT',
// options.REDIRECT.toString()
// );
// }
// if (isBroadcast(options)) { if (options?.PREFIX) {
// args.push('BCAST'); if (Array.isArray(options.PREFIX)) {
for (const prefix of options.PREFIX) {
args.push('PREFIX', prefix);
}
} else {
args.push('PREFIX', options.PREFIX);
}
}
} else if (isOptIn(options)) {
args.push('OPTIN');
} else if (isOptOut(options)) {
args.push('OPTOUT');
}
// if (options?.PREFIX) { if (options?.NOLOOP) {
// if (Array.isArray(options.PREFIX)) { args.push('NOLOOP');
// for (const prefix of options.PREFIX) { }
// args.push('PREFIX', prefix); }
// }
// } else {
// args.push('PREFIX', options.PREFIX);
// }
// }
// } else if (isOptIn(options)) {
// args.push('OPTIN');
// } else if (isOptOut(options)) {
// args.push('OPTOUT');
// }
// if (options?.NOLOOP) { return args;
// args.push('NOLOOP'); },
// } transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
// } } as const satisfies Command;
// return args; function isBroadcast(options?: ClientTrackingOptions): options is BroadcastOptions {
// } return (options as BroadcastOptions)?.BCAST === true;
}
// function isBroadcast(options?: ClientTrackingOptions): options is BroadcastOptions { function isOptIn(options?: ClientTrackingOptions): options is OptInOptions {
// return (options as BroadcastOptions)?.BCAST === true; return (options as OptInOptions)?.OPTIN === true;
// } }
// function isOptIn(options?: ClientTrackingOptions): options is OptInOptions { function isOptOut(options?: ClientTrackingOptions): options is OptOutOptions {
// return (options as OptInOptions)?.OPTIN === true; return (options as OptOutOptions)?.OPTOUT === true;
// } }
// function isOptOut(options?: ClientTrackingOptions): options is OptOutOptions {
// return (options as OptOutOptions)?.OPTOUT === true;
// }
// export declare function transformReply(): 'OK' | Buffer;

View File

@@ -1,25 +1,26 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils'; import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CLIENT_TRACKINGINFO'; import CLIENT_TRACKINGINFO from './CLIENT_TRACKINGINFO';
import { RESP_TYPES } from '../RESP/decoder';
describe('CLIENT TRACKINGINFO', () => { describe('CLIENT TRACKINGINFO', () => {
testUtils.isVersionGreaterThanHook([6, 2]); testUtils.isVersionGreaterThanHook([6, 2]);
it('transformArguments', () => { it('transformArguments', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(), CLIENT_TRACKINGINFO.transformArguments(),
['CLIENT', 'TRACKINGINFO'] ['CLIENT', 'TRACKINGINFO']
); );
}); });
testUtils.testWithClient('client.clientTrackingInfo', async client => { testUtils.testWithClient('client.clientTrackingInfo', async client => {
assert.deepEqual( assert.deepEqual(
await client.clientTrackingInfo(), await client.clientTrackingInfo(),
{ {
flags: new Set(['off']), flags: ['off'],
redirect: -1, redirect: -1,
prefixes: [] prefixes: []
} }
); );
}, GLOBAL.SERVERS.OPEN); }, GLOBAL.SERVERS.OPEN);
}); });

View File

@@ -1,28 +1,23 @@
// import { RedisCommandArguments } from '.'; import { TuplesToMapReply, BlobStringReply, SetReply, NumberReply, ArrayReply, Resp2Reply, Command } from '../RESP/types';
// export function transformArguments(): RedisCommandArguments { type TrackingInfo = TuplesToMapReply<[
// return ['CLIENT', 'TRACKINGINFO']; [BlobStringReply<'flags'>, SetReply<BlobStringReply>],
// } [BlobStringReply<'redirect'>, NumberReply],
[BlobStringReply<'prefixes'>, ArrayReply<BlobStringReply>]
]>;
// type RawReply = [ export default {
// 'flags', FIRST_KEY_INDEX: undefined,
// Array<string>, IS_READ_ONLY: true,
// 'redirect', transformArguments() {
// number, return ['CLIENT', 'TRACKINGINFO'];
// 'prefixes', },
// Array<string> transformReply: {
// ]; 2: (reply: Resp2Reply<TrackingInfo>) => ({
flags: reply[1],
// interface Reply { redirect: reply[3],
// flags: Set<string>; prefixes: reply[5]
// redirect: number; }),
// prefixes: Array<string>; 3: undefined as unknown as () => TrackingInfo
// } }
} as const satisfies Command;
// export function transformReply(reply: RawReply): Reply {
// return {
// flags: new Set(reply[1]),
// redirect: reply[3],
// prefixes: reply[5]
// };
// }

View File

@@ -1,21 +1,21 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils'; import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CLIENT_UNPAUSE'; import CLIENT_UNPAUSE from './CLIENT_UNPAUSE';
describe('CLIENT UNPAUSE', () => { describe('CLIENT UNPAUSE', () => {
testUtils.isVersionGreaterThanHook([6, 2]); testUtils.isVersionGreaterThanHook([6, 2]);
it('transformArguments', () => { it('transformArguments', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(), CLIENT_UNPAUSE.transformArguments(),
['CLIENT', 'UNPAUSE'] ['CLIENT', 'UNPAUSE']
); );
}); });
testUtils.testWithClient('client.unpause', async client => { testUtils.testWithClient('client.clientUnpause', async client => {
assert.equal( assert.equal(
await client.clientUnpause(), await client.clientUnpause(),
'OK' 'OK'
); );
}, GLOBAL.SERVERS.OPEN); }, GLOBAL.SERVERS.OPEN);
}); });

View File

@@ -1,20 +1,20 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_ADDSLOTS'; import CLUSTER_ADDSLOTS from './CLUSTER_ADDSLOTS';
describe('CLUSTER ADDSLOTS', () => { describe('CLUSTER ADDSLOTS', () => {
describe('transformArguments', () => { describe('transformArguments', () => {
it('single', () => { it('single', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(0), CLUSTER_ADDSLOTS.transformArguments(0),
['CLUSTER', 'ADDSLOTS', '0'] ['CLUSTER', 'ADDSLOTS', '0']
); );
});
it('multiple', () => {
assert.deepEqual(
transformArguments([0, 1]),
['CLUSTER', 'ADDSLOTS', '0', '1']
);
});
}); });
it('multiple', () => {
assert.deepEqual(
CLUSTER_ADDSLOTS.transformArguments([0, 1]),
['CLUSTER', 'ADDSLOTS', '0', '1']
);
});
});
}); });

View File

@@ -1,29 +1,32 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_ADDSLOTSRANGE'; import testUtils from '../test-utils';
import CLUSTER_ADDSLOTSRANGE from './CLUSTER_ADDSLOTSRANGE';
describe('CLUSTER ADDSLOTSRANGE', () => { describe('CLUSTER ADDSLOTSRANGE', () => {
describe('transformArguments', () => { testUtils.isVersionGreaterThanHook([7, 0]);
it('single', () => {
assert.deepEqual(
transformArguments({
start: 0,
end: 1
}),
['CLUSTER', 'ADDSLOTSRANGE', '0', '1']
);
});
it('multiple', () => { describe('transformArguments', () => {
assert.deepEqual( it('single', () => {
transformArguments([{ assert.deepEqual(
start: 0, CLUSTER_ADDSLOTSRANGE.transformArguments({
end: 1 start: 0,
}, { end: 1
start: 2, }),
end: 3 ['CLUSTER', 'ADDSLOTSRANGE', '0', '1']
}]), );
['CLUSTER', 'ADDSLOTSRANGE', '0', '1', '2', '3']
);
});
}); });
it('multiple', () => {
assert.deepEqual(
CLUSTER_ADDSLOTSRANGE.transformArguments([{
start: 0,
end: 1
}, {
start: 2,
end: 3
}]),
['CLUSTER', 'ADDSLOTSRANGE', '0', '1', '2', '3']
);
});
});
}); });

View File

@@ -1,20 +1,20 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils'; import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CLUSTER_BUMPEPOCH'; import CLUSTER_BUMPEPOCH from './CLUSTER_BUMPEPOCH';
describe('CLUSTER BUMPEPOCH', () => { describe('CLUSTER BUMPEPOCH', () => {
it('transformArguments', () => { it('transformArguments', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(), CLUSTER_BUMPEPOCH.transformArguments(),
['CLUSTER', 'BUMPEPOCH'] ['CLUSTER', 'BUMPEPOCH']
); );
}); });
testUtils.testWithCluster('clusterNode.clusterBumpEpoch', async cluster => { testUtils.testWithCluster('clusterNode.clusterBumpEpoch', async cluster => {
const client = await cluster.nodeClient(cluster.masters[0]); const client = await cluster.nodeClient(cluster.masters[0]);
assert.equal( assert.equal(
typeof await client.clusterBumpEpoch(), typeof await client.clusterBumpEpoch(),
'string' 'string'
); );
}, GLOBAL.SERVERS.OPEN); }, GLOBAL.SERVERS.OPEN);
}); });

View File

@@ -1,20 +1,20 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils'; import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CLUSTER_KEYSLOT'; import CLUSTER_KEYSLOT from './CLUSTER_KEYSLOT';
describe('CLUSTER KEYSLOT', () => { describe('CLUSTER KEYSLOT', () => {
it('transformArguments', () => { it('transformArguments', () => {
assert.deepEqual( assert.deepEqual(
transformArguments('key'), CLUSTER_KEYSLOT.transformArguments('key'),
['CLUSTER', 'KEYSLOT', 'key'] ['CLUSTER', 'KEYSLOT', 'key']
); );
}); });
testUtils.testWithCluster('clusterNode.clusterKeySlot', async cluster => { testUtils.testWithCluster('clusterNode.clusterKeySlot', async cluster => {
const client = await cluster.nodeClient(cluster.masters[0]); const client = await cluster.nodeClient(cluster.masters[0]);
assert.equal( assert.equal(
typeof await client.clusterKeySlot('key'), typeof await client.clusterKeySlot('key'),
'number' 'number'
); );
}, GLOBAL.CLUSTERS.OPEN); }, GLOBAL.CLUSTERS.OPEN);
}); });

View File

@@ -1,11 +1,11 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_MEET'; import CLUSTER_MEET from './CLUSTER_MEET';
describe('CLUSTER MEET', () => { describe('CLUSTER MEET', () => {
it('transformArguments', () => { it('transformArguments', () => {
assert.deepEqual( assert.deepEqual(
transformArguments('127.0.0.1', 6379), CLUSTER_MEET.transformArguments('127.0.0.1', 6379),
['CLUSTER', 'MEET', '127.0.0.1', '6379'] ['CLUSTER', 'MEET', '127.0.0.1', '6379']
); );
}); });
}); });

View File

@@ -4,5 +4,5 @@ export default {
transformArguments(host: string, port: number) { transformArguments(host: string, port: number) {
return ['CLUSTER', 'MEET', host, port.toString()]; return ['CLUSTER', 'MEET', host, port.toString()];
}, },
transformReply: undefined as unknown as () => SimpleStringReply transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command; } as const satisfies Command;

View File

@@ -1,11 +1,11 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_REPLICATE'; import CLUSTER_REPLICATE from './CLUSTER_REPLICATE';
describe('CLUSTER REPLICATE', () => { describe('CLUSTER REPLICATE', () => {
it('transformArguments', () => { it('transformArguments', () => {
assert.deepEqual( assert.deepEqual(
transformArguments('0'), CLUSTER_REPLICATE.transformArguments('0'),
['CLUSTER', 'REPLICATE', '0'] ['CLUSTER', 'REPLICATE', '0']
); );
}); });
}); });

View File

@@ -1,20 +1,22 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_RESET'; import CLUSTER_RESET from './CLUSTER_RESET';
describe('CLUSTER RESET', () => { describe('CLUSTER RESET', () => {
describe('transformArguments', () => { describe('transformArguments', () => {
it('simple', () => { it('simple', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(), CLUSTER_RESET.transformArguments(),
['CLUSTER', 'RESET'] ['CLUSTER', 'RESET']
); );
});
it('with mode', () => {
assert.deepEqual(
transformArguments('HARD'),
['CLUSTER', 'RESET', 'HARD']
);
});
}); });
it('with mode', () => {
assert.deepEqual(
CLUSTER_RESET.transformArguments({
mode: 'HARD'
}),
['CLUSTER', 'RESET', 'HARD']
);
});
});
}); });

View File

@@ -1,11 +1,20 @@
export function transformArguments(mode?: 'HARD' | 'SOFT'): Array<string> { import { SimpleStringReply, Command } from '../RESP/types';
export interface ClusterResetOptions {
mode?: 'HARD' | 'SOFT';
}
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(options?: ClusterResetOptions) {
const args = ['CLUSTER', 'RESET']; const args = ['CLUSTER', 'RESET'];
if (mode) { if (options?.mode) {
args.push(mode); args.push(options.mode);
} }
return args; return args;
} },
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
export declare function transformReply(): string; } as const satisfies Command;

View File

@@ -1,20 +1,20 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils'; import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CLUSTER_SAVECONFIG'; import CLUSTER_SAVECONFIG from './CLUSTER_SAVECONFIG';
describe('CLUSTER SAVECONFIG', () => { describe('CLUSTER SAVECONFIG', () => {
it('transformArguments', () => { it('transformArguments', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(), CLUSTER_SAVECONFIG.transformArguments(),
['CLUSTER', 'SAVECONFIG'] ['CLUSTER', 'SAVECONFIG']
); );
}); });
testUtils.testWithCluster('clusterNode.clusterSaveConfig', async cluster => { testUtils.testWithCluster('clusterNode.clusterSaveConfig', async cluster => {
const client = await cluster.nodeClient(cluster.masters[0]); const client = await cluster.nodeClient(cluster.masters[0]);
assert.equal( assert.equal(
await client.clusterSaveConfig(), await client.clusterSaveConfig(),
'OK' 'OK'
); );
}, GLOBAL.CLUSTERS.OPEN); }, GLOBAL.CLUSTERS.OPEN);
}); });

View File

@@ -1,5 +1,11 @@
export function transformArguments(): Array<string> { import { SimpleStringReply, Command } from '../RESP/types';
return ['CLUSTER', 'SAVECONFIG'];
} export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['CLUSTER', 'SAVECONFIG'];
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;
export declare function transformReply(): 'OK';

View File

@@ -1,11 +1,11 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_SET-CONFIG-EPOCH'; import CLUSTER_SET_CONFIG_EPOCH from './CLUSTER_SET-CONFIG-EPOCH';
describe('CLUSTER SET-CONFIG-EPOCH', () => { describe('CLUSTER SET-CONFIG-EPOCH', () => {
it('transformArguments', () => { it('transformArguments', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(0), CLUSTER_SET_CONFIG_EPOCH.transformArguments(0),
['CLUSTER', 'SET-CONFIG-EPOCH', '0'] ['CLUSTER', 'SET-CONFIG-EPOCH', '0']
); );
}); });
}); });

View File

@@ -1,5 +1,10 @@
export function transformArguments(configEpoch: number): Array<string> { import { SimpleStringReply, Command } from '../RESP/types';
return ['CLUSTER', 'SET-CONFIG-EPOCH', configEpoch.toString()];
}
export declare function transformReply(): 'OK'; export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(configEpoch: number) {
return ['CLUSTER', 'SET-CONFIG-EPOCH', configEpoch.toString() ];
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,20 +1,20 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { ClusterSlotStates, transformArguments } from './CLUSTER_SETSLOT'; import CLUSTER_SETSLOT, { CLUSTER_SLOT_STATES } from './CLUSTER_SETSLOT';
describe('CLUSTER SETSLOT', () => { describe('CLUSTER SETSLOT', () => {
describe('transformArguments', () => { describe('transformArguments', () => {
it('simple', () => { it('simple', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(0, ClusterSlotStates.IMPORTING), CLUSTER_SETSLOT.transformArguments(0, CLUSTER_SLOT_STATES.IMPORTING),
['CLUSTER', 'SETSLOT', '0', 'IMPORTING'] ['CLUSTER', 'SETSLOT', '0', 'IMPORTING']
); );
});
it('with nodeId', () => {
assert.deepEqual(
transformArguments(0, ClusterSlotStates.IMPORTING, 'nodeId'),
['CLUSTER', 'SETSLOT', '0', 'IMPORTING', 'nodeId']
);
});
}); });
it('with nodeId', () => {
assert.deepEqual(
CLUSTER_SETSLOT.transformArguments(0, CLUSTER_SLOT_STATES.IMPORTING, 'nodeId'),
['CLUSTER', 'SETSLOT', '0', 'IMPORTING', 'nodeId']
);
});
});
}); });

View File

@@ -1,22 +1,25 @@
export enum ClusterSlotStates { import { SimpleStringReply, Command } from '../RESP/types';
IMPORTING = 'IMPORTING',
MIGRATING = 'MIGRATING',
STABLE = 'STABLE',
NODE = 'NODE'
}
export function transformArguments( export const CLUSTER_SLOT_STATES = {
slot: number, IMPORTING: 'IMPORTING',
state: ClusterSlotStates, MIGRATING: 'MIGRATING',
nodeId?: string STABLE: 'STABLE',
): Array<string> { NODE: 'NODE'
} as const;
export type ClusterSlotStates = typeof CLUSTER_SLOT_STATES[keyof typeof CLUSTER_SLOT_STATES];
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(slot: number, state: ClusterSlotStates, nodeId?: string) {
const args = ['CLUSTER', 'SETSLOT', slot.toString(), state]; const args = ['CLUSTER', 'SETSLOT', slot.toString(), state];
if (nodeId) { if (nodeId) {
args.push(nodeId); args.push(nodeId);
} }
return args; return args;
} },
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
export declare function transformReply(): 'OK'; } as const satisfies Command;

View File

@@ -1,76 +1,30 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { transformArguments, transformReply } from './CLUSTER_SLOTS'; import testUtils, { GLOBAL } from '../test-utils';
import CLUSTER_SLOTS from './CLUSTER_SLOTS';
describe('CLUSTER SLOTS', () => { describe('CLUSTER SLOTS', () => {
it('transformArguments', () => { it('transformArguments', () => {
assert.deepEqual( assert.deepEqual(
transformArguments(), CLUSTER_SLOTS.transformArguments(),
['CLUSTER', 'SLOTS'] ['CLUSTER', 'SLOTS']
); );
}); });
it('transformReply', () => { testUtils.testWithCluster('clusterNode.clusterSlots', async cluster => {
assert.deepEqual( const client = await cluster.nodeClient(cluster.masters[0]),
transformReply([ slots = await client.clusterSlots();
[ assert.ok(Array.isArray(slots));
0, for (const { from, to, master, replicas } of slots) {
5460, assert.equal(typeof from, 'number');
['127.0.0.1', 30001, '09dbe9720cda62f7865eabc5fd8857c5d2678366'], assert.equal(typeof to, 'number');
['127.0.0.1', 30004, '821d8ca00d7ccf931ed3ffc7e3db0599d2271abf'] assert.equal(typeof master.host, 'string');
], assert.equal(typeof master.port, 'number');
[ assert.equal(typeof master.id, 'string');
5461, for (const replica of replicas) {
10922, assert.equal(typeof replica.host, 'string');
['127.0.0.1', 30002, 'c9d93d9f2c0c524ff34cc11838c2003d8c29e013'], assert.equal(typeof replica.port, 'number');
['127.0.0.1', 30005, 'faadb3eb99009de4ab72ad6b6ed87634c7ee410f'] assert.equal(typeof replica.id, 'string');
], }
[ }
10923, }, GLOBAL.CLUSTERS.OPEN);
16383,
['127.0.0.1', 30003, '044ec91f325b7595e76dbcb18cc688b6a5b434a1'],
['127.0.0.1', 30006, '58e6e48d41228013e5d9c1c37c5060693925e97e']
]
]),
[{
from: 0,
to: 5460,
master: {
ip: '127.0.0.1',
port: 30001,
id: '09dbe9720cda62f7865eabc5fd8857c5d2678366'
},
replicas: [{
ip: '127.0.0.1',
port: 30004,
id: '821d8ca00d7ccf931ed3ffc7e3db0599d2271abf'
}]
}, {
from: 5461,
to: 10922,
master: {
ip: '127.0.0.1',
port: 30002,
id: 'c9d93d9f2c0c524ff34cc11838c2003d8c29e013'
},
replicas: [{
ip: '127.0.0.1',
port: 30005,
id: 'faadb3eb99009de4ab72ad6b6ed87634c7ee410f'
}]
}, {
from: 10923,
to: 16383,
master: {
ip: '127.0.0.1',
port: 30003,
id: '044ec91f325b7595e76dbcb18cc688b6a5b434a1'
},
replicas: [{
ip: '127.0.0.1',
port: 30006,
id: '58e6e48d41228013e5d9c1c37c5060693925e97e'
}]
}]
);
});
}); });

View File

@@ -30,7 +30,9 @@ describe('FUNCTION RESTORE', () => {
await client.withTypeMapping({ await client.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: Buffer [RESP_TYPES.BLOB_STRING]: Buffer
}).functionDump(), }).functionDump(),
'FLUSH' {
mode: 'REPLACE'
}
), ),
'OK' 'OK'
); );

View File

@@ -36,11 +36,33 @@ import CLIENT_LIST from './CLIENT_LIST';
import CLIENT_NO_EVICT from './CLIENT_NO-EVICT'; import CLIENT_NO_EVICT from './CLIENT_NO-EVICT';
import CLIENT_PAUSE from './CLIENT_PAUSE'; import CLIENT_PAUSE from './CLIENT_PAUSE';
import CLIENT_SETNAME from './CLIENT_SETNAME'; import CLIENT_SETNAME from './CLIENT_SETNAME';
import CLIENT_TRACKING from './CLIENT_TRACKING';
import CLIENT_TRACKINGINFO from './CLIENT_TRACKINGINFO';
import CLIENT_UNPAUSE from './CLIENT_UNPAUSE';
import CLUSTER_ADDSLOTS from './CLUSTER_ADDSLOTS'; import CLUSTER_ADDSLOTS from './CLUSTER_ADDSLOTS';
import CLUSTER_SLOTS from './CLUSTER_SLOTS'; import CLUSTER_ADDSLOTSRANGE from './CLUSTER_ADDSLOTSRANGE';
import CLUSTER_BUMPEPOCH from './CLUSTER_BUMPEPOCH';
import CLUSTER_COUNT_FAILURE_REPORTS from './CLUSTER_COUNT-FAILURE-REPORTS';
import CLUSTER_COUNTKEYSINSLOT from './CLUSTER_COUNTKEYSINSLOT';
import CLUSTER_DELSLOTS from './CLUSTER_DELSLOTS';
import CLUSTER_DELSLOTSRANGE from './CLUSTER_DELSLOTSRANGE';
import CLUSTER_FAILOVER from './CLUSTER_FAILOVER';
import CLUSTER_FLUSHSLOTS from './CLUSTER_FLUSHSLOTS';
import CLUSTER_FORGET from './CLUSTER_FORGET';
import CLUSTER_GETKEYSINSLOT from './CLUSTER_GETKEYSINSLOT';
// import CLUSTER_INFO from './CLUSTER_INFO';
import CLUSTER_KEYSLOT from './CLUSTER_KEYSLOT';
// import CLUSTER_LINKS from './CLUSTER_LINKS';
import CLUSTER_MEET from './CLUSTER_MEET'; import CLUSTER_MEET from './CLUSTER_MEET';
import CLUSTER_MYID from './CLUSTER_MYID'; import CLUSTER_MYID from './CLUSTER_MYID';
// import CLUSTER_NODES from './CLUSTER_NODES';
// import CLUSTER_REPLICAS from './CLUSTER_REPLICAS';
import CLUSTER_REPLICATE from './CLUSTER_REPLICATE'; import CLUSTER_REPLICATE from './CLUSTER_REPLICATE';
import CLUSTER_RESET from './CLUSTER_RESET';
import CLUSTER_SAVECONFIG from './CLUSTER_SAVECONFIG';
import CLUSTER_SET_CONFIG_EPOCH from './CLUSTER_SET-CONFIG-EPOCH';
import CLUSTER_SETSLOT from './CLUSTER_SETSLOT';
import CLUSTER_SLOTS from './CLUSTER_SLOTS';
import COPY from './COPY'; import COPY from './COPY';
import DBSIZE from './DBSIZE'; import DBSIZE from './DBSIZE';
import DECR from './DECR'; import DECR from './DECR';
@@ -90,7 +112,7 @@ import FUNCTION_KILL from './FUNCTION_KILL';
import FUNCTION_LIST_WITHCODE from './FUNCTION_LIST_WITHCODE'; import FUNCTION_LIST_WITHCODE from './FUNCTION_LIST_WITHCODE';
import FUNCTION_LIST from './FUNCTION_LIST'; import FUNCTION_LIST from './FUNCTION_LIST';
import FUNCTION_LOAD from './FUNCTION_LOAD'; import FUNCTION_LOAD from './FUNCTION_LOAD';
// import FUNCTION_RESTORE from './FUNCTION_RESTORE'; import FUNCTION_RESTORE from './FUNCTION_RESTORE';
// import FUNCTION_STATS from './FUNCTION_STATS'; // import FUNCTION_STATS from './FUNCTION_STATS';
import HDEL from './HDEL'; import HDEL from './HDEL';
import HELLO from './HELLO'; import HELLO from './HELLO';
@@ -297,11 +319,33 @@ type CLIENT_LIST = typeof import('./CLIENT_LIST').default;
type CLIENT_NO_EVICT = typeof import('./CLIENT_NO-EVICT').default; type CLIENT_NO_EVICT = typeof import('./CLIENT_NO-EVICT').default;
type CLIENT_PAUSE = typeof import('./CLIENT_PAUSE').default; type CLIENT_PAUSE = typeof import('./CLIENT_PAUSE').default;
type CLIENT_SETNAME = typeof import('./CLIENT_SETNAME').default; type CLIENT_SETNAME = typeof import('./CLIENT_SETNAME').default;
type CLIENT_TRACKING = typeof import('./CLIENT_TRACKING').default;
type CLIENT_TRACKINGINFO = typeof import('./CLIENT_TRACKINGINFO').default;
type CLIENT_UNPAUSE = typeof import('./CLIENT_UNPAUSE').default;
type CLUSTER_ADDSLOTS = typeof import('./CLUSTER_ADDSLOTS').default; type CLUSTER_ADDSLOTS = typeof import('./CLUSTER_ADDSLOTS').default;
type CLUSTER_SLOTS = typeof import('./CLUSTER_SLOTS').default; type CLUSTER_ADDSLOTSRANGE = typeof import('./CLUSTER_ADDSLOTSRANGE').default;
type CLUSTER_BUMPEPOCH = typeof import('./CLUSTER_BUMPEPOCH').default;
type CLUSTER_COUNT_FAILURE_REPORTS = typeof import('./CLUSTER_COUNT-FAILURE-REPORTS').default;
type CLUSTER_COUNTKEYSINSLOT = typeof import('./CLUSTER_COUNTKEYSINSLOT').default;
type CLUSTER_DELSLOTS = typeof import('./CLUSTER_DELSLOTS').default;
type CLUSTER_DELSLOTSRANGE = typeof import('./CLUSTER_DELSLOTSRANGE').default;
type CLUSTER_FAILOVER = typeof import('./CLUSTER_FAILOVER').default;
type CLUSTER_FLUSHSLOTS = typeof import('./CLUSTER_FLUSHSLOTS').default;
type CLUSTER_FORGET = typeof import('./CLUSTER_FORGET').default;
type CLUSTER_GETKEYSINSLOT = typeof import('./CLUSTER_GETKEYSINSLOT').default;
// type CLUSTER_INFO = typeof import('./CLUSTER_INFO').default;
type CLUSTER_KEYSLOT = typeof import('./CLUSTER_KEYSLOT').default;
// type CLUSTER_LINKS = typeof import('./CLUSTER_LINKS').default;
type CLUSTER_MEET = typeof import('./CLUSTER_MEET').default; type CLUSTER_MEET = typeof import('./CLUSTER_MEET').default;
type CLUSTER_MYID = typeof import('./CLUSTER_MYID').default; type CLUSTER_MYID = typeof import('./CLUSTER_MYID').default;
// type CLUSTER_NODES = typeof import('./CLUSTER_NODES').default;
// type CLUSTER_REPLICAS = typeof import('./CLUSTER_REPLICAS').default;
type CLUSTER_REPLICATE = typeof import('./CLUSTER_REPLICATE').default; type CLUSTER_REPLICATE = typeof import('./CLUSTER_REPLICATE').default;
type CLUSTER_RESET = typeof import('./CLUSTER_RESET').default;
type CLUSTER_SAVECONFIG = typeof import('./CLUSTER_SAVECONFIG').default;
type CLUSTER_SET_CONFIG_EPOCH = typeof import('./CLUSTER_SET-CONFIG-EPOCH').default;
type CLUSTER_SETSLOT = typeof import('./CLUSTER_SETSLOT').default;
type CLUSTER_SLOTS = typeof import('./CLUSTER_SLOTS').default;
type COPY = typeof import('./COPY').default; type COPY = typeof import('./COPY').default;
type DBSIZE = typeof DBSIZE; type DBSIZE = typeof DBSIZE;
type DECR = typeof import('./DECR').default; type DECR = typeof import('./DECR').default;
@@ -351,7 +395,7 @@ type FUNCTION_KILL = typeof import('./FUNCTION_KILL').default;
type FUNCTION_LIST_WITHCODE = typeof import('./FUNCTION_LIST_WITHCODE').default; type FUNCTION_LIST_WITHCODE = typeof import('./FUNCTION_LIST_WITHCODE').default;
type FUNCTION_LIST = typeof import('./FUNCTION_LIST').default; type FUNCTION_LIST = typeof import('./FUNCTION_LIST').default;
type FUNCTION_LOAD = typeof import('./FUNCTION_LOAD').default; type FUNCTION_LOAD = typeof import('./FUNCTION_LOAD').default;
// type FUNCTION_RESTORE = typeof import('./FUNCTION_RESTORE').default; type FUNCTION_RESTORE = typeof import('./FUNCTION_RESTORE').default;
// type FUNCTION_STATS = typeof import('./FUNCTION_STATS').default; // type FUNCTION_STATS = typeof import('./FUNCTION_STATS').default;
type HDEL = typeof import('./HDEL').default; type HDEL = typeof import('./HDEL').default;
type HELLO = typeof import('./HELLO').default; type HELLO = typeof import('./HELLO').default;
@@ -596,16 +640,60 @@ type Commands = {
clientPause: CLIENT_PAUSE; clientPause: CLIENT_PAUSE;
CLIENT_SETNAME: CLIENT_SETNAME; CLIENT_SETNAME: CLIENT_SETNAME;
clientSetName: CLIENT_SETNAME; clientSetName: CLIENT_SETNAME;
CLIENT_TRACKING: CLIENT_TRACKING;
clientTracking: CLIENT_TRACKING;
CLIENT_TRACKINGINFO: CLIENT_TRACKINGINFO;
clientTrackingInfo: CLIENT_TRACKINGINFO;
CLIENT_UNPAUSE: CLIENT_UNPAUSE;
clientUnpause: CLIENT_UNPAUSE;
CLUSTER_ADDSLOTS: CLUSTER_ADDSLOTS; CLUSTER_ADDSLOTS: CLUSTER_ADDSLOTS;
clusterAddSlots: CLUSTER_ADDSLOTS; clusterAddSlots: CLUSTER_ADDSLOTS;
CLUSTER_SLOTS: CLUSTER_SLOTS; CLUSTER_ADDSLOTSRANGE: CLUSTER_ADDSLOTSRANGE;
clusterSlots: CLUSTER_SLOTS; clusterAddSlotsRange: CLUSTER_ADDSLOTSRANGE;
CLUSTER_BUMPEPOCH: CLUSTER_BUMPEPOCH;
clusterBumpEpoch: CLUSTER_BUMPEPOCH;
'CLUSTER_COUNT-FAILURE-REPORTS': CLUSTER_COUNT_FAILURE_REPORTS;
clusterCountFailureReports: CLUSTER_COUNT_FAILURE_REPORTS;
CLUSTER_COUNTKEYSINSLOT: CLUSTER_COUNTKEYSINSLOT;
clusterCountKeysInSlot: CLUSTER_COUNTKEYSINSLOT;
CLUSTER_DELSLOTS: CLUSTER_DELSLOTS;
clusterDelSlots: CLUSTER_DELSLOTS;
CLUSTER_DELSLOTSRANGE: CLUSTER_DELSLOTSRANGE;
clusterDelSlotsRange: CLUSTER_DELSLOTSRANGE;
CLUSTER_FAILOVER: CLUSTER_FAILOVER;
clusterFailover: CLUSTER_FAILOVER;
CLUSTER_FLUSHSLOTS: CLUSTER_FLUSHSLOTS;
clusterFlushSlots: CLUSTER_FLUSHSLOTS;
CLUSTER_FORGET: CLUSTER_FORGET;
clusterForget: CLUSTER_FORGET;
CLUSTER_GETKEYSINSLOT: CLUSTER_GETKEYSINSLOT;
clusterGetKeysInSlot: CLUSTER_GETKEYSINSLOT;
// CLUSTER_INFO: CLUSTER_INFO;
// clusterInfo: CLUSTER_INFO;
CLUSTER_KEYSLOT: CLUSTER_KEYSLOT;
clusterKeySlot: CLUSTER_KEYSLOT;
// CLUSTER_LINKS: CLUSTER_LINKS;
// clusterLinks: CLUSTER_LINKS;
CLUSTER_MEET: CLUSTER_MEET; CLUSTER_MEET: CLUSTER_MEET;
clusterMeet: CLUSTER_MEET; clusterMeet: CLUSTER_MEET;
CLUSTER_MYID: CLUSTER_MYID; CLUSTER_MYID: CLUSTER_MYID;
clusterMyId: CLUSTER_MYID; clusterMyId: CLUSTER_MYID;
// CLUSTER_NODES: CLUSTER_NODES;
// clusterNodes: CLUSTER_NODES;
// CLUSTER_REPLICAS: CLUSTER_REPLICAS;
// clusterReplicas: CLUSTER_REPLICAS;
CLUSTER_REPLICATE: CLUSTER_REPLICATE; CLUSTER_REPLICATE: CLUSTER_REPLICATE;
clusterReplicate: CLUSTER_REPLICATE; clusterReplicate: CLUSTER_REPLICATE;
CLUSTER_RESET: CLUSTER_RESET;
clusterReset: CLUSTER_RESET;
CLUSTER_SAVECONFIG: CLUSTER_SAVECONFIG;
clusterSaveConfig: CLUSTER_SAVECONFIG;
'CLUSTER_SET-CONFIG-EPOCH': CLUSTER_SET_CONFIG_EPOCH;
clusterSetConfigEpoch: CLUSTER_SET_CONFIG_EPOCH;
CLUSTER_SETSLOT: CLUSTER_SETSLOT;
clusterSetSlot: CLUSTER_SETSLOT;
CLUSTER_SLOTS: CLUSTER_SLOTS;
clusterSlots: CLUSTER_SLOTS;
COPY: COPY; COPY: COPY;
copy: COPY; copy: COPY;
DBSIZE: DBSIZE; DBSIZE: DBSIZE;
@@ -658,8 +746,8 @@ type Commands = {
functionList: FUNCTION_LIST; functionList: FUNCTION_LIST;
FUNCTION_LOAD: FUNCTION_LOAD; FUNCTION_LOAD: FUNCTION_LOAD;
functionLoad: FUNCTION_LOAD; functionLoad: FUNCTION_LOAD;
// FUNCTION_RESTORE: FUNCTION_RESTORE; FUNCTION_RESTORE: FUNCTION_RESTORE;
// functionRestore: FUNCTION_RESTORE; functionRestore: FUNCTION_RESTORE;
// FUNCTION_STATS: FUNCTION_STATS; // FUNCTION_STATS: FUNCTION_STATS;
// functionStats: FUNCTION_STATS; // functionStats: FUNCTION_STATS;
GEOADD: GEOADD; GEOADD: GEOADD;
@@ -1119,16 +1207,60 @@ export default {
clientPause: CLIENT_PAUSE, clientPause: CLIENT_PAUSE,
CLIENT_SETNAME, CLIENT_SETNAME,
clientSetName: CLIENT_SETNAME, clientSetName: CLIENT_SETNAME,
CLIENT_TRACKING,
clientTracking: CLIENT_TRACKING,
CLIENT_TRACKINGINFO,
clientTrackingInfo: CLIENT_TRACKINGINFO,
CLIENT_UNPAUSE,
clientUnpause: CLIENT_UNPAUSE,
CLUSTER_ADDSLOTS, CLUSTER_ADDSLOTS,
clusterAddSlots: CLUSTER_ADDSLOTS, clusterAddSlots: CLUSTER_ADDSLOTS,
CLUSTER_SLOTS, CLUSTER_ADDSLOTSRANGE,
clusterSlots: CLUSTER_SLOTS, clusterAddSlotsRange: CLUSTER_ADDSLOTSRANGE,
CLUSTER_BUMPEPOCH,
clusterBumpEpoch: CLUSTER_BUMPEPOCH,
'CLUSTER_COUNT-FAILURE-REPORTS': CLUSTER_COUNT_FAILURE_REPORTS,
clusterCountFailureReports: CLUSTER_COUNT_FAILURE_REPORTS,
CLUSTER_COUNTKEYSINSLOT,
clusterCountKeysInSlot: CLUSTER_COUNTKEYSINSLOT,
CLUSTER_DELSLOTS,
clusterDelSlots: CLUSTER_DELSLOTS,
CLUSTER_DELSLOTSRANGE,
clusterDelSlotsRange: CLUSTER_DELSLOTSRANGE,
CLUSTER_FAILOVER,
clusterFailover: CLUSTER_FAILOVER,
CLUSTER_FLUSHSLOTS,
clusterFlushSlots: CLUSTER_FLUSHSLOTS,
CLUSTER_FORGET,
clusterForget: CLUSTER_FORGET,
CLUSTER_GETKEYSINSLOT,
clusterGetKeysInSlot: CLUSTER_GETKEYSINSLOT,
// CLUSTER_INFO,
// clusterInfo: CLUSTER_INFO,
CLUSTER_KEYSLOT,
clusterKeySlot: CLUSTER_KEYSLOT,
// CLUSTER_LINKS,
// clusterLinks: CLUSTER_LINKS,
CLUSTER_MEET, CLUSTER_MEET,
clusterMeet: CLUSTER_MEET, clusterMeet: CLUSTER_MEET,
CLUSTER_MYID, CLUSTER_MYID,
clusterMyId: CLUSTER_MYID, clusterMyId: CLUSTER_MYID,
// CLUSTER_NODES,
// clusterNodes: CLUSTER_NODES,
// CLUSTER_REPLICAS,
// clusterReplicas: CLUSTER_REPLICAS,
CLUSTER_REPLICATE, CLUSTER_REPLICATE,
clusterReplicate: CLUSTER_REPLICATE, clusterReplicate: CLUSTER_REPLICATE,
CLUSTER_RESET,
clusterReset: CLUSTER_RESET,
CLUSTER_SAVECONFIG,
clusterSaveConfig: CLUSTER_SAVECONFIG,
'CLUSTER_SET-CONFIG-EPOCH': CLUSTER_SET_CONFIG_EPOCH,
clusterSetConfigEpoch: CLUSTER_SET_CONFIG_EPOCH,
CLUSTER_SETSLOT,
clusterSetSlot: CLUSTER_SETSLOT,
CLUSTER_SLOTS,
clusterSlots: CLUSTER_SLOTS,
COPY, COPY,
copy: COPY, copy: COPY,
DBSIZE, DBSIZE,
@@ -1563,4 +1695,4 @@ export default {
zUnion: ZUNION, zUnion: ZUNION,
ZUNIONSTORE, ZUNIONSTORE,
zUnionStore: ZUNIONSTORE zUnionStore: ZUNIONSTORE
} as const satisfies Record<string, Command> as Commands; } satisfies Record<string, Command> as Commands;

View File

@@ -4,7 +4,8 @@ import { setTimeout } from 'timers/promises';
const utils = new TestUtils({ const utils = new TestUtils({
dockerImageName: 'redis', dockerImageName: 'redis',
dockerImageVersionArgument: 'redis-version' dockerImageVersionArgument: 'redis-version',
defaultDockerVersion: '7.2-rc'
}); });
export default utils; export default utils;

View File

@@ -3,12 +3,15 @@ import {
RedisFunctions, RedisFunctions,
RedisScripts, RedisScripts,
RespVersions, RespVersions,
TypeMapping,
CommandPolicies,
createClient, createClient,
RedisClientOptions, RedisClientOptions,
RedisClientType, RedisClientType,
createCluster, createCluster,
RedisClusterOptions, RedisClusterOptions,
RedisClusterType RedisClusterType,
RESP_TYPES
} from '@redis/client/index'; } from '@redis/client/index';
import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers'; import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers';
import yargs from 'yargs'; import yargs from 'yargs';
@@ -28,10 +31,11 @@ interface ClientTestOptions<
M extends RedisModules, M extends RedisModules,
F extends RedisFunctions, F extends RedisFunctions,
S extends RedisScripts, S extends RedisScripts,
RESP extends RespVersions RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
> extends CommonTestOptions { > extends CommonTestOptions {
serverArguments: Array<string>; serverArguments: Array<string>;
clientOptions?: Partial<RedisClientOptions<M, F, S, RESP>>; clientOptions?: Partial<RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>>;
disableClientSetup?: boolean; disableClientSetup?: boolean;
} }
@@ -39,10 +43,12 @@ interface ClusterTestOptions<
M extends RedisModules, M extends RedisModules,
F extends RedisFunctions, F extends RedisFunctions,
S extends RedisScripts, S extends RedisScripts,
RESP extends RespVersions RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping,
POLICIES extends CommandPolicies
> extends CommonTestOptions { > extends CommonTestOptions {
serverArguments: Array<string>; serverArguments: Array<string>;
clusterConfiguration?: Partial<RedisClusterOptions<M, F, S, RESP>>; clusterConfiguration?: Partial<RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>>;
numberOfMasters?: number; numberOfMasters?: number;
numberOfReplicas?: number; numberOfReplicas?: number;
} }
@@ -51,10 +57,12 @@ interface AllTestOptions<
M extends RedisModules, M extends RedisModules,
F extends RedisFunctions, F extends RedisFunctions,
S extends RedisScripts, S extends RedisScripts,
RESP extends RespVersions RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping,
POLICIES extends CommandPolicies
> { > {
client: ClientTestOptions<M, F, S, RESP>; client: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>;
cluster: ClusterTestOptions<M, F, S, RESP>; cluster: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>;
} }
interface Version { interface Version {
@@ -135,11 +143,12 @@ export default class TestUtils {
M extends RedisModules = {}, M extends RedisModules = {},
F extends RedisFunctions = {}, F extends RedisFunctions = {},
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2 RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}
>( >(
title: string, title: string,
fn: (client: RedisClientType<M, F, S, RESP>) => unknown, fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>) => unknown,
options: ClientTestOptions<M, F, S, RESP> options: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>
): void { ): void {
let dockerPromise: ReturnType<typeof spawnRedisServer>; let dockerPromise: ReturnType<typeof spawnRedisServer>;
if (this.isVersionGreaterThan(options.minimumDockerVersion)) { if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
@@ -187,8 +196,10 @@ export default class TestUtils {
M extends RedisModules, M extends RedisModules,
F extends RedisFunctions, F extends RedisFunctions,
S extends RedisScripts, S extends RedisScripts,
RESP extends RespVersions RESP extends RespVersions,
>(cluster: RedisClusterType<M, F, S, RESP>): Promise<unknown> { TYPE_MAPPING extends TypeMapping,
POLICIES extends CommandPolicies
>(cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING, POLICIES>): Promise<unknown> {
return Promise.all( return Promise.all(
cluster.masters.map(async ({ client }) => { cluster.masters.map(async ({ client }) => {
if (client) { if (client) {
@@ -202,11 +213,13 @@ export default class TestUtils {
M extends RedisModules = {}, M extends RedisModules = {},
F extends RedisFunctions = {}, F extends RedisFunctions = {},
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2 RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {},
POLICIES extends CommandPolicies = {}
>( >(
title: string, title: string,
fn: (cluster: RedisClusterType<M, F, S, RESP>) => unknown, fn: (cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING, POLICIES>) => unknown,
options: ClusterTestOptions<M, F, S, RESP> options: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>
): void { ): void {
let dockersPromise: ReturnType<typeof spawnRedisCluster>; let dockersPromise: ReturnType<typeof spawnRedisCluster>;
if (this.isVersionGreaterThan(options.minimumDockerVersion)) { if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
@@ -225,7 +238,7 @@ export default class TestUtils {
it(title, async function () { it(title, async function () {
if (!dockersPromise) return this.skip(); if (!dockersPromise) return this.skip();
const dockers = await dockersPromise, const dockers = await dockersPromise,
cluster = createCluster({ cluster = createCluster({
rootNodes: dockers.map(({ port }) => ({ rootNodes: dockers.map(({ port }) => ({
@@ -253,11 +266,13 @@ export default class TestUtils {
M extends RedisModules = {}, M extends RedisModules = {},
F extends RedisFunctions = {}, F extends RedisFunctions = {},
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2 RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {},
POLICIES extends CommandPolicies = {}
>( >(
title: string, title: string,
fn: (client: RedisClientType<M, F, S, RESP> | RedisClusterType<M, F, S, RESP>) => unknown, fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING> | RedisClusterType<M, F, S, RESP, TYPE_MAPPING, POLICIES>) => unknown,
options: AllTestOptions<M, F, S, RESP> options: AllTestOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>
) { ) {
this.testWithClient(`client.${title}`, fn, options.client); this.testWithClient(`client.${title}`, fn, options.client);
this.testWithCluster(`cluster.${title}`, fn, options.cluster); this.testWithCluster(`cluster.${title}`, fn, options.cluster);