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/4] 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/4] 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/4] 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/4] 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; } });