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

comment cluster request & response policies (keep v4 behaver)

This commit is contained in:
Leibale
2023-09-18 17:16:41 -04:00
parent 67900a50fa
commit 4be30ccd0f
14 changed files with 546 additions and 492 deletions

View File

@@ -105,8 +105,6 @@ createCluster({
## Command Routing ## Command Routing
TODO request response policy
### Commands that operate on Redis Keys ### Commands that operate on Redis Keys
Commands such as `GET`, `SET`, etc. are routed by the first key specified. For example `MGET 1 2 3` will be routed by the key `1`. Commands such as `GET`, `SET`, etc. are routed by the first key specified. For example `MGET 1 2 3` will be routed by the key `1`.

View File

@@ -1,4 +1,4 @@
export { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, CommandPolicies } 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

@@ -200,9 +200,8 @@ export type ReplyWithTypeMapping<
REPLY extends Array<infer T> ? Array<ReplyWithTypeMapping<T, TYPE_MAPPING>> : REPLY extends Array<infer T> ? Array<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
REPLY extends Set<infer T> ? Set<ReplyWithTypeMapping<T, TYPE_MAPPING>> : REPLY extends Set<infer T> ? Set<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
REPLY extends Map<infer K, infer V> ? Map<MapKey<K, TYPE_MAPPING>, ReplyWithTypeMapping<V, TYPE_MAPPING>> : REPLY extends Map<infer K, infer V> ? Map<MapKey<K, TYPE_MAPPING>, ReplyWithTypeMapping<V, TYPE_MAPPING>> :
// `Date` & `Buffer` are supersets of `Record`, so they need to be checked first // `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first
REPLY extends Date ? REPLY : REPLY extends Date | Buffer | Error ? REPLY :
REPLY extends Buffer ? REPLY :
REPLY extends Record<PropertyKey, any> ? { REPLY extends Record<PropertyKey, any> ? {
[P in keyof REPLY]: ReplyWithTypeMapping<REPLY[P], TYPE_MAPPING>; [P in keyof REPLY]: ReplyWithTypeMapping<REPLY[P], TYPE_MAPPING>;
} : } :
@@ -222,57 +221,62 @@ export type RedisArgument = string | Buffer;
export type CommandArguments = Array<RedisArgument> & { preserve?: unknown }; export type CommandArguments = Array<RedisArgument> & { preserve?: unknown };
export const REQUEST_POLICIES = { // export const REQUEST_POLICIES = {
/** // /**
* TODO // * TODO
*/ // */
ALL_NODES: 'all_nodes', // ALL_NODES: 'all_nodes',
/** // /**
* TODO // * TODO
*/ // */
ALL_SHARDS: 'all_shards', // ALL_SHARDS: 'all_shards',
/** // /**
* TODO // * TODO
*/ // */
SPECIAL: 'special' // SPECIAL: 'special'
} as const; // } as const;
export type REQUEST_POLICIES = typeof REQUEST_POLICIES; // export type REQUEST_POLICIES = typeof REQUEST_POLICIES;
export type RequestPolicies = REQUEST_POLICIES[keyof REQUEST_POLICIES]; // export type RequestPolicies = REQUEST_POLICIES[keyof REQUEST_POLICIES];
export const RESPONSE_POLICIES = { // export const RESPONSE_POLICIES = {
/** // /**
* TODO // * TODO
*/ // */
ONE_SUCCEEDED: 'one_succeeded', // ONE_SUCCEEDED: 'one_succeeded',
/** // /**
* TODO // * TODO
*/ // */
ALL_SUCCEEDED: 'all_succeeded', // ALL_SUCCEEDED: 'all_succeeded',
/** // /**
* TODO // * TODO
*/ // */
LOGICAL_AND: 'agg_logical_and', // LOGICAL_AND: 'agg_logical_and',
/** // /**
* TODO // * TODO
*/ // */
SPECIAL: 'special' // SPECIAL: 'special'
} as const; // } as const;
export type RESPONSE_POLICIES = typeof RESPONSE_POLICIES; // export type RESPONSE_POLICIES = typeof RESPONSE_POLICIES;
export type ResponsePolicies = RESPONSE_POLICIES[keyof RESPONSE_POLICIES]; // export type ResponsePolicies = RESPONSE_POLICIES[keyof RESPONSE_POLICIES];
export type CommandPolicies = { // export type CommandPolicies = {
request?: RequestPolicies | null; // request?: RequestPolicies | null;
response?: ResponsePolicies | null; // response?: ResponsePolicies | null;
}; // };
export type Command = { export type Command = {
FIRST_KEY_INDEX?: number | ((this: void, ...args: Array<any>) => RedisArgument | undefined); FIRST_KEY_INDEX?: number | ((this: void, ...args: Array<any>) => RedisArgument | undefined);
IS_READ_ONLY?: boolean; IS_READ_ONLY?: boolean;
POLICIES?: CommandPolicies; /**
* @internal
* TODO: remove once `POLICIES` is implemented
*/
IS_FORWARD_COMMAND?: boolean;
// POLICIES?: CommandPolicies;
transformArguments(this: void, ...args: Array<any>): CommandArguments; transformArguments(this: void, ...args: Array<any>): CommandArguments;
TRANSFORM_LEGACY_REPLY?: boolean; TRANSFORM_LEGACY_REPLY?: boolean;
transformReply: TransformReply | Record<RespVersions, TransformReply>; transformReply: TransformReply | Record<RespVersions, TransformReply>;
@@ -355,32 +359,32 @@ export type CommandSignature<
TYPE_MAPPING extends TypeMapping TYPE_MAPPING extends TypeMapping
> = (...args: Parameters<COMMAND['transformArguments']>) => Promise<ReplyWithTypeMapping<CommandReply<COMMAND, RESP>, TYPE_MAPPING>>; > = (...args: Parameters<COMMAND['transformArguments']>) => Promise<ReplyWithTypeMapping<CommandReply<COMMAND, RESP>, TYPE_MAPPING>>;
export type CommandWithPoliciesSignature< // export type CommandWithPoliciesSignature<
COMMAND extends Command, // COMMAND extends Command,
RESP extends RespVersions, // RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping, // TYPE_MAPPING extends TypeMapping,
POLICIES extends CommandPolicies // POLICIES extends CommandPolicies
> = (...args: Parameters<COMMAND['transformArguments']>) => Promise< // > = (...args: Parameters<COMMAND['transformArguments']>) => Promise<
ReplyWithPolicy< // ReplyWithPolicy<
ReplyWithTypeMapping<CommandReply<COMMAND, RESP>, TYPE_MAPPING>, // ReplyWithTypeMapping<CommandReply<COMMAND, RESP>, TYPE_MAPPING>,
MergePolicies<COMMAND, POLICIES> // MergePolicies<COMMAND, POLICIES>
> // >
>; // >;
export type MergePolicies< // export type MergePolicies<
COMMAND extends Command, // COMMAND extends Command,
POLICIES extends CommandPolicies // POLICIES extends CommandPolicies
> = Omit<COMMAND['POLICIES'], keyof POLICIES> & POLICIES; // > = Omit<COMMAND['POLICIES'], keyof POLICIES> & POLICIES;
type ReplyWithPolicy< // type ReplyWithPolicy<
REPLY, // REPLY,
POLICIES extends CommandPolicies, // POLICIES extends CommandPolicies,
> = ( // > = (
POLICIES['request'] extends REQUEST_POLICIES['SPECIAL'] ? never : // POLICIES['request'] extends REQUEST_POLICIES['SPECIAL'] ? never :
POLICIES['request'] extends null | undefined ? REPLY : // POLICIES['request'] extends null | undefined ? REPLY :
unknown extends POLICIES['request'] ? REPLY : // unknown extends POLICIES['request'] ? REPLY :
POLICIES['response'] extends RESPONSE_POLICIES['SPECIAL'] ? never : // POLICIES['response'] extends RESPONSE_POLICIES['SPECIAL'] ? never :
POLICIES['response'] extends RESPONSE_POLICIES['ALL_SUCCEEDED' | 'ONE_SUCCEEDED' | 'LOGICAL_AND'] ? REPLY : // POLICIES['response'] extends RESPONSE_POLICIES['ALL_SUCCEEDED' | 'ONE_SUCCEEDED' | 'LOGICAL_AND'] ? REPLY :
// otherwise, return array of replies // // otherwise, return array of replies
Array<REPLY> // Array<REPLY>
); // );

View File

@@ -11,15 +11,19 @@ import { once } from 'node:events';
// import { promisify } from 'node:util'; // import { promisify } from 'node:util';
import { MATH_FUNCTION, loadMathFunction } from '../commands/FUNCTION_LOAD.spec'; import { MATH_FUNCTION, loadMathFunction } from '../commands/FUNCTION_LOAD.spec';
import { RESP_TYPES } from '../RESP/decoder'; import { RESP_TYPES } from '../RESP/decoder';
import { NumberReply } from '../RESP/types';
import { SortedSetMember } from '../commands/generic-transformers'; import { SortedSetMember } from '../commands/generic-transformers';
export const SQUARE_SCRIPT = defineScript({ export const SQUARE_SCRIPT = defineScript({
SCRIPT: 'return ARGV[1] * ARGV[1];', SCRIPT:
NUMBER_OF_KEYS: 0, `local number = redis.call('GET', KEYS[1])
transformArguments(number: number): Array<string> { return number * number`,
return [number.toString()]; NUMBER_OF_KEYS: 1,
FIRST_KEY_INDEX: 0,
transformArguments(key: string) {
return [key];
}, },
transformReply: undefined as unknown as () => number transformReply: undefined as unknown as () => NumberReply
}); });
describe('Client', () => { describe('Client', () => {
@@ -214,9 +218,10 @@ describe('Client', () => {
testUtils.testWithClient('with script', async client => { testUtils.testWithClient('with script', async client => {
assert.deepEqual( assert.deepEqual(
await client.multi() await client.multi()
.square(2) .set('key', '2')
.square('key')
.exec(), .exec(),
[4] ['OK', 4]
); );
}, { }, {
...GLOBAL.SERVERS.OPEN, ...GLOBAL.SERVERS.OPEN,
@@ -280,10 +285,12 @@ describe('Client', () => {
}); });
testUtils.testWithClient('scripts', async client => { testUtils.testWithClient('scripts', async client => {
assert.equal( const [, reply] = await Promise.all([
await client.square(2), client.set('key', '2'),
4 client.square('key')
); ]);
assert.equal(reply, 4);
}, { }, {
...GLOBAL.SERVERS.OPEN, ...GLOBAL.SERVERS.OPEN,
clientOptions: { clientOptions: {
@@ -319,12 +326,13 @@ describe('Client', () => {
}); });
testUtils.testWithClient('functions', async client => { testUtils.testWithClient('functions', async client => {
await loadMathFunction(client); const [,, reply] = await Promise.all([
loadMathFunction(client),
client.set('key', '2'),
client.math.square('key')
]);
assert.equal( assert.equal(reply, 4);
await client.math.square(2),
4
);
}, { }, {
...GLOBAL.SERVERS.OPEN, ...GLOBAL.SERVERS.OPEN,
minimumDockerVersion: [7, 0], minimumDockerVersion: [7, 0],

View File

@@ -1,72 +1,73 @@
// import { strict as assert } from 'node:assert'; import { strict as assert } from 'node:assert';
// import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils'; import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
// import RedisCluster from '.'; import RedisCluster from '.';
// import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT'; // import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
// import { commandOptions } from '../command-options'; import { SQUARE_SCRIPT } from '../client/index.spec';
// import { SQUARE_SCRIPT } from '../client/index.spec'; import { RootNodesUnavailableError } from '../errors';
// import { RootNodesUnavailableError } from '../errors'; import { spy } from 'sinon';
// import { spy } from 'sinon'; // import { setTimeout } from 'node:timers/promises';
// import { setTimeout } from 'timers/promises'; import RedisClient from '../client';
// import RedisClient from '../client';
// describe('Cluster', () => { describe('Cluster', () => {
// testUtils.testWithCluster('sendCommand', async cluster => { testUtils.testWithCluster('sendCommand', async cluster => {
// assert.equal( assert.equal(
// await cluster.sendCommand(undefined, true, ['PING']), await cluster.sendCommand(undefined, true, ['PING']),
// 'PONG' 'PONG'
// ); );
// }, GLOBAL.CLUSTERS.OPEN); }, GLOBAL.CLUSTERS.OPEN);
// testUtils.testWithCluster('isOpen', async cluster => { testUtils.testWithCluster('isOpen', async cluster => {
// assert.equal(cluster.isOpen, true); assert.equal(cluster.isOpen, true);
// await cluster.disconnect(); await cluster.destroy();
// assert.equal(cluster.isOpen, false); assert.equal(cluster.isOpen, false);
// }, GLOBAL.CLUSTERS.OPEN); }, GLOBAL.CLUSTERS.OPEN);
// testUtils.testWithCluster('connect should throw if already connected', async cluster => { testUtils.testWithCluster('connect should throw if already connected', async cluster => {
// await assert.rejects(cluster.connect()); await assert.rejects(cluster.connect());
// }, GLOBAL.CLUSTERS.OPEN); }, GLOBAL.CLUSTERS.OPEN);
// testUtils.testWithCluster('multi', async cluster => { testUtils.testWithCluster('multi', async cluster => {
// const key = 'key'; const key = 'key';
// assert.deepEqual( assert.deepEqual(
// await cluster.multi() await cluster.multi()
// .set(key, 'value') .set(key, 'value')
// .get(key) .get(key)
// .exec(), .exec(),
// ['OK', 'value'] ['OK', 'value']
// ); );
// }, GLOBAL.CLUSTERS.OPEN); }, GLOBAL.CLUSTERS.OPEN);
// testUtils.testWithCluster('scripts', async cluster => { testUtils.testWithCluster('scripts', async cluster => {
// assert.equal( const [, reply] = await Promise.all([
// await cluster.square(2), cluster.set('key', '2'),
// 4 cluster.square('key')
// ); ]);
// }, {
// ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: {
// scripts: {
// square: SQUARE_SCRIPT
// }
// }
// });
// it('should throw RootNodesUnavailableError', async () => { assert.equal(reply, 4);
// const cluster = RedisCluster.create({ }, {
// rootNodes: [] ...GLOBAL.CLUSTERS.OPEN,
// }); clusterConfiguration: {
scripts: {
square: SQUARE_SCRIPT
}
}
});
// try { it('should throw RootNodesUnavailableError', async () => {
// await assert.rejects( const cluster = RedisCluster.create({
// cluster.connect(), rootNodes: []
// RootNodesUnavailableError });
// );
// } catch (err) { try {
// await cluster.disconnect(); await assert.rejects(
// throw err; cluster.connect(),
// } RootNodesUnavailableError
// }); );
} catch (err) {
await cluster.disconnect();
throw err;
}
});
// testUtils.testWithCluster('should handle live resharding', async cluster => { // testUtils.testWithCluster('should handle live resharding', async cluster => {
// const slot = 12539, // const slot = 12539,
@@ -121,137 +122,122 @@
// numberOfMasters: 2 // numberOfMasters: 2
// }); // });
// testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => { testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => {
// const totalNodes = cluster.masters.length + cluster.replicas.length, const totalNodes = cluster.masters.length + cluster.replicas.length,
// ids = new Set<string>(); ids = new Set<string>();
// for (let i = 0; i < totalNodes; i++) { for (let i = 0; i < totalNodes; i++) {
// ids.add(cluster.getRandomNode().id); ids.add(cluster.getRandomNode().id);
// } }
// assert.equal(ids.size, totalNodes); assert.equal(ids.size, totalNodes);
// }, GLOBAL.CLUSTERS.WITH_REPLICAS); }, GLOBAL.CLUSTERS.WITH_REPLICAS);
// testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => { testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => {
// const totalNodes = 1 + cluster.slots[0].replicas!.length, const totalNodes = 1 + cluster.slots[0].replicas!.length,
// ids = new Set<string>(); ids = new Set<string>();
// for (let i = 0; i < totalNodes; i++) { for (let i = 0; i < totalNodes; i++) {
// ids.add(cluster.getSlotRandomNode(0).id); ids.add(cluster.getSlotRandomNode(0).id);
// } }
// assert.equal(ids.size, totalNodes); assert.equal(ids.size, totalNodes);
// }, GLOBAL.CLUSTERS.WITH_REPLICAS); }, GLOBAL.CLUSTERS.WITH_REPLICAS);
// testUtils.testWithCluster('cluster topology', async cluster => { testUtils.testWithCluster('cluster topology', async cluster => {
// assert.equal(cluster.slots.length, 16384); assert.equal(cluster.slots.length, 16384);
// const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS; const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS;
// assert.equal(cluster.shards.length, numberOfMasters); assert.equal(cluster.shards.length, numberOfMasters);
// assert.equal(cluster.masters.length, numberOfMasters); assert.equal(cluster.masters.length, numberOfMasters);
// assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters); assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters);
// assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas); assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas);
// }, GLOBAL.CLUSTERS.WITH_REPLICAS); }, GLOBAL.CLUSTERS.WITH_REPLICAS);
// testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => { testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => {
// const masters = cluster.getMasters(); const masters = cluster.getMasters();
// assert.ok(Array.isArray(masters)); assert.ok(Array.isArray(masters));
// for (const master of masters) { for (const master of masters) {
// assert.equal(typeof master.id, 'string'); assert.equal(typeof master.id, 'string');
// assert.ok(master.client instanceof RedisClient); assert.ok(master.client instanceof RedisClient);
// } }
// }, { }, {
// ...GLOBAL.CLUSTERS.OPEN, ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: { clusterConfiguration: {
// minimizeConnections: undefined // reset to default minimizeConnections: undefined // reset to default
// } }
// }); });
// testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => { testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => {
// const master = cluster.getSlotMaster(0); const master = cluster.getSlotMaster(0);
// assert.equal(typeof master.id, 'string'); assert.equal(typeof master.id, 'string');
// assert.ok(master.client instanceof RedisClient); assert.ok(master.client instanceof RedisClient);
// }, { }, {
// ...GLOBAL.CLUSTERS.OPEN, ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: { clusterConfiguration: {
// minimizeConnections: undefined // reset to default minimizeConnections: undefined // reset to default
// } }
// }); });
// testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => { testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => {
// await assert.rejects(cluster.mGet(['a', 'b'])); await assert.rejects(cluster.mGet(['a', 'b']));
// }, GLOBAL.CLUSTERS.OPEN); }, GLOBAL.CLUSTERS.OPEN);
// testUtils.testWithCluster('should send commands with commandOptions to correct cluster slot (without redirections)', async cluster => { describe('minimizeConnections', () => {
// // 'a' and 'b' hash to different cluster slots (see previous unit test) testUtils.testWithCluster('false', async cluster => {
// // -> maxCommandRedirections 0: rejects on MOVED/ASK reply for (const master of cluster.masters) {
// await cluster.set(commandOptions({ isolated: true }), 'a', '1'), assert.ok(master.client instanceof RedisClient);
// await cluster.set(commandOptions({ isolated: true }), 'b', '2'), }
}, {
...GLOBAL.CLUSTERS.OPEN,
clusterConfiguration: {
minimizeConnections: false
}
});
// assert.equal(await cluster.get('a'), '1'); testUtils.testWithCluster('true', async cluster => {
// assert.equal(await cluster.get('b'), '2'); for (const master of cluster.masters) {
// }, { assert.equal(master.client, undefined);
// ...GLOBAL.CLUSTERS.OPEN, }
// clusterConfiguration: { }, {
// maxCommandRedirections: 0 ...GLOBAL.CLUSTERS.OPEN,
// } clusterConfiguration: {
// }); minimizeConnections: true
}
});
});
// describe('minimizeConnections', () => { describe('PubSub', () => {
// testUtils.testWithCluster('false', async cluster => { testUtils.testWithCluster('subscribe & unsubscribe', async cluster => {
// for (const master of cluster.masters) { const listener = spy();
// assert.ok(master.client instanceof RedisClient);
// }
// }, {
// ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: {
// minimizeConnections: false
// }
// });
// testUtils.testWithCluster('true', async cluster => { await cluster.subscribe('channel', listener);
// for (const master of cluster.masters) {
// assert.equal(master.client, undefined);
// }
// }, {
// ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: {
// minimizeConnections: true
// }
// });
// });
// describe('PubSub', () => { await Promise.all([
// testUtils.testWithCluster('subscribe & unsubscribe', async cluster => { waitTillBeenCalled(listener),
// const listener = spy(); cluster.publish('channel', 'message')
]);
// await cluster.subscribe('channel', listener); assert.ok(listener.calledOnceWithExactly('message', 'channel'));
// await Promise.all([ await cluster.unsubscribe('channel', listener);
// waitTillBeenCalled(listener),
// cluster.publish('channel', 'message')
// ]);
// assert.ok(listener.calledOnceWithExactly('message', 'channel')); assert.equal(cluster.pubSubNode, undefined);
}, GLOBAL.CLUSTERS.OPEN);
// await cluster.unsubscribe('channel', listener); testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => {
const listener = spy();
// assert.equal(cluster.pubSubNode, undefined); await cluster.pSubscribe('channe*', listener);
// }, GLOBAL.CLUSTERS.OPEN);
// testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => { await Promise.all([
// const listener = spy(); waitTillBeenCalled(listener),
cluster.publish('channel', 'message')
]);
// await cluster.pSubscribe('channe*', listener); assert.ok(listener.calledOnceWithExactly('message', 'channel'));
// await Promise.all([ await cluster.pUnsubscribe('channe*', listener);
// waitTillBeenCalled(listener),
// cluster.publish('channel', 'message')
// ]);
// assert.ok(listener.calledOnceWithExactly('message', 'channel')); assert.equal(cluster.pubSubNode, undefined);
}, GLOBAL.CLUSTERS.OPEN);
// await cluster.pUnsubscribe('channe*', listener);
// assert.equal(cluster.pubSubNode, undefined);
// }, GLOBAL.CLUSTERS.OPEN);
// testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => { // testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => {
// const listener = spy(); // const listener = spy();
@@ -302,26 +288,26 @@
// minimumDockerVersion: [7] // minimumDockerVersion: [7]
// }); // });
// testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => { testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => {
// const listener = spy(); const listener = spy();
// await cluster.sSubscribe('channel', listener); await cluster.sSubscribe('channel', listener);
// await Promise.all([ await Promise.all([
// waitTillBeenCalled(listener), waitTillBeenCalled(listener),
// cluster.sPublish('channel', 'message') cluster.sPublish('channel', 'message')
// ]); ]);
// assert.ok(listener.calledOnceWithExactly('message', 'channel')); assert.ok(listener.calledOnceWithExactly('message', 'channel'));
// await cluster.sUnsubscribe('channel', listener); await cluster.sUnsubscribe('channel', listener);
// // 10328 is the slot of `channel` // 10328 is the slot of `channel`
// assert.equal(cluster.slots[10328].master.pubSubClient, undefined); assert.equal(cluster.slots[10328].master.pubSubClient, undefined);
// }, { }, {
// ...GLOBAL.CLUSTERS.OPEN, ...GLOBAL.CLUSTERS.OPEN,
// minimumDockerVersion: [7] minimumDockerVersion: [7]
// }); });
// testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => { // testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => {
// const SLOT = 10328, // const SLOT = 10328,
@@ -358,5 +344,5 @@
// serverArguments: [], // serverArguments: [],
// minimumDockerVersion: [7] // minimumDockerVersion: [7]
// }); // });
// }); });
// }); });

View File

@@ -1,6 +1,6 @@
import { RedisClientOptions, RedisClientType } from '../client'; import { RedisClientOptions, RedisClientType } from '../client';
import { CommandOptions } from '../client/commands-queue'; import { CommandOptions } from '../client/commands-queue';
import { Command, CommandArguments, CommanderConfig, CommandPolicies, CommandWithPoliciesSignature, TypeMapping, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions } from '../RESP/types'; import { Command, CommandArguments, CommanderConfig, CommandSignature, /*CommandPolicies, CommandWithPoliciesSignature,*/ TypeMapping, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions } from '../RESP/types';
import COMMANDS from '../commands'; import COMMANDS from '../commands';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
@@ -65,12 +65,48 @@ export interface RedisClusterOptions<
nodeAddressMap?: NodeAddressMap; nodeAddressMap?: NodeAddressMap;
} }
// remove once request & response policies are ready
type ClusterCommand<
NAME extends PropertyKey,
COMMAND extends Command
> = COMMAND['FIRST_KEY_INDEX'] extends undefined ? (
COMMAND['IS_FORWARD_COMMAND'] extends true ? NAME : never
) : NAME;
// CommandWithPoliciesSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING, POLICIES>
type WithCommands< type WithCommands<
RESP extends RespVersions, RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping, TYPE_MAPPING extends TypeMapping
// POLICIES extends CommandPolicies
> = { > = {
[P in keyof typeof COMMANDS]: CommandWithPoliciesSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING, POLICIES>; [P in keyof typeof COMMANDS as ClusterCommand<P, (typeof COMMANDS)[P]>]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>;
};
type WithModules<
M extends RedisModules,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
> = {
[P in keyof M]: {
[C in keyof M[P] as ClusterCommand<C, M[P][C]>]: CommandSignature<M[P][C], RESP, TYPE_MAPPING>;
};
};
type WithFunctions<
F extends RedisFunctions,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
> = {
[L in keyof F]: {
[C in keyof F[L] as ClusterCommand<C, F[L][C]>]: CommandSignature<F[L][C], RESP, TYPE_MAPPING>;
};
};
type WithScripts<
S extends RedisScripts,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
> = {
[P in keyof S as ClusterCommand<P, S[P]>]: CommandSignature<S[P], RESP, TYPE_MAPPING>;
}; };
export type RedisClusterType< export type RedisClusterType<
@@ -80,17 +116,22 @@ export type RedisClusterType<
RESP extends RespVersions = 2, RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}, TYPE_MAPPING extends TypeMapping = {},
// POLICIES extends CommandPolicies = {} // POLICIES extends CommandPolicies = {}
> = RedisCluster<M, F, S, RESP, TYPE_MAPPING, POLICIES> & WithCommands<RESP, TYPE_MAPPING/*, POLICIES*/>; > = (
// & WithModules<M> & WithFunctions<F> & WithScripts<S> RedisCluster<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/> &
WithCommands<RESP, TYPE_MAPPING> &
WithModules<M, RESP, TYPE_MAPPING> &
WithFunctions<F, RESP, TYPE_MAPPING> &
WithScripts<S, RESP, TYPE_MAPPING>
);
export interface ClusterCommandOptions< export interface ClusterCommandOptions<
TYPE_MAPPING extends TypeMapping = TypeMapping, TYPE_MAPPING extends TypeMapping = TypeMapping
POLICIES extends CommandPolicies = CommandPolicies // POLICIES extends CommandPolicies = CommandPolicies
> extends CommandOptions<TYPE_MAPPING> { > extends CommandOptions<TYPE_MAPPING> {
policies?: POLICIES; // policies?: POLICIES;
} }
type ProxyCluster = RedisCluster<any, any, any, any, any, any>; type ProxyCluster = RedisCluster<any, any, any, any, any/*, any*/>;
type NamespaceProxyCluster = { self: ProxyCluster }; type NamespaceProxyCluster = { self: ProxyCluster };
@@ -100,20 +141,30 @@ export default class RedisCluster<
S extends RedisScripts, S extends RedisScripts,
RESP extends RespVersions, RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping, TYPE_MAPPING extends TypeMapping,
POLICIES extends CommandPolicies // POLICIES extends CommandPolicies
> extends EventEmitter { > extends EventEmitter {
static extractFirstKey<C extends Command>( static extractFirstKey<C extends Command>(
command: C, command: C,
args: Parameters<C['transformArguments']>, args: Parameters<C['transformArguments']>,
redisArgs: Array<RedisArgument> redisArgs: Array<RedisArgument>
): RedisArgument | undefined { ) {
if (command.FIRST_KEY_INDEX === undefined) { let key: RedisArgument | undefined;
return undefined; switch (typeof command.FIRST_KEY_INDEX) {
} else if (typeof command.FIRST_KEY_INDEX === 'number') { case 'number':
return redisArgs[command.FIRST_KEY_INDEX]; key = redisArgs[command.FIRST_KEY_INDEX];
break;
case 'function':
key = command.FIRST_KEY_INDEX(...args);
break;
} }
return command.FIRST_KEY_INDEX(...args); // TODO: remove once request & response policies are ready
if (key === undefined && !command.IS_FORWARD_COMMAND) {
throw new Error('TODO');
}
return key;
} }
private static _createCommand(command: Command, resp: RespVersions) { private static _createCommand(command: Command, resp: RespVersions) {
@@ -130,7 +181,7 @@ export default class RedisCluster<
command.IS_READ_ONLY, command.IS_READ_ONLY,
redisArgs, redisArgs,
this._commandOptions, this._commandOptions,
command.POLICIES // command.POLICIES
); );
return transformReply ? return transformReply ?
@@ -153,7 +204,7 @@ export default class RedisCluster<
command.IS_READ_ONLY, command.IS_READ_ONLY,
redisArgs, redisArgs,
this.self._commandOptions, this.self._commandOptions,
command.POLICIES // command.POLICIES
); );
return transformReply ? return transformReply ?
@@ -167,18 +218,18 @@ export default class RedisCluster<
transformReply = getTransformReply(fn, resp); transformReply = getTransformReply(fn, resp);
return async function (this: NamespaceProxyCluster, ...args: Array<unknown>) { return async function (this: NamespaceProxyCluster, ...args: Array<unknown>) {
const fnArgs = fn.transformArguments(...args), const fnArgs = fn.transformArguments(...args),
redisArgs = prefix.concat(fnArgs),
firstKey = RedisCluster.extractFirstKey( firstKey = RedisCluster.extractFirstKey(
fn, fn,
fnArgs, args,
redisArgs fnArgs
), ),
redisArgs = prefix.concat(fnArgs),
reply = await this.self.sendCommand( reply = await this.self.sendCommand(
firstKey, firstKey,
fn.IS_READ_ONLY, fn.IS_READ_ONLY,
redisArgs, redisArgs,
this.self._commandOptions, this.self._commandOptions,
fn.POLICIES // fn.POLICIES
); );
return transformReply ? return transformReply ?
@@ -192,18 +243,19 @@ export default class RedisCluster<
transformReply = getTransformReply(script, resp); transformReply = getTransformReply(script, resp);
return async function (this: ProxyCluster, ...args: Array<unknown>) { return async function (this: ProxyCluster, ...args: Array<unknown>) {
const scriptArgs = script.transformArguments(...args), const scriptArgs = script.transformArguments(...args),
redisArgs = prefix.concat(scriptArgs),
firstKey = RedisCluster.extractFirstKey( firstKey = RedisCluster.extractFirstKey(
script, script,
scriptArgs, args,
redisArgs scriptArgs
), ),
reply = await this.sendCommand( redisArgs = prefix.concat(scriptArgs),
reply = await this.executeScript(
script,
firstKey, firstKey,
script.IS_READ_ONLY, script.IS_READ_ONLY,
redisArgs, redisArgs,
this._commandOptions, this._commandOptions,
script.POLICIES // script.POLICIES
); );
return transformReply ? return transformReply ?
@@ -218,8 +270,8 @@ export default class RedisCluster<
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2, RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}, TYPE_MAPPING extends TypeMapping = {},
POLICIES extends CommandPolicies = {} // POLICIES extends CommandPolicies = {}
>(config?: ClusterCommander<M, F, S, RESP, TYPE_MAPPING, POLICIES>) { >(config?: ClusterCommander<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) {
const Cluster = attachConfig({ const Cluster = attachConfig({
BaseClass: RedisCluster, BaseClass: RedisCluster,
commands: COMMANDS, commands: COMMANDS,
@@ -234,7 +286,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" to prevent the namespaces.self to leak between "proxies" // returning a "proxy" to prevent the namespaces.self to leak between "proxies"
return Object.create(new Cluster(options)) as RedisClusterType<M, F, S, RESP, TYPE_MAPPING, POLICIES>; return Object.create(new Cluster(options)) as RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>;
}; };
} }
@@ -244,16 +296,16 @@ export default class RedisCluster<
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2, RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}, TYPE_MAPPING extends TypeMapping = {},
POLICIES extends CommandPolicies = {} // POLICIES extends CommandPolicies = {}
>(options?: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>) { >(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, TYPE_MAPPING, POLICIES>; 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>; 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`.
@@ -306,7 +358,7 @@ export default class RedisCluster<
return this._slots.isOpen; return this._slots.isOpen;
} }
constructor(options: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>) { constructor(options: RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) {
super(); super();
this._options = options; this._options = options;
@@ -336,9 +388,9 @@ export default class RedisCluster<
} }
withCommandOptions< withCommandOptions<
OPTIONS extends ClusterCommandOptions<TYPE_MAPPING, CommandPolicies>, OPTIONS extends ClusterCommandOptions<TYPE_MAPPING/*, CommandPolicies*/>,
TYPE_MAPPING extends TypeMapping, TYPE_MAPPING extends TypeMapping,
POLICIES extends CommandPolicies // POLICIES extends CommandPolicies
>(options: OPTIONS) { >(options: OPTIONS) {
const proxy = Object.create(this); const proxy = Object.create(this);
proxy._commandOptions = options; proxy._commandOptions = options;
@@ -347,8 +399,8 @@ export default class RedisCluster<
F, F,
S, S,
RESP, RESP,
TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}, TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}
POLICIES extends CommandPolicies ? POLICIES : {} // POLICIES extends CommandPolicies ? POLICIES : {}
>; >;
} }
@@ -367,8 +419,8 @@ export default class RedisCluster<
F, F,
S, S,
RESP, RESP,
K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING, K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING
K extends 'policies' ? V extends CommandPolicies ? V : {} : POLICIES // K extends 'policies' ? V extends CommandPolicies ? V : {} : POLICIES
>; >;
} }
@@ -379,15 +431,15 @@ export default class RedisCluster<
return this._commandOptionsProxy('typeMapping', typeMapping); return this._commandOptionsProxy('typeMapping', typeMapping);
} }
/** // /**
* Override the `policies` command option // * Override the `policies` command option
* TODO // * TODO
*/ // */
withPolicies<POLICIES extends CommandPolicies> (policies: POLICIES) { // withPolicies<POLICIES extends CommandPolicies> (policies: POLICIES) {
return this._commandOptionsProxy('policies', policies); // return this._commandOptionsProxy('policies', policies);
} // }
async #execute<T>( private async _execute<T>(
firstKey: RedisArgument | undefined, firstKey: RedisArgument | undefined,
isReadonly: boolean | undefined, isReadonly: boolean | undefined,
fn: (client: RedisClientType<M, F, S, RESP>) => Promise<T> fn: (client: RedisClientType<M, F, S, RESP>) => Promise<T>
@@ -437,9 +489,9 @@ export default class RedisCluster<
isReadonly: boolean | undefined, isReadonly: boolean | undefined,
args: CommandArguments, args: CommandArguments,
options?: ClusterCommandOptions, options?: ClusterCommandOptions,
defaultPolicies?: CommandPolicies // defaultPolicies?: CommandPolicies
): Promise<T> { ): Promise<T> {
return this.#execute( return this._execute(
firstKey, firstKey,
isReadonly, isReadonly,
client => client.sendCommand(args, options) client => client.sendCommand(args, options)
@@ -453,7 +505,7 @@ export default class RedisCluster<
args: Array<RedisArgument>, args: Array<RedisArgument>,
options?: CommandOptions options?: CommandOptions
) { ) {
return this.#execute( return this._execute(
firstKey, firstKey,
isReadonly, isReadonly,
client => client.executeScript(script, args, options) client => client.executeScript(script, args, options)

View File

@@ -1,4 +1,4 @@
import { VerbatimStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { VerbatimStringReply, Command } from '../RESP/types';
export default { export default {
FIRST_KEY_INDEX: undefined, FIRST_KEY_INDEX: undefined,

View File

@@ -1,4 +1,4 @@
import { VerbatimStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { VerbatimStringReply, Command } from '../RESP/types';
export default { export default {
FIRST_KEY_INDEX: undefined, FIRST_KEY_INDEX: undefined,

View File

@@ -1,4 +1,4 @@
import { RedisArgument, VerbatimStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types';
export default { export default {
FIRST_KEY_INDEX: undefined, FIRST_KEY_INDEX: undefined,

View File

@@ -17,13 +17,14 @@ describe('FCALL', () => {
}); });
testUtils.testWithClient('client.fCall', async client => { testUtils.testWithClient('client.fCall', async client => {
await loadMathFunction(client); const [,, reply] = await Promise.all([
loadMathFunction(client),
client.set('key', '2'),
client.fCall(MATH_FUNCTION.library.square.NAME, {
arguments: ['key']
})
]);
assert.equal( assert.equal(reply, 4);
await client.fCall(MATH_FUNCTION.library.square.NAME, {
arguments: ['2']
}),
4
);
}, GLOBAL.SERVERS.OPEN); }, GLOBAL.SERVERS.OPEN);
}); });

View File

@@ -17,13 +17,14 @@ describe('FCALL_RO', () => {
}); });
testUtils.testWithClient('client.fCallRo', async client => { testUtils.testWithClient('client.fCallRo', async client => {
await loadMathFunction(client); const [,, reply] = await Promise.all([
loadMathFunction(client),
client.set('key', '2'),
client.fCallRo(MATH_FUNCTION.library.square.NAME, {
arguments: ['key']
})
]);
assert.equal( assert.equal(reply, 4);
await client.fCallRo(MATH_FUNCTION.library.square.NAME, {
arguments: ['2']
}),
4
);
}, GLOBAL.SERVERS.OPEN); }, GLOBAL.SERVERS.OPEN);
}); });

View File

@@ -11,16 +11,20 @@ export const MATH_FUNCTION = {
`#!LUA name=math `#!LUA name=math
redis.register_function { redis.register_function {
function_name = "square", function_name = "square",
callback = function(keys, args) return args[1] * args[1] end, callback = function(keys, args) {
local number = redis.call('GET', keys[1])
return number * number
},
flags = { "no-writes" } flags = { "no-writes" }
}`, }`,
library: { library: {
square: { square: {
NAME: 'square', NAME: 'square',
IS_READ_ONLY: true, IS_READ_ONLY: true,
NUMBER_OF_KEYS: 0, NUMBER_OF_KEYS: 1,
transformArguments(number: number) { FIRST_KEY_INDEX: 0,
return [number.toString()]; transformArguments(key: string) {
return [key];
}, },
transformReply: undefined as unknown as () => NumberReply transformReply: undefined as unknown as () => NumberReply
} }

View File

@@ -3,6 +3,7 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types';
export default { export default {
FIRST_KEY_INDEX: undefined, FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true, IS_READ_ONLY: true,
IS_FORWARD_COMMAND: true,
transformArguments(channel: RedisArgument, message: RedisArgument) { transformArguments(channel: RedisArgument, message: RedisArgument) {
return ['PUBLISH', channel, message]; return ['PUBLISH', channel, message];
}, },

View File

@@ -4,14 +4,13 @@ import {
RedisScripts, RedisScripts,
RespVersions, RespVersions,
TypeMapping, TypeMapping,
CommandPolicies, // 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';
@@ -44,11 +43,11 @@ interface ClusterTestOptions<
F extends RedisFunctions, F extends RedisFunctions,
S extends RedisScripts, S extends RedisScripts,
RESP extends RespVersions, RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping, TYPE_MAPPING extends TypeMapping
POLICIES extends CommandPolicies // POLICIES extends CommandPolicies
> extends CommonTestOptions { > extends CommonTestOptions {
serverArguments: Array<string>; serverArguments: Array<string>;
clusterConfiguration?: Partial<RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>>; clusterConfiguration?: Partial<RedisClusterOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>>;
numberOfMasters?: number; numberOfMasters?: number;
numberOfReplicas?: number; numberOfReplicas?: number;
} }
@@ -58,11 +57,11 @@ interface AllTestOptions<
F extends RedisFunctions, F extends RedisFunctions,
S extends RedisScripts, S extends RedisScripts,
RESP extends RespVersions, RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping, TYPE_MAPPING extends TypeMapping
POLICIES extends CommandPolicies // POLICIES extends CommandPolicies
> { > {
client: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>; client: ClientTestOptions<M, F, S, RESP, TYPE_MAPPING>;
cluster: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES>; cluster: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>;
} }
interface Version { interface Version {
@@ -197,9 +196,9 @@ export default class TestUtils {
F extends RedisFunctions, F extends RedisFunctions,
S extends RedisScripts, S extends RedisScripts,
RESP extends RespVersions, RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping, TYPE_MAPPING extends TypeMapping
POLICIES extends CommandPolicies // POLICIES extends CommandPolicies
>(cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING, POLICIES>): Promise<unknown> { >(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) {
@@ -214,12 +213,12 @@ export default class TestUtils {
F extends RedisFunctions = {}, F extends RedisFunctions = {},
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2, RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}, TYPE_MAPPING extends TypeMapping = {}
POLICIES extends CommandPolicies = {} // POLICIES extends CommandPolicies = {}
>( >(
title: string, title: string,
fn: (cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING, POLICIES>) => unknown, fn: (cluster: RedisClusterType<M, F, S, RESP, TYPE_MAPPING/*, POLICIES*/>) => unknown,
options: ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING, POLICIES> 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)) {
@@ -267,12 +266,12 @@ export default class TestUtils {
F extends RedisFunctions = {}, F extends RedisFunctions = {},
S extends RedisScripts = {}, S extends RedisScripts = {},
RESP extends RespVersions = 2, RESP extends RespVersions = 2,
TYPE_MAPPING extends TypeMapping = {}, TYPE_MAPPING extends TypeMapping = {}
POLICIES extends CommandPolicies = {} // POLICIES extends CommandPolicies = {}
>( >(
title: string, title: string,
fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING> | RedisClusterType<M, F, S, RESP, TYPE_MAPPING, POLICIES>) => 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, TYPE_MAPPING, POLICIES> 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);