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 @@
}
}
});

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

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

Check failure on line 871 in packages/client/lib/client/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

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;

Check failure on line 885 in packages/client/lib/client/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
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;

Check failure on line 892 in packages/client/lib/client/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

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 @@
await task1Promise;
}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClientPool(' proxy inheritance completes', async pool => {
const TIMEOUT = 1234;

(pool as any)._commandOptions = { timeout: TIMEOUT };

Check failure on line 207 in packages/client/lib/client/pool.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

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;

Check failure on line 222 in packages/client/lib/client/pool.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

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;

Check failure on line 259 in packages/client/lib/client/pool.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

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
63 changes: 63 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,69 @@
minimizeConnections: true
}
});

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

(cluster as any)._commandOptions = {
timeout: 5000,
asap: false
};

const proxy1 = cluster.withTypeMapping(CUSTOM_MAPPING);

const controller = new AbortController();
const proxy2 = proxy1.withCommandOptions({
asap: true,
signal: controller.signal
});

const proxyOptions = (proxy2 as any)._commandOptions;

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');

const baseReply = await cluster.echo('hello');
assert.equal(
typeof baseReply,
'string',
'Base cluster unexpectedly inherited custom 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'
);

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
);

Check failure on line 349 in packages/client/lib/cluster/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});

describe('PubSub', () => {
Expand All @@ -296,7 +359,7 @@
waitTillBeenCalled(listener),
cluster.publish('channel', 'message')
]);

Check failure on line 362 in packages/client/lib/cluster/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
assert.ok(listener.calledOnceWithExactly('message', 'channel'));

await cluster.unsubscribe('channel', listener);
Expand Down Expand Up @@ -327,7 +390,7 @@

assert.ok(cluster.pubSubNode);
const [migrating, importing] = cluster.masters[0].address === cluster.pubSubNode.address ?
cluster.masters :

Check failure on line 393 in packages/client/lib/cluster/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
[cluster.masters[1], cluster.masters[0]],
[migratingClient, importingClient] = await Promise.all([
cluster.nodeClient(migrating),
Expand Down
Loading
Loading