diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 0d4d479a85e..089eda5e375 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -225,7 +225,7 @@ export default class RedisClusterSlots< this._randomNodeIterator = undefined; } - async #discover(rootNode: RedisClusterClientOptions) { + async #discover(rootNode: RedisClusterClientOptions) { this.clientSideCache?.clear(); this.clientSideCache?.disable(); @@ -509,16 +509,21 @@ export default class RedisClusterSlots< } } - async #getShards(rootNode: RedisClusterClientOptions) { + async #getShards(rootNode: RedisClusterClientOptions) { const options = this.#clientOptionsDefaults(rootNode)!; - options.socket ??= {}; - options.socket.reconnectStrategy = false; - options.RESP = this.#options.RESP; - options.commandOptions = undefined; - options.maintNotifications = 'disabled'; - - // TODO: find a way to avoid type casting - const client = await this.#clientFactory(options as RedisClientOptions) + + const clientOptions: RedisClientOptions = { + ...options, + socket: { + ...options.socket, + reconnectStrategy: false + }, + RESP: this.#options.RESP, + commandOptions: undefined, + maintNotifications: 'disabled' + }; + + const client = await this.#clientFactory<{}>(clientOptions) .on('error', err => this.#emit('error', err)) .connect(); @@ -540,7 +545,7 @@ export default class RedisClusterSlots< } } - #nodeClientOptions(node: NodeAddress & { address: string }): RedisClusterClientOptions { + #nodeClientOptions(node: NodeAddress & { address: string }): RedisClusterClientOptions { return { socket: this.#getNodeAddress(node.address) ?? { host: node.host, @@ -549,7 +554,9 @@ export default class RedisClusterSlots< }; } - #clientOptionsDefaults(options?: RedisClientOptions) { + #clientOptionsDefaults(options: RedisClusterClientOptions): RedisClusterClientOptions; + #clientOptionsDefaults(options?: RedisClusterClientOptions): RedisClusterClientOptions | undefined; + #clientOptionsDefaults(options?: RedisClusterClientOptions) { if (!this.#options.defaults) return options; let socket; @@ -614,13 +621,16 @@ export default class RedisClusterSlots< const address = node.address; const emit = this.#emit; let wasReady = false; - const client = this.#clientFactory( this.#clientOptionsDefaults({ + const clientOptions: RedisClientOptions = { + ...this.#clientOptionsDefaults({ clientSideCache: this.clientSideCache, himportRegistry: this.#himportRegistry, - RESP: this.#options.RESP, socket, readonly, - })); + }), + RESP: this.#options.RESP, + }; + const client = this.#clientFactory(clientOptions); client._setIdentity(ClientRole.CLUSTER_NODE, this.#clusterClientId); client .on('error', error => emit('node-error', error, clientInfo)) diff --git a/packages/client/lib/cluster/index.spec.ts b/packages/client/lib/cluster/index.spec.ts index f4b94fc81b8..9464453ee83 100644 --- a/packages/client/lib/cluster/index.spec.ts +++ b/packages/client/lib/cluster/index.spec.ts @@ -270,6 +270,18 @@ describe('Cluster', () => { assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas); }, GLOBAL.CLUSTERS.WITH_REPLICAS); + testUtils.testWithCluster('node clients inherit configured cluster RESP version', async cluster => { + for (const master of cluster.masters) { + assert.ok(master.client instanceof RedisClient); + assert.equal(master.client.options.RESP, cluster._options?.RESP ?? 3); + } + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + minimizeConnections: false + } + }); + testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => { const masters = cluster.getMasters(); assert.ok(Array.isArray(masters)); diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index d9ebdedd4e5..6bc91ee08d2 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -54,9 +54,15 @@ interface ClusterCommander< keyPrefix?: RedisArgument; } -export type RedisClusterClientOptions = Omit< - RedisClientOptions, - keyof ClusterCommander +export type RedisClusterClientOptions< + M extends RedisModules = RedisModules, + F extends RedisFunctions = RedisFunctions, + S extends RedisScripts = RedisScripts, + RESP extends RespVersions = RespVersions, + TYPE_MAPPING extends TypeMapping = TypeMapping +> = Omit< + RedisClientOptions, + keyof ClusterCommander >; export interface RedisClusterOptions< @@ -75,7 +81,7 @@ export interface RedisClusterOptions< * not inherited by the connections made to the discovered nodes. Settings that should apply to * every connection (e.g. credentials, TLS) must be specified via `defaults`. */ - rootNodes: Array; + rootNodes: Array>; /** * Default values used for every client in the cluster. Use this to specify global values, * for example: ACL credentials, timeouts, TLS configuration etc. @@ -83,7 +89,7 @@ export interface RedisClusterOptions< * The connections to the discovered cluster nodes are created from these defaults (plus the * discovered host and port) — they do not inherit any other settings from `rootNodes`. */ - defaults?: Partial; + defaults?: Partial>; /** * When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes. * Useful for short-term or PubSub-only connections. @@ -285,7 +291,9 @@ export default class RedisCluster< RedisCluster.#SingleEntryCache.set(config, Cluster); } - return (options?: Omit>) => { + return ( + options?: Omit, keyof Exclude> + ) => { // returning a "proxy" to prevent the namespaces._self to leak between "proxies" return Object.create(new Cluster(options)) as RedisClusterType; }; @@ -317,7 +325,7 @@ export default class RedisCluster< TYPE_MAPPING extends TypeMapping = {}, // POLICIES extends CommandPolicies = {} >(options?: RedisClusterOptions) { - return RedisCluster.factory(options)(options); + return RedisCluster.factory(options)(options); } readonly _options: RedisClusterOptions;