From 4be30ccd0f861cbf2d132a39f05c8ab5be0fbb62 Mon Sep 17 00:00:00 2001 From: Leibale Date: Mon, 18 Sep 2023 17:16:41 -0400 Subject: [PATCH] comment cluster request & response policies (keep v4 behaver) --- docs/clustering.md | 2 - packages/client/index.ts | 2 +- packages/client/lib/RESP/types.ts | 146 ++--- packages/client/lib/client/index.spec.ts | 40 +- packages/client/lib/cluster/index.spec.ts | 602 +++++++++--------- packages/client/lib/cluster/index.ts | 158 +++-- packages/client/lib/commands/CLUSTER_INFO.ts | 2 +- packages/client/lib/commands/CLUSTER_NODES.ts | 2 +- .../client/lib/commands/CLUSTER_REPLICAS.ts | 2 +- packages/client/lib/commands/FCALL.spec.ts | 15 +- packages/client/lib/commands/FCALL_RO.spec.ts | 15 +- .../client/lib/commands/FUNCTION_LOAD.spec.ts | 12 +- packages/client/lib/commands/PUBLISH.ts | 1 + packages/test-utils/lib/index.ts | 39 +- 14 files changed, 546 insertions(+), 492 deletions(-) diff --git a/docs/clustering.md b/docs/clustering.md index b5594e49a7..7b8b66a9ad 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -105,8 +105,6 @@ createCluster({ ## Command Routing -TODO request response policy - ### 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`. diff --git a/packages/client/index.ts b/packages/client/index.ts index 06392e970e..8864cf65f2 100644 --- a/packages/client/index.ts +++ b/packages/client/index.ts @@ -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 { VerbatimString } from './lib/RESP/verbatim-string'; export { defineScript } from './lib/lua-script'; diff --git a/packages/client/lib/RESP/types.ts b/packages/client/lib/RESP/types.ts index 004da46756..c4ad18e1a1 100644 --- a/packages/client/lib/RESP/types.ts +++ b/packages/client/lib/RESP/types.ts @@ -200,9 +200,8 @@ export type ReplyWithTypeMapping< REPLY extends Array ? Array> : REPLY extends Set ? Set> : REPLY extends Map ? Map, ReplyWithTypeMapping> : - // `Date` & `Buffer` are supersets of `Record`, so they need to be checked first - REPLY extends Date ? REPLY : - REPLY extends Buffer ? REPLY : + // `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first + REPLY extends Date | Buffer | Error ? REPLY : REPLY extends Record ? { [P in keyof REPLY]: ReplyWithTypeMapping; } : @@ -222,57 +221,62 @@ export type RedisArgument = string | Buffer; export type CommandArguments = Array & { preserve?: unknown }; -export const REQUEST_POLICIES = { - /** - * TODO - */ - ALL_NODES: 'all_nodes', - /** - * TODO - */ - ALL_SHARDS: 'all_shards', - /** - * TODO - */ - SPECIAL: 'special' -} as const; +// export const REQUEST_POLICIES = { +// /** +// * TODO +// */ +// ALL_NODES: 'all_nodes', +// /** +// * TODO +// */ +// ALL_SHARDS: 'all_shards', +// /** +// * TODO +// */ +// SPECIAL: 'special' +// } 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 = { - /** - * TODO - */ - ONE_SUCCEEDED: 'one_succeeded', - /** - * TODO - */ - ALL_SUCCEEDED: 'all_succeeded', - /** - * TODO - */ - LOGICAL_AND: 'agg_logical_and', - /** - * TODO - */ - SPECIAL: 'special' -} as const; +// export const RESPONSE_POLICIES = { +// /** +// * TODO +// */ +// ONE_SUCCEEDED: 'one_succeeded', +// /** +// * TODO +// */ +// ALL_SUCCEEDED: 'all_succeeded', +// /** +// * TODO +// */ +// LOGICAL_AND: 'agg_logical_and', +// /** +// * TODO +// */ +// SPECIAL: 'special' +// } 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 = { - request?: RequestPolicies | null; - response?: ResponsePolicies | null; -}; +// export type CommandPolicies = { +// request?: RequestPolicies | null; +// response?: ResponsePolicies | null; +// }; export type Command = { FIRST_KEY_INDEX?: number | ((this: void, ...args: Array) => RedisArgument | undefined); IS_READ_ONLY?: boolean; - POLICIES?: CommandPolicies; + /** + * @internal + * TODO: remove once `POLICIES` is implemented + */ + IS_FORWARD_COMMAND?: boolean; + // POLICIES?: CommandPolicies; transformArguments(this: void, ...args: Array): CommandArguments; TRANSFORM_LEGACY_REPLY?: boolean; transformReply: TransformReply | Record; @@ -355,32 +359,32 @@ export type CommandSignature< TYPE_MAPPING extends TypeMapping > = (...args: Parameters) => Promise, TYPE_MAPPING>>; -export type CommandWithPoliciesSignature< - COMMAND extends Command, - RESP extends RespVersions, - TYPE_MAPPING extends TypeMapping, - POLICIES extends CommandPolicies -> = (...args: Parameters) => Promise< - ReplyWithPolicy< - ReplyWithTypeMapping, TYPE_MAPPING>, - MergePolicies - > ->; +// export type CommandWithPoliciesSignature< +// COMMAND extends Command, +// RESP extends RespVersions, +// TYPE_MAPPING extends TypeMapping, +// POLICIES extends CommandPolicies +// > = (...args: Parameters) => Promise< +// ReplyWithPolicy< +// ReplyWithTypeMapping, TYPE_MAPPING>, +// MergePolicies +// > +// >; -export type MergePolicies< - COMMAND extends Command, - POLICIES extends CommandPolicies -> = Omit & POLICIES; +// export type MergePolicies< +// COMMAND extends Command, +// POLICIES extends CommandPolicies +// > = Omit & POLICIES; -type ReplyWithPolicy< - REPLY, - POLICIES extends CommandPolicies, -> = ( - POLICIES['request'] extends REQUEST_POLICIES['SPECIAL'] ? never : - POLICIES['request'] extends null | undefined ? REPLY : - unknown extends POLICIES['request'] ? REPLY : - POLICIES['response'] extends RESPONSE_POLICIES['SPECIAL'] ? never : - POLICIES['response'] extends RESPONSE_POLICIES['ALL_SUCCEEDED' | 'ONE_SUCCEEDED' | 'LOGICAL_AND'] ? REPLY : - // otherwise, return array of replies - Array -); +// type ReplyWithPolicy< +// REPLY, +// POLICIES extends CommandPolicies, +// > = ( +// POLICIES['request'] extends REQUEST_POLICIES['SPECIAL'] ? never : +// POLICIES['request'] extends null | undefined ? REPLY : +// unknown extends POLICIES['request'] ? REPLY : +// POLICIES['response'] extends RESPONSE_POLICIES['SPECIAL'] ? never : +// POLICIES['response'] extends RESPONSE_POLICIES['ALL_SUCCEEDED' | 'ONE_SUCCEEDED' | 'LOGICAL_AND'] ? REPLY : +// // otherwise, return array of replies +// Array +// ); diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 4c77a257cd..60e776b0dd 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -11,15 +11,19 @@ import { once } from 'node:events'; // import { promisify } from 'node:util'; import { MATH_FUNCTION, loadMathFunction } from '../commands/FUNCTION_LOAD.spec'; import { RESP_TYPES } from '../RESP/decoder'; +import { NumberReply } from '../RESP/types'; import { SortedSetMember } from '../commands/generic-transformers'; export const SQUARE_SCRIPT = defineScript({ - SCRIPT: 'return ARGV[1] * ARGV[1];', - NUMBER_OF_KEYS: 0, - transformArguments(number: number): Array { - return [number.toString()]; + SCRIPT: + `local number = redis.call('GET', KEYS[1]) + return number * number`, + 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', () => { @@ -214,9 +218,10 @@ describe('Client', () => { testUtils.testWithClient('with script', async client => { assert.deepEqual( await client.multi() - .square(2) + .set('key', '2') + .square('key') .exec(), - [4] + ['OK', 4] ); }, { ...GLOBAL.SERVERS.OPEN, @@ -280,10 +285,12 @@ describe('Client', () => { }); testUtils.testWithClient('scripts', async client => { - assert.equal( - await client.square(2), - 4 - ); + const [, reply] = await Promise.all([ + client.set('key', '2'), + client.square('key') + ]); + + assert.equal(reply, 4); }, { ...GLOBAL.SERVERS.OPEN, clientOptions: { @@ -319,12 +326,13 @@ describe('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( - await client.math.square(2), - 4 - ); + assert.equal(reply, 4); }, { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [7, 0], diff --git a/packages/client/lib/cluster/index.spec.ts b/packages/client/lib/cluster/index.spec.ts index 4a50f7f801..f163086357 100644 --- a/packages/client/lib/cluster/index.spec.ts +++ b/packages/client/lib/cluster/index.spec.ts @@ -1,362 +1,348 @@ -// import { strict as assert } from 'node:assert'; -// import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils'; -// import RedisCluster from '.'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils'; +import RedisCluster from '.'; // import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT'; -// import { commandOptions } from '../command-options'; -// import { SQUARE_SCRIPT } from '../client/index.spec'; -// import { RootNodesUnavailableError } from '../errors'; -// import { spy } from 'sinon'; -// import { setTimeout } from 'timers/promises'; -// import RedisClient from '../client'; +import { SQUARE_SCRIPT } from '../client/index.spec'; +import { RootNodesUnavailableError } from '../errors'; +import { spy } from 'sinon'; +// import { setTimeout } from 'node:timers/promises'; +import RedisClient from '../client'; -// describe('Cluster', () => { -// testUtils.testWithCluster('sendCommand', async cluster => { -// assert.equal( -// await cluster.sendCommand(undefined, true, ['PING']), -// 'PONG' -// ); -// }, GLOBAL.CLUSTERS.OPEN); +describe('Cluster', () => { + testUtils.testWithCluster('sendCommand', async cluster => { + assert.equal( + await cluster.sendCommand(undefined, true, ['PING']), + 'PONG' + ); + }, GLOBAL.CLUSTERS.OPEN); -// testUtils.testWithCluster('isOpen', async cluster => { -// assert.equal(cluster.isOpen, true); -// await cluster.disconnect(); -// assert.equal(cluster.isOpen, false); -// }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('isOpen', async cluster => { + assert.equal(cluster.isOpen, true); + await cluster.destroy(); + assert.equal(cluster.isOpen, false); + }, GLOBAL.CLUSTERS.OPEN); -// testUtils.testWithCluster('connect should throw if already connected', async cluster => { -// await assert.rejects(cluster.connect()); -// }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('connect should throw if already connected', async cluster => { + await assert.rejects(cluster.connect()); + }, GLOBAL.CLUSTERS.OPEN); -// testUtils.testWithCluster('multi', async cluster => { -// const key = 'key'; -// assert.deepEqual( -// await cluster.multi() -// .set(key, 'value') -// .get(key) -// .exec(), -// ['OK', 'value'] -// ); -// }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('multi', async cluster => { + const key = 'key'; + assert.deepEqual( + await cluster.multi() + .set(key, 'value') + .get(key) + .exec(), + ['OK', 'value'] + ); + }, GLOBAL.CLUSTERS.OPEN); -// testUtils.testWithCluster('scripts', async cluster => { -// assert.equal( -// await cluster.square(2), -// 4 -// ); -// }, { -// ...GLOBAL.CLUSTERS.OPEN, -// clusterConfiguration: { -// scripts: { -// square: SQUARE_SCRIPT -// } -// } -// }); + testUtils.testWithCluster('scripts', async cluster => { + const [, reply] = await Promise.all([ + cluster.set('key', '2'), + cluster.square('key') + ]); -// it('should throw RootNodesUnavailableError', async () => { -// const cluster = RedisCluster.create({ -// rootNodes: [] -// }); + assert.equal(reply, 4); + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + scripts: { + square: SQUARE_SCRIPT + } + } + }); -// try { -// await assert.rejects( -// cluster.connect(), -// RootNodesUnavailableError -// ); -// } catch (err) { -// await cluster.disconnect(); -// throw err; -// } -// }); + it('should throw RootNodesUnavailableError', async () => { + const cluster = RedisCluster.create({ + rootNodes: [] + }); -// testUtils.testWithCluster('should handle live resharding', async cluster => { -// const slot = 12539, -// key = 'key', -// value = 'value'; -// await cluster.set(key, value); + try { + await assert.rejects( + cluster.connect(), + RootNodesUnavailableError + ); + } catch (err) { + await cluster.disconnect(); + throw err; + } + }); -// const importing = cluster.slots[0].master, -// migrating = cluster.slots[slot].master, -// [ importingClient, migratingClient ] = await Promise.all([ -// cluster.nodeClient(importing), -// cluster.nodeClient(migrating) -// ]); + // testUtils.testWithCluster('should handle live resharding', async cluster => { + // const slot = 12539, + // key = 'key', + // value = 'value'; + // await cluster.set(key, value); -// await Promise.all([ -// importingClient.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, migrating.id), -// migratingClient.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, importing.id) -// ]); + // const importing = cluster.slots[0].master, + // migrating = cluster.slots[slot].master, + // [importingClient, migratingClient] = await Promise.all([ + // cluster.nodeClient(importing), + // cluster.nodeClient(migrating) + // ]); -// // should be able to get the key from the migrating node -// assert.equal( -// await cluster.get(key), -// value -// ); + // await Promise.all([ + // importingClient.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, migrating.id), + // migratingClient.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, importing.id) + // ]); -// await migratingClient.migrate( -// importing.host, -// importing.port, -// key, -// 0, -// 10 -// ); + // // should be able to get the key from the migrating node + // assert.equal( + // await cluster.get(key), + // value + // ); -// // should be able to get the key from the importing node using `ASKING` -// assert.equal( -// await cluster.get(key), -// value -// ); + // await migratingClient.migrate( + // importing.host, + // importing.port, + // key, + // 0, + // 10 + // ); -// await Promise.all([ -// importingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id), -// migratingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id), -// ]); + // // should be able to get the key from the importing node using `ASKING` + // assert.equal( + // await cluster.get(key), + // value + // ); -// // should handle `MOVED` errors -// assert.equal( -// await cluster.get(key), -// value -// ); -// }, { -// serverArguments: [], -// numberOfMasters: 2 -// }); + // await Promise.all([ + // importingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id), + // migratingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id), + // ]); -// testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => { -// const totalNodes = cluster.masters.length + cluster.replicas.length, -// ids = new Set(); -// for (let i = 0; i < totalNodes; i++) { -// ids.add(cluster.getRandomNode().id); -// } - -// assert.equal(ids.size, totalNodes); -// }, GLOBAL.CLUSTERS.WITH_REPLICAS); + // // should handle `MOVED` errors + // assert.equal( + // await cluster.get(key), + // value + // ); + // }, { + // serverArguments: [], + // numberOfMasters: 2 + // }); -// testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => { -// const totalNodes = 1 + cluster.slots[0].replicas!.length, -// ids = new Set(); -// for (let i = 0; i < totalNodes; i++) { -// ids.add(cluster.getSlotRandomNode(0).id); -// } - -// assert.equal(ids.size, totalNodes); -// }, GLOBAL.CLUSTERS.WITH_REPLICAS); + testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => { + const totalNodes = cluster.masters.length + cluster.replicas.length, + ids = new Set(); + for (let i = 0; i < totalNodes; i++) { + ids.add(cluster.getRandomNode().id); + } -// testUtils.testWithCluster('cluster topology', async cluster => { -// assert.equal(cluster.slots.length, 16384); -// const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS; -// assert.equal(cluster.shards.length, numberOfMasters); -// assert.equal(cluster.masters.length, numberOfMasters); -// assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters); -// assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas); -// }, GLOBAL.CLUSTERS.WITH_REPLICAS); + assert.equal(ids.size, totalNodes); + }, GLOBAL.CLUSTERS.WITH_REPLICAS); -// testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => { -// const masters = cluster.getMasters(); -// assert.ok(Array.isArray(masters)); -// for (const master of masters) { -// assert.equal(typeof master.id, 'string'); -// assert.ok(master.client instanceof RedisClient); -// } -// }, { -// ...GLOBAL.CLUSTERS.OPEN, -// clusterConfiguration: { -// minimizeConnections: undefined // reset to default -// } -// }); + testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => { + const totalNodes = 1 + cluster.slots[0].replicas!.length, + ids = new Set(); + for (let i = 0; i < totalNodes; i++) { + ids.add(cluster.getSlotRandomNode(0).id); + } -// testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => { -// const master = cluster.getSlotMaster(0); -// assert.equal(typeof master.id, 'string'); -// assert.ok(master.client instanceof RedisClient); -// }, { -// ...GLOBAL.CLUSTERS.OPEN, -// clusterConfiguration: { -// minimizeConnections: undefined // reset to default -// } -// }); + assert.equal(ids.size, totalNodes); + }, GLOBAL.CLUSTERS.WITH_REPLICAS); -// testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => { -// await assert.rejects(cluster.mGet(['a', 'b'])); -// }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('cluster topology', async cluster => { + assert.equal(cluster.slots.length, 16384); + const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS; + assert.equal(cluster.shards.length, numberOfMasters); + assert.equal(cluster.masters.length, numberOfMasters); + assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters); + assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas); + }, GLOBAL.CLUSTERS.WITH_REPLICAS); -// testUtils.testWithCluster('should send commands with commandOptions to correct cluster slot (without redirections)', async cluster => { -// // 'a' and 'b' hash to different cluster slots (see previous unit test) -// // -> maxCommandRedirections 0: rejects on MOVED/ASK reply -// await cluster.set(commandOptions({ isolated: true }), 'a', '1'), -// await cluster.set(commandOptions({ isolated: true }), 'b', '2'), + testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => { + const masters = cluster.getMasters(); + assert.ok(Array.isArray(masters)); + for (const master of masters) { + assert.equal(typeof master.id, 'string'); + assert.ok(master.client instanceof RedisClient); + } + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + minimizeConnections: undefined // reset to default + } + }); -// assert.equal(await cluster.get('a'), '1'); -// assert.equal(await cluster.get('b'), '2'); -// }, { -// ...GLOBAL.CLUSTERS.OPEN, -// clusterConfiguration: { -// maxCommandRedirections: 0 -// } -// }); + testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => { + const master = cluster.getSlotMaster(0); + assert.equal(typeof master.id, 'string'); + assert.ok(master.client instanceof RedisClient); + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + minimizeConnections: undefined // reset to default + } + }); -// describe('minimizeConnections', () => { -// testUtils.testWithCluster('false', async cluster => { -// for (const master of cluster.masters) { -// assert.ok(master.client instanceof RedisClient); -// } -// }, { -// ...GLOBAL.CLUSTERS.OPEN, -// clusterConfiguration: { -// minimizeConnections: false -// } -// }); + testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => { + await assert.rejects(cluster.mGet(['a', 'b'])); + }, GLOBAL.CLUSTERS.OPEN); -// testUtils.testWithCluster('true', async cluster => { -// for (const master of cluster.masters) { -// assert.equal(master.client, undefined); -// } -// }, { -// ...GLOBAL.CLUSTERS.OPEN, -// clusterConfiguration: { -// minimizeConnections: true -// } -// }); -// }); + describe('minimizeConnections', () => { + testUtils.testWithCluster('false', async cluster => { + for (const master of cluster.masters) { + assert.ok(master.client instanceof RedisClient); + } + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + minimizeConnections: false + } + }); -// describe('PubSub', () => { -// testUtils.testWithCluster('subscribe & unsubscribe', async cluster => { -// const listener = spy(); + testUtils.testWithCluster('true', async cluster => { + for (const master of cluster.masters) { + assert.equal(master.client, undefined); + } + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + minimizeConnections: true + } + }); + }); -// await cluster.subscribe('channel', listener); + describe('PubSub', () => { + testUtils.testWithCluster('subscribe & unsubscribe', async cluster => { + const listener = spy(); -// await Promise.all([ -// waitTillBeenCalled(listener), -// cluster.publish('channel', 'message') -// ]); - -// assert.ok(listener.calledOnceWithExactly('message', 'channel')); + await cluster.subscribe('channel', listener); -// await cluster.unsubscribe('channel', listener); + await Promise.all([ + waitTillBeenCalled(listener), + cluster.publish('channel', 'message') + ]); -// assert.equal(cluster.pubSubNode, undefined); -// }, GLOBAL.CLUSTERS.OPEN); + assert.ok(listener.calledOnceWithExactly('message', 'channel')); -// testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => { -// const listener = spy(); + await cluster.unsubscribe('channel', listener); -// await cluster.pSubscribe('channe*', listener); + assert.equal(cluster.pubSubNode, undefined); + }, GLOBAL.CLUSTERS.OPEN); -// await Promise.all([ -// waitTillBeenCalled(listener), -// cluster.publish('channel', 'message') -// ]); - -// assert.ok(listener.calledOnceWithExactly('message', 'channel')); + testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => { + const listener = spy(); -// await cluster.pUnsubscribe('channe*', listener); + await cluster.pSubscribe('channe*', listener); -// assert.equal(cluster.pubSubNode, undefined); -// }, GLOBAL.CLUSTERS.OPEN); + await Promise.all([ + waitTillBeenCalled(listener), + cluster.publish('channel', 'message') + ]); -// testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => { -// const listener = spy(); -// await cluster.subscribe('channel', listener); + assert.ok(listener.calledOnceWithExactly('message', 'channel')); -// assert.ok(cluster.pubSubNode); -// const [ migrating, importing ] = cluster.masters[0].address === cluster.pubSubNode.address ? -// cluster.masters : -// [cluster.masters[1], cluster.masters[0]], -// [ migratingClient, importingClient ] = await Promise.all([ -// cluster.nodeClient(migrating), -// cluster.nodeClient(importing) -// ]); + await cluster.pUnsubscribe('channe*', listener); -// const range = cluster.slots[0].master === migrating ? { -// key: 'bar', // 5061 -// start: 0, -// end: 8191 -// } : { -// key: 'foo', // 12182 -// start: 8192, -// end: 16383 -// }; + assert.equal(cluster.pubSubNode, undefined); + }, GLOBAL.CLUSTERS.OPEN); -// await Promise.all([ -// migratingClient.clusterDelSlotsRange(range), -// importingClient.clusterDelSlotsRange(range), -// importingClient.clusterAddSlotsRange(range) -// ]); + // testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => { + // const listener = spy(); + // await cluster.subscribe('channel', listener); -// // wait for migrating node to be notified about the new topology -// while ((await migratingClient.clusterInfo()).state !== 'ok') { -// await setTimeout(50); -// } + // assert.ok(cluster.pubSubNode); + // const [migrating, importing] = cluster.masters[0].address === cluster.pubSubNode.address ? + // cluster.masters : + // [cluster.masters[1], cluster.masters[0]], + // [migratingClient, importingClient] = await Promise.all([ + // cluster.nodeClient(migrating), + // cluster.nodeClient(importing) + // ]); -// // make sure to cause `MOVED` error -// await cluster.get(range.key); + // const range = cluster.slots[0].master === migrating ? { + // key: 'bar', // 5061 + // start: 0, + // end: 8191 + // } : { + // key: 'foo', // 12182 + // start: 8192, + // end: 16383 + // }; -// await Promise.all([ -// cluster.publish('channel', 'message'), -// waitTillBeenCalled(listener) -// ]); - -// assert.ok(listener.calledOnceWithExactly('message', 'channel')); -// }, { -// serverArguments: [], -// numberOfMasters: 2, -// minimumDockerVersion: [7] -// }); + // await Promise.all([ + // migratingClient.clusterDelSlotsRange(range), + // importingClient.clusterDelSlotsRange(range), + // importingClient.clusterAddSlotsRange(range) + // ]); -// testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => { -// const listener = spy(); + // // wait for migrating node to be notified about the new topology + // while ((await migratingClient.clusterInfo()).state !== 'ok') { + // await setTimeout(50); + // } -// await cluster.sSubscribe('channel', listener); + // // make sure to cause `MOVED` error + // await cluster.get(range.key); -// await Promise.all([ -// waitTillBeenCalled(listener), -// cluster.sPublish('channel', 'message') -// ]); - -// assert.ok(listener.calledOnceWithExactly('message', 'channel')); + // await Promise.all([ + // cluster.publish('channel', 'message'), + // waitTillBeenCalled(listener) + // ]); -// await cluster.sUnsubscribe('channel', listener); + // assert.ok(listener.calledOnceWithExactly('message', 'channel')); + // }, { + // serverArguments: [], + // numberOfMasters: 2, + // minimumDockerVersion: [7] + // }); -// // 10328 is the slot of `channel` -// assert.equal(cluster.slots[10328].master.pubSubClient, undefined); -// }, { -// ...GLOBAL.CLUSTERS.OPEN, -// minimumDockerVersion: [7] -// }); + testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => { + const listener = spy(); -// testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => { -// const SLOT = 10328, -// migrating = cluster.slots[SLOT].master, -// importing = cluster.masters.find(master => master !== migrating)!, -// [ migratingClient, importingClient ] = await Promise.all([ -// cluster.nodeClient(migrating), -// cluster.nodeClient(importing) -// ]); + await cluster.sSubscribe('channel', listener); -// await Promise.all([ -// migratingClient.clusterDelSlots(SLOT), -// importingClient.clusterDelSlots(SLOT), -// importingClient.clusterAddSlots(SLOT) -// ]); + await Promise.all([ + waitTillBeenCalled(listener), + cluster.sPublish('channel', 'message') + ]); -// // wait for migrating node to be notified about the new topology -// while ((await migratingClient.clusterInfo()).state !== 'ok') { -// await setTimeout(50); -// } + assert.ok(listener.calledOnceWithExactly('message', 'channel')); -// const listener = spy(); + await cluster.sUnsubscribe('channel', listener); -// // will trigger `MOVED` error -// await cluster.sSubscribe('channel', listener); + // 10328 is the slot of `channel` + assert.equal(cluster.slots[10328].master.pubSubClient, undefined); + }, { + ...GLOBAL.CLUSTERS.OPEN, + minimumDockerVersion: [7] + }); -// await Promise.all([ -// waitTillBeenCalled(listener), -// cluster.sPublish('channel', 'message') -// ]); - -// assert.ok(listener.calledOnceWithExactly('message', 'channel')); -// }, { -// serverArguments: [], -// minimumDockerVersion: [7] -// }); -// }); -// }); + // testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => { + // const SLOT = 10328, + // migrating = cluster.slots[SLOT].master, + // importing = cluster.masters.find(master => master !== migrating)!, + // [migratingClient, importingClient] = await Promise.all([ + // cluster.nodeClient(migrating), + // cluster.nodeClient(importing) + // ]); + + // await Promise.all([ + // migratingClient.clusterDelSlots(SLOT), + // importingClient.clusterDelSlots(SLOT), + // importingClient.clusterAddSlots(SLOT) + // ]); + + // // wait for migrating node to be notified about the new topology + // while ((await migratingClient.clusterInfo()).state !== 'ok') { + // await setTimeout(50); + // } + + // const listener = spy(); + + // // will trigger `MOVED` error + // await cluster.sSubscribe('channel', listener); + + // await Promise.all([ + // waitTillBeenCalled(listener), + // cluster.sPublish('channel', 'message') + // ]); + + // assert.ok(listener.calledOnceWithExactly('message', 'channel')); + // }, { + // serverArguments: [], + // minimumDockerVersion: [7] + // }); + }); +}); diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index d52ad94497..cc0e895ade 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -1,6 +1,6 @@ import { RedisClientOptions, RedisClientType } from '../client'; 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 { EventEmitter } from 'node:events'; import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; @@ -65,12 +65,48 @@ export interface RedisClusterOptions< 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< RESP extends RespVersions, - TYPE_MAPPING extends TypeMapping, - // POLICIES extends CommandPolicies + TYPE_MAPPING extends TypeMapping > = { -[P in keyof typeof COMMANDS]: CommandWithPoliciesSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING, POLICIES>; + [P in keyof typeof COMMANDS as ClusterCommand]: 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]: CommandSignature; + }; +}; + +type WithFunctions< + F extends RedisFunctions, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [L in keyof F]: { + [C in keyof F[L] as ClusterCommand]: CommandSignature; + }; +}; + +type WithScripts< + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof S as ClusterCommand]: CommandSignature; }; export type RedisClusterType< @@ -80,17 +116,22 @@ export type RedisClusterType< RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {}, // POLICIES extends CommandPolicies = {} -> = RedisCluster & WithCommands; -// & WithModules & WithFunctions & WithScripts +> = ( + RedisCluster & + WithCommands & + WithModules & + WithFunctions & + WithScripts +); export interface ClusterCommandOptions< - TYPE_MAPPING extends TypeMapping = TypeMapping, - POLICIES extends CommandPolicies = CommandPolicies + TYPE_MAPPING extends TypeMapping = TypeMapping + // POLICIES extends CommandPolicies = CommandPolicies > extends CommandOptions { - policies?: POLICIES; + // policies?: POLICIES; } -type ProxyCluster = RedisCluster; +type ProxyCluster = RedisCluster; type NamespaceProxyCluster = { self: ProxyCluster }; @@ -100,20 +141,30 @@ export default class RedisCluster< S extends RedisScripts, RESP extends RespVersions, TYPE_MAPPING extends TypeMapping, - POLICIES extends CommandPolicies + // POLICIES extends CommandPolicies > extends EventEmitter { static extractFirstKey( command: C, args: Parameters, redisArgs: Array - ): RedisArgument | undefined { - if (command.FIRST_KEY_INDEX === undefined) { - return undefined; - } else if (typeof command.FIRST_KEY_INDEX === 'number') { - return redisArgs[command.FIRST_KEY_INDEX]; + ) { + let key: RedisArgument | undefined; + switch (typeof command.FIRST_KEY_INDEX) { + case 'number': + 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) { @@ -130,7 +181,7 @@ export default class RedisCluster< command.IS_READ_ONLY, redisArgs, this._commandOptions, - command.POLICIES + // command.POLICIES ); return transformReply ? @@ -153,7 +204,7 @@ export default class RedisCluster< command.IS_READ_ONLY, redisArgs, this.self._commandOptions, - command.POLICIES + // command.POLICIES ); return transformReply ? @@ -167,18 +218,18 @@ export default class RedisCluster< transformReply = getTransformReply(fn, resp); return async function (this: NamespaceProxyCluster, ...args: Array) { const fnArgs = fn.transformArguments(...args), - redisArgs = prefix.concat(fnArgs), firstKey = RedisCluster.extractFirstKey( fn, - fnArgs, - redisArgs + args, + fnArgs ), + redisArgs = prefix.concat(fnArgs), reply = await this.self.sendCommand( firstKey, fn.IS_READ_ONLY, redisArgs, this.self._commandOptions, - fn.POLICIES + // fn.POLICIES ); return transformReply ? @@ -192,18 +243,19 @@ export default class RedisCluster< transformReply = getTransformReply(script, resp); return async function (this: ProxyCluster, ...args: Array) { const scriptArgs = script.transformArguments(...args), - redisArgs = prefix.concat(scriptArgs), firstKey = RedisCluster.extractFirstKey( script, - scriptArgs, - redisArgs + args, + scriptArgs ), - reply = await this.sendCommand( + redisArgs = prefix.concat(scriptArgs), + reply = await this.executeScript( + script, firstKey, script.IS_READ_ONLY, redisArgs, this._commandOptions, - script.POLICIES + // script.POLICIES ); return transformReply ? @@ -218,8 +270,8 @@ export default class RedisCluster< S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {}, - POLICIES extends CommandPolicies = {} - >(config?: ClusterCommander) { + // POLICIES extends CommandPolicies = {} + >(config?: ClusterCommander) { const Cluster = attachConfig({ BaseClass: RedisCluster, commands: COMMANDS, @@ -234,7 +286,7 @@ export default class RedisCluster< return (options?: Omit>) => { // returning a "proxy" to prevent the namespaces.self to leak between "proxies" - return Object.create(new Cluster(options)) as RedisClusterType; + return Object.create(new Cluster(options)) as RedisClusterType; }; } @@ -244,16 +296,16 @@ export default class RedisCluster< S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {}, - POLICIES extends CommandPolicies = {} - >(options?: RedisClusterOptions) { + // POLICIES extends CommandPolicies = {} + >(options?: RedisClusterOptions) { return RedisCluster.factory(options)(options); } - private readonly _options: RedisClusterOptions; + private readonly _options: RedisClusterOptions; private readonly _slots: RedisClusterSlots; - private _commandOptions?: ClusterCommandOptions; + private _commandOptions?: ClusterCommandOptions; /** * 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; } - constructor(options: RedisClusterOptions) { + constructor(options: RedisClusterOptions) { super(); this._options = options; @@ -336,9 +388,9 @@ export default class RedisCluster< } withCommandOptions< - OPTIONS extends ClusterCommandOptions, + OPTIONS extends ClusterCommandOptions, TYPE_MAPPING extends TypeMapping, - POLICIES extends CommandPolicies + // POLICIES extends CommandPolicies >(options: OPTIONS) { const proxy = Object.create(this); proxy._commandOptions = options; @@ -347,8 +399,8 @@ export default class RedisCluster< F, S, RESP, - TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {}, - POLICIES extends CommandPolicies ? POLICIES : {} + TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {} + // POLICIES extends CommandPolicies ? POLICIES : {} >; } @@ -367,8 +419,8 @@ export default class RedisCluster< F, S, RESP, - K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING, - K extends 'policies' ? V extends CommandPolicies ? V : {} : POLICIES + K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING + // K extends 'policies' ? V extends CommandPolicies ? V : {} : POLICIES >; } @@ -379,15 +431,15 @@ export default class RedisCluster< return this._commandOptionsProxy('typeMapping', typeMapping); } - /** - * Override the `policies` command option - * TODO - */ - withPolicies (policies: POLICIES) { - return this._commandOptionsProxy('policies', policies); - } + // /** + // * Override the `policies` command option + // * TODO + // */ + // withPolicies (policies: POLICIES) { + // return this._commandOptionsProxy('policies', policies); + // } - async #execute( + private async _execute( firstKey: RedisArgument | undefined, isReadonly: boolean | undefined, fn: (client: RedisClientType) => Promise @@ -437,9 +489,9 @@ export default class RedisCluster< isReadonly: boolean | undefined, args: CommandArguments, options?: ClusterCommandOptions, - defaultPolicies?: CommandPolicies + // defaultPolicies?: CommandPolicies ): Promise { - return this.#execute( + return this._execute( firstKey, isReadonly, client => client.sendCommand(args, options) @@ -453,7 +505,7 @@ export default class RedisCluster< args: Array, options?: CommandOptions ) { - return this.#execute( + return this._execute( firstKey, isReadonly, client => client.executeScript(script, args, options) diff --git a/packages/client/lib/commands/CLUSTER_INFO.ts b/packages/client/lib/commands/CLUSTER_INFO.ts index c541de1729..4605efbe81 100644 --- a/packages/client/lib/commands/CLUSTER_INFO.ts +++ b/packages/client/lib/commands/CLUSTER_INFO.ts @@ -1,4 +1,4 @@ -import { VerbatimStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { VerbatimStringReply, Command } from '../RESP/types'; export default { FIRST_KEY_INDEX: undefined, diff --git a/packages/client/lib/commands/CLUSTER_NODES.ts b/packages/client/lib/commands/CLUSTER_NODES.ts index 9166ce52f0..64dd505623 100644 --- a/packages/client/lib/commands/CLUSTER_NODES.ts +++ b/packages/client/lib/commands/CLUSTER_NODES.ts @@ -1,4 +1,4 @@ -import { VerbatimStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { VerbatimStringReply, Command } from '../RESP/types'; export default { FIRST_KEY_INDEX: undefined, diff --git a/packages/client/lib/commands/CLUSTER_REPLICAS.ts b/packages/client/lib/commands/CLUSTER_REPLICAS.ts index 7cb0eaae43..831c6bc505 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICAS.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICAS.ts @@ -1,4 +1,4 @@ -import { RedisArgument, VerbatimStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types'; export default { FIRST_KEY_INDEX: undefined, diff --git a/packages/client/lib/commands/FCALL.spec.ts b/packages/client/lib/commands/FCALL.spec.ts index 06616ba1db..35ae8c87c2 100644 --- a/packages/client/lib/commands/FCALL.spec.ts +++ b/packages/client/lib/commands/FCALL.spec.ts @@ -17,13 +17,14 @@ describe('FCALL', () => { }); 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( - await client.fCall(MATH_FUNCTION.library.square.NAME, { - arguments: ['2'] - }), - 4 - ); + assert.equal(reply, 4); }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FCALL_RO.spec.ts b/packages/client/lib/commands/FCALL_RO.spec.ts index 114430eb58..0b172d1e21 100644 --- a/packages/client/lib/commands/FCALL_RO.spec.ts +++ b/packages/client/lib/commands/FCALL_RO.spec.ts @@ -17,13 +17,14 @@ describe('FCALL_RO', () => { }); 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( - await client.fCallRo(MATH_FUNCTION.library.square.NAME, { - arguments: ['2'] - }), - 4 - ); + assert.equal(reply, 4); }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FUNCTION_LOAD.spec.ts b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts index a739d8f7f4..657f6d0325 100644 --- a/packages/client/lib/commands/FUNCTION_LOAD.spec.ts +++ b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts @@ -11,16 +11,20 @@ export const MATH_FUNCTION = { `#!LUA name=math redis.register_function { 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" } }`, library: { square: { NAME: 'square', IS_READ_ONLY: true, - NUMBER_OF_KEYS: 0, - transformArguments(number: number) { - return [number.toString()]; + NUMBER_OF_KEYS: 1, + FIRST_KEY_INDEX: 0, + transformArguments(key: string) { + return [key]; }, transformReply: undefined as unknown as () => NumberReply } diff --git a/packages/client/lib/commands/PUBLISH.ts b/packages/client/lib/commands/PUBLISH.ts index 1566651d9f..e790ff16c4 100644 --- a/packages/client/lib/commands/PUBLISH.ts +++ b/packages/client/lib/commands/PUBLISH.ts @@ -3,6 +3,7 @@ import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { FIRST_KEY_INDEX: undefined, IS_READ_ONLY: true, + IS_FORWARD_COMMAND: true, transformArguments(channel: RedisArgument, message: RedisArgument) { return ['PUBLISH', channel, message]; }, diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 7ccc2a2d48..68e9630769 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -4,14 +4,13 @@ import { RedisScripts, RespVersions, TypeMapping, - CommandPolicies, + // CommandPolicies, createClient, RedisClientOptions, RedisClientType, createCluster, RedisClusterOptions, - RedisClusterType, - RESP_TYPES + RedisClusterType } from '@redis/client/index'; import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers'; import yargs from 'yargs'; @@ -44,11 +43,11 @@ interface ClusterTestOptions< F extends RedisFunctions, S extends RedisScripts, RESP extends RespVersions, - TYPE_MAPPING extends TypeMapping, - POLICIES extends CommandPolicies + TYPE_MAPPING extends TypeMapping + // POLICIES extends CommandPolicies > extends CommonTestOptions { serverArguments: Array; - clusterConfiguration?: Partial>; + clusterConfiguration?: Partial>; numberOfMasters?: number; numberOfReplicas?: number; } @@ -58,11 +57,11 @@ interface AllTestOptions< F extends RedisFunctions, S extends RedisScripts, RESP extends RespVersions, - TYPE_MAPPING extends TypeMapping, - POLICIES extends CommandPolicies + TYPE_MAPPING extends TypeMapping + // POLICIES extends CommandPolicies > { client: ClientTestOptions; - cluster: ClusterTestOptions; + cluster: ClusterTestOptions; } interface Version { @@ -197,9 +196,9 @@ export default class TestUtils { F extends RedisFunctions, S extends RedisScripts, RESP extends RespVersions, - TYPE_MAPPING extends TypeMapping, - POLICIES extends CommandPolicies - >(cluster: RedisClusterType): Promise { + TYPE_MAPPING extends TypeMapping + // POLICIES extends CommandPolicies + >(cluster: RedisClusterType): Promise { return Promise.all( cluster.masters.map(async ({ client }) => { if (client) { @@ -214,12 +213,12 @@ export default class TestUtils { F extends RedisFunctions = {}, S extends RedisScripts = {}, RESP extends RespVersions = 2, - TYPE_MAPPING extends TypeMapping = {}, - POLICIES extends CommandPolicies = {} + TYPE_MAPPING extends TypeMapping = {} + // POLICIES extends CommandPolicies = {} >( title: string, - fn: (cluster: RedisClusterType) => unknown, - options: ClusterTestOptions + fn: (cluster: RedisClusterType) => unknown, + options: ClusterTestOptions ): void { let dockersPromise: ReturnType; if (this.isVersionGreaterThan(options.minimumDockerVersion)) { @@ -267,12 +266,12 @@ export default class TestUtils { F extends RedisFunctions = {}, S extends RedisScripts = {}, RESP extends RespVersions = 2, - TYPE_MAPPING extends TypeMapping = {}, - POLICIES extends CommandPolicies = {} + TYPE_MAPPING extends TypeMapping = {} + // POLICIES extends CommandPolicies = {} >( title: string, - fn: (client: RedisClientType | RedisClusterType) => unknown, - options: AllTestOptions + fn: (client: RedisClientType | RedisClusterType) => unknown, + options: AllTestOptions ) { this.testWithClient(`client.${title}`, fn, options.client); this.testWithCluster(`cluster.${title}`, fn, options.cluster);