From f659173f819d40cd01649d2ab8c40e41ff59b738 Mon Sep 17 00:00:00 2001 From: "bramwelbarack89@gmail.com" Date: Sat, 2 May 2026 18:08:59 +0300 Subject: [PATCH 1/7] fix: ensure module commands respect proxy typeMapping. closes #3055 --- packages/client/lib/client/index.spec.ts | 25 +++- packages/client/lib/client/index.ts | 169 ++++++++++++----------- 2 files changed, 106 insertions(+), 88 deletions(-) diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 128499851e..e22d813230 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -412,11 +412,11 @@ describe('Client', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('AbortError', async client => { - await blockSetImmediate(async () => { - await assert.rejects(client.sendCommand(['PING'], { - abortSignal: AbortSignal.timeout(5) - }), AbortError); - }) + await blockSetImmediate(async () => { + await assert.rejects(client.sendCommand(['PING'], { + abortSignal: AbortSignal.timeout(5) + }), AbortError); + }) }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('Timeout with custom timeout config', async client => { @@ -689,6 +689,21 @@ describe('Client', () => { } }); + testUtils.testWithClient('Module TypeMapping Fix', async (client) => { + const bufferProxy = client.withCommandOptions({ + typeMapping: { [RESP_TYPES.BLOB_STRING]: Buffer } + }); + const bufferReply = await bufferProxy.module.echo('hi'); + const stringReply = await client.module.echo('hi'); + + assert.ok((bufferReply as unknown) instanceof Buffer, 'Proxy failed to return Buffer'); + assert.strictEqual(typeof stringReply, 'string', 'Original client was corrupted'); + assert.equal(bufferReply.toString(), stringReply); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { modules: { module } } + }) + testUtils.testWithClient('duplicate should reuse command options', async client => { const duplicate = client.duplicate(); diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index c20c75830e..4aceb5013d 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -25,7 +25,7 @@ import { ClientMetricsHandle, ClientRegistry } from '../opentelemetry'; import { ClientIdentity, ClientRole, generateClientId } from './identity'; import { trace, sanitizeArgs, publish, CHANNELS, type CommandTraceContext } from './tracing'; -const noop = () => {}; +const noop = () => { }; export interface RedisClientOptions< M extends RedisModules = RedisModules, @@ -257,7 +257,10 @@ export type RedisClientType< type ProxyClient = RedisClient; -type NamespaceProxyClient = { _self: ProxyClient }; +type NamespaceProxyClient = { + _self: ProxyClient; + _commandOptions?: CommandOptions +}; interface ScanIteratorOptions { cursor?: RedisArgument; @@ -290,7 +293,7 @@ export default class RedisClient< const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); - return this._self._executeCommand(command, parser, this._self._commandOptions, transformReply); + return this._self._executeCommand(command, parser, this._commandOptions, transformReply); }; } @@ -303,7 +306,7 @@ export default class RedisClient< parser.push(...prefix); fn.parseCommand(parser, ...args); - return this._self._executeCommand(fn, parser, this._self._commandOptions, transformReply); + return this._self._executeCommand(fn, parser, this._commandOptions, transformReply); }; } @@ -587,7 +590,7 @@ export default class RedisClient< this.#registerForMetrics(); - if(this.#options.maintNotifications !== 'disabled') { + if (this.#options.maintNotifications !== 'disabled') { new EnterpriseMaintenanceManager(this.#queue, this, this.#options); }; @@ -664,7 +667,7 @@ export default class RedisClient< this._commandOptions = options.commandOptions; } - if(options.maintNotifications !== 'disabled') { + if (options.maintNotifications !== 'disabled') { EnterpriseMaintenanceManager.setupDefaultMaintOptions(options); } @@ -847,16 +850,16 @@ export default class RedisClient< } if (this.#clientSideCache) { - commands.push({cmd: this.#clientSideCache.trackingOn()}); + commands.push({ cmd: this.#clientSideCache.trackingOn() }); } if (this.#options?.emitInvalidate) { - commands.push({cmd: ['CLIENT', 'TRACKING', 'ON']}); + commands.push({ cmd: ['CLIENT', 'TRACKING', 'ON'] }); } const maintenanceHandshakeCmd = await EnterpriseMaintenanceManager.getHandshakeCommand(this.#options, this._clientId); - if(maintenanceHandshakeCmd) { + if (maintenanceHandshakeCmd) { commands.push(maintenanceHandshakeCmd); }; @@ -872,24 +875,24 @@ export default class RedisClient< this.emit('error', err); } }) - .on('error', err => { - this.emit('error', err); - this.#clientSideCache?.onError(); - if (this.#socket.isOpen && !this.#options.disableOfflineQueue) { - this.#queue.flushWaitingForReply(err); - } else { - this.#queue.flushAll(err); - } - }) - .on('connect', () => this.emit('connect')) - .on('ready', () => { - this.emit('ready'); - this.#setPingTimer(); - this.#maybeScheduleWrite(); - }) - .on('reconnecting', () => this.emit('reconnecting')) - .on('drain', () => this.#maybeScheduleWrite()) - .on('end', () => this.emit('end')); + .on('error', err => { + this.emit('error', err); + this.#clientSideCache?.onError(); + if (this.#socket.isOpen && !this.#options.disableOfflineQueue) { + this.#queue.flushWaitingForReply(err); + } else { + this.#queue.flushAll(err); + } + }) + .on('connect', () => this.emit('connect')) + .on('ready', () => { + this.emit('ready'); + this.#setPingTimer(); + this.#maybeScheduleWrite(); + }) + .on('reconnecting', () => this.emit('reconnecting')) + .on('drain', () => this.#maybeScheduleWrite()) + .on('end', () => this.emit('end')); } #initiateSocket(clientId: string): RedisSocket { @@ -1055,61 +1058,61 @@ export default class RedisClient< /** * @internal */ - _ejectSocket(): RedisSocket { - const socket = this._self.#socket; - // @ts-ignore - this._self.#socket = null; - socket.removeAllListeners(); - return socket; - } - - /** - * @internal - */ - _insertSocket(socket: RedisSocket) { - if(this._self.#socket) { + _ejectSocket(): RedisSocket { + const socket = this._self.#socket; + // @ts-ignore + this._self.#socket = null; + socket.removeAllListeners(); + return socket; + } + + /** + * @internal + */ + _insertSocket(socket: RedisSocket) { + if (this._self.#socket) { this._self._ejectSocket().destroy(); - } - this._self.#socket = socket; - this._self.#attachListeners(this._self.#socket); - } - - /** - * @internal - */ - _maintenanceUpdate(update: MaintenanceUpdate) { - this._self.#socket.setMaintenanceTimeout(update.relaxedSocketTimeout); - this._self.#queue.setMaintenanceCommandTimeout(update.relaxedCommandTimeout); - } - - /** - * @internal - */ - _pause() { - this._self.#paused = true; - } - - /** - * @internal - */ - _unpause() { - this._self.#paused = false; - this._self.#maybeScheduleWrite(); - } - - /** - * @internal - */ - _handleSmigrated(smigratedEvent: SMigratedEvent) { - this._self.emit(SMIGRATED_EVENT, smigratedEvent); - } - - /** - * @internal - */ - _getQueue(): RedisCommandsQueue { - return this._self.#queue; - } + } + this._self.#socket = socket; + this._self.#attachListeners(this._self.#socket); + } + + /** + * @internal + */ + _maintenanceUpdate(update: MaintenanceUpdate) { + this._self.#socket.setMaintenanceTimeout(update.relaxedSocketTimeout); + this._self.#queue.setMaintenanceCommandTimeout(update.relaxedCommandTimeout); + } + + /** + * @internal + */ + _pause() { + this._self.#paused = true; + } + + /** + * @internal + */ + _unpause() { + this._self.#paused = false; + this._self.#maybeScheduleWrite(); + } + + /** + * @internal + */ + _handleSmigrated(smigratedEvent: SMigratedEvent) { + this._self.emit(SMIGRATED_EVENT, smigratedEvent); + } + + /** + * @internal + */ + _getQueue(): RedisCommandsQueue { + return this._self.#queue; + } /** * @internal @@ -1183,7 +1186,7 @@ export default class RedisClient< // Merge global options with provided options const opts = { - ...this._self._commandOptions, + ...this._commandOptions, ...options, }; @@ -1371,7 +1374,7 @@ export default class RedisClient< } #write() { - if(this.#paused) { + if (this.#paused) { return } this.#socket.write(this.#queue.commandsToWrite()); From 791698446935e39a0df00500d64b5cb2874c1661 Mon Sep 17 00:00:00 2001 From: "bramwelbarack89@gmail.com" Date: Sat, 2 May 2026 20:55:49 +0300 Subject: [PATCH 2/7] Fix namespace --- packages/client/lib/client/index.spec.ts | 23 +++++++++++++++++++---- packages/client/lib/client/index.ts | 11 +++++------ packages/client/lib/commander.ts | 3 ++- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index e22d813230..b037eeb789 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -689,19 +689,34 @@ describe('Client', () => { } }); - testUtils.testWithClient('Module TypeMapping Fix', async (client) => { + + testUtils.testWithClient('Module TypeMapping Fix', async client => { + + const TIMEOUT = 1234; + (client as any)._commandOptions = { timeout: TIMEOUT }; + const bufferProxy = client.withCommandOptions({ typeMapping: { [RESP_TYPES.BLOB_STRING]: Buffer } }); + const bufferReply = await bufferProxy.module.echo('hi'); const stringReply = await client.module.echo('hi'); - assert.ok((bufferReply as unknown) instanceof Buffer, 'Proxy failed to return Buffer'); - assert.strictEqual(typeof stringReply, 'string', 'Original client was corrupted'); + assert.ok((bufferReply as unknown) instanceof Buffer, 'Proxy failed to return Buffer.'); + assert.strictEqual(typeof stringReply, 'string', 'Original client was corrupted.'); assert.equal(bufferReply.toString(), stringReply); + + const proxyOptions = (bufferProxy.module as any)._commandOptions; + assert.equal(proxyOptions.timeout, TIMEOUT, 'Inherited options (timeout) were lost in the proxy chain.') + + assert.ok(!Object.prototype.hasOwnProperty.call(proxyOptions, 'timeout'), 'Timeout should be inherited, not copied.'); }, { ...GLOBAL.SERVERS.OPEN, - clientOptions: { modules: { module } } + clientOptions: { + modules: { + module + } + } }) testUtils.testWithClient('duplicate should reuse command options', async client => { diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 4aceb5013d..06d4fb28f3 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -257,9 +257,9 @@ export type RedisClientType< type ProxyClient = RedisClient; -type NamespaceProxyClient = { +type NamespaceProxyClient = { _self: ProxyClient; - _commandOptions?: CommandOptions + _commandOptions?: CommandOptions }; interface ScanIteratorOptions { @@ -1185,10 +1185,9 @@ export default class RedisClient< } // Merge global options with provided options - const opts = { - ...this._commandOptions, - ...options, - }; + const opts = options ? + Object.assign(Object.create(this._commandOptions ?? null), options) : + this._commandOptions; const promise = this._self.#queue.addCommand(args, opts); this._self.#scheduleWrite(); diff --git a/packages/client/lib/commander.ts b/packages/client/lib/commander.ts index 628b29972c..8dc124323f 100644 --- a/packages/client/lib/commander.ts +++ b/packages/client/lib/commander.ts @@ -35,7 +35,7 @@ export function attachConfig< config }: AttachConfigOptions) { const RESP = config?.RESP ?? 2, - Class: any = class extends BaseClass {}; + Class: any = class extends BaseClass { }; for (const [name, command] of Object.entries(commands)) { if (config?.RESP == 3 && command.unstableResp3 && !config.unstableResp3) { @@ -85,6 +85,7 @@ function attachNamespace(prototype: any, name: PropertyKey, fns: any) { get() { const value = Object.create(fns); value._self = this; + value._commandOptions = (this as any)._commandOptions ?? null; Object.defineProperty(this, name, { value }); return value; } From bfb72065939545efc41e92297fc238a19124ee18 Mon Sep 17 00:00:00 2001 From: blackman <125454400+watersRand@users.noreply.github.com> Date: Sat, 2 May 2026 19:29:48 +0000 Subject: [PATCH 3/7] fix prototype delegation --- packages/client/lib/client/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 06d4fb28f3..cc41f37827 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -961,7 +961,8 @@ export default class RedisClient< TYPE_MAPPING extends TypeMapping >(options: OPTIONS) { const proxy = Object.create(this._self); - proxy._commandOptions = options; + proxy._commandOptions = Object.assign( + Object.create(this._commandOptions ?? null),options); return proxy as RedisClientType< M, F, From 1fde3a23c265aae095e3cdea94d3600f79fd6323 Mon Sep 17 00:00:00 2001 From: "bramwelbarack89@gmail.com" Date: Tue, 5 May 2026 15:38:05 +0300 Subject: [PATCH 4/7] Remove caching to access correct client context --- packages/client/lib/client/index.spec.ts | 3 ++- packages/client/lib/commander.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index b037eeb789..0115142cf9 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -699,8 +699,9 @@ describe('Client', () => { typeMapping: { [RESP_TYPES.BLOB_STRING]: Buffer } }); - const bufferReply = await bufferProxy.module.echo('hi'); const stringReply = await client.module.echo('hi'); + const bufferReply = await bufferProxy.module.echo('hi'); + assert.ok((bufferReply as unknown) instanceof Buffer, 'Proxy failed to return Buffer.'); assert.strictEqual(typeof stringReply, 'string', 'Original client was corrupted.'); diff --git a/packages/client/lib/commander.ts b/packages/client/lib/commander.ts index 8dc124323f..d70d2aa2b2 100644 --- a/packages/client/lib/commander.ts +++ b/packages/client/lib/commander.ts @@ -86,7 +86,6 @@ function attachNamespace(prototype: any, name: PropertyKey, fns: any) { const value = Object.create(fns); value._self = this; value._commandOptions = (this as any)._commandOptions ?? null; - Object.defineProperty(this, name, { value }); return value; } }); From 5ad91d1de1a90fd6ea3518fa7ffda028e87e3dfe Mon Sep 17 00:00:00 2001 From: "bramwelbarack89@gmail.com" Date: Sun, 10 May 2026 21:04:34 +0300 Subject: [PATCH 5/7] fix: respect proxy command options in modul/function commands --- packages/client/lib/client/index.spec.ts | 2 +- packages/client/lib/client/pool.spec.ts | 64 ++++++++++++++++++++++- packages/client/lib/client/pool.ts | 44 +++++++++------- packages/client/lib/cluster/index.spec.ts | 45 ++++++++++++++++ packages/client/lib/cluster/index.ts | 59 +++++++++++---------- packages/client/lib/commander.ts | 25 +++++++-- packages/client/lib/sentinel/types.ts | 62 ++++++++++++---------- packages/client/lib/sentinel/utils.ts | 6 +-- 8 files changed, 222 insertions(+), 85 deletions(-) diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 0115142cf9..c73ad9f1b0 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -690,7 +690,7 @@ describe('Client', () => { }); - testUtils.testWithClient('Module TypeMapping Fix', async client => { + testUtils.testWithClient('proxies respect RedisClient command options', async client => { const TIMEOUT = 1234; (client as any)._commandOptions = { timeout: TIMEOUT }; diff --git a/packages/client/lib/client/pool.spec.ts b/packages/client/lib/client/pool.spec.ts index 676186f7ce..a43f250258 100644 --- a/packages/client/lib/client/pool.spec.ts +++ b/packages/client/lib/client/pool.spec.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import { RESP_TYPES } from '../RESP/decoder'; import { RedisClientPool } from './pool'; describe('RedisClientPool', () => { @@ -91,7 +92,7 @@ describe('RedisClientPool', () => { assert.equal(result2, 'task2'); }, { ...GLOBAL.SERVERS.OPEN, - poolOptions: { minimum: 1, maximum: 1, acquireTimeout: 2000, cleanupDelay: 400 } + poolOptions: { minimum: 1, maximum: 1, acquireTimeout: 2000, cleanupDelay: 400 } }); testUtils.testWithClientPool('execute rejects when pool is closing', async pool => { @@ -230,4 +231,65 @@ describe('RedisClientPool', () => { }, GLOBAL.SERVERS.OPEN ); + + testUtils.testWithClientPool('sendCommand respects proxy command options', async pool => { + const TIMEOUT = 1234; + + + (pool as any)._commandOptions = { timeout: TIMEOUT }; + + + const bufferProxy = pool.withCommandOptions({ + typeMapping: { + [RESP_TYPES.BLOB_STRING]: Buffer + } + }); + + + const stringReply = await pool.sendCommand(['ECHO', 'hello']); + assert.equal(typeof stringReply, 'string', 'Base pool should return a string'); + + + const bufferReply = await bufferProxy.sendCommand(['ECHO', 'hello']); + assert.ok(bufferReply instanceof Buffer, 'Proxy should return a Buffer'); + assert.equal(bufferReply.toString(), 'hello'); + + + const proxyOptions = (bufferProxy as any)._commandOptions; + + + assert.equal( + proxyOptions.timeout, + TIMEOUT, + 'Proxy should inherit timeout from base pool' + ); + + + assert.equal( + Object.prototype.hasOwnProperty.call(proxyOptions, 'timeout'), + false, + 'Timeout should be inherited via prototype chain, not copied' + ); + + assert.equal( + Object.prototype.hasOwnProperty.call(proxyOptions, 'typeMapping'), + true, + 'TypeMapping should be a direct property of the proxy options' + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientPool('namespace proxy sees updated base options', async pool => { + + const module = (pool as any).module; + + assert.equal(module._commandOptions, null); + + (pool as any)._commandOptions = { timeout: 5000 }; + + assert.equal(module._commandOptions.timeout, 5000); + + (pool as any)._commandOptions = { timeout: 9999 }; + + assert.equal(module._commandOptions.timeout, 9999); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/client/pool.ts b/packages/client/lib/client/pool.ts index 65f9875dc9..8b73a1fb88 100644 --- a/packages/client/lib/client/pool.ts +++ b/packages/client/lib/client/pool.ts @@ -96,8 +96,8 @@ type PoolWithCommands< RESP extends RespVersions, TYPE_MAPPING extends TypeMapping > = { - [P in keyof typeof NON_STICKY_COMMANDS]: CommandSignature<(typeof NON_STICKY_COMMANDS)[P], RESP, TYPE_MAPPING>; -}; + [P in keyof typeof NON_STICKY_COMMANDS]: CommandSignature<(typeof NON_STICKY_COMMANDS)[P], RESP, TYPE_MAPPING>; + }; export type RedisClientPoolType< M extends RedisModules = {}, @@ -106,16 +106,19 @@ export type RedisClientPoolType< RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {} > = ( - RedisClientPool & - PoolWithCommands & - WithModules & - WithFunctions & - WithScripts -); + RedisClientPool & + PoolWithCommands & + WithModules & + WithFunctions & + WithScripts + ); type ProxyPool = RedisClientPoolType; -type NamespaceProxyPool = { _self: ProxyPool }; +type NamespaceProxyPool = { + _self: ProxyPool; + _commandOptions?: CommandOptions +}; export class RedisClientPool< M extends RedisModules = {}, @@ -131,7 +134,7 @@ export class RedisClientPool< const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); - return this.execute(client => client._executeCommand(command, parser, this._commandOptions, transformReply)) + return this._self.execute(client => client._executeCommand(command, parser, this._commandOptions, transformReply)) }; } @@ -142,7 +145,7 @@ export class RedisClientPool< const parser = new BasicCommandParser(); command.parseCommand(parser, ...args); - return this._self.execute(client => client._executeCommand(command, parser, this._self._commandOptions, transformReply)) + return this._self.execute(client => client._executeCommand(command, parser, this._commandOptions, transformReply)) }; } @@ -155,7 +158,8 @@ export class RedisClientPool< parser.push(...prefix); fn.parseCommand(parser, ...args); - return this._self.execute(client => client._executeCommand(fn, parser, this._self._commandOptions, transformReply)) }; + return this._self.execute(client => client._executeCommand(fn, parser, this._commandOptions, transformReply)) + }; } static #createScriptCommand(script: RedisScript, resp: RespVersions) { @@ -167,7 +171,7 @@ export class RedisClientPool< parser.pushVariadic(prefix); script.parseCommand(parser, ...args); - return this.execute(client => client._executeScript(script, parser, this._commandOptions, transformReply)) + return this._self.execute(client => client._executeScript(script, parser, this._commandOptions, transformReply)) }; } @@ -185,7 +189,7 @@ export class RedisClientPool< ) { let Pool = RedisClientPool.#SingleEntryCache.get(clientOptions); - if(!Pool) { + if (!Pool) { Pool = attachConfig({ BaseClass: RedisClientPool, commands: NON_STICKY_COMMANDS, @@ -309,7 +313,7 @@ export class RedisClientPool< super(); const socketOpts = clientOptions?.socket as { host?: string; port?: number } | undefined; - + this.#identity = { id: generateClientId(socketOpts?.host, socketOpts?.port, clientOptions?.database), role: ClientRole.POOL, @@ -328,7 +332,7 @@ export class RedisClientPool< } else { const cscConfig = options.clientSideCache; this.#clientSideCache = clientOptions.clientSideCache = new BasicPooledClientSideCache(cscConfig); -// this.#clientSideCache = clientOptions.clientSideCache = new PooledNoRedirectClientSideCache(cscConfig); + // this.#clientSideCache = clientOptions.clientSideCache = new PooledNoRedirectClientSideCache(cscConfig); } } @@ -485,10 +489,10 @@ export class RedisClientPool< const result = fn(node.value); if (result instanceof Promise) { result - .then(resolve, reject) - .finally(() => { - this.#returnClient(node); - }) + .then(resolve, reject) + .finally(() => { + this.#returnClient(node); + }) } else { resolve(result); this.#returnClient(node); diff --git a/packages/client/lib/cluster/index.spec.ts b/packages/client/lib/cluster/index.spec.ts index 5b97949166..123a5d6727 100644 --- a/packages/client/lib/cluster/index.spec.ts +++ b/packages/client/lib/cluster/index.spec.ts @@ -4,6 +4,7 @@ import RedisCluster from '.'; import { SQUARE_SCRIPT } from '../client/index.spec'; import { RootNodesUnavailableError } from '../errors'; import { spy } from 'sinon'; +import { RESP_TYPES } from '../RESP/decoder'; import RedisClient from '../client'; describe('Cluster', () => { @@ -229,6 +230,50 @@ describe('Cluster', () => { minimizeConnections: true } }); + + testUtils.testWithCluster('proxies respect RedisCluster command options', async cluster => { + + const CUSTOM_MAPPING = { [RESP_TYPES.BLOB_STRING]: Buffer }; + + + (cluster as any)._commandOptions = { timeout: 5000 }; + + + const bufferProxy = cluster.withTypeMapping(CUSTOM_MAPPING); + + const baseModule = (cluster as any).module; + const proxyModule = (bufferProxy as any).module; + + + assert.equal( + proxyModule._commandOptions.timeout, + 5000, + 'Namespace proxy should inherit timeout from base cluster' + ); + + + const stringReply = await baseModule.echo('hello'); + assert.equal(typeof stringReply, 'string', 'Base module should return a string'); + + const bufferReply = await proxyModule.echo('hello'); + assert.ok( + bufferReply instanceof Buffer, + 'Proxied module command should return a Buffer' + ); + + + assert.equal( + Object.prototype.hasOwnProperty.call(proxyModule._commandOptions, 'typeMapping'), + true, + 'typeMapping should be an "own" property of the proxy options' + ); + assert.equal( + Object.prototype.hasOwnProperty.call(proxyModule._commandOptions, 'timeout'), + false, + 'timeout should be inherited, not copied' + ); + + }, GLOBAL.CLUSTERS.OPEN); }); describe('PubSub', () => { diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index fbdebad16a..5428478d08 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -20,8 +20,8 @@ type WithCommands< RESP extends RespVersions, TYPE_MAPPING extends TypeMapping > = { - [P in keyof typeof NON_STICKY_COMMANDS]: CommandSignature<(typeof NON_STICKY_COMMANDS)[P], RESP, TYPE_MAPPING>; -}; + [P in keyof typeof NON_STICKY_COMMANDS]: CommandSignature<(typeof NON_STICKY_COMMANDS)[P], RESP, TYPE_MAPPING>; + }; interface ClusterCommander< @@ -30,7 +30,7 @@ interface ClusterCommander< S extends RedisScripts, RESP extends RespVersions, TYPE_MAPPING extends TypeMapping, - // POLICIES extends CommandPolicies +// POLICIES extends CommandPolicies > extends CommanderConfig { commandOptions?: ClusterCommandOptions; } @@ -46,7 +46,7 @@ export interface RedisClusterOptions< S extends RedisScripts = RedisScripts, RESP extends RespVersions = RespVersions, TYPE_MAPPING extends TypeMapping = TypeMapping, - // POLICIES extends CommandPolicies = CommandPolicies +// POLICIES extends CommandPolicies = CommandPolicies > extends ClusterCommander { /** * Should contain details for some of the cluster nodes that the client will use to discover @@ -120,25 +120,28 @@ export type RedisClusterType< S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {}, - // POLICIES extends CommandPolicies = {} +// 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 +// POLICIES extends CommandPolicies = CommandPolicies > extends CommandOptions { // policies?: POLICIES; } type ProxyCluster = RedisCluster; -type NamespaceProxyCluster = { _self: ProxyCluster }; +type NamespaceProxyCluster = { + _self: ProxyCluster; + _commandOptions?: ClusterCommandOptions; +}; export default class RedisCluster< M extends RedisModules, @@ -146,7 +149,7 @@ export default class RedisCluster< S extends RedisScripts, RESP extends RespVersions, TYPE_MAPPING extends TypeMapping, - // POLICIES extends CommandPolicies +// POLICIES extends CommandPolicies > extends EventEmitter { static #createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); @@ -174,7 +177,7 @@ export default class RedisCluster< return this._self._execute( parser.firstKey, command.IS_READ_ONLY, - this._self._commandOptions, + this._commandOptions, (client, opts) => client._executeCommand(command, parser, opts, transformReply) ); }; @@ -192,7 +195,7 @@ export default class RedisCluster< return this._self._execute( parser.firstKey, fn.IS_READ_ONLY, - this._self._commandOptions, + this._commandOptions, (client, opts) => client._executeCommand(fn, parser, opts, transformReply) ); }; @@ -224,7 +227,7 @@ export default class RedisCluster< S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {}, - // POLICIES extends CommandPolicies = {} + // POLICIES extends CommandPolicies = {} >(config?: ClusterCommander) { let Cluster = RedisCluster.#SingleEntryCache.get(config); @@ -255,7 +258,7 @@ export default class RedisCluster< S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {}, - // POLICIES extends CommandPolicies = {} + // POLICIES extends CommandPolicies = {} >(options?: RedisClusterOptions) { return RedisCluster.factory(options)(options); } @@ -362,7 +365,7 @@ export default class RedisCluster< withCommandOptions< OPTIONS extends ClusterCommandOptions, TYPE_MAPPING extends TypeMapping, - // POLICIES extends CommandPolicies + // POLICIES extends CommandPolicies >(options: OPTIONS) { const proxy = Object.create(this); proxy._commandOptions = options; @@ -372,7 +375,7 @@ export default class RedisCluster< S, RESP, TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {} - // POLICIES extends CommandPolicies ? POLICIES : {} + // POLICIES extends CommandPolicies ? POLICIES : {} >; } @@ -392,7 +395,7 @@ export default class RedisCluster< S, RESP, 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 >; } @@ -416,14 +419,14 @@ export default class RedisCluster< ) { return async (client: RedisClientType, options?: ClusterCommandOptions) => { const chainId = Symbol("asking chain"); - const opts = options ? {...options} : {}; + const opts = options ? { ...options } : {}; opts.chainId = chainId; const ret = await Promise.all( [ - client.sendCommand([ASKING_CMD], {chainId: chainId}), + client.sendCommand([ASKING_CMD], { chainId: chainId }), fn(client, opts) ] ); @@ -521,10 +524,10 @@ export default class RedisCluster< ): Promise { // Merge global options with local options - const opts = { - ...this._self._commandOptions, - ...options - } + const opts = options ? + Object.assign(Object.create(this._commandOptions ?? null), options) : + (this._commandOptions ?? {}); + return this._self._execute( firstKey, isReadonly, @@ -639,7 +642,7 @@ export default class RedisCluster< resubscribeAllPubSubListeners(allListeners: Partial) { if (allListeners.CHANNELS) { - for(const [channel, listeners] of allListeners.CHANNELS) { + for (const [channel, listeners] of allListeners.CHANNELS) { listeners.buffers.forEach(bufListener => { this.subscribe(channel, bufListener, true); }); diff --git a/packages/client/lib/commander.ts b/packages/client/lib/commander.ts index d70d2aa2b2..b9b7c1c619 100644 --- a/packages/client/lib/commander.ts +++ b/packages/client/lib/commander.ts @@ -79,13 +79,30 @@ export function attachConfig< return Class; } - +const namespaceCache = new WeakMap>(); function attachNamespace(prototype: any, name: PropertyKey, fns: any) { + Object.defineProperty(prototype, name, { get() { - const value = Object.create(fns); - value._self = this; - value._commandOptions = (this as any)._commandOptions ?? null; + + let instanceCache = namespaceCache.get(this); + if (!instanceCache) { + instanceCache = new Map(); + namespaceCache.set(this, instanceCache); + } + + let value = instanceCache.get(name); + if (!value) { + value = Object.create(fns); + value._self = this; + + Object.defineProperty(value, '_commandOptions', { + get() { return this._self._commandOptions ?? null; }, + enumerable: true, + configurable: false + }) + instanceCache.set(name, value); + } return value; } }); diff --git a/packages/client/lib/sentinel/types.ts b/packages/client/lib/sentinel/types.ts index d6c9f5011c..cd686ac13b 100644 --- a/packages/client/lib/sentinel/types.ts +++ b/packages/client/lib/sentinel/types.ts @@ -120,7 +120,7 @@ export interface SentinelCommander< S extends RedisScripts, RESP extends RespVersions, TYPE_MAPPING extends TypeMapping, - // POLICIES extends CommandPolicies +// POLICIES extends CommandPolicies > extends CommanderConfig { commandOptions?: CommandOptions; } @@ -135,36 +135,36 @@ type WithCommands< RESP extends RespVersions, TYPE_MAPPING extends TypeMapping > = { - [P in keyof typeof NON_STICKY_COMMANDS]: CommandSignature<(typeof NON_STICKY_COMMANDS)[P], RESP, TYPE_MAPPING>; -}; + [P in keyof typeof NON_STICKY_COMMANDS]: CommandSignature<(typeof NON_STICKY_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]]: CommandSignature; + [P in keyof M]: { + [C in keyof M[P]]: CommandSignature; + }; }; -}; type WithFunctions< F extends RedisFunctions, RESP extends RespVersions, TYPE_MAPPING extends TypeMapping > = { - [L in keyof F]: { - [C in keyof F[L]]: CommandSignature; + [L in keyof F]: { + [C in keyof F[L]]: CommandSignature; + }; }; -}; type WithScripts< S extends RedisScripts, RESP extends RespVersions, TYPE_MAPPING extends TypeMapping > = { - [P in keyof S]: CommandSignature; -}; + [P in keyof S]: CommandSignature; + }; export type RedisSentinelClientType< M extends RedisModules = {}, @@ -173,12 +173,12 @@ export type RedisSentinelClientType< RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {}, > = ( - RedisSentinelClient & - WithCommands & - WithModules & - WithFunctions & - WithScripts -); + RedisSentinelClient & + WithCommands & + WithModules & + WithFunctions & + WithScripts + ); export type RedisSentinelType< M extends RedisModules = {}, @@ -186,23 +186,29 @@ export type RedisSentinelType< S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {}, - // POLICIES extends CommandPolicies = {} +// POLICIES extends CommandPolicies = {} > = ( - RedisSentinel & - WithCommands & - WithModules & - WithFunctions & - WithScripts -); + RedisSentinel & + WithCommands & + WithModules & + WithFunctions & + WithScripts + ); export interface SentinelCommandOptions< TYPE_MAPPING extends TypeMapping = TypeMapping -> extends CommandOptions {} +> extends CommandOptions { } export type ProxySentinel = RedisSentinel; export type ProxySentinelClient = RedisSentinelClient; -export type NamespaceProxySentinel = { _self: ProxySentinel }; -export type NamespaceProxySentinelClient = { _self: ProxySentinelClient }; +export type NamespaceProxySentinel = { + _self: ProxySentinel; + _commandOptions?: CommandOptions +}; +export type NamespaceProxySentinelClient = { + _self: ProxySentinelClient; + _commandOptions?: CommandOptions; +}; export type NodeInfo = { ip: any, @@ -211,7 +217,7 @@ export type NodeInfo = { }; export type RedisSentinelEvent = NodeChangeEvent | SizeChangeEvent; - + export type NodeChangeEvent = { type: "SENTINEL_CHANGE" | "MASTER_CHANGE" | "REPLICA_ADD" | "REPLICA_REMOVE"; node: RedisNode; diff --git a/packages/client/lib/sentinel/utils.ts b/packages/client/lib/sentinel/utils.ts index c2024497a2..3f8bbf8c37 100644 --- a/packages/client/lib/sentinel/utils.ts +++ b/packages/client/lib/sentinel/utils.ts @@ -5,7 +5,7 @@ import { functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } fro import { NamespaceProxySentinel, NamespaceProxySentinelClient, NodeAddressMap, ProxySentinel, ProxySentinelClient, RedisNode } from './types'; /* TODO: should use map interface, would need a transform reply probably? as resp2 is list form, which this depends on */ -export function parseNode(node: Record): RedisNode | undefined{ +export function parseNode(node: Record): RedisNode | undefined { if (node.flags.includes("s_down") || node.flags.includes("disconnected") || node.flags.includes("failover_in_progress")) { return undefined; @@ -62,7 +62,7 @@ export function createFunctionCommand client._executeCommand(fn, parser, this._self.commandOptions, transformReply) + client => client._executeCommand(fn, parser, this._commandOptions, transformReply) ); } }; @@ -76,7 +76,7 @@ export function createModuleCommand client._executeCommand(command, parser, this._self.commandOptions, transformReply) + client => client._executeCommand(command, parser, this._commandOptions, transformReply) ); } }; From 8eab2907b17498735c0783fffe476c2f3f62b7f5 Mon Sep 17 00:00:00 2001 From: "bramwelbarack89@gmail.com" Date: Sun, 31 May 2026 01:11:23 +0300 Subject: [PATCH 6/7] fix: complete proxy command options for sentinel regular and script commands --- packages/client/lib/sentinel/types.ts | 8 ++++++-- packages/client/lib/sentinel/utils.ts | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/client/lib/sentinel/types.ts b/packages/client/lib/sentinel/types.ts index cd686ac13b..569a14d838 100644 --- a/packages/client/lib/sentinel/types.ts +++ b/packages/client/lib/sentinel/types.ts @@ -199,8 +199,12 @@ export interface SentinelCommandOptions< TYPE_MAPPING extends TypeMapping = TypeMapping > extends CommandOptions { } -export type ProxySentinel = RedisSentinel; -export type ProxySentinelClient = RedisSentinelClient; +export type ProxySentinel = RedisSentinel & { + _commandOptions?: SentinelCommandOptions; +}; +export type ProxySentinelClient = RedisSentinelClient & { + _commandOptions?: SentinelCommandOptions; +}; export type NamespaceProxySentinel = { _self: ProxySentinel; _commandOptions?: CommandOptions diff --git a/packages/client/lib/sentinel/utils.ts b/packages/client/lib/sentinel/utils.ts index 3f8bbf8c37..62cda4dc66 100644 --- a/packages/client/lib/sentinel/utils.ts +++ b/packages/client/lib/sentinel/utils.ts @@ -46,7 +46,7 @@ export function createCommand(com return this._self._execute( command.IS_READ_ONLY, - client => client._executeCommand(command, parser, this.commandOptions, transformReply) + client => client._executeCommand(command, parser, this._commandOptions, transformReply) ); }; } @@ -92,7 +92,7 @@ export function createScriptCommand client._executeScript(script, parser, this.commandOptions, transformReply) + client => client._executeScript(script, parser, this._commandOptions, transformReply) ); }; } From 3b671da6bd8115c0d80aac2e3382eb1a1221b715 Mon Sep 17 00:00:00 2001 From: "bramwelbarack89@gmail.com" Date: Sun, 31 May 2026 23:27:35 +0300 Subject: [PATCH 7/7] Add inheritance for pool.ts and getters for sentinel --- packages/client/lib/client/pool.ts | 5 ++++- packages/client/lib/sentinel/index.ts | 27 ++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/client/lib/client/pool.ts b/packages/client/lib/client/pool.ts index 8b73a1fb88..17416a799c 100644 --- a/packages/client/lib/client/pool.ts +++ b/packages/client/lib/client/pool.ts @@ -347,7 +347,10 @@ export class RedisClientPool< TYPE_MAPPING extends TypeMapping >(options: OPTIONS) { const proxy = Object.create(this._self); - proxy._commandOptions = options; + proxy._commandOptions = Object.assign( + Object.create(this._commandOptions ?? null), + options + ); return proxy as RedisClientPoolType< M, F, diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index 9f2ed98c71..33b0f11ac0 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -62,6 +62,14 @@ export class RedisSentinelClient< return this._self.#commandOptions; } + /** + * Internal getter for command dispatch functions. + * @returns the effective command options of the proxy. + */ + get _commandOptions(): CommandOptions | undefined { + return this.commandOptions; + } + #commandOptions?: CommandOptions; constructor( @@ -304,6 +312,15 @@ export default class RedisSentinel< return this._self.#identity; } + /** + * @internal + * Internal getter for command dispatch functions. + * Returns the effective command options of the proxy. + */ + get _commandOptions(): CommandOptions | undefined { + return this.commandOptions; + } + #commandOptions?: CommandOptions; #trace: (msg: string) => unknown = () => { }; @@ -738,7 +755,7 @@ class RedisSentinelInternal< this.#scanInterval = options.scanInterval ?? 0; this.#passthroughClientErrorEvents = options.passthroughClientErrorEvents ?? false; - this.#nodeClientOptions = (options.nodeClientOptions ? {...options.nodeClientOptions} : {}) as RedisClientOptions; + this.#nodeClientOptions = (options.nodeClientOptions ? { ...options.nodeClientOptions } : {}) as RedisClientOptions; if (this.#nodeClientOptions.url !== undefined) { throw new Error("invalid nodeClientOptions for Sentinel"); } @@ -749,7 +766,7 @@ class RedisSentinelInternal< } else { const cscConfig = options.clientSideCache; this.#clientSideCache = this.#nodeClientOptions.clientSideCache = new BasicPooledClientSideCache(cscConfig); -// this.#clientSideCache = this.#nodeClientOptions.clientSideCache = new PooledNoRedirectClientSideCache(cscConfig); + // this.#clientSideCache = this.#nodeClientOptions.clientSideCache = new PooledNoRedirectClientSideCache(cscConfig); } } @@ -853,7 +870,7 @@ class RedisSentinelInternal< while (true) { this.#trace("starting connect loop"); - count+=1; + count += 1; if (this.#destroy) { this.#trace("in #connect and want to destroy") return; @@ -1002,10 +1019,10 @@ class RedisSentinelInternal< #handleSentinelFailure(node: RedisNode) { const found = this.#sentinelRootNodes.findIndex( - (rootNode) => rootNode.host === node.host && rootNode.port === node.port + (rootNode) => rootNode.host === node.host && rootNode.port === node.port ); if (found !== -1) { - this.#sentinelRootNodes.splice(found, 1); + this.#sentinelRootNodes.splice(found, 1); } this.#restoreSentinelRootNodesIfEmpty(); this.#reset();