Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/client/lib/client/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,52 @@ describe('Client', () => {
}
}
});

testUtils.testWithClient('proxies respect RedisClient command options', async client => {

const TIMEOUT = 1234;
(client as any)._commandOptions = { timeout: TIMEOUT };

const bufferProxy = client.withCommandOptions({
typeMapping: { [RESP_TYPES.BLOB_STRING]: Buffer }
});

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.');
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.');
Comment thread
cursor[bot] marked this conversation as resolved.

const duplicate = bufferProxy.duplicate();

const duplicateOptions = (duplicate as any)._commandOptions;

assert.equal(
duplicateOptions.timeout,
TIMEOUT,
'duplicate() lost inherited timeout.'
);

assert.deepEqual(
duplicateOptions.typeMapping,
{ [RESP_TYPES.BLOB_STRING]: Buffer },
'duplicate() lost typeMapping.'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
modules: {
module
}
}
})

testUtils.testWithClient('duplicate should reuse command options', async client => {
const duplicate = client.duplicate();
Expand Down
37 changes: 27 additions & 10 deletions packages/client/lib/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,9 @@ export type RedisClientType<

type ProxyClient = RedisClient<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>;

type NamespaceProxyClient = { _self: ProxyClient };
type NamespaceProxyClient = {
_self: ProxyClient
_commandOptions?:CommandOptions };

export interface ScanIteratorOptions {
cursor?: RedisArgument;
Expand Down Expand Up @@ -305,7 +307,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);
Comment thread
cursor[bot] marked this conversation as resolved.
};
}

Expand All @@ -318,7 +320,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);
};
}

Expand All @@ -335,6 +337,19 @@ export default class RedisClient<
}
}

static #flattenCommandOptions(options?: CommandOptions) {
if (!options) return options;

const flattened = {};
const chain = [];

for (let current = options; current; current = Object.getPrototypeOf(current)) {
chain.unshift(current);
}

return Object.assign(flattened, ...chain);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
static #SingleEntryCache = new SingleEntryCache<any, any>()

Expand Down Expand Up @@ -1038,7 +1053,9 @@ export default class RedisClient<
TYPE_MAPPING extends TypeMapping
>(options: OPTIONS) {
const proxy = Object.create(this._self);
proxy._commandOptions = { ...this._commandOptions, ...options };
proxy._commandOptions = Object.assign(
Object.create(this._commandOptions ?? null),options
);
return proxy as RedisClientType<
M,
F,
Expand All @@ -1056,7 +1073,8 @@ export default class RedisClient<
value: V
) {
const proxy = Object.create(this._self);
proxy._commandOptions = { ...this._commandOptions, [key]: value };
proxy._commandOptions = Object.assign(Object.create(
this._commandOptions ?? null),{ [key]: value });
return proxy as RedisClientType<
M,
F,
Expand Down Expand Up @@ -1115,7 +1133,7 @@ export default class RedisClient<
>(overrides?: Partial<RedisClientOptions<_M, _F, _S, _RESP, _TYPE_MAPPING>>) {
return new (Object.getPrototypeOf(this).constructor)({
...this._self.#options,
commandOptions: this._commandOptions,
commandOptions: RedisClient.#flattenCommandOptions(this._commandOptions),
...overrides
}) as RedisClientType<_M, _F, _S, _RESP, _TYPE_MAPPING>;
}
Expand Down Expand Up @@ -1261,10 +1279,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<T>(args, opts);
this._self.#scheduleWrite();
Expand Down
69 changes: 69 additions & 0 deletions packages/client/lib/client/pool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,75 @@ describe('RedisClientPool', () => {
await task1Promise;
}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClientPool(' proxy inheritance completes', 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('nested proxy inheritance',async pool=>{
const TIMEOUT = 1234;

const timeoutProxy = pool.withCommandOptions({
timeout: TIMEOUT
});

const chainedProxy = timeoutProxy.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: Buffer
});

const chainedReply = await chainedProxy.sendCommand(['ECHO', 'hello']);

assert.ok(chainedReply instanceof Buffer);
assert.equal(chainedReply.toString(), 'hello');

const chainedOptions = (chainedProxy as any)._commandOptions;

assert.equal(chainedOptions.timeout, TIMEOUT);
assert.equal(
Object.prototype.hasOwnProperty.call(chainedOptions, 'timeout'),
false
);

assert.equal(
Object.prototype.hasOwnProperty.call(chainedOptions, 'typeMapping'),
true
);
},GLOBAL.SERVERS.OPEN)

testUtils.testWithClientPool('execute rejects when pool is closed', async pool => {
await pool.close();

Expand Down
24 changes: 16 additions & 8 deletions packages/client/lib/client/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ export type RedisClientPoolType<
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- variance markers for pool generics
type ProxyPool = RedisClientPoolType<any, any, any, any, any>;

type NamespaceProxyPool = { _self: ProxyPool };
type NamespaceProxyPool = {
_self: ProxyPool
_commandOptions?: CommandOptions };

export class RedisClientPool<
M extends RedisModules = {},
Expand All @@ -122,7 +124,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))
};
}

Expand All @@ -133,7 +135,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))
};
}

Expand All @@ -146,7 +148,7 @@ 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) {
Expand All @@ -158,7 +160,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))
};
}

Expand Down Expand Up @@ -336,7 +338,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,
Expand All @@ -358,7 +363,8 @@ export class RedisClientPool<
value: V
) {
const proxy = Object.create(this._self);
proxy._commandOptions = { ...this._commandOptions, [key]: value };
proxy._commandOptions = Object.assign( Object.create(
this._commandOptions ?? null), {[key]: value });
return proxy as RedisClientPoolType<
M,
F,
Expand Down Expand Up @@ -534,7 +540,9 @@ export class RedisClientPool<
args: Array<RedisArgument>,
options?: CommandOptions
) {
const mergedOptions = { ...this._commandOptions, ...options };
const mergedOptions = options
? Object.assign(Object.create(this._commandOptions ?? null), options)
: this._commandOptions;
return this.execute(client => client.sendCommand(args, mergedOptions));
}

Expand Down
77 changes: 77 additions & 0 deletions packages/client/lib/cluster/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,83 @@ describe('Cluster', () => {
minimizeConnections: true
}
});

testUtils.testWithCluster('proxy inheritance completes cluster routing chains', async cluster => {
const CUSTOM_MAPPING = {
[RESP_TYPES.BLOB_STRING]: Buffer
};

// 1. Set global base option
(cluster as any)._commandOptions = {
timeout: 5000,
asap: false
};

// 2. Chain Proxy Layer 1: Add type mapping
const proxy1 = cluster.withTypeMapping(CUSTOM_MAPPING);

// 3. Chain Proxy Layer 2: Override an intermediate property
// AND add a localized abort signal or different option.
// This creates a 3-tier deep prototype chain.
const controller = new AbortController();
const proxy2 = proxy1.withCommandOptions({
asap: true,
signal: controller.signal
});

const proxyOptions = (proxy2 as any)._commandOptions;

// --- Structural Assertions ---
assert.equal(proxyOptions.timeout, 5000, 'Root level options (timeout) were lost in the chain');
assert.equal(proxyOptions.asap, true, 'Intermediate overrides (asap) failed to apply');
assert.deepEqual(proxyOptions.typeMapping, CUSTOM_MAPPING, 'Nested object mappings were lost');

assert.equal(Object.prototype.hasOwnProperty.call(proxyOptions, 'signal'), true, 'Signal should belong to layer 2');
assert.equal(Object.prototype.hasOwnProperty.call(proxyOptions, 'asap'), true, 'Asap override should belong to layer 2');
assert.equal(Object.prototype.hasOwnProperty.call(proxyOptions, 'typeMapping'), false, 'TypeMapping should be inherited from layer 1');
assert.equal(Object.prototype.hasOwnProperty.call(proxyOptions, 'timeout'), false, 'Timeout should be inherited from the base cluster root');

// ------------------------------------------------------------------
// Execution diagnostics
// ------------------------------------------------------------------

// Base cluster should NOT use the custom type mapping.
const baseReply = await cluster.echo('hello');
assert.equal(
typeof baseReply,
'string',
'Base cluster unexpectedly inherited custom typeMapping'
);

// Layer 1 owns the typeMapping.

const proxy1Reply = await proxy1.echo('hello');
assert.ok(
proxy1Reply instanceof Buffer,
'Layer 1 execution stripped typeMapping'
);
assert.deepEqual(
proxy1Reply,
Buffer.from('hello'),
'Layer 1 payload was mutated'
);

// Layer 2 inherits typeMapping through the prototype chain.
const proxy2Reply = await proxy2.echo('hello');
assert.ok(
(proxy2Reply as any) instanceof Buffer,
'Cluster execution stripped typeMapping from deep proxy chain'
);
assert.deepEqual(
proxy2Reply,
Buffer.from('hello'),
'Data payload was mutated during cluster execution proxying'
);

controller.abort();
},
GLOBAL.CLUSTERS.OPEN
);
});

describe('PubSub', () => {
Expand Down
Loading