1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-09 00:22:08 +03:00

ref #1653 - better types

This commit is contained in:
leibale
2021-09-18 05:52:54 -04:00
parent d5fa6a3dc6
commit 0ab2245049
8 changed files with 176 additions and 190 deletions

View File

@@ -13,7 +13,7 @@ import { encodeCommand, extendWithDefaultCommands, extendWithModulesAndScripts,
import { Pool, Options as PoolOptions, createPool } from 'generic-pool'; import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
import { ClientClosedError } from './errors'; import { ClientClosedError } from './errors';
export interface RedisClientOptions<M = RedisModules, S = RedisLuaScripts> { export interface RedisClientOptions<M, S> {
socket?: RedisSocketOptions; socket?: RedisSocketOptions;
modules?: M; modules?: M;
scripts?: S; scripts?: S;
@@ -43,51 +43,25 @@ type WithScripts<S extends RedisLuaScripts> = {
export type WithPlugins<M extends RedisModules, S extends RedisLuaScripts> = export type WithPlugins<M extends RedisModules, S extends RedisLuaScripts> =
WithCommands & WithModules<M> & WithScripts<S>; WithCommands & WithModules<M> & WithScripts<S>;
export type RedisClientType<M extends RedisModules, S extends RedisLuaScripts> = export type RedisClientType<M extends RedisModules = {}, S extends RedisLuaScripts = {}> =
WithPlugins<M, S> & RedisClient<M, S>; WithPlugins<M, S> & RedisClient<M, S>;
export interface ClientCommandOptions extends QueueCommandOptions { export interface ClientCommandOptions extends QueueCommandOptions {
isolated?: boolean; isolated?: boolean;
} }
export default class RedisClient<M extends RedisModules = RedisModules, S extends RedisLuaScripts = RedisLuaScripts> extends EventEmitter { export default class RedisClient<M extends RedisModules, S extends RedisLuaScripts> extends EventEmitter {
static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> { static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> {
return commandOptions(options); return commandOptions(options);
} }
static async commandsExecutor( static create<M extends RedisModules = {}, S extends RedisLuaScripts = {}>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
this: RedisClient,
command: RedisCommand,
args: Array<unknown>
): Promise<ReturnType<typeof command['transformReply']>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
return command.transformReply(
await this.#sendCommand(redisArgs, options, command.BUFFER_MODE),
redisArgs.preserve,
);
}
static async #scriptsExecutor(
this: RedisClient,
script: RedisLuaScript,
args: Array<unknown>
): Promise<typeof script['transformArguments']> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
return script.transformReply(
await this.executeScript(script, redisArgs, options, script.BUFFER_MODE),
redisArgs.preserve
);
}
static create<M extends RedisModules, S extends RedisLuaScripts>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
const Client = (<any>extendWithModulesAndScripts({ const Client = (<any>extendWithModulesAndScripts({
BaseClass: RedisClient, BaseClass: RedisClient,
modules: options?.modules, modules: options?.modules,
modulesCommandsExecutor: RedisClient.commandsExecutor, modulesCommandsExecutor: RedisClient.prototype.commandsExecutor,
scripts: options?.scripts, scripts: options?.scripts,
scriptsExecutor: RedisClient.#scriptsExecutor scriptsExecutor: RedisClient.prototype.scriptsExecutor
})); }));
if (Client !== RedisClient) { if (Client !== RedisClient) {
@@ -104,7 +78,7 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
readonly #v4: Record<string, any> = {}; readonly #v4: Record<string, any> = {};
#selectedDB = 0; #selectedDB = 0;
get options(): RedisClientOptions<M> | null | undefined { get options(): RedisClientOptions<M, S> | undefined {
return this.#options; return this.#options;
} }
@@ -240,6 +214,72 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
await this.#socket.connect(); await this.#socket.connect();
} }
async commandsExecutor(command: RedisCommand, args: Array<unknown>): Promise<ReturnType<typeof command['transformReply']>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
return command.transformReply(
await this.#sendCommand(redisArgs, options, command.BUFFER_MODE),
redisArgs.preserve,
);
}
sendCommand<T = RedisReply>(args: TransformArgumentsReply, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
return this.#sendCommand(args, options, bufferMode);
}
// using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
async #sendCommand<T = RedisReply>(args: TransformArgumentsReply, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
if (!this.#socket.isOpen) {
throw new ClientClosedError();
}
if (options?.isolated) {
return this.executeIsolated(isolatedClient =>
isolatedClient.sendCommand(args, {
...options,
isolated: false
})
);
}
const promise = this.#queue.addCommand<T>(args, options, bufferMode);
this.#tick();
return await promise;
}
async scriptsExecutor(script: RedisLuaScript, args: Array<unknown>): Promise<ReturnType<typeof script['transformReply']>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
return script.transformReply(
await this.executeScript(script, redisArgs, options, script.BUFFER_MODE),
redisArgs.preserve
);
}
async executeScript(script: RedisLuaScript, args: TransformArgumentsReply, options?: ClientCommandOptions, bufferMode?: boolean): Promise<ReturnType<typeof script['transformReply']>> {
try {
return await this.#sendCommand([
'EVALSHA',
script.SHA1,
script.NUMBER_OF_KEYS.toString(),
...args
], options, bufferMode);
} catch (err: any) {
if (!err?.message?.startsWith?.('NOSCRIPT')) {
throw err;
}
return await this.#sendCommand([
'EVAL',
script.SCRIPT,
script.NUMBER_OF_KEYS.toString(),
...args
], options, bufferMode);
}
}
async SELECT(db: number): Promise<void>; async SELECT(db: number): Promise<void>;
async SELECT(options: CommandOptions<ClientCommandOptions>, db: number): Promise<void>; async SELECT(options: CommandOptions<ClientCommandOptions>, db: number): Promise<void>;
async SELECT(options?: any, db?: any): Promise<void> { async SELECT(options?: any, db?: any): Promise<void> {
@@ -300,30 +340,6 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
quit = this.QUIT; quit = this.QUIT;
sendCommand<T = RedisReply>(args: TransformArgumentsReply, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
return this.#sendCommand(args, options, bufferMode);
}
// using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
async #sendCommand<T = RedisReply>(args: TransformArgumentsReply, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
if (!this.#socket.isOpen) {
throw new ClientClosedError();
}
if (options?.isolated) {
return this.executeIsolated(isolatedClient =>
isolatedClient.sendCommand(args, {
...options,
isolated: false
})
);
}
const promise = this.#queue.addCommand<T>(args, options, bufferMode);
this.#tick();
return await promise;
}
#tick(): void { #tick(): void {
if (!this.#socket.isSocketExists) { if (!this.#socket.isSocketExists) {
return; return;
@@ -350,26 +366,11 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
return this.#isolationPool.use(fn); return this.#isolationPool.use(fn);
} }
async executeScript(script: RedisLuaScript, args: TransformArgumentsReply, options?: ClientCommandOptions, bufferMode?: boolean): Promise<ReturnType<typeof script['transformReply']>> { multi(): RedisMultiCommandType<M, S> {
try { return new (this as any).Multi(
return await this.#sendCommand([ this.#multiExecutor.bind(this),
'EVALSHA', this.#options
script.SHA1, );
script.NUMBER_OF_KEYS.toString(),
...args
], options, bufferMode);
} catch (err: any) {
if (!err?.message?.startsWith?.('NOSCRIPT')) {
throw err;
}
return await this.#sendCommand([
'EVAL',
script.SCRIPT,
script.NUMBER_OF_KEYS.toString(),
...args
], options, bufferMode);
}
} }
#multiExecutor(commands: Array<MultiQueuedCommand>, chainId?: symbol): Promise<Array<RedisReply>> { #multiExecutor(commands: Array<MultiQueuedCommand>, chainId?: symbol): Promise<Array<RedisReply>> {
@@ -386,13 +387,6 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
return promise; return promise;
} }
multi(): RedisMultiCommandType<M, S> {
return new (this as any).Multi(
this.#multiExecutor.bind(this),
this.#options
);
}
async* scanIterator(options?: ScanCommandOptions): AsyncIterable<string> { async* scanIterator(options?: ScanCommandOptions): AsyncIterable<string> {
let cursor = 0; let cursor = 0;
do { do {
@@ -451,5 +445,5 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
} }
} }
extendWithDefaultCommands(RedisClient, RedisClient.commandsExecutor); extendWithDefaultCommands(RedisClient, RedisClient.prototype.commandsExecutor);
(RedisClient.prototype as any).Multi = RedisMultiCommand.extend(); (RedisClient.prototype as any).Multi = RedisMultiCommand.extend();

View File

@@ -16,10 +16,10 @@ export interface RedisClusterOptions<M = RedisModules, S = RedisLuaScripts> {
maxCommandRedirections?: number; maxCommandRedirections?: number;
} }
export type RedisClusterType<M extends RedisModules, S extends RedisLuaScripts> = export type RedisClusterType<M extends RedisModules = {}, S extends RedisLuaScripts = {}> =
WithPlugins<M, S> & RedisCluster; WithPlugins<M, S> & RedisCluster<M, S>;
export default class RedisCluster<M extends RedisModules = RedisModules, S extends RedisLuaScripts = RedisLuaScripts> extends EventEmitter { export default class RedisCluster<M extends RedisModules = {}, S extends RedisLuaScripts = {}> extends EventEmitter {
static #extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: TransformArgumentsReply): string | Buffer | undefined { static #extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: TransformArgumentsReply): string | Buffer | undefined {
if (command.FIRST_KEY_INDEX === undefined) { if (command.FIRST_KEY_INDEX === undefined) {
return undefined; return undefined;
@@ -30,54 +30,13 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
return command.FIRST_KEY_INDEX(...originalArgs); return command.FIRST_KEY_INDEX(...originalArgs);
} }
static async commandsExecutor( static create<M extends RedisModules = {}, S extends RedisLuaScripts = {}>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
this: RedisCluster,
command: RedisCommand,
args: Array<unknown>
): Promise<ReturnType<typeof command['transformReply']>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
const reply = command.transformReply(
await this.sendCommand(
RedisCluster.#extractFirstKey(command, args, redisArgs),
command.IS_READ_ONLY,
redisArgs,
options,
command.BUFFER_MODE
),
redisArgs.preserve
);
return reply;
}
static async #scriptsExecutor(
this: RedisCluster,
script: RedisLuaScript,
args: Array<unknown>
): Promise<typeof script['transformArguments']> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
const reply = script.transformReply(
await this.executeScript(
script,
args,
redisArgs,
options
),
redisArgs.preserve
);
return reply;
}
static create<M extends RedisModules, S extends RedisLuaScripts>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
return new (<any>extendWithModulesAndScripts({ return new (<any>extendWithModulesAndScripts({
BaseClass: RedisCluster, BaseClass: RedisCluster,
modules: options?.modules, modules: options?.modules,
modulesCommandsExecutor: RedisCluster.commandsExecutor, modulesCommandsExecutor: RedisCluster.prototype.commandsExecutor,
scripts: options?.scripts, scripts: options?.scripts,
scriptsExecutor: RedisCluster.#scriptsExecutor scriptsExecutor: RedisCluster.prototype.scriptsExecutor
}))(options); }))(options);
} }
@@ -101,6 +60,23 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
return this.#slots.connect(); return this.#slots.connect();
} }
async commandsExecutor(command: RedisCommand, args: Array<unknown>): Promise<ReturnType<typeof command['transformReply']>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
const reply = command.transformReply(
await this.sendCommand(
RedisCluster.#extractFirstKey(command, args, redisArgs),
command.IS_READ_ONLY,
redisArgs,
options,
command.BUFFER_MODE
),
redisArgs.preserve
);
return reply;
}
async sendCommand<C extends RedisCommand>( async sendCommand<C extends RedisCommand>(
firstKey: string | Buffer | undefined, firstKey: string | Buffer | undefined,
isReadonly: boolean | undefined, isReadonly: boolean | undefined,
@@ -125,6 +101,22 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
} }
} }
async scriptsExecutor(script: RedisLuaScript, args: Array<unknown>): Promise<ReturnType<typeof script['transformReply']>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
const reply = script.transformReply(
await this.executeScript(
script,
args,
redisArgs,
options
),
redisArgs.preserve
);
return reply;
}
async executeScript( async executeScript(
script: RedisLuaScript, script: RedisLuaScript,
originalArgs: Array<unknown>, originalArgs: Array<unknown>,
@@ -208,5 +200,4 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
} }
} }
extendWithDefaultCommands(RedisCluster, RedisCluster.commandsExecutor); extendWithDefaultCommands(RedisCluster, RedisCluster.prototype.commandsExecutor);

View File

@@ -8,7 +8,7 @@ describe('GEOSEARCHSTORE', () => {
describe('transformArguments', () => { describe('transformArguments', () => {
it('simple', () => { it('simple', () => {
assert.deepEqual( assert.deepEqual(
transformArguments('destination', 'source', 'member', { transformArguments('destination', '/home/leibale/Workspace/node-redis/lib/commands/GEOSEARCHSTORE.spec.tssource', 'member', {
radius: 1, radius: 1,
unit: 'm' unit: 'm'
}, { }, {

View File

@@ -1,3 +1,4 @@
import { TransformArgumentsReply } from '.';
import { transformEXAT, transformPXAT, transformReplyStringNull } from './generic-transformers'; import { transformEXAT, transformPXAT, transformReplyStringNull } from './generic-transformers';
export const FIRST_KEY_INDEX = 1; export const FIRST_KEY_INDEX = 1;
@@ -14,7 +15,7 @@ type GetExModes = {
PERSIST: true; PERSIST: true;
}; };
export function transformArguments(key: string, mode: GetExModes) { export function transformArguments(key: string, mode: GetExModes): TransformArgumentsReply {
const args = ['GETEX', key]; const args = ['GETEX', key];
if ('EX' in mode) { if ('EX' in mode) {

View File

@@ -753,7 +753,10 @@ export interface RedisCommands {
} }
export interface RedisModule { export interface RedisModule {
[key: string]: RedisCommand; [command: string]: RedisCommand;
} }
export type RedisModules = Record<string, RedisModule>; export interface RedisModules {
[module: string]: RedisModule;
}
// export type RedisModules = Record<string, RedisModule>;

View File

@@ -13,10 +13,10 @@ export interface SHA1 {
export type RedisLuaScript = RedisLuaScriptConfig & SHA1; export type RedisLuaScript = RedisLuaScriptConfig & SHA1;
export interface RedisLuaScripts { export interface RedisLuaScripts {
[key: string]: RedisLuaScript; [script: string]: RedisLuaScript;
} }
export function defineScript<S extends RedisLuaScriptConfig>(script: S): S & SHA1 { export function defineScript(script: RedisLuaScriptConfig): typeof script & SHA1 {
return { return {
...script, ...script,
SHA1: scriptSha1(script.SCRIPT) SHA1: scriptSha1(script.SCRIPT)

View File

@@ -21,7 +21,8 @@ type WithScripts<M extends RedisModules, S extends RedisLuaScripts> = {
[P in keyof S]: RedisMultiCommandSignature<S[P], M, S> [P in keyof S]: RedisMultiCommandSignature<S[P], M, S>
}; };
export type RedisMultiCommandType<M extends RedisModules, S extends RedisLuaScripts> = RedisMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>; export type RedisMultiCommandType<M extends RedisModules = {}, S extends RedisLuaScripts = {}> =
RedisMultiCommand<M, S> & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
export interface MultiQueuedCommand { export interface MultiQueuedCommand {
args: TransformArgumentsReply; args: TransformArgumentsReply;
@@ -31,60 +32,20 @@ export interface MultiQueuedCommand {
export type RedisMultiExecutor = (queue: Array<MultiQueuedCommand>, chainId?: symbol) => Promise<Array<RedisReply>>; export type RedisMultiExecutor = (queue: Array<MultiQueuedCommand>, chainId?: symbol) => Promise<Array<RedisReply>>;
export default class RedisMultiCommand<M extends RedisModules = RedisModules, S extends RedisLuaScripts = RedisLuaScripts> { export default class RedisMultiCommand<M extends RedisModules, S extends RedisLuaScripts> {
static commandsExecutor(this: RedisMultiCommand, command: RedisCommand, args: Array<unknown>): RedisMultiCommand {
return this.addCommand(
command.transformArguments(...args),
command.transformReply
);
}
static #scriptsExecutor(
this: RedisMultiCommand,
script: RedisLuaScript,
args: Array<unknown>
): RedisMultiCommand {
const transformedArguments: TransformArgumentsReply = [];
if (this.#scriptsInUse.has(script.SHA1)) {
transformedArguments.push(
'EVALSHA',
script.SHA1
);
} else {
this.#scriptsInUse.add(script.SHA1);
transformedArguments.push(
'EVAL',
script.SCRIPT
);
}
transformedArguments.push(script.NUMBER_OF_KEYS.toString());
const scriptArguments = script.transformArguments(...args);
transformedArguments.push(...scriptArguments);
if (scriptArguments.preserve) {
transformedArguments.preserve = scriptArguments.preserve;
}
return this.addCommand(
transformedArguments,
script.transformReply
);
}
static extend<M extends RedisModules, S extends RedisLuaScripts>( static extend<M extends RedisModules, S extends RedisLuaScripts>(
clientOptions?: RedisClientOptions<M, S> clientOptions?: RedisClientOptions<M, S>
): new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisMultiCommandType<M, S> { ): new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisMultiCommandType<M, S> {
return <any>extendWithModulesAndScripts({ return <any>extendWithModulesAndScripts({
BaseClass: RedisMultiCommand, BaseClass: RedisMultiCommand,
modules: clientOptions?.modules, modules: clientOptions?.modules,
modulesCommandsExecutor: RedisMultiCommand.commandsExecutor, modulesCommandsExecutor: RedisMultiCommand.prototype.commandsExecutor,
scripts: clientOptions?.scripts, scripts: clientOptions?.scripts,
scriptsExecutor: RedisMultiCommand.#scriptsExecutor scriptsExecutor: RedisMultiCommand.prototype.scriptsExecutor
}); });
} }
static create<M extends RedisModules, S extends RedisLuaScripts>( static create<M extends RedisModules = {}, S extends RedisLuaScripts = {}>(
executor: RedisMultiExecutor, executor: RedisMultiExecutor,
clientOptions?: RedisClientOptions<M, S> clientOptions?: RedisClientOptions<M, S>
): RedisMultiCommandType<M, S> { ): RedisMultiCommandType<M, S> {
@@ -153,6 +114,42 @@ export default class RedisMultiCommand<M extends RedisModules = RedisModules, S
(this as any)[name] = (...args: Array<unknown>): void => (this as any).addCommand(name, args); (this as any)[name] = (...args: Array<unknown>): void => (this as any).addCommand(name, args);
} }
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
return this.addCommand(
command.transformArguments(...args),
command.transformReply
);
}
scriptsExecutor(script: RedisLuaScript, args: Array<unknown>): this {
const transformedArguments: TransformArgumentsReply = [];
if (this.#scriptsInUse.has(script.SHA1)) {
transformedArguments.push(
'EVALSHA',
script.SHA1
);
} else {
this.#scriptsInUse.add(script.SHA1);
transformedArguments.push(
'EVAL',
script.SCRIPT
);
}
transformedArguments.push(script.NUMBER_OF_KEYS.toString());
const scriptArguments = script.transformArguments(...args);
transformedArguments.push(...scriptArguments);
if (scriptArguments.preserve) {
transformedArguments.preserve = scriptArguments.preserve;
}
return this.addCommand(
transformedArguments,
script.transformReply
);
}
addCommand(args: TransformArgumentsReply, transformReply?: RedisCommand['transformReply']): this { addCommand(args: TransformArgumentsReply, transformReply?: RedisCommand['transformReply']): this {
this.#queue.push({ this.#queue.push({
args, args,
@@ -205,4 +202,4 @@ export default class RedisMultiCommand<M extends RedisModules = RedisModules, S
} }
} }
extendWithDefaultCommands(RedisMultiCommand, RedisMultiCommand.commandsExecutor); extendWithDefaultCommands(RedisMultiCommand, RedisMultiCommand.prototype.commandsExecutor);

View File

@@ -112,7 +112,7 @@ async function spawnGlobalRedisServer(args?: Array<string>): Promise<number> {
const SLOTS = 16384; const SLOTS = 16384;
interface SpawnRedisClusterNodeResult extends SpawnRedisServerResult { interface SpawnRedisClusterNodeResult extends SpawnRedisServerResult {
client: RedisClientType<RedisModules, RedisLuaScripts> client: RedisClientType
} }
async function spawnRedisClusterNode( async function spawnRedisClusterNode(
@@ -281,7 +281,7 @@ export function describeHandleMinimumRedisVersion(minimumVersion: PartialRedisVe
export function itWithClient( export function itWithClient(
type: TestRedisServers, type: TestRedisServers,
title: string, title: string,
fn: (client: RedisClientType<RedisModules, RedisLuaScripts>) => Promise<void>, fn: (client: RedisClientType) => Promise<void>,
options?: RedisTestOptions options?: RedisTestOptions
): void { ): void {
it(title, async function () { it(title, async function () {
@@ -306,7 +306,7 @@ export function itWithClient(
export function itWithCluster( export function itWithCluster(
type: TestRedisClusters, type: TestRedisClusters,
title: string, title: string,
fn: (cluster: RedisClusterType<RedisModules, RedisLuaScripts>) => Promise<void>, fn: (cluster: RedisClusterType) => Promise<void>,
options?: RedisTestOptions options?: RedisTestOptions
): void { ): void {
it(title, async function () { it(title, async function () {
@@ -328,7 +328,7 @@ export function itWithCluster(
}); });
} }
export function itWithDedicatedCluster(title: string, fn: (cluster: RedisClusterType<RedisModules, RedisLuaScripts>) => Promise<void>): void { export function itWithDedicatedCluster(title: string, fn: (cluster: RedisClusterType) => Promise<void>): void {
it(title, async function () { it(title, async function () {
this.timeout(10000); this.timeout(10000);