You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-04 15:02:09 +03:00
Add support for redis functions (#2020)
* fix #1906 - implement BITFIELD_RO * initial support for redis functions * fix test utils * redis functions commands and tests * upgrade deps * fix "Property 'uninstall' does not exist on type 'SinonFakeTimers'" * upgrade dockers version * Merge branch 'master' of github.com:redis/node-redis into functions * fix FUNCTION LIST WITHCODE and FUNCTION STATS * upgrade deps * set minimum version for FCALL and FCALL_RO * fix FUNCTION LOAD * FUNCTION LOAD * fix FUNCTION LOAD & FUNCTION LIST & FUNCTION LOAD WITHCODE * fix FUNCTION_LIST_WITHCODE test
This commit is contained in:
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version: ['12', '14', '16']
|
node-version: ['12', '14', '16']
|
||||||
redis-version: ['5', '6.0', '6.2', '7.0-rc2']
|
redis-version: ['5', '6.0', '6.2', '7.0-rc3']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.3.4
|
- uses: actions/checkout@v2.3.4
|
||||||
with:
|
with:
|
||||||
|
27
index.ts
27
index.ts
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
RedisModules,
|
RedisModules,
|
||||||
|
RedisFunctions,
|
||||||
RedisScripts,
|
RedisScripts,
|
||||||
createClient as _createClient,
|
createClient as _createClient,
|
||||||
RedisClientOptions,
|
RedisClientOptions,
|
||||||
@@ -33,12 +34,17 @@ export type RedisDefaultModules = typeof modules;
|
|||||||
|
|
||||||
export type RedisClientType<
|
export type RedisClientType<
|
||||||
M extends RedisModules = RedisDefaultModules,
|
M extends RedisModules = RedisDefaultModules,
|
||||||
|
F extends RedisFunctions = Record<string, never>,
|
||||||
S extends RedisScripts = Record<string, never>
|
S extends RedisScripts = Record<string, never>
|
||||||
> = _RedisClientType<M, S>;
|
> = _RedisClientType<M, F, S>;
|
||||||
|
|
||||||
export function createClient<M extends RedisModules, S extends RedisScripts>(
|
export function createClient<
|
||||||
options?: RedisClientOptions<M, S>
|
M extends RedisModules,
|
||||||
): _RedisClientType<RedisDefaultModules & M, S> {
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(
|
||||||
|
options?: RedisClientOptions<M, F, S>
|
||||||
|
): _RedisClientType<RedisDefaultModules & M, F, S> {
|
||||||
return _createClient({
|
return _createClient({
|
||||||
...options,
|
...options,
|
||||||
modules: {
|
modules: {
|
||||||
@@ -50,12 +56,17 @@ export function createClient<M extends RedisModules, S extends RedisScripts>(
|
|||||||
|
|
||||||
export type RedisClusterType<
|
export type RedisClusterType<
|
||||||
M extends RedisModules = RedisDefaultModules,
|
M extends RedisModules = RedisDefaultModules,
|
||||||
|
F extends RedisFunctions = Record<string, never>,
|
||||||
S extends RedisScripts = Record<string, never>
|
S extends RedisScripts = Record<string, never>
|
||||||
> = _RedisClusterType<M, S>;
|
> = _RedisClusterType<M, F, S>;
|
||||||
|
|
||||||
export function createCluster<M extends RedisModules, S extends RedisScripts>(
|
export function createCluster<
|
||||||
options: RedisClusterOptions<M, S>
|
M extends RedisModules,
|
||||||
): RedisClusterType<RedisDefaultModules & M, S> {
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(
|
||||||
|
options: RedisClusterOptions<M, F, S>
|
||||||
|
): RedisClusterType<RedisDefaultModules & M, F, S> {
|
||||||
return _createCluster({
|
return _createCluster({
|
||||||
...options,
|
...options,
|
||||||
modules: {
|
modules: {
|
||||||
|
45
package-lock.json
generated
45
package-lock.json
generated
@@ -1865,6 +1865,19 @@
|
|||||||
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
|
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/compress-brotli": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/json-buffer": "~3.0.0",
|
||||||
|
"json-buffer": "~3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -3332,9 +3345,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/inquirer": {
|
"node_modules/inquirer": {
|
||||||
"version": "8.2.0",
|
"version": "8.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.2.tgz",
|
||||||
"integrity": "sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==",
|
"integrity": "sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-escapes": "^4.2.1",
|
"ansi-escapes": "^4.2.1",
|
||||||
@@ -3347,13 +3360,13 @@
|
|||||||
"mute-stream": "0.0.8",
|
"mute-stream": "0.0.8",
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
"run-async": "^2.4.0",
|
"run-async": "^2.4.0",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.5.5",
|
||||||
"string-width": "^4.1.0",
|
"string-width": "^4.1.0",
|
||||||
"strip-ansi": "^6.0.0",
|
"strip-ansi": "^6.0.0",
|
||||||
"through": "^2.3.6"
|
"through": "^2.3.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
@@ -5453,7 +5466,7 @@
|
|||||||
"globby": "11.0.4",
|
"globby": "11.0.4",
|
||||||
"got": "9.6.0",
|
"got": "9.6.0",
|
||||||
"import-cwd": "3.0.0",
|
"import-cwd": "3.0.0",
|
||||||
"inquirer": "8.2.0",
|
"inquirer": "8.2.2",
|
||||||
"is-ci": "3.0.1",
|
"is-ci": "3.0.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
@@ -8382,6 +8395,16 @@
|
|||||||
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
|
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"compress-brotli": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/json-buffer": "~3.0.0",
|
||||||
|
"json-buffer": "~3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -9474,9 +9497,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"inquirer": {
|
"inquirer": {
|
||||||
"version": "8.2.0",
|
"version": "8.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.2.tgz",
|
||||||
"integrity": "sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==",
|
"integrity": "sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-escapes": "^4.2.1",
|
"ansi-escapes": "^4.2.1",
|
||||||
@@ -9489,7 +9512,7 @@
|
|||||||
"mute-stream": "0.0.8",
|
"mute-stream": "0.0.8",
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
"run-async": "^2.4.0",
|
"run-async": "^2.4.0",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.5.5",
|
||||||
"string-width": "^4.1.0",
|
"string-width": "^4.1.0",
|
||||||
"strip-ansi": "^6.0.0",
|
"strip-ansi": "^6.0.0",
|
||||||
"through": "^2.3.6"
|
"through": "^2.3.6"
|
||||||
@@ -11034,7 +11057,7 @@
|
|||||||
"globby": "11.0.4",
|
"globby": "11.0.4",
|
||||||
"got": "9.6.0",
|
"got": "9.6.0",
|
||||||
"import-cwd": "3.0.0",
|
"import-cwd": "3.0.0",
|
||||||
"inquirer": "8.2.0",
|
"inquirer": "8.2.2",
|
||||||
"is-ci": "3.0.1",
|
"is-ci": "3.0.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
|
@@ -3,7 +3,7 @@ import RedisCluster from './lib/cluster';
|
|||||||
|
|
||||||
export { RedisClientType, RedisClientOptions } from './lib/client';
|
export { RedisClientType, RedisClientOptions } from './lib/client';
|
||||||
|
|
||||||
export { RedisModules, RedisScripts } from './lib/commands';
|
export { RedisModules, RedisFunctions, RedisScripts } from './lib/commands';
|
||||||
|
|
||||||
export const createClient = RedisClient.create;
|
export const createClient = RedisClient.create;
|
||||||
|
|
||||||
|
@@ -62,6 +62,15 @@ import * as ECHO from '../commands/ECHO';
|
|||||||
import * as FAILOVER from '../commands/FAILOVER';
|
import * as FAILOVER from '../commands/FAILOVER';
|
||||||
import * as FLUSHALL from '../commands/FLUSHALL';
|
import * as FLUSHALL from '../commands/FLUSHALL';
|
||||||
import * as FLUSHDB from '../commands/FLUSHDB';
|
import * as FLUSHDB from '../commands/FLUSHDB';
|
||||||
|
import * as FUNCTION_DELETE from '../commands/FUNCTION_DELETE';
|
||||||
|
import * as FUNCTION_DUMP from '../commands/FUNCTION_DUMP';
|
||||||
|
import * as FUNCTION_FLUSH from '../commands/FUNCTION_FLUSH';
|
||||||
|
import * as FUNCTION_KILL from '../commands/FUNCTION_KILL';
|
||||||
|
import * as FUNCTION_LIST_WITHCODE from '../commands/FUNCTION_LIST_WITHCODE';
|
||||||
|
import * as FUNCTION_LIST from '../commands/FUNCTION_LIST';
|
||||||
|
import * as FUNCTION_LOAD from '../commands/FUNCTION_LOAD';
|
||||||
|
import * as FUNCTION_RESTORE from '../commands/FUNCTION_RESTORE';
|
||||||
|
import * as FUNCTION_STATS from '../commands/FUNCTION_STATS';
|
||||||
import * as HELLO from '../commands/HELLO';
|
import * as HELLO from '../commands/HELLO';
|
||||||
import * as INFO from '../commands/INFO';
|
import * as INFO from '../commands/INFO';
|
||||||
import * as KEYS from '../commands/KEYS';
|
import * as KEYS from '../commands/KEYS';
|
||||||
@@ -228,6 +237,24 @@ export default {
|
|||||||
flushAll: FLUSHALL,
|
flushAll: FLUSHALL,
|
||||||
FLUSHDB,
|
FLUSHDB,
|
||||||
flushDb: FLUSHDB,
|
flushDb: FLUSHDB,
|
||||||
|
FUNCTION_DELETE,
|
||||||
|
functionDelete: FUNCTION_DELETE,
|
||||||
|
FUNCTION_DUMP,
|
||||||
|
functionDump: FUNCTION_DUMP,
|
||||||
|
FUNCTION_FLUSH,
|
||||||
|
functionFlush: FUNCTION_FLUSH,
|
||||||
|
FUNCTION_KILL,
|
||||||
|
functionKill: FUNCTION_KILL,
|
||||||
|
FUNCTION_LIST_WITHCODE,
|
||||||
|
functionListWithCode: FUNCTION_LIST_WITHCODE,
|
||||||
|
FUNCTION_LIST,
|
||||||
|
functionList: FUNCTION_LIST,
|
||||||
|
FUNCTION_LOAD,
|
||||||
|
functionLoad: FUNCTION_LOAD,
|
||||||
|
FUNCTION_RESTORE,
|
||||||
|
functionRestore: FUNCTION_RESTORE,
|
||||||
|
FUNCTION_STATS,
|
||||||
|
functionStats: FUNCTION_STATS,
|
||||||
HELLO,
|
HELLO,
|
||||||
hello: HELLO,
|
hello: HELLO,
|
||||||
INFO,
|
INFO,
|
||||||
|
@@ -2,7 +2,7 @@ import { strict as assert } from 'assert';
|
|||||||
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
|
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
|
||||||
import RedisClient, { RedisClientType } from '.';
|
import RedisClient, { RedisClientType } from '.';
|
||||||
import { RedisClientMultiCommandType } from './multi-command';
|
import { RedisClientMultiCommandType } from './multi-command';
|
||||||
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands';
|
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisFunctions, RedisScripts } from '../commands';
|
||||||
import { AbortError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
|
import { AbortError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
|
||||||
import { defineScript } from '../lua-script';
|
import { defineScript } from '../lua-script';
|
||||||
import { spy } from 'sinon';
|
import { spy } from 'sinon';
|
||||||
@@ -10,16 +10,42 @@ import { once } from 'events';
|
|||||||
import { ClientKillFilters } from '../commands/CLIENT_KILL';
|
import { ClientKillFilters } from '../commands/CLIENT_KILL';
|
||||||
|
|
||||||
export const SQUARE_SCRIPT = defineScript({
|
export const SQUARE_SCRIPT = defineScript({
|
||||||
NUMBER_OF_KEYS: 0,
|
|
||||||
SCRIPT: 'return ARGV[1] * ARGV[1];',
|
SCRIPT: 'return ARGV[1] * ARGV[1];',
|
||||||
|
NUMBER_OF_KEYS: 0,
|
||||||
transformArguments(number: number): Array<string> {
|
transformArguments(number: number): Array<string> {
|
||||||
return [number.toString()];
|
return [number.toString()];
|
||||||
},
|
|
||||||
transformReply(reply: number): number {
|
|
||||||
return reply;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const MATH_FUNCTION = {
|
||||||
|
name: 'math',
|
||||||
|
engine: 'LUA',
|
||||||
|
code: `#!LUA name=math
|
||||||
|
redis.register_function{
|
||||||
|
function_name = "square",
|
||||||
|
callback = function(keys, args) return args[1] * args[1] end,
|
||||||
|
flags = { "no-writes" }
|
||||||
|
}`,
|
||||||
|
library: {
|
||||||
|
square: {
|
||||||
|
NAME: 'square',
|
||||||
|
NUMBER_OF_KEYS: 0,
|
||||||
|
transformArguments(number: number): Array<string> {
|
||||||
|
return [number.toString()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadMathFunction(
|
||||||
|
client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>
|
||||||
|
): Promise<void> {
|
||||||
|
await client.functionLoad(
|
||||||
|
MATH_FUNCTION.code,
|
||||||
|
{ REPLACE: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe('Client', () => {
|
describe('Client', () => {
|
||||||
describe('parseURL', () => {
|
describe('parseURL', () => {
|
||||||
it('redis://user:secret@localhost:6379/0', () => {
|
it('redis://user:secret@localhost:6379/0', () => {
|
||||||
@@ -115,7 +141,14 @@ describe('Client', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('legacyMode', () => {
|
describe('legacyMode', () => {
|
||||||
function sendCommandAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, args: RedisCommandArguments): Promise<RedisCommandRawReply> {
|
function sendCommandAsync<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(
|
||||||
|
client: RedisClientType<M, F, S>,
|
||||||
|
args: RedisCommandArguments
|
||||||
|
): Promise<RedisCommandRawReply> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
(client as any).sendCommand(args, (err: Error | undefined, reply: RedisCommandRawReply) => {
|
(client as any).sendCommand(args, (err: Error | undefined, reply: RedisCommandRawReply) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
@@ -159,7 +192,14 @@ describe('Client', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: Array<any>): Promise<RedisCommandRawReply> {
|
function setAsync<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(
|
||||||
|
client: RedisClientType<M, F, S>,
|
||||||
|
...args: Array<any>
|
||||||
|
): Promise<RedisCommandRawReply> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
(client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
|
(client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
@@ -205,7 +245,11 @@ describe('Client', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function multiExecAsync<M extends RedisModules, S extends RedisScripts>(multi: RedisClientMultiCommandType<M, S>): Promise<Array<RedisCommandRawReply>> {
|
function multiExecAsync<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(multi: RedisClientMultiCommandType<M, F, S>): Promise<Array<RedisCommandRawReply>> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
(multi as any).exec((err: Error | undefined, replies: Array<RedisCommandRawReply>) => {
|
(multi as any).exec((err: Error | undefined, replies: Array<RedisCommandRawReply>) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
@@ -439,16 +483,7 @@ describe('Client', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('modules', async client => {
|
const module = {
|
||||||
// assert.equal(
|
|
||||||
// await client.module.echo('message'),
|
|
||||||
// 'message'
|
|
||||||
// );
|
|
||||||
}, {
|
|
||||||
...GLOBAL.SERVERS.OPEN,
|
|
||||||
clientOptions: {
|
|
||||||
modules: {
|
|
||||||
module: {
|
|
||||||
echo: {
|
echo: {
|
||||||
transformArguments(message: string): Array<string> {
|
transformArguments(message: string): Array<string> {
|
||||||
return ['ECHO', message];
|
return ['ECHO', message];
|
||||||
@@ -457,8 +492,36 @@ describe('Client', () => {
|
|||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
testUtils.testWithClient('modules', async client => {
|
||||||
|
assert.equal(
|
||||||
|
await client.module.echo('message'),
|
||||||
|
'message'
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
modules: {
|
||||||
|
module
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('functions', async client => {
|
||||||
|
await loadMathFunction(client);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.math.square(2),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
minimumDockerVersion: [7, 0],
|
||||||
|
clientOptions: {
|
||||||
|
functions: {
|
||||||
|
math: MATH_FUNCTION.library
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -468,9 +531,13 @@ describe('Client', () => {
|
|||||||
assert.ok(id !== isolatedId);
|
assert.ok(id !== isolatedId);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
|
||||||
async function killClient<M extends RedisModules, S extends RedisScripts>(
|
async function killClient<
|
||||||
client: RedisClientType<M, S>,
|
M extends RedisModules,
|
||||||
errorClient: RedisClientType<M, S> = client
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(
|
||||||
|
client: RedisClientType<M, F, S>,
|
||||||
|
errorClient: RedisClientType<M, F, S> = client
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const onceErrorPromise = once(errorClient, 'error');
|
const onceErrorPromise = once(errorClient, 'error');
|
||||||
await client.sendCommand(['QUIT']);
|
await client.sendCommand(['QUIT']);
|
||||||
@@ -684,7 +751,9 @@ describe('Client', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await assert.doesNotReject(Promise.all([
|
await assert.doesNotReject(Promise.all([
|
||||||
subscriber.subscribe('channel', () => {}),
|
subscriber.subscribe('channel', () => {
|
||||||
|
// noop
|
||||||
|
}),
|
||||||
publisher.publish('channel', 'message')
|
publisher.publish('channel', 'message')
|
||||||
]));
|
]));
|
||||||
} finally {
|
} finally {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import COMMANDS from './commands';
|
import COMMANDS from './commands';
|
||||||
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString } from '../commands';
|
||||||
import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket';
|
import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket';
|
||||||
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
|
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
|
||||||
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
|
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
|
||||||
@@ -9,16 +9,17 @@ import { CommandOptions, commandOptions, isCommandOptions } from '../command-opt
|
|||||||
import { ScanOptions, ZMember } from '../commands/generic-transformers';
|
import { ScanOptions, ZMember } from '../commands/generic-transformers';
|
||||||
import { ScanCommandOptions } from '../commands/SCAN';
|
import { ScanCommandOptions } from '../commands/SCAN';
|
||||||
import { HScanTuple } from '../commands/HSCAN';
|
import { HScanTuple } from '../commands/HSCAN';
|
||||||
import { extendWithCommands, extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
|
import { attachCommands, attachExtensions, fCallArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
|
||||||
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
||||||
import { ClientClosedError, DisconnectsClientError } from '../errors';
|
import { ClientClosedError, DisconnectsClientError } from '../errors';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { TcpSocketConnectOpts } from 'net';
|
import { TcpSocketConnectOpts } from 'net';
|
||||||
|
|
||||||
export interface RedisClientOptions<
|
export interface RedisClientOptions<
|
||||||
M extends RedisModules = Record<string, never>,
|
M extends RedisModules = RedisModules,
|
||||||
S extends RedisScripts = Record<string, never>
|
F extends RedisFunctions = RedisFunctions,
|
||||||
> extends RedisPlugins<M, S> {
|
S extends RedisScripts = RedisScripts
|
||||||
|
> extends RedisExtensions<M, F, S> {
|
||||||
url?: string;
|
url?: string;
|
||||||
socket?: RedisSocketOptions;
|
socket?: RedisSocketOptions;
|
||||||
username?: string;
|
username?: string;
|
||||||
@@ -32,58 +33,37 @@ export interface RedisClientOptions<
|
|||||||
isolationPoolOptions?: PoolOptions;
|
isolationPoolOptions?: PoolOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConvertArgumentType<Type, ToType> =
|
|
||||||
Type extends RedisCommandArgument ? (
|
|
||||||
Type extends (string & ToType) ? Type : ToType
|
|
||||||
) : (
|
|
||||||
Type extends Set<infer Member> ? Set<ConvertArgumentType<Member, ToType>> : (
|
|
||||||
Type extends Map<infer Key, infer Value> ? Map<Key, ConvertArgumentType<Value, ToType>> : (
|
|
||||||
Type extends Array<infer Member> ? Array<ConvertArgumentType<Member, ToType>> : (
|
|
||||||
Type extends Date ? Type : (
|
|
||||||
Type extends Record<keyof any, any> ? {
|
|
||||||
[Property in keyof Type]: ConvertArgumentType<Type[Property], ToType>
|
|
||||||
} : Type
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export type RedisClientCommandSignature<
|
|
||||||
Command extends RedisCommand,
|
|
||||||
Params extends Array<unknown> = Parameters<Command['transformArguments']>
|
|
||||||
> = <Options extends CommandOptions<ClientCommandOptions>>(
|
|
||||||
...args: Params | [options: Options, ...rest: Params]
|
|
||||||
) => Promise<
|
|
||||||
ConvertArgumentType<
|
|
||||||
RedisCommandReply<Command>,
|
|
||||||
Options['returnBuffers'] extends true ? Buffer : string
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
|
|
||||||
type WithCommands = {
|
type WithCommands = {
|
||||||
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
|
[P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExcludeMappedString<S> = string extends S ? never : S;
|
|
||||||
|
|
||||||
export type WithModules<M extends RedisModules> = {
|
export type WithModules<M extends RedisModules> = {
|
||||||
[P in keyof M as ExcludeMappedString<P>]: {
|
[P in keyof M as ExcludeMappedString<P>]: {
|
||||||
[C in keyof M[P] as ExcludeMappedString<C>]: RedisClientCommandSignature<M[P][C]>;
|
[C in keyof M[P] as ExcludeMappedString<C>]: RedisCommandSignature<M[P][C]>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WithFunctions<F extends RedisFunctions> = {
|
||||||
|
[P in keyof F as ExcludeMappedString<P>]: {
|
||||||
|
[FF in keyof F[P] as ExcludeMappedString<FF>]: RedisCommandSignature<F[P][FF]>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WithScripts<S extends RedisScripts> = {
|
export type WithScripts<S extends RedisScripts> = {
|
||||||
[P in keyof S as ExcludeMappedString<P>]: RedisClientCommandSignature<S[P]>;
|
[P in keyof S as ExcludeMappedString<P>]: RedisCommandSignature<S[P]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RedisClientType<
|
export type RedisClientType<
|
||||||
M extends RedisModules = Record<string, never>,
|
M extends RedisModules = Record<string, never>,
|
||||||
|
F extends RedisFunctions = Record<string, never>,
|
||||||
S extends RedisScripts = Record<string, never>
|
S extends RedisScripts = Record<string, never>
|
||||||
> = RedisClient<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
|
> = RedisClient<M, F, S> & WithCommands & WithModules<M> & WithFunctions<F> & WithScripts<S>;
|
||||||
|
|
||||||
export type InstantiableRedisClient<M extends RedisModules, S extends RedisScripts> =
|
export type InstantiableRedisClient<
|
||||||
new (options?: RedisClientOptions<M, S>) => RedisClientType<M, S>;
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = new (options?: RedisClientOptions<M, F, S>) => RedisClientType<M, F, S>;
|
||||||
|
|
||||||
export interface ClientCommandOptions extends QueueCommandOptions {
|
export interface ClientCommandOptions extends QueueCommandOptions {
|
||||||
isolated?: boolean;
|
isolated?: boolean;
|
||||||
@@ -91,30 +71,44 @@ export interface ClientCommandOptions extends QueueCommandOptions {
|
|||||||
|
|
||||||
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
|
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
|
||||||
|
|
||||||
export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
|
export default class RedisClient<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> extends EventEmitter {
|
||||||
static commandOptions<T extends ClientCommandOptions>(options: T): CommandOptions<T> {
|
static commandOptions<T extends ClientCommandOptions>(options: T): CommandOptions<T> {
|
||||||
return commandOptions(options);
|
return commandOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
commandOptions = RedisClient.commandOptions;
|
commandOptions = RedisClient.commandOptions;
|
||||||
|
|
||||||
static extend<M extends RedisModules, S extends RedisScripts>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
|
static extend<
|
||||||
const Client = <any>extendWithModulesAndScripts({
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(extensions?: RedisExtensions<M, F, S>): InstantiableRedisClient<M, F, S> {
|
||||||
|
const Client = attachExtensions({
|
||||||
BaseClass: RedisClient,
|
BaseClass: RedisClient,
|
||||||
modules: plugins?.modules,
|
modulesExecutor: RedisClient.prototype.commandsExecutor,
|
||||||
modulesCommandsExecutor: RedisClient.prototype.commandsExecutor,
|
modules: extensions?.modules,
|
||||||
scripts: plugins?.scripts,
|
functionsExecutor: RedisClient.prototype.functionsExecuter,
|
||||||
scriptsExecutor: RedisClient.prototype.scriptsExecutor
|
functions: extensions?.functions,
|
||||||
|
scriptsExecutor: RedisClient.prototype.scriptsExecuter,
|
||||||
|
scripts: extensions?.scripts
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Client !== RedisClient) {
|
if (Client !== RedisClient) {
|
||||||
Client.prototype.Multi = RedisClientMultiCommand.extend(plugins);
|
Client.prototype.Multi = RedisClientMultiCommand.extend(extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Client;
|
return Client;
|
||||||
}
|
}
|
||||||
|
|
||||||
static create<M extends RedisModules, S extends RedisScripts>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
|
static create<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(options?: RedisClientOptions<M, F, S>): RedisClientType<M, F, S> {
|
||||||
return new (RedisClient.extend(options))(options);
|
return new (RedisClient.extend(options))(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,14 +151,14 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly #options?: RedisClientOptions<M, S>;
|
readonly #options?: RedisClientOptions<M, F, S>;
|
||||||
readonly #queue: RedisCommandsQueue;
|
|
||||||
readonly #socket: RedisSocket;
|
readonly #socket: RedisSocket;
|
||||||
readonly #isolationPool: Pool<RedisClientType<M, S>>;
|
readonly #queue: RedisCommandsQueue;
|
||||||
|
readonly #isolationPool: Pool<RedisClientType<M, F, S>>;
|
||||||
readonly #v4: Record<string, any> = {};
|
readonly #v4: Record<string, any> = {};
|
||||||
#selectedDB = 0;
|
#selectedDB = 0;
|
||||||
|
|
||||||
get options(): RedisClientOptions<M, S> | undefined {
|
get options(): RedisClientOptions<M, F, S> | undefined {
|
||||||
return this.#options;
|
return this.#options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +174,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
return this.#v4;
|
return this.#v4;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(options?: RedisClientOptions<M, S>) {
|
constructor(options?: RedisClientOptions<M, F, S>) {
|
||||||
super();
|
super();
|
||||||
this.#options = this.#initiateOptions(options);
|
this.#options = this.#initiateOptions(options);
|
||||||
this.#queue = this.#initiateQueue();
|
this.#queue = this.#initiateQueue();
|
||||||
@@ -198,7 +192,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
this.#legacyMode();
|
this.#legacyMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
#initiateOptions(options?: RedisClientOptions<M, S>): RedisClientOptions<M, S> | undefined {
|
#initiateOptions(options?: RedisClientOptions<M, F, S>): RedisClientOptions<M, F, S> | undefined {
|
||||||
if (options?.url) {
|
if (options?.url) {
|
||||||
const parsed = RedisClient.parseURL(options.url);
|
const parsed = RedisClient.parseURL(options.url);
|
||||||
if (options.socket) {
|
if (options.socket) {
|
||||||
@@ -350,7 +344,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
(...args: Array<unknown>): void => (this as any).sendCommand(name, ...args);
|
(...args: Array<unknown>): void => (this as any).sendCommand(name, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
duplicate(overrides?: Partial<RedisClientOptions<M, S>>): RedisClientType<M, S> {
|
duplicate(overrides?: Partial<RedisClientOptions<M, F, S>>): RedisClientType<M, F, S> {
|
||||||
return new (Object.getPrototypeOf(this).constructor)({
|
return new (Object.getPrototypeOf(this).constructor)({
|
||||||
...this.#options,
|
...this.#options,
|
||||||
...overrides
|
...overrides
|
||||||
@@ -361,9 +355,11 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
await this.#socket.connect();
|
await this.#socket.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
async commandsExecutor(command: RedisCommand, args: Array<unknown>): Promise<RedisCommandReply<typeof command>> {
|
async commandsExecutor<C extends RedisCommand>(
|
||||||
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
|
command: C,
|
||||||
|
args: Array<unknown>
|
||||||
|
): Promise<RedisCommandReply<C>> {
|
||||||
|
const { args: redisArgs, options } = transformCommandArguments(command, args);
|
||||||
return transformCommandReply(
|
return transformCommandReply(
|
||||||
command,
|
command,
|
||||||
await this.#sendCommand(redisArgs, options),
|
await this.#sendCommand(redisArgs, options),
|
||||||
@@ -371,12 +367,18 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions): Promise<T> {
|
sendCommand<T = RedisCommandRawReply>(
|
||||||
|
args: RedisCommandArguments,
|
||||||
|
options?: ClientCommandOptions
|
||||||
|
): Promise<T> {
|
||||||
return this.#sendCommand(args, options);
|
return this.#sendCommand(args, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
|
// using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
|
||||||
#sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions): Promise<T> {
|
#sendCommand<T = RedisCommandRawReply>(
|
||||||
|
args: RedisCommandArguments,
|
||||||
|
options?: ClientCommandOptions
|
||||||
|
): Promise<T> {
|
||||||
if (!this.#socket.isOpen) {
|
if (!this.#socket.isOpen) {
|
||||||
return Promise.reject(new ClientClosedError());
|
return Promise.reject(new ClientClosedError());
|
||||||
}
|
}
|
||||||
@@ -395,9 +397,34 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> {
|
async functionsExecuter<F extends RedisFunction>(
|
||||||
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
|
fn: F,
|
||||||
|
args: Array<unknown>
|
||||||
|
): Promise<RedisCommandReply<F>> {
|
||||||
|
const { args: redisArgs, options } = transformCommandArguments(fn, args);
|
||||||
|
return transformCommandReply(
|
||||||
|
fn,
|
||||||
|
await this.executeFunction(fn, redisArgs, options),
|
||||||
|
redisArgs.preserve
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeFunction(
|
||||||
|
fn: RedisFunction,
|
||||||
|
args: RedisCommandArguments,
|
||||||
|
options?: ClientCommandOptions
|
||||||
|
): Promise<RedisCommandRawReply> {
|
||||||
|
return this.#sendCommand(
|
||||||
|
fCallArguments(fn, args),
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async scriptsExecuter<S extends RedisScript>(
|
||||||
|
script: S,
|
||||||
|
args: Array<unknown>
|
||||||
|
): Promise<RedisCommandReply<S>> {
|
||||||
|
const { args: redisArgs, options } = transformCommandArguments(script, args);
|
||||||
return transformCommandReply(
|
return transformCommandReply(
|
||||||
script,
|
script,
|
||||||
await this.executeScript(script, redisArgs, options),
|
await this.executeScript(script, redisArgs, options),
|
||||||
@@ -405,25 +432,29 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeScript(script: RedisScript, args: RedisCommandArguments, options?: ClientCommandOptions): Promise<RedisCommandReply<typeof script>> {
|
async executeScript(
|
||||||
|
script: RedisScript,
|
||||||
|
args: RedisCommandArguments,
|
||||||
|
options?: ClientCommandOptions
|
||||||
|
): Promise<RedisCommandRawReply> {
|
||||||
|
const redisArgs: RedisCommandArguments = ['EVALSHA', script.SHA1];
|
||||||
|
|
||||||
|
if (script.NUMBER_OF_KEYS !== undefined) {
|
||||||
|
redisArgs.push(script.NUMBER_OF_KEYS.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
redisArgs.push(...args);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.#sendCommand([
|
return await this.#sendCommand(redisArgs, options);
|
||||||
'EVALSHA',
|
|
||||||
script.SHA1,
|
|
||||||
script.NUMBER_OF_KEYS.toString(),
|
|
||||||
...args
|
|
||||||
], options);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (!err?.message?.startsWith?.('NOSCRIPT')) {
|
if (!err?.message?.startsWith?.('NOSCRIPT')) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.#sendCommand([
|
redisArgs[0] = 'EVAL';
|
||||||
'EVAL',
|
redisArgs[1] = script.SCRIPT;
|
||||||
script.SCRIPT,
|
return this.#sendCommand(redisArgs, options);
|
||||||
script.NUMBER_OF_KEYS.toString(),
|
|
||||||
...args
|
|
||||||
], options);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,11 +589,11 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
executeIsolated<T>(fn: (client: RedisClientType<M, S>) => T | Promise<T>): Promise<T> {
|
executeIsolated<T>(fn: (client: RedisClientType<M, F, S>) => T | Promise<T>): Promise<T> {
|
||||||
return this.#isolationPool.use(fn);
|
return this.#isolationPool.use(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
multi(): RedisClientMultiCommandType<M, S> {
|
multi(): RedisClientMultiCommandType<M, F, S> {
|
||||||
return new (this as any).Multi(
|
return new (this as any).Multi(
|
||||||
this.multiExecutor.bind(this),
|
this.multiExecutor.bind(this),
|
||||||
this.#options?.legacyMode
|
this.#options?.legacyMode
|
||||||
@@ -639,7 +670,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extendWithCommands({
|
attachCommands({
|
||||||
BaseClass: RedisClient,
|
BaseClass: RedisClient,
|
||||||
commands: COMMANDS,
|
commands: COMMANDS,
|
||||||
executor: RedisClient.prototype.commandsExecutor
|
executor: RedisClient.prototype.commandsExecutor
|
||||||
|
@@ -1,28 +1,63 @@
|
|||||||
import COMMANDS from './commands';
|
import COMMANDS from './commands';
|
||||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction } from '../commands';
|
||||||
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
||||||
import { extendWithCommands, extendWithModulesAndScripts, transformLegacyCommandArguments } from '../commander';
|
import { attachCommands, attachExtensions, transformLegacyCommandArguments } from '../commander';
|
||||||
import { ExcludeMappedString } from '.';
|
|
||||||
|
|
||||||
type RedisClientMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
|
type CommandSignature<
|
||||||
(...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, S>;
|
C extends RedisCommand,
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = (...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, F, S>;
|
||||||
|
|
||||||
type WithCommands<M extends RedisModules, S extends RedisScripts> = {
|
type WithCommands<
|
||||||
[P in keyof typeof COMMANDS]: RedisClientMultiCommandSignature<(typeof COMMANDS)[P], M, S>;
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = {
|
||||||
|
[P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], M, F, S>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type WithModules<M extends RedisModules, S extends RedisScripts> = {
|
type WithModules<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = {
|
||||||
[P in keyof M as ExcludeMappedString<P>]: {
|
[P in keyof M as ExcludeMappedString<P>]: {
|
||||||
[C in keyof M[P] as ExcludeMappedString<C>]: RedisClientMultiCommandSignature<M[P][C], M, S>;
|
[C in keyof M[P] as ExcludeMappedString<C>]: CommandSignature<M[P][C], M, F, S>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
|
type WithFunctions<
|
||||||
[P in keyof S as ExcludeMappedString<P>]: RedisClientMultiCommandSignature<S[P], M, S>;
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = {
|
||||||
|
[P in keyof F as ExcludeMappedString<P>]: {
|
||||||
|
[FF in keyof F[P] as ExcludeMappedString<FF>]: CommandSignature<F[P][FF], M, F, S>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RedisClientMultiCommandType<M extends RedisModules, S extends RedisScripts> =
|
type WithScripts<
|
||||||
RedisClientMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = {
|
||||||
|
[P in keyof S as ExcludeMappedString<P>]: CommandSignature<S[P], M, F, S>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RedisClientMultiCommandType<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = RedisClientMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
|
||||||
|
|
||||||
|
type InstantiableRedisMultiCommand<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = new (...args: ConstructorParameters<typeof RedisClientMultiCommand>) => RedisClientMultiCommandType<M, F, S>;
|
||||||
|
|
||||||
|
|
||||||
export type RedisClientMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
|
export type RedisClientMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
|
||||||
|
|
||||||
@@ -30,15 +65,19 @@ export default class RedisClientMultiCommand {
|
|||||||
readonly #multi = new RedisMultiCommand();
|
readonly #multi = new RedisMultiCommand();
|
||||||
readonly #executor: RedisClientMultiExecutor;
|
readonly #executor: RedisClientMultiExecutor;
|
||||||
|
|
||||||
static extend<M extends RedisModules, S extends RedisScripts>(
|
static extend<
|
||||||
plugins?: RedisPlugins<M, S>
|
M extends RedisModules,
|
||||||
): new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisClientMultiCommandType<M, S> {
|
F extends RedisFunctions,
|
||||||
return <any>extendWithModulesAndScripts({
|
S extends RedisScripts
|
||||||
|
>(extensions?: RedisExtensions<M, F, S>): InstantiableRedisMultiCommand<M, F, S> {
|
||||||
|
return attachExtensions({
|
||||||
BaseClass: RedisClientMultiCommand,
|
BaseClass: RedisClientMultiCommand,
|
||||||
modules: plugins?.modules,
|
modulesExecutor: RedisClientMultiCommand.prototype.commandsExecutor,
|
||||||
modulesCommandsExecutor: RedisClientMultiCommand.prototype.commandsExecutor,
|
modules: extensions?.modules,
|
||||||
scripts: plugins?.scripts,
|
functionsExecutor: RedisClientMultiCommand.prototype.functionsExecutor,
|
||||||
scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor
|
functions: extensions?.functions,
|
||||||
|
scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor,
|
||||||
|
scripts: extensions?.scripts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +141,11 @@ export default class RedisClientMultiCommand {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
functionsExecutor(fn: RedisFunction, args: Array<unknown>): this {
|
||||||
|
this.#multi.addFunction(fn, args);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
|
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
|
||||||
this.#multi.addScript(script, args);
|
this.#multi.addScript(script, args);
|
||||||
return this;
|
return this;
|
||||||
@@ -123,15 +167,13 @@ export default class RedisClientMultiCommand {
|
|||||||
EXEC = this.exec;
|
EXEC = this.exec;
|
||||||
|
|
||||||
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
|
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
|
||||||
if (!this.#multi.queue.length) return [];
|
|
||||||
|
|
||||||
return this.#multi.transformReplies(
|
return this.#multi.transformReplies(
|
||||||
await this.#executor(this.#multi.queue)
|
await this.#executor(this.#multi.queue)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extendWithCommands({
|
attachCommands({
|
||||||
BaseClass: RedisClientMultiCommand,
|
BaseClass: RedisClientMultiCommand,
|
||||||
commands: COMMANDS,
|
commands: COMMANDS,
|
||||||
executor: RedisClientMultiCommand.prototype.commandsExecutor
|
executor: RedisClientMultiCommand.prototype.commandsExecutor
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
|
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
|
||||||
import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES';
|
import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES';
|
||||||
import { RedisClusterClientOptions, RedisClusterOptions } from '.';
|
import { RedisClusterClientOptions, RedisClusterOptions } from '.';
|
||||||
import { RedisCommandArgument, RedisModules, RedisScripts } from '../commands';
|
import { RedisCommandArgument, RedisFunctions, RedisModules, RedisScripts } from '../commands';
|
||||||
import { RootNodesUnavailableError } from '../errors';
|
import { RootNodesUnavailableError } from '../errors';
|
||||||
|
|
||||||
// We need to use 'require', because it's not possible with Typescript to import
|
// We need to use 'require', because it's not possible with Typescript to import
|
||||||
@@ -9,9 +9,13 @@ import { RootNodesUnavailableError } from '../errors';
|
|||||||
// set to true.
|
// set to true.
|
||||||
const calculateSlot = require('cluster-key-slot');
|
const calculateSlot = require('cluster-key-slot');
|
||||||
|
|
||||||
export interface ClusterNode<M extends RedisModules, S extends RedisScripts> {
|
export interface ClusterNode<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> {
|
||||||
id: string;
|
id: string;
|
||||||
client: RedisClientType<M, S>;
|
client: RedisClientType<M, F, S>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NodeAddress {
|
interface NodeAddress {
|
||||||
@@ -23,22 +27,30 @@ export type NodeAddressMap = {
|
|||||||
[address: string]: NodeAddress;
|
[address: string]: NodeAddress;
|
||||||
} | ((address: string) => NodeAddress | undefined);
|
} | ((address: string) => NodeAddress | undefined);
|
||||||
|
|
||||||
interface SlotNodes<M extends RedisModules, S extends RedisScripts> {
|
interface SlotNodes<
|
||||||
master: ClusterNode<M, S>;
|
M extends RedisModules,
|
||||||
replicas: Array<ClusterNode<M, S>>;
|
F extends RedisFunctions,
|
||||||
clientIterator: IterableIterator<RedisClientType<M, S>> | undefined;
|
S extends RedisScripts
|
||||||
|
> {
|
||||||
|
master: ClusterNode<M, F, S>;
|
||||||
|
replicas: Array<ClusterNode<M, F, S>>;
|
||||||
|
clientIterator: IterableIterator<RedisClientType<M, F, S>> | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OnError = (err: unknown) => void;
|
type OnError = (err: unknown) => void;
|
||||||
|
|
||||||
export default class RedisClusterSlots<M extends RedisModules, S extends RedisScripts> {
|
export default class RedisClusterSlots<
|
||||||
readonly #options: RedisClusterOptions<M, S>;
|
M extends RedisModules,
|
||||||
readonly #Client: InstantiableRedisClient<M, S>;
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> {
|
||||||
|
readonly #options: RedisClusterOptions<M, F, S>;
|
||||||
|
readonly #Client: InstantiableRedisClient<M, F, S>;
|
||||||
readonly #onError: OnError;
|
readonly #onError: OnError;
|
||||||
readonly #nodeByAddress = new Map<string, ClusterNode<M, S>>();
|
readonly #nodeByAddress = new Map<string, ClusterNode<M, F, S>>();
|
||||||
readonly #slots: Array<SlotNodes<M, S>> = [];
|
readonly #slots: Array<SlotNodes<M, F, S>> = [];
|
||||||
|
|
||||||
constructor(options: RedisClusterOptions<M, S>, onError: OnError) {
|
constructor(options: RedisClusterOptions<M, F, S>, onError: OnError) {
|
||||||
this.#options = options;
|
this.#options = options;
|
||||||
this.#Client = RedisClient.extend(options);
|
this.#Client = RedisClient.extend(options);
|
||||||
this.#onError = onError;
|
this.#onError = onError;
|
||||||
@@ -72,7 +84,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
|
|
||||||
#runningRediscoverPromise?: Promise<void>;
|
#runningRediscoverPromise?: Promise<void>;
|
||||||
|
|
||||||
async rediscover(startWith: RedisClientType<M, S>): Promise<void> {
|
async rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
|
||||||
if (!this.#runningRediscoverPromise) {
|
if (!this.#runningRediscoverPromise) {
|
||||||
this.#runningRediscoverPromise = this.#rediscover(startWith)
|
this.#runningRediscoverPromise = this.#rediscover(startWith)
|
||||||
.finally(() => this.#runningRediscoverPromise = undefined);
|
.finally(() => this.#runningRediscoverPromise = undefined);
|
||||||
@@ -81,7 +93,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
return this.#runningRediscoverPromise;
|
return this.#runningRediscoverPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #rediscover(startWith: RedisClientType<M, S>): Promise<void> {
|
async #rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
|
||||||
if (await this.#discoverNodes(startWith.options)) return;
|
if (await this.#discoverNodes(startWith.options)) return;
|
||||||
|
|
||||||
for (const { client } of this.#nodeByAddress.values()) {
|
for (const { client } of this.#nodeByAddress.values()) {
|
||||||
@@ -137,7 +149,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#initiateClient(options?: RedisClusterClientOptions): RedisClientType<M, S> {
|
#initiateClient(options?: RedisClusterClientOptions): RedisClientType<M, F, S> {
|
||||||
return new this.#Client(this.#clientOptionsDefaults(options))
|
return new this.#Client(this.#clientOptionsDefaults(options))
|
||||||
.on('error', this.#onError);
|
.on('error', this.#onError);
|
||||||
}
|
}
|
||||||
@@ -152,7 +164,12 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#initiateClientForNode(nodeData: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set<string>, promises: Array<Promise<void>>): ClusterNode<M, S> {
|
#initiateClientForNode(
|
||||||
|
nodeData: RedisClusterMasterNode | RedisClusterReplicaNode,
|
||||||
|
readonly: boolean,
|
||||||
|
clientsInUse: Set<string>,
|
||||||
|
promises: Array<Promise<void>>
|
||||||
|
): ClusterNode<M, F, S> {
|
||||||
const address = `${nodeData.host}:${nodeData.port}`;
|
const address = `${nodeData.host}:${nodeData.port}`;
|
||||||
clientsInUse.add(address);
|
clientsInUse.add(address);
|
||||||
|
|
||||||
@@ -175,11 +192,11 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSlotMaster(slot: number): ClusterNode<M, S> {
|
getSlotMaster(slot: number): ClusterNode<M, F, S> {
|
||||||
return this.#slots[slot].master;
|
return this.#slots[slot].master;
|
||||||
}
|
}
|
||||||
|
|
||||||
*#slotClientIterator(slotNumber: number): IterableIterator<RedisClientType<M, S>> {
|
*#slotClientIterator(slotNumber: number): IterableIterator<RedisClientType<M, F, S>> {
|
||||||
const slot = this.#slots[slotNumber];
|
const slot = this.#slots[slotNumber];
|
||||||
yield slot.master.client;
|
yield slot.master.client;
|
||||||
|
|
||||||
@@ -188,7 +205,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#getSlotClient(slotNumber: number): RedisClientType<M, S> {
|
#getSlotClient(slotNumber: number): RedisClientType<M, F, S> {
|
||||||
const slot = this.#slots[slotNumber];
|
const slot = this.#slots[slotNumber];
|
||||||
if (!slot.clientIterator) {
|
if (!slot.clientIterator) {
|
||||||
slot.clientIterator = this.#slotClientIterator(slotNumber);
|
slot.clientIterator = this.#slotClientIterator(slotNumber);
|
||||||
@@ -203,9 +220,9 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
#randomClientIterator?: IterableIterator<ClusterNode<M, S>>;
|
#randomClientIterator?: IterableIterator<ClusterNode<M, F, S>>;
|
||||||
|
|
||||||
#getRandomClient(): RedisClientType<M, S> {
|
#getRandomClient(): RedisClientType<M, F, S> {
|
||||||
if (!this.#nodeByAddress.size) {
|
if (!this.#nodeByAddress.size) {
|
||||||
throw new Error('Cluster is not connected');
|
throw new Error('Cluster is not connected');
|
||||||
}
|
}
|
||||||
@@ -223,7 +240,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
return value.client;
|
return value.client;
|
||||||
}
|
}
|
||||||
|
|
||||||
getClient(firstKey?: RedisCommandArgument, isReadonly?: boolean): RedisClientType<M, S> {
|
getClient(firstKey?: RedisCommandArgument, isReadonly?: boolean): RedisClientType<M, F, S> {
|
||||||
if (!firstKey) {
|
if (!firstKey) {
|
||||||
return this.#getRandomClient();
|
return this.#getRandomClient();
|
||||||
}
|
}
|
||||||
@@ -236,7 +253,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
return this.#getSlotClient(slot);
|
return this.#getSlotClient(slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMasters(): Array<ClusterNode<M, S>> {
|
getMasters(): Array<ClusterNode<M, F, S>> {
|
||||||
const masters = [];
|
const masters = [];
|
||||||
for (const node of this.#nodeByAddress.values()) {
|
for (const node of this.#nodeByAddress.values()) {
|
||||||
if (node.client.options?.readonly) continue;
|
if (node.client.options?.readonly) continue;
|
||||||
@@ -247,7 +264,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
return masters;
|
return masters;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeByAddress(address: string): ClusterNode<M, S> | undefined {
|
getNodeByAddress(address: string): ClusterNode<M, F, S> | undefined {
|
||||||
const mappedAddress = this.#getNodeAddress(address);
|
const mappedAddress = this.#getNodeAddress(address);
|
||||||
return this.#nodeByAddress.get(
|
return this.#nodeByAddress.get(
|
||||||
mappedAddress ? `${mappedAddress.host}:${mappedAddress.port}` : address
|
mappedAddress ? `${mappedAddress.host}:${mappedAddress.port}` : address
|
||||||
@@ -262,7 +279,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
|||||||
return this.#destroy(client => client.disconnect());
|
return this.#destroy(client => client.disconnect());
|
||||||
}
|
}
|
||||||
|
|
||||||
async #destroy(fn: (client: RedisClientType<M, S>) => Promise<unknown>): Promise<void> {
|
async #destroy(fn: (client: RedisClientType<M, F, S>) => Promise<unknown>): Promise<void> {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const { client } of this.#nodeByAddress.values()) {
|
for (const { client } of this.#nodeByAddress.values()) {
|
||||||
promises.push(fn(client));
|
promises.push(fn(client));
|
||||||
|
@@ -18,12 +18,16 @@ import * as DECR from '../commands/DECR';
|
|||||||
import * as DECRBY from '../commands/DECRBY';
|
import * as DECRBY from '../commands/DECRBY';
|
||||||
import * as DEL from '../commands/DEL';
|
import * as DEL from '../commands/DEL';
|
||||||
import * as DUMP from '../commands/DUMP';
|
import * as DUMP from '../commands/DUMP';
|
||||||
|
import * as EVAL_RO from '../commands/EVAL_RO';
|
||||||
import * as EVAL from '../commands/EVAL';
|
import * as EVAL from '../commands/EVAL';
|
||||||
|
import * as EVALSHA_RO from '../commands/EVALSHA_RO';
|
||||||
import * as EVALSHA from '../commands/EVALSHA';
|
import * as EVALSHA from '../commands/EVALSHA';
|
||||||
import * as EXISTS from '../commands/EXISTS';
|
import * as EXISTS from '../commands/EXISTS';
|
||||||
import * as EXPIRE from '../commands/EXPIRE';
|
import * as EXPIRE from '../commands/EXPIRE';
|
||||||
import * as EXPIREAT from '../commands/EXPIREAT';
|
import * as EXPIREAT from '../commands/EXPIREAT';
|
||||||
import * as EXPIRETIME from '../commands/EXPIRETIME';
|
import * as EXPIRETIME from '../commands/EXPIRETIME';
|
||||||
|
import * as FCALL_RO from '../commands/FCALL_RO';
|
||||||
|
import * as FCALL from '../commands/FCALL';
|
||||||
import * as GEOADD from '../commands/GEOADD';
|
import * as GEOADD from '../commands/GEOADD';
|
||||||
import * as GEODIST from '../commands/GEODIST';
|
import * as GEODIST from '../commands/GEODIST';
|
||||||
import * as GEOHASH from '../commands/GEOHASH';
|
import * as GEOHASH from '../commands/GEOHASH';
|
||||||
@@ -230,10 +234,14 @@ export default {
|
|||||||
del: DEL,
|
del: DEL,
|
||||||
DUMP,
|
DUMP,
|
||||||
dump: DUMP,
|
dump: DUMP,
|
||||||
|
EVAL_RO,
|
||||||
|
evalRo: EVAL_RO,
|
||||||
EVAL,
|
EVAL,
|
||||||
eval: EVAL,
|
eval: EVAL,
|
||||||
EVALSHA,
|
EVALSHA,
|
||||||
evalSha: EVALSHA,
|
evalSha: EVALSHA,
|
||||||
|
EVALSHA_RO,
|
||||||
|
evalShaRo: EVALSHA_RO,
|
||||||
EXISTS,
|
EXISTS,
|
||||||
exists: EXISTS,
|
exists: EXISTS,
|
||||||
EXPIRE,
|
EXPIRE,
|
||||||
@@ -242,6 +250,10 @@ export default {
|
|||||||
expireAt: EXPIREAT,
|
expireAt: EXPIREAT,
|
||||||
EXPIRETIME,
|
EXPIRETIME,
|
||||||
expireTime: EXPIRETIME,
|
expireTime: EXPIRETIME,
|
||||||
|
FCALL_RO,
|
||||||
|
fCallRo: FCALL_RO,
|
||||||
|
FCALL,
|
||||||
|
fCall: FCALL,
|
||||||
GEOADD,
|
GEOADD,
|
||||||
geoAdd: GEOADD,
|
geoAdd: GEOADD,
|
||||||
GEODIST,
|
GEODIST,
|
||||||
|
@@ -1,18 +1,22 @@
|
|||||||
import COMMANDS from './commands';
|
import COMMANDS from './commands';
|
||||||
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, RedisFunction } from '../commands';
|
||||||
import { ClientCommandOptions, RedisClientCommandSignature, RedisClientOptions, RedisClientType, WithModules, WithScripts } from '../client';
|
import { ClientCommandOptions, RedisClientOptions, RedisClientType, WithFunctions, WithModules, WithScripts } from '../client';
|
||||||
import RedisClusterSlots, { ClusterNode, NodeAddressMap } from './cluster-slots';
|
import RedisClusterSlots, { ClusterNode, NodeAddressMap } from './cluster-slots';
|
||||||
import { extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, extendWithCommands } from '../commander';
|
import { attachExtensions, transformCommandReply, attachCommands, transformCommandArguments } from '../commander';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
|
import RedisClusterMultiCommand, { InstantiableRedisClusterMultiCommandType, RedisClusterMultiCommandType } from './multi-command';
|
||||||
import { RedisMultiQueuedCommand } from '../multi-command';
|
import { RedisMultiQueuedCommand } from '../multi-command';
|
||||||
|
|
||||||
export type RedisClusterClientOptions = Omit<RedisClientOptions, 'modules' | 'scripts'>;
|
export type RedisClusterClientOptions = Omit<
|
||||||
|
RedisClientOptions,
|
||||||
|
'modules' | 'functions' | 'scripts' | 'database'
|
||||||
|
>;
|
||||||
|
|
||||||
export interface RedisClusterOptions<
|
export interface RedisClusterOptions<
|
||||||
M extends RedisModules = Record<string, never>,
|
M extends RedisModules = Record<string, never>,
|
||||||
|
F extends RedisFunctions = Record<string, never>,
|
||||||
S extends RedisScripts = Record<string, never>
|
S extends RedisScripts = Record<string, never>
|
||||||
> extends RedisPlugins<M, S> {
|
> extends RedisExtensions<M, F, S> {
|
||||||
rootNodes: Array<RedisClusterClientOptions>;
|
rootNodes: Array<RedisClusterClientOptions>;
|
||||||
defaults?: Partial<RedisClusterClientOptions>;
|
defaults?: Partial<RedisClusterClientOptions>;
|
||||||
useReplicas?: boolean;
|
useReplicas?: boolean;
|
||||||
@@ -21,16 +25,25 @@ export interface RedisClusterOptions<
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WithCommands = {
|
type WithCommands = {
|
||||||
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
|
[P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RedisClusterType<
|
export type RedisClusterType<
|
||||||
M extends RedisModules = Record<string, never>,
|
M extends RedisModules = Record<string, never>,
|
||||||
|
F extends RedisFunctions = Record<string, never>,
|
||||||
S extends RedisScripts = Record<string, never>
|
S extends RedisScripts = Record<string, never>
|
||||||
> = RedisCluster<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
|
> = RedisCluster<M, F, S> & WithCommands & WithModules<M> & WithFunctions<F> & WithScripts<S>;
|
||||||
|
|
||||||
export default class RedisCluster<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
|
export default class RedisCluster<
|
||||||
static extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: RedisCommandArguments): RedisCommandArgument | undefined {
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> extends EventEmitter {
|
||||||
|
static extractFirstKey(
|
||||||
|
command: RedisCommand,
|
||||||
|
originalArgs: Array<unknown>,
|
||||||
|
redisArgs: RedisCommandArguments
|
||||||
|
): RedisCommandArgument | undefined {
|
||||||
if (command.FIRST_KEY_INDEX === undefined) {
|
if (command.FIRST_KEY_INDEX === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
} else if (typeof command.FIRST_KEY_INDEX === 'number') {
|
} else if (typeof command.FIRST_KEY_INDEX === 'number') {
|
||||||
@@ -40,21 +53,27 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
|
|||||||
return command.FIRST_KEY_INDEX(...originalArgs);
|
return command.FIRST_KEY_INDEX(...originalArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
static create<M extends RedisModules, S extends RedisScripts>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
|
static create<
|
||||||
return new (<any>extendWithModulesAndScripts({
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(options?: RedisClusterOptions<M, F, S>): RedisClusterType<M, F, S> {
|
||||||
|
return new (attachExtensions({
|
||||||
BaseClass: RedisCluster,
|
BaseClass: RedisCluster,
|
||||||
|
modulesExecutor: RedisCluster.prototype.commandsExecutor,
|
||||||
modules: options?.modules,
|
modules: options?.modules,
|
||||||
modulesCommandsExecutor: RedisCluster.prototype.commandsExecutor,
|
functionsExecutor: RedisCluster.prototype.functionsExecutor,
|
||||||
scripts: options?.scripts,
|
functions: options?.functions,
|
||||||
scriptsExecutor: RedisCluster.prototype.scriptsExecutor
|
scriptsExecutor: RedisCluster.prototype.scriptsExecutor,
|
||||||
|
scripts: options?.scripts
|
||||||
}))(options);
|
}))(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly #options: RedisClusterOptions<M, S>;
|
readonly #options: RedisClusterOptions<M, F, S>;
|
||||||
readonly #slots: RedisClusterSlots<M, S>;
|
readonly #slots: RedisClusterSlots<M, F, S>;
|
||||||
readonly #Multi: new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<M, S>;
|
readonly #Multi: InstantiableRedisClusterMultiCommandType<M, F, S>;
|
||||||
|
|
||||||
constructor(options: RedisClusterOptions<M, S>) {
|
constructor(options: RedisClusterOptions<M, F, S>) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.#options = options;
|
this.#options = options;
|
||||||
@@ -62,7 +81,7 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
|
|||||||
this.#Multi = RedisClusterMultiCommand.extend(options);
|
this.#Multi = RedisClusterMultiCommand.extend(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
duplicate(overrides?: Partial<RedisClusterOptions<M, S>>): RedisClusterType<M, S> {
|
duplicate(overrides?: Partial<RedisClusterOptions<M, F, S>>): RedisClusterType<M, F, S> {
|
||||||
return new (Object.getPrototypeOf(this).constructor)({
|
return new (Object.getPrototypeOf(this).constructor)({
|
||||||
...this.#options,
|
...this.#options,
|
||||||
...overrides
|
...overrides
|
||||||
@@ -73,9 +92,11 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
|
|||||||
return this.#slots.connect();
|
return this.#slots.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
async commandsExecutor(command: RedisCommand, args: Array<unknown>): Promise<RedisCommandReply<typeof command>> {
|
async commandsExecutor<C extends RedisCommand>(
|
||||||
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
|
command: C,
|
||||||
|
args: Array<unknown>
|
||||||
|
): Promise<RedisCommandReply<C>> {
|
||||||
|
const { args: redisArgs, options } = transformCommandArguments(command, args);
|
||||||
return transformCommandReply(
|
return transformCommandReply(
|
||||||
command,
|
command,
|
||||||
await this.sendCommand(
|
await this.sendCommand(
|
||||||
@@ -101,9 +122,38 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> {
|
async functionsExecutor<F extends RedisFunction>(
|
||||||
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
|
fn: F,
|
||||||
|
args: Array<unknown>
|
||||||
|
): Promise<RedisCommandReply<F>> {
|
||||||
|
const { args: redisArgs, options } = transformCommandArguments(fn, args);
|
||||||
|
return transformCommandReply(
|
||||||
|
fn,
|
||||||
|
await this.executeFunction(
|
||||||
|
fn,
|
||||||
|
args,
|
||||||
|
redisArgs,
|
||||||
|
options
|
||||||
|
),
|
||||||
|
redisArgs.preserve
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeFunction(
|
||||||
|
fn: RedisFunction,
|
||||||
|
originalArgs: Array<unknown>,
|
||||||
|
redisArgs: RedisCommandArguments,
|
||||||
|
options?: ClientCommandOptions
|
||||||
|
): Promise<RedisCommandRawReply> {
|
||||||
|
return this.#execute(
|
||||||
|
RedisCluster.extractFirstKey(fn, originalArgs, redisArgs),
|
||||||
|
fn.IS_READ_ONLY,
|
||||||
|
client => client.executeFunction(fn, redisArgs, options)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async scriptsExecutor<S extends RedisScript>(script: S, args: Array<unknown>): Promise<RedisCommandReply<S>> {
|
||||||
|
const { args: redisArgs, options } = transformCommandArguments(script, args);
|
||||||
return transformCommandReply(
|
return transformCommandReply(
|
||||||
script,
|
script,
|
||||||
await this.executeScript(
|
await this.executeScript(
|
||||||
@@ -121,7 +171,7 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
|
|||||||
originalArgs: Array<unknown>,
|
originalArgs: Array<unknown>,
|
||||||
redisArgs: RedisCommandArguments,
|
redisArgs: RedisCommandArguments,
|
||||||
options?: ClientCommandOptions
|
options?: ClientCommandOptions
|
||||||
): Promise<RedisCommandReply<typeof script>> {
|
): Promise<RedisCommandRawReply> {
|
||||||
return this.#execute(
|
return this.#execute(
|
||||||
RedisCluster.extractFirstKey(script, originalArgs, redisArgs),
|
RedisCluster.extractFirstKey(script, originalArgs, redisArgs),
|
||||||
script.IS_READ_ONLY,
|
script.IS_READ_ONLY,
|
||||||
@@ -132,7 +182,7 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
|
|||||||
async #execute<Reply>(
|
async #execute<Reply>(
|
||||||
firstKey: RedisCommandArgument | undefined,
|
firstKey: RedisCommandArgument | undefined,
|
||||||
isReadonly: boolean | undefined,
|
isReadonly: boolean | undefined,
|
||||||
executor: (client: RedisClientType<M, S>) => Promise<Reply>
|
executor: (client: RedisClientType<M, F, S>) => Promise<Reply>
|
||||||
): Promise<Reply> {
|
): Promise<Reply> {
|
||||||
const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16;
|
const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16;
|
||||||
let client = this.#slots.getClient(firstKey, isReadonly);
|
let client = this.#slots.getClient(firstKey, isReadonly);
|
||||||
@@ -171,7 +221,7 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
multi(routing?: RedisCommandArgument): RedisClusterMultiCommandType<M, S> {
|
multi(routing?: RedisCommandArgument): RedisClusterMultiCommandType<M, F, S> {
|
||||||
return new this.#Multi(
|
return new this.#Multi(
|
||||||
(commands: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => {
|
(commands: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => {
|
||||||
return this.#execute(
|
return this.#execute(
|
||||||
@@ -184,11 +234,11 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMasters(): Array<ClusterNode<M, S>> {
|
getMasters(): Array<ClusterNode<M, F, S>> {
|
||||||
return this.#slots.getMasters();
|
return this.#slots.getMasters();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSlotMaster(slot: number): ClusterNode<M, S> {
|
getSlotMaster(slot: number): ClusterNode<M, F, S> {
|
||||||
return this.#slots.getSlotMaster(slot);
|
return this.#slots.getSlotMaster(slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +251,7 @@ export default class RedisCluster<M extends RedisModules, S extends RedisScripts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extendWithCommands({
|
attachCommands({
|
||||||
BaseClass: RedisCluster,
|
BaseClass: RedisCluster,
|
||||||
commands: COMMANDS,
|
commands: COMMANDS,
|
||||||
executor: RedisCluster.prototype.commandsExecutor
|
executor: RedisCluster.prototype.commandsExecutor
|
||||||
|
@@ -1,29 +1,63 @@
|
|||||||
import COMMANDS from './commands';
|
import COMMANDS from './commands';
|
||||||
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction } from '../commands';
|
||||||
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
||||||
import { extendWithCommands, extendWithModulesAndScripts } from '../commander';
|
import { attachCommands, attachExtensions } from '../commander';
|
||||||
import RedisCluster from '.';
|
import RedisCluster from '.';
|
||||||
import { ExcludeMappedString } from '../client';
|
|
||||||
|
|
||||||
type RedisClusterMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
|
type RedisClusterMultiCommandSignature<
|
||||||
(...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<M, S>;
|
C extends RedisCommand,
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = (...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<M, F, S>;
|
||||||
|
|
||||||
type WithCommands<M extends RedisModules, S extends RedisScripts> = {
|
type WithCommands<
|
||||||
[P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, S>
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = {
|
||||||
|
[P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, F, S>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type WithModules<M extends RedisModules, S extends RedisScripts> = {
|
type WithModules<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = {
|
||||||
[P in keyof M as ExcludeMappedString<P>]: {
|
[P in keyof M as ExcludeMappedString<P>]: {
|
||||||
[C in keyof M[P] as ExcludeMappedString<C>]: RedisClusterMultiCommandSignature<M[P][C], M, S>;
|
[C in keyof M[P] as ExcludeMappedString<C>]: RedisClusterMultiCommandSignature<M[P][C], M, F, S>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
|
type WithFunctions<
|
||||||
[P in keyof S as ExcludeMappedString<P>]: RedisClusterMultiCommandSignature<S[P], M, S>
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = {
|
||||||
|
[P in keyof F as ExcludeMappedString<P>]: {
|
||||||
|
[FF in keyof F[P] as ExcludeMappedString<FF>]: RedisClusterMultiCommandSignature<F[P][FF], M, F, S>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RedisClusterMultiCommandType<M extends RedisModules, S extends RedisScripts> =
|
type WithScripts<
|
||||||
RedisClusterMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = {
|
||||||
|
[P in keyof S as ExcludeMappedString<P>]: RedisClusterMultiCommandSignature<S[P], M, F, S>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RedisClusterMultiCommandType<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = RedisClusterMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
|
||||||
|
|
||||||
|
export type InstantiableRedisClusterMultiCommandType<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<M, F, S>;
|
||||||
|
|
||||||
export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
|
export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
|
||||||
|
|
||||||
@@ -32,15 +66,19 @@ export default class RedisClusterMultiCommand {
|
|||||||
readonly #executor: RedisClusterMultiExecutor;
|
readonly #executor: RedisClusterMultiExecutor;
|
||||||
#firstKey: RedisCommandArgument | undefined;
|
#firstKey: RedisCommandArgument | undefined;
|
||||||
|
|
||||||
static extend<M extends RedisModules, S extends RedisScripts>(
|
static extend<
|
||||||
plugins?: RedisPlugins<M, S>
|
M extends RedisModules,
|
||||||
): new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisClusterMultiCommandType<M, S> {
|
F extends RedisFunctions,
|
||||||
return <any>extendWithModulesAndScripts({
|
S extends RedisScripts
|
||||||
|
>(extensions?: RedisExtensions<M, F, S>): InstantiableRedisClusterMultiCommandType<M, F, S> {
|
||||||
|
return attachExtensions({
|
||||||
BaseClass: RedisClusterMultiCommand,
|
BaseClass: RedisClusterMultiCommand,
|
||||||
modules: plugins?.modules,
|
modulesExecutor: RedisClusterMultiCommand.prototype.commandsExecutor,
|
||||||
modulesCommandsExecutor: RedisClusterMultiCommand.prototype.commandsExecutor,
|
modules: extensions?.modules,
|
||||||
scripts: plugins?.scripts,
|
functionsExecutor: RedisClusterMultiCommand.prototype.functionsExecutor,
|
||||||
scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor
|
functions: extensions?.functions,
|
||||||
|
scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor,
|
||||||
|
scripts: extensions?.scripts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,15 +89,8 @@ export default class RedisClusterMultiCommand {
|
|||||||
|
|
||||||
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
|
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
|
||||||
const transformedArguments = command.transformArguments(...args);
|
const transformedArguments = command.transformArguments(...args);
|
||||||
if (!this.#firstKey) {
|
this.#firstKey ??= RedisCluster.extractFirstKey(command, args, transformedArguments);
|
||||||
this.#firstKey = RedisCluster.extractFirstKey(command, args, transformedArguments);
|
return this.addCommand(undefined, transformedArguments, command.transformReply);
|
||||||
}
|
|
||||||
|
|
||||||
return this.addCommand(
|
|
||||||
undefined,
|
|
||||||
transformedArguments,
|
|
||||||
command.transformReply
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addCommand(
|
addCommand(
|
||||||
@@ -67,21 +98,21 @@ export default class RedisClusterMultiCommand {
|
|||||||
args: RedisCommandArguments,
|
args: RedisCommandArguments,
|
||||||
transformReply?: RedisCommand['transformReply']
|
transformReply?: RedisCommand['transformReply']
|
||||||
): this {
|
): this {
|
||||||
if (!this.#firstKey) {
|
this.#firstKey ??= firstKey;
|
||||||
this.#firstKey = firstKey;
|
this.#multi.addCommand(args, transformReply);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#multi.addCommand(args, transformReply);
|
functionsExecutor(fn: RedisFunction, args: Array<unknown>): this {
|
||||||
|
const transformedArguments = this.#multi.addFunction(fn, args);
|
||||||
|
this.#firstKey ??= RedisCluster.extractFirstKey(fn, args, transformedArguments);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
|
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
|
||||||
const transformedArguments = this.#multi.addScript(script, args);
|
const transformedArguments = this.#multi.addScript(script, args);
|
||||||
if (!this.#firstKey) {
|
this.#firstKey ??= RedisCluster.extractFirstKey(script, args, transformedArguments);
|
||||||
this.#firstKey = RedisCluster.extractFirstKey(script, args, transformedArguments);
|
return this;
|
||||||
}
|
|
||||||
|
|
||||||
return this.addCommand(undefined, transformedArguments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
|
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
|
||||||
@@ -106,7 +137,7 @@ export default class RedisClusterMultiCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extendWithCommands({
|
attachCommands({
|
||||||
BaseClass: RedisClusterMultiCommand,
|
BaseClass: RedisClusterMultiCommand,
|
||||||
commands: COMMANDS,
|
commands: COMMANDS,
|
||||||
executor: RedisClusterMultiCommand.prototype.commandsExecutor
|
executor: RedisClusterMultiCommand.prototype.commandsExecutor
|
||||||
|
@@ -1,16 +1,23 @@
|
|||||||
|
|
||||||
import { CommandOptions, isCommandOptions } from './command-options';
|
import { CommandOptions, isCommandOptions } from './command-options';
|
||||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisCommands, RedisModules, RedisScript, RedisScripts } from './commands';
|
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts } from './commands';
|
||||||
|
|
||||||
type Instantiable<T = any> = new(...args: Array<any>) => T;
|
type Instantiable<T = any> = new (...args: Array<any>) => T;
|
||||||
|
|
||||||
interface ExtendWithCommandsConfig<T extends Instantiable> {
|
type CommandsExecutor<C extends RedisCommand = RedisCommand> =
|
||||||
BaseClass: T;
|
(command: C, args: Array<unknown>) => unknown;
|
||||||
commands: RedisCommands;
|
|
||||||
executor(command: RedisCommand, args: Array<unknown>): unknown;
|
interface AttachCommandsConfig<C extends RedisCommand> {
|
||||||
|
BaseClass: Instantiable;
|
||||||
|
commands: Record<string, C>;
|
||||||
|
executor: CommandsExecutor<C>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extendWithCommands<T extends Instantiable>({ BaseClass, commands, executor }: ExtendWithCommandsConfig<T>): void {
|
export function attachCommands<C extends RedisCommand>({
|
||||||
|
BaseClass,
|
||||||
|
commands,
|
||||||
|
executor
|
||||||
|
}: AttachCommandsConfig<C>): void {
|
||||||
for (const [name, command] of Object.entries(commands)) {
|
for (const [name, command] of Object.entries(commands)) {
|
||||||
BaseClass.prototype[name] = function (...args: Array<unknown>): unknown {
|
BaseClass.prototype[name] = function (...args: Array<unknown>): unknown {
|
||||||
return executor.call(this, command, args);
|
return executor.call(this, command, args);
|
||||||
@@ -18,56 +25,82 @@ export function extendWithCommands<T extends Instantiable>({ BaseClass, commands
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExtendWithModulesAndScriptsConfig<T extends Instantiable> {
|
interface AttachExtensionsConfig<T extends Instantiable = Instantiable> {
|
||||||
BaseClass: T;
|
BaseClass: T;
|
||||||
|
modulesExecutor: CommandsExecutor;
|
||||||
modules?: RedisModules;
|
modules?: RedisModules;
|
||||||
modulesCommandsExecutor(this: InstanceType<T>, command: RedisCommand, args: Array<unknown>): unknown;
|
functionsExecutor: CommandsExecutor<RedisFunction>;
|
||||||
|
functions?: RedisFunctions;
|
||||||
|
scriptsExecutor: CommandsExecutor<RedisScript>;
|
||||||
scripts?: RedisScripts;
|
scripts?: RedisScripts;
|
||||||
scriptsExecutor(this: InstanceType<T>, script: RedisScript, args: Array<unknown>): unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extendWithModulesAndScripts<T extends Instantiable>(config: ExtendWithModulesAndScriptsConfig<T>): T {
|
export function attachExtensions(config: AttachExtensionsConfig): any {
|
||||||
let Commander: T | undefined;
|
let Commander;
|
||||||
|
|
||||||
if (config.modules) {
|
if (config.modules) {
|
||||||
Commander = class extends config.BaseClass {
|
Commander = attachWithNamespaces({
|
||||||
constructor(...args: Array<any>) {
|
BaseClass: config.BaseClass,
|
||||||
super(...args);
|
namespaces: config.modules,
|
||||||
|
executor: config.modulesExecutor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const module of Object.keys(config.modules!)) {
|
if (config.functions) {
|
||||||
this[module] = new this[module](this);
|
Commander = attachWithNamespaces({
|
||||||
}
|
BaseClass: Commander ?? config.BaseClass,
|
||||||
}
|
namespaces: config.functions,
|
||||||
};
|
executor: config.functionsExecutor
|
||||||
|
});
|
||||||
for (const [moduleName, module] of Object.entries(config.modules)) {
|
|
||||||
Commander.prototype[moduleName] = class {
|
|
||||||
readonly self: T;
|
|
||||||
|
|
||||||
constructor(self: InstanceType<T>) {
|
|
||||||
this.self = self;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [commandName, command] of Object.entries(module)) {
|
|
||||||
Commander.prototype[moduleName].prototype[commandName] = function (...args: Array<unknown>): unknown {
|
|
||||||
return config.modulesCommandsExecutor.call(this.self, command, args);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.scripts) {
|
if (config.scripts) {
|
||||||
Commander ??= class extends config.BaseClass {};
|
Commander ??= class extends config.BaseClass {};
|
||||||
|
attachCommands({
|
||||||
|
BaseClass: Commander,
|
||||||
|
commands: config.scripts,
|
||||||
|
executor: config.scriptsExecutor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const [name, script] of Object.entries(config.scripts)) {
|
return Commander ?? config.BaseClass;
|
||||||
Commander.prototype[name] = function (...args: Array<unknown>): unknown {
|
}
|
||||||
return config.scriptsExecutor.call(this, script, args);
|
|
||||||
|
interface AttachWithNamespacesConfig<C extends RedisCommand> {
|
||||||
|
BaseClass: Instantiable;
|
||||||
|
namespaces: Record<string, Record<string, C>>;
|
||||||
|
executor: CommandsExecutor<C>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachWithNamespaces<C extends RedisCommand>({
|
||||||
|
BaseClass,
|
||||||
|
namespaces,
|
||||||
|
executor
|
||||||
|
}: AttachWithNamespacesConfig<C>): any {
|
||||||
|
const Commander = class extends BaseClass {
|
||||||
|
constructor(...args: Array<any>) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
for (const namespace of Object.keys(namespaces)) {
|
||||||
|
this[namespace] = Object.create(this[namespace], {
|
||||||
|
self: {
|
||||||
|
value: this
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [namespace, commands] of Object.entries(namespaces)) {
|
||||||
|
Commander.prototype[namespace] = {};
|
||||||
|
for (const [name, command] of Object.entries(commands)) {
|
||||||
|
Commander.prototype[namespace][name] = function (...args: Array<unknown>): unknown {
|
||||||
|
return executor.call(this.self, command, args);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (Commander ?? config.BaseClass) as any;
|
return Commander;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformCommandArguments<T>(
|
export function transformCommandArguments<T>(
|
||||||
@@ -93,14 +126,29 @@ export function transformLegacyCommandArguments(args: Array<any>): Array<any> {
|
|||||||
return args.flat().map(x => x?.toString?.());
|
return args.flat().map(x => x?.toString?.());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformCommandReply(
|
export function transformCommandReply<C extends RedisCommand>(
|
||||||
command: RedisCommand,
|
command: C,
|
||||||
rawReply: RedisCommandRawReply,
|
rawReply: unknown,
|
||||||
preserved: unknown
|
preserved: unknown
|
||||||
): RedisCommandReply<typeof command> {
|
): RedisCommandReply<C> {
|
||||||
if (!command.transformReply) {
|
if (!command.transformReply) {
|
||||||
return rawReply;
|
return rawReply as RedisCommandReply<C>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return command.transformReply(rawReply, preserved);
|
return command.transformReply(rawReply, preserved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fCallArguments(fn: RedisFunction, args: RedisCommandArguments): RedisCommandArguments {
|
||||||
|
const actualArgs: RedisCommandArguments = [
|
||||||
|
fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL',
|
||||||
|
fn.NAME
|
||||||
|
];
|
||||||
|
|
||||||
|
if (fn.NUMBER_OF_KEYS !== undefined) {
|
||||||
|
actualArgs.push(fn.NUMBER_OF_KEYS.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
actualArgs.push(...args);
|
||||||
|
|
||||||
|
return actualArgs;
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import { EvalOptions, pushEvalArguments } from './generic-transformers';
|
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
|
||||||
|
|
||||||
export function transformArguments(script: string, options?: EvalOptions): Array<string> {
|
export function transformArguments(script: string, options?: EvalOptions): Array<string> {
|
||||||
return pushEvalArguments(['EVAL', script], options);
|
return pushEvalArguments(['EVAL', script], options);
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import { EvalOptions, pushEvalArguments } from './generic-transformers';
|
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
|
||||||
|
|
||||||
export function transformArguments(sha1: string, options?: EvalOptions): Array<string> {
|
export function transformArguments(sha1: string, options?: EvalOptions): Array<string> {
|
||||||
return pushEvalArguments(['EVALSHA', sha1], options);
|
return pushEvalArguments(['EVALSHA', sha1], options);
|
||||||
|
17
packages/client/lib/commands/EVALSHA_RO.spec.ts
Normal file
17
packages/client/lib/commands/EVALSHA_RO.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils from '../test-utils';
|
||||||
|
import { transformArguments } from './EVALSHA_RO';
|
||||||
|
|
||||||
|
describe('EVALSHA_RO', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('sha1', {
|
||||||
|
keys: ['key'],
|
||||||
|
arguments: ['argument']
|
||||||
|
}),
|
||||||
|
['EVALSHA_RO', 'sha1', '1', 'key', 'argument']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
9
packages/client/lib/commands/EVALSHA_RO.ts
Normal file
9
packages/client/lib/commands/EVALSHA_RO.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
|
||||||
|
|
||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
|
export function transformArguments(sha1: string, options?: EvalOptions): Array<string> {
|
||||||
|
return pushEvalArguments(['EVALSHA_RO', sha1], options);
|
||||||
|
}
|
31
packages/client/lib/commands/EVAL_RO.spec.ts
Normal file
31
packages/client/lib/commands/EVAL_RO.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './EVAL_RO';
|
||||||
|
|
||||||
|
describe('EVAL_RO', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('return KEYS[1] + ARGV[1]', {
|
||||||
|
keys: ['key'],
|
||||||
|
arguments: ['argument']
|
||||||
|
}),
|
||||||
|
['EVAL_RO', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.evalRo', async client => {
|
||||||
|
assert.equal(
|
||||||
|
await client.evalRo('return 1'),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
|
||||||
|
testUtils.testWithCluster('cluster.evalRo', async cluster => {
|
||||||
|
assert.equal(
|
||||||
|
await cluster.evalRo('return 1'),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
});
|
9
packages/client/lib/commands/EVAL_RO.ts
Normal file
9
packages/client/lib/commands/EVAL_RO.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
|
||||||
|
|
||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
|
export function transformArguments(script: string, options?: EvalOptions): Array<string> {
|
||||||
|
return pushEvalArguments(['EVAL_RO', script], options);
|
||||||
|
}
|
29
packages/client/lib/commands/FCALL.spec.ts
Normal file
29
packages/client/lib/commands/FCALL.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
|
||||||
|
import { transformArguments } from './FCALL';
|
||||||
|
|
||||||
|
describe('FCALL', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('function', {
|
||||||
|
keys: ['key'],
|
||||||
|
arguments: ['argument']
|
||||||
|
}),
|
||||||
|
['FCALL', 'function', '1', 'key', 'argument']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.fCall', async client => {
|
||||||
|
await loadMathFunction(client);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.fCall(MATH_FUNCTION.library.square.NAME, {
|
||||||
|
arguments: ['2']
|
||||||
|
}),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
7
packages/client/lib/commands/FCALL.ts
Normal file
7
packages/client/lib/commands/FCALL.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
|
||||||
|
|
||||||
|
export function transformArguments(fn: string, options?: EvalOptions): Array<string> {
|
||||||
|
return pushEvalArguments(['FCALL', fn], options);
|
||||||
|
}
|
29
packages/client/lib/commands/FCALL_RO.spec.ts
Normal file
29
packages/client/lib/commands/FCALL_RO.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
|
||||||
|
import { transformArguments } from './FCALL_RO';
|
||||||
|
|
||||||
|
describe('FCALL_RO', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('function', {
|
||||||
|
keys: ['key'],
|
||||||
|
arguments: ['argument']
|
||||||
|
}),
|
||||||
|
['FCALL_RO', 'function', '1', 'key', 'argument']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.fCallRo', async client => {
|
||||||
|
await loadMathFunction(client);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.fCallRo(MATH_FUNCTION.library.square.NAME, {
|
||||||
|
arguments: ['2']
|
||||||
|
}),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
9
packages/client/lib/commands/FCALL_RO.ts
Normal file
9
packages/client/lib/commands/FCALL_RO.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = evalFirstKeyIndex;
|
||||||
|
|
||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
|
export function transformArguments(fn: string, options?: EvalOptions): Array<string> {
|
||||||
|
return pushEvalArguments(['FCALL_RO', fn], options);
|
||||||
|
}
|
24
packages/client/lib/commands/FUNCTION_DELETE.spec.ts
Normal file
24
packages/client/lib/commands/FUNCTION_DELETE.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './FUNCTION_DELETE';
|
||||||
|
|
||||||
|
describe('FUNCTION DELETE', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('library'),
|
||||||
|
['FUNCTION', 'DELETE', 'library']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.functionDelete', async client => {
|
||||||
|
await loadMathFunction(client);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.functionDelete(MATH_FUNCTION.name),
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
7
packages/client/lib/commands/FUNCTION_DELETE.ts
Normal file
7
packages/client/lib/commands/FUNCTION_DELETE.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { RedisCommandArguments } from '.';
|
||||||
|
|
||||||
|
export function transformArguments(library: string): RedisCommandArguments {
|
||||||
|
return ['FUNCTION', 'DELETE', library];
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): 'OK';
|
21
packages/client/lib/commands/FUNCTION_DUMP.spec.ts
Normal file
21
packages/client/lib/commands/FUNCTION_DUMP.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './FUNCTION_DUMP';
|
||||||
|
|
||||||
|
describe('FUNCTION DUMP', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(),
|
||||||
|
['FUNCTION', 'DUMP']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.functionDump', async client => {
|
||||||
|
assert.equal(
|
||||||
|
typeof await client.functionDump(),
|
||||||
|
'string'
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
7
packages/client/lib/commands/FUNCTION_DUMP.ts
Normal file
7
packages/client/lib/commands/FUNCTION_DUMP.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||||
|
|
||||||
|
export function transformArguments(): RedisCommandArguments {
|
||||||
|
return ['FUNCTION', 'DUMP'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): RedisCommandArgument;
|
30
packages/client/lib/commands/FUNCTION_FLUSH.spec.ts
Normal file
30
packages/client/lib/commands/FUNCTION_FLUSH.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './FUNCTION_FLUSH';
|
||||||
|
|
||||||
|
describe('FUNCTION FLUSH', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('simple', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(),
|
||||||
|
['FUNCTION', 'FLUSH']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with mode', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('SYNC'),
|
||||||
|
['FUNCTION', 'FLUSH', 'SYNC']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.functionFlush', async client => {
|
||||||
|
assert.equal(
|
||||||
|
await client.functionFlush(),
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
13
packages/client/lib/commands/FUNCTION_FLUSH.ts
Normal file
13
packages/client/lib/commands/FUNCTION_FLUSH.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { RedisCommandArguments } from '.';
|
||||||
|
|
||||||
|
export function transformArguments(mode?: 'ASYNC' | 'SYNC'): RedisCommandArguments {
|
||||||
|
const args = ['FUNCTION', 'FLUSH'];
|
||||||
|
|
||||||
|
if (mode) {
|
||||||
|
args.push(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): 'OK';
|
14
packages/client/lib/commands/FUNCTION_KILL.spec.ts
Normal file
14
packages/client/lib/commands/FUNCTION_KILL.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils from '../test-utils';
|
||||||
|
import { transformArguments } from './FUNCTION_KILL';
|
||||||
|
|
||||||
|
describe('FUNCTION KILL', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(),
|
||||||
|
['FUNCTION', 'KILL']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
7
packages/client/lib/commands/FUNCTION_KILL.ts
Normal file
7
packages/client/lib/commands/FUNCTION_KILL.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { RedisCommandArguments } from '.';
|
||||||
|
|
||||||
|
export function transformArguments(): RedisCommandArguments {
|
||||||
|
return ['FUNCTION', 'KILL'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): 'OK';
|
41
packages/client/lib/commands/FUNCTION_LIST.spec.ts
Normal file
41
packages/client/lib/commands/FUNCTION_LIST.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
|
||||||
|
import { transformArguments } from './FUNCTION_LIST';
|
||||||
|
|
||||||
|
describe('FUNCTION LIST', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('simple', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(),
|
||||||
|
['FUNCTION', 'LIST']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with pattern', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('patter*'),
|
||||||
|
['FUNCTION', 'LIST', 'patter*']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.functionList', async client => {
|
||||||
|
await loadMathFunction(client);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.functionList(),
|
||||||
|
[{
|
||||||
|
libraryName: MATH_FUNCTION.name,
|
||||||
|
engine: MATH_FUNCTION.engine,
|
||||||
|
functions: [{
|
||||||
|
name: MATH_FUNCTION.library.square.NAME,
|
||||||
|
description: null,
|
||||||
|
flags: ['no-writes']
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
16
packages/client/lib/commands/FUNCTION_LIST.ts
Normal file
16
packages/client/lib/commands/FUNCTION_LIST.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { RedisCommandArguments } from '.';
|
||||||
|
import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers';
|
||||||
|
|
||||||
|
export function transformArguments(pattern?: string): RedisCommandArguments {
|
||||||
|
const args = ['FUNCTION', 'LIST'];
|
||||||
|
|
||||||
|
if (pattern) {
|
||||||
|
args.push(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformReply(reply: Array<FunctionListRawItemReply>): Array<FunctionListItemReply> {
|
||||||
|
return reply.map(transformFunctionListItemReply);
|
||||||
|
}
|
42
packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts
Normal file
42
packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec';
|
||||||
|
import { transformArguments } from './FUNCTION_LIST_WITHCODE';
|
||||||
|
|
||||||
|
describe('FUNCTION LIST WITHCODE', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('simple', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(),
|
||||||
|
['FUNCTION', 'LIST', 'WITHCODE']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with pattern', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('patter*'),
|
||||||
|
['FUNCTION', 'LIST', 'patter*', 'WITHCODE']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.functionListWithCode', async client => {
|
||||||
|
await loadMathFunction(client);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.functionListWithCode(),
|
||||||
|
[{
|
||||||
|
libraryName: MATH_FUNCTION.name,
|
||||||
|
engine: MATH_FUNCTION.engine,
|
||||||
|
functions: [{
|
||||||
|
name: MATH_FUNCTION.library.square.NAME,
|
||||||
|
description: null,
|
||||||
|
flags: ['no-writes']
|
||||||
|
}],
|
||||||
|
libraryCode: MATH_FUNCTION.code
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
26
packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts
Normal file
26
packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { RedisCommandArguments } from '.';
|
||||||
|
import { transformArguments as transformFunctionListArguments } from './FUNCTION_LIST';
|
||||||
|
import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers';
|
||||||
|
|
||||||
|
export function transformArguments(pattern?: string): RedisCommandArguments {
|
||||||
|
const args = transformFunctionListArguments(pattern);
|
||||||
|
args.push('WITHCODE');
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FunctionListWithCodeRawItemReply = [
|
||||||
|
...FunctionListRawItemReply,
|
||||||
|
'library_code',
|
||||||
|
string
|
||||||
|
];
|
||||||
|
|
||||||
|
interface FunctionListWithCodeItemReply extends FunctionListItemReply {
|
||||||
|
libraryCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformReply(reply: Array<FunctionListWithCodeRawItemReply>): Array<FunctionListWithCodeItemReply> {
|
||||||
|
return reply.map(library => ({
|
||||||
|
...transformFunctionListItemReply(library as unknown as FunctionListRawItemReply),
|
||||||
|
libraryCode: library[7]
|
||||||
|
}));
|
||||||
|
}
|
36
packages/client/lib/commands/FUNCTION_LOAD.spec.ts
Normal file
36
packages/client/lib/commands/FUNCTION_LOAD.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { MATH_FUNCTION } from '../client/index.spec';
|
||||||
|
import { transformArguments } from './FUNCTION_LOAD';
|
||||||
|
|
||||||
|
describe('FUNCTION LOAD', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('simple', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments( 'code'),
|
||||||
|
['FUNCTION', 'LOAD', 'code']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with REPLACE', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('code', {
|
||||||
|
REPLACE: true
|
||||||
|
}),
|
||||||
|
['FUNCTION', 'LOAD', 'REPLACE', 'code']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.functionLoad', async client => {
|
||||||
|
assert.equal(
|
||||||
|
await client.functionLoad(
|
||||||
|
MATH_FUNCTION.code,
|
||||||
|
{ REPLACE: true }
|
||||||
|
),
|
||||||
|
MATH_FUNCTION.name
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
22
packages/client/lib/commands/FUNCTION_LOAD.ts
Normal file
22
packages/client/lib/commands/FUNCTION_LOAD.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { RedisCommandArguments } from '.';
|
||||||
|
|
||||||
|
interface FunctionLoadOptions {
|
||||||
|
REPLACE?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformArguments(
|
||||||
|
code: string,
|
||||||
|
options?: FunctionLoadOptions
|
||||||
|
): RedisCommandArguments {
|
||||||
|
const args = ['FUNCTION', 'LOAD'];
|
||||||
|
|
||||||
|
if (options?.REPLACE) {
|
||||||
|
args.push('REPLACE');
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(code);
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): string;
|
37
packages/client/lib/commands/FUNCTION_RESTORE.spec.ts
Normal file
37
packages/client/lib/commands/FUNCTION_RESTORE.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './FUNCTION_RESTORE';
|
||||||
|
|
||||||
|
describe('FUNCTION RESTORE', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('simple', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('dump'),
|
||||||
|
['FUNCTION', 'RESTORE', 'dump']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with mode', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('dump', 'APPEND'),
|
||||||
|
['FUNCTION', 'RESTORE', 'dump', 'APPEND']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.functionRestore', async client => {
|
||||||
|
assert.equal(
|
||||||
|
await client.functionRestore(
|
||||||
|
await client.functionDump(
|
||||||
|
client.commandOptions({
|
||||||
|
returnBuffers: true
|
||||||
|
})
|
||||||
|
),
|
||||||
|
'FLUSH'
|
||||||
|
),
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
16
packages/client/lib/commands/FUNCTION_RESTORE.ts
Normal file
16
packages/client/lib/commands/FUNCTION_RESTORE.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||||
|
|
||||||
|
export function transformArguments(
|
||||||
|
dump: RedisCommandArgument,
|
||||||
|
mode?: 'FLUSH' | 'APPEND' | 'REPLACE'
|
||||||
|
): RedisCommandArguments {
|
||||||
|
const args = ['FUNCTION', 'RESTORE', dump];
|
||||||
|
|
||||||
|
if (mode) {
|
||||||
|
args.push(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): 'OK';
|
25
packages/client/lib/commands/FUNCTION_STATS.spec.ts
Normal file
25
packages/client/lib/commands/FUNCTION_STATS.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './FUNCTION_STATS';
|
||||||
|
|
||||||
|
describe('FUNCTION STATS', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 0]);
|
||||||
|
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(),
|
||||||
|
['FUNCTION', 'STATS']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.functionStats', async client => {
|
||||||
|
const stats = await client.functionStats();
|
||||||
|
assert.equal(stats.runningScript, null);
|
||||||
|
assert.equal(typeof stats.engines, 'object');
|
||||||
|
for (const [engine, { librariesCount, functionsCount }] of Object.entries(stats.engines)) {
|
||||||
|
assert.equal(typeof engine, 'string');
|
||||||
|
assert.equal(typeof librariesCount, 'number');
|
||||||
|
assert.equal(typeof functionsCount, 'number');
|
||||||
|
}
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
56
packages/client/lib/commands/FUNCTION_STATS.ts
Normal file
56
packages/client/lib/commands/FUNCTION_STATS.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { RedisCommandArguments } from '.';
|
||||||
|
|
||||||
|
export function transformArguments(): RedisCommandArguments {
|
||||||
|
return ['FUNCTION', 'STATS'];
|
||||||
|
}
|
||||||
|
|
||||||
|
type FunctionStatsRawReply = [
|
||||||
|
'running_script',
|
||||||
|
null | [
|
||||||
|
'name',
|
||||||
|
string,
|
||||||
|
'command',
|
||||||
|
string,
|
||||||
|
'duration_ms',
|
||||||
|
number
|
||||||
|
],
|
||||||
|
'engines',
|
||||||
|
Array<any> // "flat tuples" (there is no way to type that)
|
||||||
|
// ...[string, [
|
||||||
|
// 'libraries_count',
|
||||||
|
// number,
|
||||||
|
// 'functions_count',
|
||||||
|
// number
|
||||||
|
// ]]
|
||||||
|
];
|
||||||
|
|
||||||
|
interface FunctionStatsReply {
|
||||||
|
runningScript: null | {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
durationMs: number;
|
||||||
|
};
|
||||||
|
engines: Record<string, {
|
||||||
|
librariesCount: number;
|
||||||
|
functionsCount: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformReply(reply: FunctionStatsRawReply): FunctionStatsReply {
|
||||||
|
const engines = Object.create(null);
|
||||||
|
for (let i = 0; i < reply[3].length; i++) {
|
||||||
|
engines[reply[3][i]] = {
|
||||||
|
librariesCount: reply[3][++i][1],
|
||||||
|
functionsCount: reply[3][i][3]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runningScript: reply[1] === null ? null : {
|
||||||
|
name: reply[1][1],
|
||||||
|
command: reply[1][3],
|
||||||
|
durationMs: reply[1][5]
|
||||||
|
},
|
||||||
|
engines
|
||||||
|
};
|
||||||
|
}
|
@@ -348,6 +348,10 @@ export interface EvalOptions {
|
|||||||
arguments?: Array<string>;
|
arguments?: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function evalFirstKeyIndex(options?: EvalOptions): string | undefined {
|
||||||
|
return options?.keys?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
export function pushEvalArguments(args: Array<string>, options?: EvalOptions): Array<string> {
|
export function pushEvalArguments(args: Array<string>, options?: EvalOptions): Array<string> {
|
||||||
if (options?.keys) {
|
if (options?.keys) {
|
||||||
args.push(
|
args.push(
|
||||||
@@ -491,6 +495,51 @@ export function transformCommandReply(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum RedisFunctionFlags {
|
||||||
|
NO_WRITES = 'no-writes',
|
||||||
|
ALLOW_OOM = 'allow-oom',
|
||||||
|
ALLOW_STALE = 'allow-stale',
|
||||||
|
NO_CLUSTER = 'no-cluster'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FunctionListRawItemReply = [
|
||||||
|
'library_name',
|
||||||
|
string,
|
||||||
|
'engine',
|
||||||
|
string,
|
||||||
|
'functions',
|
||||||
|
Array<[
|
||||||
|
'name',
|
||||||
|
string,
|
||||||
|
'description',
|
||||||
|
string | null,
|
||||||
|
'flags',
|
||||||
|
Array<RedisFunctionFlags>
|
||||||
|
]>
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface FunctionListItemReply {
|
||||||
|
libraryName: string;
|
||||||
|
engine: string;
|
||||||
|
functions: Array<{
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
flags: Array<RedisFunctionFlags>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformFunctionListItemReply(reply: FunctionListRawItemReply): FunctionListItemReply {
|
||||||
|
return {
|
||||||
|
libraryName: reply[1],
|
||||||
|
engine: reply[3],
|
||||||
|
functions: reply[5].map(fn => ({
|
||||||
|
name: fn[1],
|
||||||
|
description: fn[3],
|
||||||
|
flags: fn[5]
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface SortOptions {
|
export interface SortOptions {
|
||||||
BY?: string;
|
BY?: string;
|
||||||
LIMIT?: {
|
LIMIT?: {
|
||||||
|
@@ -1,22 +1,51 @@
|
|||||||
|
import { ClientCommandOptions } from '../client';
|
||||||
|
import { CommandOptions } from '../command-options';
|
||||||
import { RedisScriptConfig, SHA1 } from '../lua-script';
|
import { RedisScriptConfig, SHA1 } from '../lua-script';
|
||||||
|
|
||||||
// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540
|
export type RedisCommandRawReply = string | number | Buffer | null | undefined | Array<RedisCommandRawReply>;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
||||||
interface RedisCommandRawReplyArray extends Array<RedisCommandRawReply> {}
|
|
||||||
export type RedisCommandRawReply = string | number | Buffer | null | undefined | RedisCommandRawReplyArray;
|
|
||||||
|
|
||||||
export type RedisCommandArgument = string | Buffer;
|
export type RedisCommandArgument = string | Buffer;
|
||||||
|
|
||||||
export type RedisCommandArguments = Array<RedisCommandArgument> & { preserve?: unknown };
|
export type RedisCommandArguments = Array<RedisCommandArgument> & { preserve?: unknown };
|
||||||
|
|
||||||
export interface RedisCommand {
|
export interface RedisCommand {
|
||||||
FIRST_KEY_INDEX?: number | ((...args: Array<any>) => RedisCommandArgument);
|
FIRST_KEY_INDEX?: number | ((...args: Array<any>) => RedisCommandArgument | undefined);
|
||||||
IS_READ_ONLY?: boolean;
|
IS_READ_ONLY?: boolean;
|
||||||
transformArguments(this: void, ...args: Array<any>): RedisCommandArguments;
|
transformArguments(this: void, ...args: Array<any>): RedisCommandArguments;
|
||||||
transformReply?(this: void, reply: any, preserved?: any): any;
|
transformReply?(this: void, reply: any, preserved?: any): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RedisCommandReply<C extends RedisCommand> = C['transformReply'] extends (...args: any) => infer T ? T : RedisCommandRawReply;
|
export type RedisCommandReply<C extends RedisCommand> =
|
||||||
|
C['transformReply'] extends (...args: any) => infer T ? T : RedisCommandRawReply;
|
||||||
|
|
||||||
|
export type ConvertArgumentType<Type, ToType> =
|
||||||
|
Type extends RedisCommandArgument ? (
|
||||||
|
Type extends (string & ToType) ? Type : ToType
|
||||||
|
) : (
|
||||||
|
Type extends Set<infer Member> ? Set<ConvertArgumentType<Member, ToType>> : (
|
||||||
|
Type extends Map<infer Key, infer Value> ? Map<Key, ConvertArgumentType<Value, ToType>> : (
|
||||||
|
Type extends Array<infer Member> ? Array<ConvertArgumentType<Member, ToType>> : (
|
||||||
|
Type extends Date ? Type : (
|
||||||
|
Type extends Record<PropertyKey, any> ? {
|
||||||
|
[Property in keyof Type]: ConvertArgumentType<Type[Property], ToType>
|
||||||
|
} : Type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export type RedisCommandSignature<
|
||||||
|
Command extends RedisCommand,
|
||||||
|
Params extends Array<unknown> = Parameters<Command['transformArguments']>
|
||||||
|
> = <Options extends CommandOptions<ClientCommandOptions>>(
|
||||||
|
...args: Params | [options: Options, ...rest: Params]
|
||||||
|
) => Promise<
|
||||||
|
ConvertArgumentType<
|
||||||
|
RedisCommandReply<Command>,
|
||||||
|
Options['returnBuffers'] extends true ? Buffer : string
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
export interface RedisCommands {
|
export interface RedisCommands {
|
||||||
[command: string]: RedisCommand;
|
[command: string]: RedisCommand;
|
||||||
@@ -30,13 +59,33 @@ export interface RedisModules {
|
|||||||
[module: string]: RedisModule;
|
[module: string]: RedisModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RedisFunction extends RedisCommand {
|
||||||
|
NAME: string;
|
||||||
|
NUMBER_OF_KEYS?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RedisFunctionLibrary {
|
||||||
|
[fn: string]: RedisFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RedisFunctions {
|
||||||
|
[library: string]: RedisFunctionLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
export type RedisScript = RedisScriptConfig & SHA1;
|
export type RedisScript = RedisScriptConfig & SHA1;
|
||||||
|
|
||||||
export interface RedisScripts {
|
export interface RedisScripts {
|
||||||
[script: string]: RedisScript;
|
[script: string]: RedisScript;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RedisPlugins<M extends RedisModules, S extends RedisScripts> {
|
export interface RedisExtensions<
|
||||||
|
M extends RedisModules = RedisModules,
|
||||||
|
F extends RedisFunctions = RedisFunctions,
|
||||||
|
S extends RedisScripts = RedisScripts
|
||||||
|
> {
|
||||||
modules?: M;
|
modules?: M;
|
||||||
|
functions?: F;
|
||||||
scripts?: S;
|
scripts?: S;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ExcludeMappedString<S> = string extends S ? never : S;
|
||||||
|
@@ -3,7 +3,7 @@ import { RedisCommand } from './commands';
|
|||||||
|
|
||||||
export interface RedisScriptConfig extends RedisCommand {
|
export interface RedisScriptConfig extends RedisCommand {
|
||||||
SCRIPT: string;
|
SCRIPT: string;
|
||||||
NUMBER_OF_KEYS: number;
|
NUMBER_OF_KEYS?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SHA1 {
|
export interface SHA1 {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisScript } from './commands';
|
import { fCallArguments } from './commander';
|
||||||
|
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunction, RedisScript } from './commands';
|
||||||
import { WatchError } from './errors';
|
import { WatchError } from './errors';
|
||||||
|
|
||||||
export interface RedisMultiQueuedCommand {
|
export interface RedisMultiQueuedCommand {
|
||||||
@@ -22,6 +23,18 @@ export default class RedisMultiCommand {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addFunction(fn: RedisFunction, args: Array<unknown>): RedisCommandArguments {
|
||||||
|
const transformedArguments = fCallArguments(
|
||||||
|
fn,
|
||||||
|
fn.transformArguments(...args)
|
||||||
|
);
|
||||||
|
this.queue.push({
|
||||||
|
args: transformedArguments,
|
||||||
|
transformReply: fn.transformReply
|
||||||
|
});
|
||||||
|
return transformedArguments;
|
||||||
|
}
|
||||||
|
|
||||||
addScript(script: RedisScript, args: Array<unknown>): RedisCommandArguments {
|
addScript(script: RedisScript, args: Array<unknown>): RedisCommandArguments {
|
||||||
const transformedArguments: RedisCommandArguments = [];
|
const transformedArguments: RedisCommandArguments = [];
|
||||||
if (this.scriptsInUse.has(script.SHA1)) {
|
if (this.scriptsInUse.has(script.SHA1)) {
|
||||||
@@ -37,7 +50,9 @@ export default class RedisMultiCommand {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (script.NUMBER_OF_KEYS !== undefined) {
|
||||||
transformedArguments.push(script.NUMBER_OF_KEYS.toString());
|
transformedArguments.push(script.NUMBER_OF_KEYS.toString());
|
||||||
|
}
|
||||||
|
|
||||||
const scriptArguments = script.transformArguments(...args);
|
const scriptArguments = script.transformArguments(...args);
|
||||||
transformedArguments.push(...scriptArguments);
|
transformedArguments.push(...scriptArguments);
|
||||||
|
@@ -3,7 +3,7 @@ import { SinonSpy } from 'sinon';
|
|||||||
import { promiseTimeout } from './utils';
|
import { promiseTimeout } from './utils';
|
||||||
|
|
||||||
export default new TestUtils({
|
export default new TestUtils({
|
||||||
defaultDockerVersion: '6.2',
|
defaultDockerVersion: '7.0-rc3',
|
||||||
dockerImageName: 'redis',
|
dockerImageName: 'redis',
|
||||||
dockerImageVersionArgument: 'redis-version'
|
dockerImageVersionArgument: 'redis-version'
|
||||||
});
|
});
|
||||||
|
@@ -4,7 +4,7 @@ import RedisGraph from '.';
|
|||||||
export default new TestUtils({
|
export default new TestUtils({
|
||||||
dockerImageName: 'redislabs/redisgraph',
|
dockerImageName: 'redislabs/redisgraph',
|
||||||
dockerImageVersionArgument: 'redisgraph-version',
|
dockerImageVersionArgument: 'redisgraph-version',
|
||||||
defaultDockerVersion: '2.8.7'
|
defaultDockerVersion: '2.8.9'
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GLOBAL = {
|
export const GLOBAL = {
|
||||||
|
@@ -4,7 +4,7 @@ import RedisJSON from '.';
|
|||||||
export default new TestUtils({
|
export default new TestUtils({
|
||||||
dockerImageName: 'redislabs/rejson',
|
dockerImageName: 'redislabs/rejson',
|
||||||
dockerImageVersionArgument: 'rejson-version',
|
dockerImageVersionArgument: 'rejson-version',
|
||||||
defaultDockerVersion: '2.0.6'
|
defaultDockerVersion: '2.0.7'
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GLOBAL = {
|
export const GLOBAL = {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { createConnection } from 'net';
|
import { createConnection } from 'net';
|
||||||
import { once } from 'events';
|
import { once } from 'events';
|
||||||
import { RedisModules, RedisScripts } from '@node-redis/client/lib/commands';
|
import { RedisModules, RedisFunctions, RedisScripts } from '@node-redis/client/lib/commands';
|
||||||
import RedisClient, { RedisClientType } from '@node-redis/client/lib/client';
|
import RedisClient, { RedisClientType } from '@node-redis/client/lib/client';
|
||||||
import { promiseTimeout } from '@node-redis/client/lib/utils';
|
import { promiseTimeout } from '@node-redis/client/lib/utils';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -152,7 +152,11 @@ async function spawnRedisClusterNodeDocker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForClusterState<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>): Promise<void> {
|
async function waitForClusterState<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(client: RedisClientType<M, F, S>): Promise<void> {
|
||||||
while ((await client.clusterInfo()).state !== 'ok') {
|
while ((await client.clusterInfo()).state !== 'ok') {
|
||||||
await promiseTimeout(500);
|
await promiseTimeout(500);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { RedisModules, RedisScripts } from '@node-redis/client/lib/commands';
|
import { RedisModules, RedisFunctions, RedisScripts } from '@node-redis/client/lib/commands';
|
||||||
import RedisClient, { RedisClientOptions, RedisClientType } from '@node-redis/client/lib/client';
|
import RedisClient, { RedisClientOptions, RedisClientType } from '@node-redis/client/lib/client';
|
||||||
import RedisCluster, { RedisClusterOptions, RedisClusterType } from '@node-redis/client/lib/cluster';
|
import RedisCluster, { RedisClusterOptions, RedisClusterType } from '@node-redis/client/lib/cluster';
|
||||||
import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers';
|
import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers';
|
||||||
@@ -15,15 +15,23 @@ interface CommonTestOptions {
|
|||||||
minimumDockerVersion?: Array<number>;
|
minimumDockerVersion?: Array<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClientTestOptions<M extends RedisModules, S extends RedisScripts> extends CommonTestOptions {
|
interface ClientTestOptions<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> extends CommonTestOptions {
|
||||||
serverArguments: Array<string>;
|
serverArguments: Array<string>;
|
||||||
clientOptions?: Partial<RedisClientOptions<M, S>>;
|
clientOptions?: Partial<RedisClientOptions<M, F, S>>;
|
||||||
disableClientSetup?: boolean;
|
disableClientSetup?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClusterTestOptions<M extends RedisModules, S extends RedisScripts> extends CommonTestOptions {
|
interface ClusterTestOptions<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> extends CommonTestOptions {
|
||||||
serverArguments: Array<string>;
|
serverArguments: Array<string>;
|
||||||
clusterConfiguration?: Partial<RedisClusterOptions<M, S>>;
|
clusterConfiguration?: Partial<RedisClusterOptions<M, F, S>>;
|
||||||
numberOfNodes?: number;
|
numberOfNodes?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,10 +101,14 @@ export default class TestUtils {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
testWithClient<M extends RedisModules, S extends RedisScripts>(
|
testWithClient<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(
|
||||||
title: string,
|
title: string,
|
||||||
fn: (client: RedisClientType<M, S>) => Promise<unknown>,
|
fn: (client: RedisClientType<M, F, S>) => Promise<unknown>,
|
||||||
options: ClientTestOptions<M, S>
|
options: ClientTestOptions<M, F, S>
|
||||||
): void {
|
): void {
|
||||||
let dockerPromise: ReturnType<typeof spawnRedisServer>;
|
let dockerPromise: ReturnType<typeof spawnRedisServer>;
|
||||||
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
|
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
|
||||||
@@ -138,16 +150,24 @@ export default class TestUtils {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #clusterFlushAll<M extends RedisModules, S extends RedisScripts>(cluster: RedisClusterType<M, S>): Promise<void> {
|
static async #clusterFlushAll<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(cluster: RedisClusterType<M, F, S>): Promise<void> {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
cluster.getMasters().map(({ client }) => client.flushAll())
|
cluster.getMasters().map(({ client }) => client.flushAll())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
testWithCluster<M extends RedisModules, S extends RedisScripts>(
|
testWithCluster<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
>(
|
||||||
title: string,
|
title: string,
|
||||||
fn: (cluster: RedisClusterType<M, S>) => Promise<void>,
|
fn: (cluster: RedisClusterType<M, F, S>) => Promise<void>,
|
||||||
options: ClusterTestOptions<M, S>
|
options: ClusterTestOptions<M, F, S>
|
||||||
): void {
|
): void {
|
||||||
let dockersPromise: ReturnType<typeof spawnRedisCluster>;
|
let dockersPromise: ReturnType<typeof spawnRedisCluster>;
|
||||||
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
|
if (this.isVersionGreaterThan(options.minimumDockerVersion)) {
|
||||||
|
@@ -4,7 +4,7 @@ import TimeSeries from '.';
|
|||||||
export default new TestUtils({
|
export default new TestUtils({
|
||||||
dockerImageName: 'redislabs/redistimeseries',
|
dockerImageName: 'redislabs/redistimeseries',
|
||||||
dockerImageVersionArgument: 'timeseries-version',
|
dockerImageVersionArgument: 'timeseries-version',
|
||||||
defaultDockerVersion: '1.6.8'
|
defaultDockerVersion: '1.6.9'
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GLOBAL = {
|
export const GLOBAL = {
|
||||||
|
Reference in New Issue
Block a user