1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00

v4.0.0-rc.4 (#1723)

* update workflows & README

* add .deepsource.toml

* fix client.quit, add error events on cluster, fix some "deepsource.io" warnings

* Release 4.0.0-rc.1

* add cluster.duplicate, add some tests

* fix #1650 - add support for Buffer in some commands, add GET_BUFFER command

* fix GET and GET_BUFFER return type

* update FAQ

* Update invalid code example in README.md (#1654)

* Update invalid code example in README.md

* Update README.md

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* fix #1652

* ref #1653 - better types

* better types

* fix 54124793ad

* Update GEOSEARCHSTORE.spec.ts

* fix #1660 - add support for client.HSET('key', 'field', 'value')

* upgrade dependencies, update README

* fix #1659 - add support for db-number in client options url

* fix README, remove unused import, downgrade typedoc & typedoc-plugin-markdown

* update client-configurations.md

* fix README

* add CLUSTER_SLOTS, add some tests

* fix "createClient with url" test with redis 5

* remove unused imports

* Release 4.0.0-rc.2

* add missing semicolon

* replace empty "transformReply" functions with typescript "declare"

* fix EVAL & EVALSHA, add some tests, npm update

* fix #1665 - add ZRANGEBYLEX, ZRANGEBYSCORE, ZRANGEBYSCORE_WITHSCORES

* new issue templates

* add all COMMAND commands

* run COMMAND & COMMAND INFO tests only on redis >6

* Create SECURITY.md

* fix #1671 - add support for all client configurations in cluster

* ref #1671 - add support for defaults

* remove some commands from cluster, npm update, clean code,

* lock benny version

* fix #1674 - remove `isolationPoolOptions` when creating isolated connection

* increase test coverage

* update .npmignore

* Release 4.0.0-rc.3

* fix README

* remove whitespace from LICENSE

* use "export { x as y }" instead of import & const

* move from "NodeRedis" to "Redis"

* fix #1676

* update comments

* Auth before select database (#1679)

* Auth before select database

* fix #1681

Co-authored-by: leibale <leibale1998@gmail.com>

* Adds connect-as-acl-user example. (#1684)

* Adds connect-as-acl-user example.

* Adds blank line at end.

* Set to private.

* Adds examples folder to npmignore.

* Adds Apple .DS_Store file to .gitignore (#1685)

* Adds Apple .DS_Store file.

* Add .DS_Store to .npmignore too

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* move examples

* clean some tests

* clean code

* Adds examples table of contents and contribution guidelines. (#1686)

* Updated examples to use named functions. (#1687)

* Updated examples to user named functions.

* Update README.md

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* update docs, add 6.0.x to the tests matrix, add eslint, npm update, fix some commands, fix some types

Co-authored-by: Simon Prickett <simon@crudworks.org>

* fix tests with redis 6.0.x

* fix ACL GETUSER test

* fix client.quit and client.disconnect

* fix ACL GETUSER

* Adds TypeScript note and corrects a typo.

* Fixes a bug in the Scan Iterator section. (#1694)

* Made examples use local version.

* Add `lua-multi-incr.js` example (#1692)

Also fix syntax error in the lua example in the README

Closes #1689.

* Add(examples): Create an example for blPop & lPush (#1696)

* Add(examples): Create an example for blPop & lPush

Signed-off-by: Aditya Rastogi <adit.rastogi2014@gmail.com>

* Update(examples): fix case, add timeout, update readme

Signed-off-by: Aditya Rastogi <adit.rastogi2014@gmail.com>

Closes #1693.

* Add command-with-modifiers.js example (#1695)

* Adds TypeScript note and corrects a typo.

* Adds command-with-modifiers example. (redis#1688)

* Adds command-with-modifiers example. (redis#1688)

* Adds command-with-modifiers example. (redis#1688)

* Removed callbacks.

Co-authored-by: Simon Prickett <simon@redislabs.com>

Closes #1688.

* Issue # 1697 FIX - creates an example script that shows how to use the SSCAN iterator (#1699)

* #1697 fix for set scan example

* adds the js file

* adds comment

* Minor layout and comment adjustment.

Co-authored-by: srawat2 <shashank19aug>
Co-authored-by: Simon Prickett <simon@redislabs.com>

Closes #1697.

* fix #1706 - HSET return type should be number

* use dockers for tests, fix some bugs

* increase dockers timeout to 30s

* release drafter (#1683)

* release drafter

* fixing contributors

* use dockers for tests, use npm workspaces, add rejson & redisearch modules, fix some bugs

* fix #1712 - fix LINDEX return type

* uncomment TIME tests

* use codecov

* fix tests.yml

* uncomment "should handle live resharding" test

* fix #1714 - update README(s)

* add package-lock.json

* update CONTRIBUTING.md

* update examples

* uncomment some tests

* fix test-utils

* move "all-in-one" to root folder

* fix tests workflow

* fix bug in cluster slots, enhance live resharding test

* fix live resharding test

* fix #1707 - handle number arguments in legacy mode

* Add rejectedUnauthorized and other TLS options (#1708)

* Update socket.ts

* fix #1716 - decode username and password from url

* fix some Z (sorted list) commands, increase commands test coverage

* remove empty lines

* fix 'Scenario' typo (#1720)

* update readmes, add createCluster to the `redis` package

* add .release-it.json files, update some md files

* run tests on pull requests too

* Support esModuleInterop set to false. (#1717)

* Support esModuleInterop set to false.

When testing the upcoming 4.x release, we got a bunch of typescript
errors emitted from this project.

We quickly realized this is because the library uses the esModuleInterop
flag. This makes some imports _slightly_ easier to write, but it comes
at a cost: it forces any application or library using this library to
*also* have esModuleInterop on.

The `esModuleInterop` flag is a bit of a holdover from an earlier time,
and I would not recommend using it in libraries. The main issue is that
if it's set to true, you are forcing any users of the library to also
have `esModuleInterop`, where if you keep have it set to `false` (the
default), you leave the decision to the user.

This change should have no rammifications to users with
`esModuleInterop` on, but it will enable support for those that have it
off.

This is especially good for library authors such as myself, because I
would also like to keep this flag off to not force *my* users into this
feature.

* All tests now pass!

* Move @types/redis-parser into client sub-package

and removed a comma

* npm update, remove html from readme

* add tests and licence badges

* update changelog.md

* update .npmignore and .release-it.json

* update .release-it.json

* Release client@1.0.0-rc.0

* revert d32f1edf8a

* fix .npmignore

* replace @redis with @node-redis

* Release client@1.0.0-rc.0

* update json & search version

* Release json@1.0.0-rc.0

* Release search@1.0.0-rc.0

* update dependencies

* Release redis@4.0.0-rc.4

Co-authored-by: Richard Samuelsson <noobtoothfairy@gmail.com>
Co-authored-by: mustard <mhqnwt@gmail.com>
Co-authored-by: Simon Prickett <simon@redislabs.com>
Co-authored-by: Simon Prickett <simon@crudworks.org>
Co-authored-by: Suze Shardlow <SuzeShardlow@users.noreply.github.com>
Co-authored-by: Joshua T <buildingsomethingfun@gmail.com>
Co-authored-by: Aditya Rastogi <adit.rastogi2014@gmail.com>
Co-authored-by: Rohan Kumar <rohan.kr20@gmail.com>
Co-authored-by: Kalki <shashank.kviit@gmail.com>
Co-authored-by: Chayim <chayim@users.noreply.github.com>
Co-authored-by: Da-Jin Chu <dajinchu@gmail.com>
Co-authored-by: Henrique Corrêa <75134774+HeCorr@users.noreply.github.com>
Co-authored-by: Evert Pot <me@evertpot.com>
This commit is contained in:
Leibale Eidelman
2021-11-16 02:48:20 -05:00
committed by GitHub
parent 199285aa71
commit eed479778f
705 changed files with 9959 additions and 4349 deletions

View File

@@ -0,0 +1,302 @@
import * as LinkedList from 'yallist';
import { AbortError } from '../errors';
import { RedisCommandArguments, RedisCommandRawReply } from '../commands';
// We need to use 'require', because it's not possible with Typescript to import
// classes that are exported as 'module.exports = class`, without esModuleInterop
// set to true.
const RedisParser = require('redis-parser');
export interface QueueCommandOptions {
asap?: boolean;
chainId?: symbol;
signal?: AbortSignal;
}
interface CommandWaitingToBeSent extends CommandWaitingForReply {
args: RedisCommandArguments;
chainId?: symbol;
abort?: {
signal: AbortSignal;
listener(): void;
};
}
interface CommandWaitingForReply {
resolve(reply?: unknown): void;
reject(err: Error): void;
channelsCounter?: number;
bufferMode?: boolean;
}
export enum PubSubSubscribeCommands {
SUBSCRIBE = 'SUBSCRIBE',
PSUBSCRIBE = 'PSUBSCRIBE'
}
export enum PubSubUnsubscribeCommands {
UNSUBSCRIBE = 'UNSUBSCRIBE',
PUNSUBSCRIBE = 'PUNSUBSCRIBE'
}
export type PubSubListener = (message: string, channel: string) => unknown;
export type PubSubListenersMap = Map<string, Set<PubSubListener>>;
export default class RedisCommandsQueue {
static #flushQueue<T extends CommandWaitingForReply>(queue: LinkedList<T>, err: Error): void {
while (queue.length) {
queue.shift()!.reject(err);
}
}
static #emitPubSubMessage(listeners: Set<PubSubListener>, message: string, channel: string): void {
for (const listener of listeners) {
listener(message, channel);
}
}
readonly #maxLength: number | null | undefined;
readonly #waitingToBeSent = new LinkedList<CommandWaitingToBeSent>();
readonly #waitingForReply = new LinkedList<CommandWaitingForReply>();
readonly #pubSubState = {
subscribing: 0,
subscribed: 0,
unsubscribing: 0
};
readonly #pubSubListeners = {
channels: <PubSubListenersMap>new Map(),
patterns: <PubSubListenersMap>new Map()
};
readonly #parser = new RedisParser({
returnReply: (reply: unknown) => {
if ((this.#pubSubState.subscribing || this.#pubSubState.subscribed) && Array.isArray(reply)) {
switch (reply[0]) {
case 'message':
return RedisCommandsQueue.#emitPubSubMessage(
this.#pubSubListeners.channels.get(reply[1])!,
reply[2],
reply[1]
);
case 'pmessage':
return RedisCommandsQueue.#emitPubSubMessage(
this.#pubSubListeners.patterns.get(reply[1])!,
reply[3],
reply[2]
);
case 'subscribe':
case 'psubscribe':
if (--this.#waitingForReply.head!.value.channelsCounter! === 0) {
this.#shiftWaitingForReply().resolve();
}
return;
}
}
this.#shiftWaitingForReply().resolve(reply);
},
returnError: (err: Error) => this.#shiftWaitingForReply().reject(err)
});
#chainInExecution: symbol | undefined;
constructor(maxLength: number | null | undefined) {
this.#maxLength = maxLength;
}
addCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: QueueCommandOptions, bufferMode?: boolean): Promise<T> {
if (this.#pubSubState.subscribing || this.#pubSubState.subscribed) {
return Promise.reject(new Error('Cannot send commands in PubSub mode'));
} else if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
return Promise.reject(new Error('The queue is full'));
} else if (options?.signal?.aborted) {
return Promise.reject(new AbortError());
}
return new Promise((resolve, reject) => {
const node = new LinkedList.Node<CommandWaitingToBeSent>({
args,
chainId: options?.chainId,
bufferMode,
resolve,
reject,
});
if (options?.signal) {
const listener = () => {
this.#waitingToBeSent.removeNode(node);
node.value.reject(new AbortError());
};
node.value.abort = {
signal: options.signal,
listener
};
// AbortSignal type is incorrent
(options.signal as any).addEventListener('abort', listener, {
once: true
});
}
if (options?.asap) {
this.#waitingToBeSent.unshiftNode(node);
} else {
this.#waitingToBeSent.pushNode(node);
}
});
}
subscribe(command: PubSubSubscribeCommands, channels: string | Array<string>, listener: PubSubListener): Promise<void> {
const channelsToSubscribe: Array<string> = [],
listeners = command === PubSubSubscribeCommands.SUBSCRIBE ? this.#pubSubListeners.channels : this.#pubSubListeners.patterns;
for (const channel of (Array.isArray(channels) ? channels : [channels])) {
if (listeners.has(channel)) {
listeners.get(channel)!.add(listener);
continue;
}
listeners.set(channel, new Set([listener]));
channelsToSubscribe.push(channel);
}
if (!channelsToSubscribe.length) {
return Promise.resolve();
}
return this.#pushPubSubCommand(command, channelsToSubscribe);
}
unsubscribe(command: PubSubUnsubscribeCommands, channels?: string | Array<string>, listener?: PubSubListener): Promise<void> {
const listeners = command === PubSubUnsubscribeCommands.UNSUBSCRIBE ? this.#pubSubListeners.channels : this.#pubSubListeners.patterns;
if (!channels) {
const size = listeners.size;
listeners.clear();
return this.#pushPubSubCommand(command, size);
}
const channelsToUnsubscribe = [];
for (const channel of (Array.isArray(channels) ? channels : [channels])) {
const set = listeners.get(channel);
if (!set) continue;
let shouldUnsubscribe = !listener;
if (listener) {
set.delete(listener);
shouldUnsubscribe = set.size === 0;
}
if (shouldUnsubscribe) {
channelsToUnsubscribe.push(channel);
listeners.delete(channel);
}
}
if (!channelsToUnsubscribe.length) {
return Promise.resolve();
}
return this.#pushPubSubCommand(command, channelsToUnsubscribe);
}
#pushPubSubCommand(command: PubSubSubscribeCommands | PubSubUnsubscribeCommands, channels: number | Array<string>): Promise<void> {
return new Promise((resolve, reject) => {
const isSubscribe = command === PubSubSubscribeCommands.SUBSCRIBE || command === PubSubSubscribeCommands.PSUBSCRIBE,
inProgressKey = isSubscribe ? 'subscribing' : 'unsubscribing',
commandArgs: Array<string> = [command];
let channelsCounter: number;
if (typeof channels === 'number') { // unsubscribe only
channelsCounter = channels;
} else {
commandArgs.push(...channels);
channelsCounter = channels.length;
}
this.#pubSubState[inProgressKey] += channelsCounter;
this.#waitingToBeSent.push({
args: commandArgs,
channelsCounter,
resolve: () => {
this.#pubSubState[inProgressKey] -= channelsCounter;
this.#pubSubState.subscribed += channelsCounter * (isSubscribe ? 1 : -1);
resolve();
},
reject: () => {
this.#pubSubState[inProgressKey] -= channelsCounter;
reject();
}
});
});
}
resubscribe(): Promise<any> | undefined {
if (!this.#pubSubState.subscribed && !this.#pubSubState.subscribing) {
return;
}
this.#pubSubState.subscribed = this.#pubSubState.subscribing = 0;
// TODO: acl error on one channel/pattern will reject the whole command
return Promise.all([
this.#pushPubSubCommand(PubSubSubscribeCommands.SUBSCRIBE, [...this.#pubSubListeners.channels.keys()]),
this.#pushPubSubCommand(PubSubSubscribeCommands.PSUBSCRIBE, [...this.#pubSubListeners.patterns.keys()])
]);
}
getCommandToSend(): RedisCommandArguments | undefined {
const toSend = this.#waitingToBeSent.shift();
if (toSend) {
this.#waitingForReply.push({
resolve: toSend.resolve,
reject: toSend.reject,
channelsCounter: toSend.channelsCounter,
bufferMode: toSend.bufferMode
});
}
this.#chainInExecution = toSend?.chainId;
return toSend?.args;
}
parseResponse(data: Buffer): void {
this.#parser.setReturnBuffers(!!this.#waitingForReply.head?.value.bufferMode);
this.#parser.execute(data);
}
#shiftWaitingForReply(): CommandWaitingForReply {
if (!this.#waitingForReply.length) {
throw new Error('Got an unexpected reply from Redis');
}
return this.#waitingForReply.shift()!;
}
flushWaitingForReply(err: Error): void {
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
if (!this.#chainInExecution) {
return;
}
while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) {
this.#waitingToBeSent.shift();
}
this.#chainInExecution = undefined;
}
flushAll(err: Error): void {
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err);
}
}

View File

@@ -0,0 +1,233 @@
import CLUSTER_COMMANDS from '../cluster/commands';
import * as ACL_CAT from '../commands/ACL_CAT';
import * as ACL_DELUSER from '../commands/ACL_DELUSER';
import * as ACL_GENPASS from '../commands/ACL_GENPASS';
import * as ACL_GETUSER from '../commands/ACL_GETUSER';
import * as ACL_LIST from '../commands/ACL_LIST';
import * as ACL_LOAD from '../commands/ACL_LOAD';
import * as ACL_LOG_RESET from '../commands/ACL_LOG_RESET';
import * as ACL_LOG from '../commands/ACL_LOG';
import * as ACL_SAVE from '../commands/ACL_SAVE';
import * as ACL_SETUSER from '../commands/ACL_SETUSER';
import * as ACL_USERS from '../commands/ACL_USERS';
import * as ACL_WHOAMI from '../commands/ACL_WHOAMI';
import * as ASKING from '../commands/ASKING';
import * as AUTH from '../commands/AUTH';
import * as BGREWRITEAOF from '../commands/BGREWRITEAOF';
import * as BGSAVE from '../commands/BGSAVE';
import * as CLIENT_ID from '../commands/CLIENT_ID';
import * as CLIENT_INFO from '../commands/CLIENT_INFO';
import * as CLUSTER_ADDSLOTS from '../commands/CLUSTER_ADDSLOTS';
import * as CLUSTER_FLUSHSLOTS from '../commands/CLUSTER_FLUSHSLOTS';
import * as CLUSTER_INFO from '../commands/CLUSTER_INFO';
import * as CLUSTER_NODES from '../commands/CLUSTER_NODES';
import * as CLUSTER_MEET from '../commands/CLUSTER_MEET';
import * as CLUSTER_RESET from '../commands/CLUSTER_RESET';
import * as CLUSTER_SETSLOT from '../commands/CLUSTER_SETSLOT';
import * as CLUSTER_SLOTS from '../commands/CLUSTER_SLOTS';
import * as COMMAND_COUNT from '../commands/COMMAND_COUNT';
import * as COMMAND_GETKEYS from '../commands/COMMAND_GETKEYS';
import * as COMMAND_INFO from '../commands/COMMAND_INFO';
import * as COMMAND from '../commands/COMMAND';
import * as CONFIG_GET from '../commands/CONFIG_GET';
import * as CONFIG_RESETASTAT from '../commands/CONFIG_RESETSTAT';
import * as CONFIG_REWRITE from '../commands/CONFIG_REWRITE';
import * as CONFIG_SET from '../commands/CONFIG_SET';
import * as DBSIZE from '../commands/DBSIZE';
import * as DISCARD from '../commands/DISCARD';
import * as ECHO from '../commands/ECHO';
import * as FAILOVER from '../commands/FAILOVER';
import * as FLUSHALL from '../commands/FLUSHALL';
import * as FLUSHDB from '../commands/FLUSHDB';
import * as HELLO from '../commands/HELLO';
import * as INFO from '../commands/INFO';
import * as KEYS from '../commands/KEYS';
import * as LASTSAVE from '../commands/LASTSAVE';
import * as LOLWUT from '../commands/LOLWUT';
import * as MEMOERY_DOCTOR from '../commands/MEMORY_DOCTOR';
import * as MEMORY_MALLOC_STATS from '../commands/MEMORY_MALLOC-STATS';
import * as MEMORY_PURGE from '../commands/MEMORY_PURGE';
import * as MEMORY_STATS from '../commands/MEMORY_STATS';
import * as MEMORY_USAGE from '../commands/MEMORY_USAGE';
import * as MODULE_LIST from '../commands/MODULE_LIST';
import * as MODULE_LOAD from '../commands/MODULE_LOAD';
import * as MODULE_UNLOAD from '../commands/MODULE_UNLOAD';
import * as MOVE from '../commands/MOVE';
import * as PING from '../commands/PING';
import * as PUBSUB_CHANNELS from '../commands/PUBSUB_CHANNELS';
import * as PUBSUB_NUMPAT from '../commands/PUBSUB_NUMPAT';
import * as PUBSUB_NUMSUB from '../commands/PUBSUB_NUMSUB';
import * as RANDOMKEY from '../commands/RANDOMKEY';
import * as READONLY from '../commands/READONLY';
import * as READWRITE from '../commands/READWRITE';
import * as REPLICAOF from '../commands/REPLICAOF';
import * as RESTORE_ASKING from '../commands/RESTORE-ASKING';
import * as ROLE from '../commands/ROLE';
import * as SAVE from '../commands/SAVE';
import * as SCAN from '../commands/SCAN';
import * as SCRIPT_DEBUG from '../commands/SCRIPT_DEBUG';
import * as SCRIPT_EXISTS from '../commands/SCRIPT_EXISTS';
import * as SCRIPT_FLUSH from '../commands/SCRIPT_FLUSH';
import * as SCRIPT_KILL from '../commands/SCRIPT_KILL';
import * as SCRIPT_LOAD from '../commands/SCRIPT_LOAD';
import * as SHUTDOWN from '../commands/SHUTDOWN';
import * as SWAPDB from '../commands/SWAPDB';
import * as TIME from '../commands/TIME';
import * as UNWATCH from '../commands/UNWATCH';
import * as WAIT from '../commands/WAIT';
export default {
...CLUSTER_COMMANDS,
ACL_CAT,
aclCat: ACL_CAT,
ACL_DELUSER,
aclDelUser: ACL_DELUSER,
ACL_GENPASS,
aclGenPass: ACL_GENPASS,
ACL_GETUSER,
aclGetUser: ACL_GETUSER,
ACL_LIST,
aclList: ACL_LIST,
ACL_LOAD,
aclLoad: ACL_LOAD,
ACL_LOG_RESET,
aclLogReset: ACL_LOG_RESET,
ACL_LOG,
aclLog: ACL_LOG,
ACL_SAVE,
aclSave: ACL_SAVE,
ACL_SETUSER,
aclSetUser: ACL_SETUSER,
ACL_USERS,
aclUsers: ACL_USERS,
ACL_WHOAMI,
aclWhoAmI: ACL_WHOAMI,
ASKING,
asking: ASKING,
AUTH,
auth: AUTH,
BGREWRITEAOF,
bgRewriteAof: BGREWRITEAOF,
BGSAVE,
bgSave: BGSAVE,
CLIENT_ID,
clientId: CLIENT_ID,
CLIENT_INFO,
clientInfo: CLIENT_INFO,
CLUSTER_ADDSLOTS,
clusterAddSlots: CLUSTER_ADDSLOTS,
CLUSTER_FLUSHSLOTS,
clusterFlushSlots: CLUSTER_FLUSHSLOTS,
CLUSTER_INFO,
clusterInfo: CLUSTER_INFO,
CLUSTER_NODES,
clusterNodes: CLUSTER_NODES,
CLUSTER_MEET,
clusterMeet: CLUSTER_MEET,
CLUSTER_RESET,
clusterReset: CLUSTER_RESET,
CLUSTER_SETSLOT,
clusterSetSlot: CLUSTER_SETSLOT,
CLUSTER_SLOTS,
clusterSlots: CLUSTER_SLOTS,
COMMAND_COUNT,
commandCount: COMMAND_COUNT,
COMMAND_GETKEYS,
commandGetKeys: COMMAND_GETKEYS,
COMMAND_INFO,
commandInfo: COMMAND_INFO,
COMMAND,
command: COMMAND,
CONFIG_GET,
configGet: CONFIG_GET,
CONFIG_RESETASTAT,
configResetStat: CONFIG_RESETASTAT,
CONFIG_REWRITE,
configRewrite: CONFIG_REWRITE,
CONFIG_SET,
configSet: CONFIG_SET,
DBSIZE,
dbSize: DBSIZE,
DISCARD,
discard: DISCARD,
ECHO,
echo: ECHO,
FAILOVER,
failover: FAILOVER,
FLUSHALL,
flushAll: FLUSHALL,
FLUSHDB,
flushDb: FLUSHDB,
HELLO,
hello: HELLO,
INFO,
info: INFO,
KEYS,
keys: KEYS,
LASTSAVE,
lastSave: LASTSAVE,
LOLWUT,
lolwut: LOLWUT,
MEMOERY_DOCTOR,
memoryDoctor: MEMOERY_DOCTOR,
'MEMORY_MALLOC-STATS': MEMORY_MALLOC_STATS,
memoryMallocStats: MEMORY_MALLOC_STATS,
MEMORY_PURGE,
memoryPurge: MEMORY_PURGE,
MEMORY_STATS,
memoryStats: MEMORY_STATS,
MEMORY_USAGE,
memoryUsage: MEMORY_USAGE,
MODULE_LIST,
moduleList: MODULE_LIST,
MODULE_LOAD,
moduleLoad: MODULE_LOAD,
MODULE_UNLOAD,
moduleUnload: MODULE_UNLOAD,
MOVE,
move: MOVE,
PING,
ping: PING,
PUBSUB_CHANNELS,
pubSubChannels: PUBSUB_CHANNELS,
PUBSUB_NUMPAT,
pubSubNumPat: PUBSUB_NUMPAT,
PUBSUB_NUMSUB,
pubSubNumSub: PUBSUB_NUMSUB,
RANDOMKEY,
randomKey: RANDOMKEY,
READONLY,
readonly: READONLY,
READWRITE,
readwrite: READWRITE,
REPLICAOF,
replicaOf: REPLICAOF,
'RESTORE-ASKING': RESTORE_ASKING,
restoreAsking: RESTORE_ASKING,
ROLE,
role: ROLE,
SAVE,
save: SAVE,
SCAN,
scan: SCAN,
SCRIPT_DEBUG,
scriptDebug: SCRIPT_DEBUG,
SCRIPT_EXISTS,
scriptExists: SCRIPT_EXISTS,
SCRIPT_FLUSH,
scriptFlush: SCRIPT_FLUSH,
SCRIPT_KILL,
scriptKill: SCRIPT_KILL,
SCRIPT_LOAD,
scriptLoad: SCRIPT_LOAD,
SHUTDOWN,
shutdown: SHUTDOWN,
SWAPDB,
swapDb: SWAPDB,
TIME,
time: TIME,
UNWATCH,
unwatch: UNWATCH,
WAIT,
wait: WAIT
};

View File

@@ -0,0 +1,681 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
import RedisClient, { ClientLegacyCommandArguments, RedisClientType } from '.';
import { RedisClientMultiCommandType } from './multi-command';
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands';
import { AbortError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
import { defineScript } from '../lua-script';
import { spy } from 'sinon';
import { once } from 'events';
export const SQUARE_SCRIPT = defineScript({
NUMBER_OF_KEYS: 0,
SCRIPT: 'return ARGV[1] * ARGV[1];',
transformArguments(number: number): Array<string> {
return [number.toString()];
},
transformReply(reply: number): number {
return reply;
}
});
describe('Client', () => {
describe('parseURL', () => {
it('redis://user:secret@localhost:6379/0', () => {
assert.deepEqual(
RedisClient.parseURL('redis://user:secret@localhost:6379/0'),
{
socket: {
host: 'localhost',
port: 6379
},
username: 'user',
password: 'secret',
database: 0
}
);
});
it('rediss://user:secret@localhost:6379/0', () => {
assert.deepEqual(
RedisClient.parseURL('rediss://user:secret@localhost:6379/0'),
{
socket: {
host: 'localhost',
port: 6379,
tls: true
},
username: 'user',
password: 'secret',
database: 0
}
);
});
it('Invalid protocol', () => {
assert.throws(
() => RedisClient.parseURL('redi://user:secret@localhost:6379/0'),
TypeError
);
});
it('Invalid pathname', () => {
assert.throws(
() => RedisClient.parseURL('redis://user:secret@localhost:6379/NaN'),
TypeError
);
});
it('redis://localhost', () => {
assert.deepEqual(
RedisClient.parseURL('redis://localhost'),
{
socket: {
host: 'localhost',
}
}
);
});
});
describe('authentication', () => {
testUtils.testWithClient('Client should be authenticated', async client => {
assert.equal(
await client.ping(),
'PONG'
);
}, GLOBAL.SERVERS.PASSWORD);
testUtils.testWithClient('should not retry connecting if failed due to wrong auth', async client => {
let message;
if (testUtils.isVersionGreaterThan([6, 2])) {
message = 'WRONGPASS invalid username-password pair or user is disabled.';
} else if (testUtils.isVersionGreaterThan([6])) {
message = 'WRONGPASS invalid username-password pair';
} else {
message = 'ERR invalid password';
}
await assert.rejects(
client.connect(),
{ message }
);
assert.equal(client.isOpen, false);
}, {
...GLOBAL.SERVERS.PASSWORD,
clientOptions: {
password: 'wrongpassword'
},
disableClientSetup: true
});
testUtils.testWithClient('should execute AUTH before SELECT', async client => {
assert.equal(
(await client.clientInfo()).db,
2
);
}, {
...GLOBAL.SERVERS.PASSWORD,
clientOptions: {
...GLOBAL.SERVERS.PASSWORD.clientOptions,
database: 2
},
minimumDockerVersion: [6, 2]
});
});
describe('legacyMode', () => {
function sendCommandAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, args: RedisCommandArguments): Promise<RedisCommandRawReply> {
return new Promise((resolve, reject) => {
(client as any).sendCommand(args, (err: Error | undefined, reply: RedisCommandRawReply) => {
if (err) return reject(err);
resolve(reply);
});
});
}
testUtils.testWithClient('client.sendCommand should call the callback', async client => {
assert.equal(
await sendCommandAsync(client, ['PING']),
'PONG'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
testUtils.testWithClient('client.sendCommand should work without callback', async client => {
client.sendCommand(['PING']);
await client.v4.ping(); // make sure the first command was replied
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
testUtils.testWithClient('client.v4.sendCommand should return a promise', async client => {
assert.equal(
await client.v4.sendCommand(['PING']),
'PONG'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: ClientLegacyCommandArguments): Promise<RedisCommandRawReply> {
return new Promise((resolve, reject) => {
(client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
if (err) return reject(err);
resolve(reply);
});
});
}
testUtils.testWithClient('client.{command} should accept vardict arguments', async client => {
assert.equal(
await setAsync(client, 'a', 'b'),
'OK'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
testUtils.testWithClient('client.{command} should accept arguments array', async client => {
assert.equal(
await setAsync(client, ['a', 'b']),
'OK'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
testUtils.testWithClient('client.{command} should accept mix of arrays and arguments', async client => {
assert.equal(
await setAsync(client, ['a'], 'b', ['EX', 1]),
'OK'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
function multiExecAsync<M extends RedisModules, S extends RedisScripts>(multi: RedisClientMultiCommandType<M, S>): Promise<Array<RedisCommandRawReply>> {
return new Promise((resolve, reject) => {
(multi as any).exec((err: Error | undefined, replies: Array<RedisCommandRawReply>) => {
if (err) return reject(err);
resolve(replies);
});
});
}
testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => {
assert.deepEqual(
await multiExecAsync(
client.multi().ping()
),
['PONG']
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => {
client.multi()
.ping()
.exec();
await client.v4.ping(); // make sure the first command was replied
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
testUtils.testWithClient('client.multi.ping.v4.ping.v4.exec should return a promise', async client => {
assert.deepEqual(
await client.multi()
.ping()
.v4.ping()
.v4.exec(),
['PONG', 'PONG']
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
testUtils.testWithClient('client.{script} should return a promise', async client => {
assert.equal(
await client.square(2),
4
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true,
scripts: {
square: SQUARE_SCRIPT
}
}
});
});
describe('events', () => {
testUtils.testWithClient('connect, ready, end', async client => {
await Promise.all([
once(client, 'connect'),
once(client, 'ready'),
client.connect()
]);
await Promise.all([
once(client, 'end'),
client.disconnect()
]);
}, {
...GLOBAL.SERVERS.OPEN,
disableClientSetup: true
});
});
describe('sendCommand', () => {
testUtils.testWithClient('PING', async client => {
assert.equal(await client.sendCommand(['PING']), 'PONG');
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('bufferMode', async client => {
assert.deepEqual(
await client.sendCommand(['PING'], undefined, true),
Buffer.from('PONG')
);
}, GLOBAL.SERVERS.OPEN);
describe('AbortController', () => {
before(function () {
if (!global.AbortController) {
this.skip();
}
});
testUtils.testWithClient('success', async client => {
await client.sendCommand(['PING'], {
signal: new AbortController().signal
});
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('AbortError', client => {
const controller = new AbortController();
controller.abort();
return assert.rejects(
client.sendCommand(['PING'], {
signal: controller.signal
}),
AbortError
);
}, GLOBAL.SERVERS.OPEN);
});
});
describe('multi', () => {
testUtils.testWithClient('simple', async client => {
assert.deepEqual(
await client.multi()
.ping()
.set('key', 'value')
.get('key')
.exec(),
['PONG', 'OK', 'value']
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('should reject the whole chain on error', client => {
return assert.rejects(
client.multi()
.ping()
.addCommand(['INVALID COMMAND'])
.ping()
.exec()
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with script', async client => {
assert.deepEqual(
await client.multi()
.square(2)
.exec(),
[4]
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
scripts: {
square: SQUARE_SCRIPT
}
}
});
testUtils.testWithClient('WatchError', async client => {
await client.watch('key');
await client.set(
RedisClient.commandOptions({
isolated: true
}),
'key',
'1'
);
await assert.rejects(
client.multi()
.decr('key')
.exec(),
WatchError
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('execAsPipeline', async client => {
assert.deepEqual(
await client.multi()
.ping()
.exec(true),
['PONG']
);
}, GLOBAL.SERVERS.OPEN);
});
testUtils.testWithClient('scripts', async client => {
assert.equal(
await client.square(2),
4
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
scripts: {
square: SQUARE_SCRIPT
}
}
});
testUtils.testWithClient('modules', async client => {
assert.equal(
await client.module.echo('message'),
'message'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
modules: {
module: {
echo: {
transformArguments(message: string): Array<string> {
return ['ECHO', message];
},
transformReply(reply: string): string {
return reply;
}
}
}
}
}
});
testUtils.testWithClient('executeIsolated', async client => {
await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
assert.equal(
await client.executeIsolated(isolatedClient =>
isolatedClient.sendCommand(['CLIENT', 'GETNAME'])
),
null
);
}, GLOBAL.SERVERS.OPEN);
async function killClient<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>): Promise<void> {
const onceErrorPromise = once(client, 'error');
await client.sendCommand(['QUIT']);
await Promise.all([
onceErrorPromise,
assert.rejects(client.ping(), SocketClosedUnexpectedlyError)
]);
}
testUtils.testWithClient('should reconnect when socket disconnects', async client => {
await killClient(client);
await assert.doesNotReject(client.ping());
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('should remember selected db', async client => {
await client.select(1);
await killClient(client);
assert.equal(
(await client.clientInfo()).db,
1
);
}, {
...GLOBAL.SERVERS.OPEN,
minimumDockerVersion: [6, 2] // CLIENT INFO
});
testUtils.testWithClient('scanIterator', async client => {
const promises = [],
keys = new Set();
for (let i = 0; i < 100; i++) {
const key = i.toString();
keys.add(key);
promises.push(client.set(key, ''));
}
await Promise.all(promises);
const results = new Set();
for await (const key of client.scanIterator()) {
results.add(key);
}
assert.deepEqual(keys, results);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('hScanIterator', async client => {
const hash: Record<string, string> = {};
for (let i = 0; i < 100; i++) {
hash[i.toString()] = i.toString();
}
await client.hSet('key', hash);
const results: Record<string, string> = {};
for await (const { field, value } of client.hScanIterator('key')) {
results[field] = value;
}
assert.deepEqual(hash, results);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('sScanIterator', async client => {
const members = new Set<string>();
for (let i = 0; i < 100; i++) {
members.add(i.toString());
}
await client.sAdd('key', Array.from(members));
const results = new Set<string>();
for await (const key of client.sScanIterator('key')) {
results.add(key);
}
assert.deepEqual(members, results);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('zScanIterator', async client => {
const members = [];
for (let i = 0; i < 100; i++) {
members.push({
score: 1,
value: i.toString()
});
}
await client.zAdd('key', members);
const map = new Map();
for await (const member of client.zScanIterator('key')) {
map.set(member.value, member.score);
}
type MemberTuple = [string, number];
function sort(a: MemberTuple, b: MemberTuple) {
return Number(b[0]) - Number(a[0]);
}
assert.deepEqual(
[...map.entries()].sort(sort),
members.map<MemberTuple>(member => [member.value, member.score]).sort(sort)
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('PubSub', async publisher => {
const subscriber = publisher.duplicate();
await subscriber.connect();
try {
const channelListener1 = spy(),
channelListener2 = spy(),
patternListener = spy();
await Promise.all([
subscriber.subscribe('channel', channelListener1),
subscriber.subscribe('channel', channelListener2),
subscriber.pSubscribe('channel*', patternListener)
]);
await Promise.all([
waitTillBeenCalled(channelListener1),
waitTillBeenCalled(channelListener2),
waitTillBeenCalled(patternListener),
publisher.publish('channel', 'message')
]);
assert.ok(channelListener1.calledOnceWithExactly('message', 'channel'));
assert.ok(channelListener2.calledOnceWithExactly('message', 'channel'));
assert.ok(patternListener.calledOnceWithExactly('message', 'channel'));
await subscriber.unsubscribe('channel', channelListener1);
await Promise.all([
waitTillBeenCalled(channelListener2),
waitTillBeenCalled(patternListener),
publisher.publish('channel', 'message')
]);
assert.ok(channelListener1.calledOnce);
assert.ok(channelListener2.calledTwice);
assert.ok(channelListener2.secondCall.calledWithExactly('message', 'channel'));
assert.ok(patternListener.calledTwice);
assert.ok(patternListener.secondCall.calledWithExactly('message', 'channel'));
await subscriber.unsubscribe('channel');
await Promise.all([
waitTillBeenCalled(patternListener),
publisher.publish('channel', 'message')
]);
assert.ok(channelListener1.calledOnce);
assert.ok(channelListener2.calledTwice);
assert.ok(patternListener.calledThrice);
assert.ok(patternListener.thirdCall.calledWithExactly('message', 'channel'));
await subscriber.pUnsubscribe();
await publisher.publish('channel', 'message');
assert.ok(channelListener1.calledOnce);
assert.ok(channelListener2.calledTwice);
assert.ok(patternListener.calledThrice);
// should be able to send commands when unsubsribed from all channels (see #1652)
await assert.doesNotReject(subscriber.ping());
} finally {
await subscriber.disconnect();
}
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('ConnectionTimeoutError', async client => {
const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
start = process.hrtime.bigint();
while (process.hrtime.bigint() - start < 1_000_000) {
// block the event loop for 1ms, to make sure the connection will timeout
}
await promise;
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
socket: {
connectTimeout: 1
}
},
disableClientSetup: true
});
testUtils.testWithClient('client.quit', async client => {
await client.connect();
const pingPromise = client.ping(),
quitPromise = client.quit();
assert.equal(client.isOpen, false);
const [ping] = await Promise.all([
pingPromise,
assert.doesNotReject(quitPromise),
assert.rejects(client.ping(), ClientClosedError)
]);
assert.equal(ping, 'PONG');
}, {
...GLOBAL.SERVERS.OPEN,
disableClientSetup: true
});
testUtils.testWithClient('client.disconnect', async client => {
await client.connect();
const pingPromise = client.ping(),
disconnectPromise = client.disconnect();
assert.equal(client.isOpen, false);
await Promise.all([
assert.rejects(pingPromise, DisconnectsClientError),
assert.doesNotReject(disconnectPromise),
assert.rejects(client.ping(), ClientClosedError)
]);
}, {
...GLOBAL.SERVERS.OPEN,
disableClientSetup: true
});
});

View File

@@ -0,0 +1,541 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
import RedisSocket, { RedisSocketOptions, RedisNetSocketOptions, RedisTlsSocketOptions } from './socket';
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command';
import { EventEmitter } from 'events';
import { CommandOptions, commandOptions, isCommandOptions } from '../command-options';
import { ScanOptions, ZMember } from '../commands/generic-transformers';
import { ScanCommandOptions } from '../commands/SCAN';
import { HScanTuple } from '../commands/HSCAN';
import { extendWithCommands, extendWithModulesAndScripts, LegacyCommandArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
import { ClientClosedError, DisconnectsClientError } from '../errors';
import { URL } from 'url';
export interface RedisClientOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
url?: string;
socket?: RedisSocketOptions;
username?: string;
password?: string;
database?: number;
commandsQueueMaxLength?: number;
readonly?: boolean;
legacyMode?: boolean;
isolationPoolOptions?: PoolOptions;
}
export type RedisClientCommandSignature<C extends RedisCommand> =
(...args: Parameters<C['transformArguments']> | [options: CommandOptions<ClientCommandOptions>, ...rest: Parameters<C['transformArguments']>]) => Promise<RedisCommandReply<C>>;
type WithCommands = {
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
};
export type WithModules<M extends RedisModules> = {
[P in keyof M as M[P] extends never ? never : P]: {
[C in keyof M[P]]: RedisClientCommandSignature<M[P][C]>;
};
};
export type WithScripts<S extends RedisScripts> = {
[P in keyof S as S[P] extends never ? never : P]: RedisClientCommandSignature<S[P]>;
};
export type RedisClientType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
RedisClient<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
export type InstantiableRedisClient<M extends RedisModules, S extends RedisScripts> =
new (...args: ConstructorParameters<typeof RedisClient>) => RedisClientType<M, S>;
export interface ClientCommandOptions extends QueueCommandOptions {
isolated?: boolean;
}
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
export type ClientLegacyCommandArguments = LegacyCommandArguments | [...LegacyCommandArguments, ClientLegacyCallback];
export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> {
return commandOptions(options);
}
static extend<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
const Client = <any>extendWithModulesAndScripts({
BaseClass: RedisClient,
modules: plugins?.modules,
modulesCommandsExecutor: RedisClient.prototype.commandsExecutor,
scripts: plugins?.scripts,
scriptsExecutor: RedisClient.prototype.scriptsExecutor
});
if (Client !== RedisClient) {
Client.prototype.Multi = RedisClientMultiCommand.extend(plugins);
}
return Client;
}
static create<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
return new (RedisClient.extend(options))(options);
}
static parseURL(url: string): RedisClientOptions<Record<string, never>, Record<string, never>> {
// https://www.iana.org/assignments/uri-schemes/prov/redis
const { hostname, port, protocol, username, password, pathname } = new URL(url),
parsed: RedisClientOptions<Record<string, never>, Record<string, never>> = {
socket: {
host: hostname
}
};
if (protocol === 'rediss:') {
(parsed.socket as RedisTlsSocketOptions).tls = true;
} else if (protocol !== 'redis:') {
throw new TypeError('Invalid protocol');
}
if (port) {
(parsed.socket as RedisNetSocketOptions).port = Number(port);
}
if (username) {
parsed.username = decodeURIComponent(username);
}
if (password) {
parsed.password = decodeURIComponent(password);
}
if (pathname.length > 1) {
const database = Number(pathname.substring(1));
if (isNaN(database)) {
throw new TypeError('Invalid pathname');
}
parsed.database = database;
}
return parsed;
}
readonly #options?: RedisClientOptions<M, S>;
readonly #socket: RedisSocket;
readonly #queue: RedisCommandsQueue;
readonly #isolationPool: Pool<RedisClientType<M, S>>;
readonly #v4: Record<string, any> = {};
#selectedDB = 0;
get options(): RedisClientOptions<M, S> | undefined {
return this.#options;
}
get isOpen(): boolean {
return this.#socket.isOpen;
}
get v4(): Record<string, any> {
if (!this.#options?.legacyMode) {
throw new Error('the client is not in "legacy mode"');
}
return this.#v4;
}
constructor(options?: RedisClientOptions<M, S>) {
super();
this.#options = this.#initiateOptions(options);
this.#socket = this.#initiateSocket();
this.#queue = this.#initiateQueue();
this.#isolationPool = createPool({
create: async () => {
const duplicate = this.duplicate({
isolationPoolOptions: undefined
});
await duplicate.connect();
return duplicate;
},
destroy: client => client.disconnect()
}, options?.isolationPoolOptions);
this.#legacyMode();
}
#initiateOptions(options?: RedisClientOptions<M, S>): RedisClientOptions<M, S> | undefined {
if (options?.url) {
const parsed = RedisClient.parseURL(options.url);
if (options.socket) {
parsed.socket = Object.assign(options.socket, parsed.socket);
}
Object.assign(options, parsed);
}
if (options?.database) {
this.#selectedDB = options.database;
}
return options;
}
#initiateSocket(): RedisSocket {
const socketInitiator = async (): Promise<void> => {
const promises = [];
if (this.#selectedDB !== 0) {
promises.push(
this.#queue.addCommand(
['SELECT', this.#selectedDB.toString()],
{ asap: true }
)
);
}
if (this.#options?.readonly) {
promises.push(
this.#queue.addCommand(
COMMANDS.READONLY.transformArguments(),
{ asap: true }
)
);
}
if (this.#options?.username || this.#options?.password) {
promises.push(
this.#queue.addCommand(
COMMANDS.AUTH.transformArguments({
username: this.#options.username,
password: this.#options.password ?? ''
}),
{ asap: true }
)
);
}
const resubscribePromise = this.#queue.resubscribe();
if (resubscribePromise) {
promises.push(resubscribePromise);
}
if (promises.length) {
this.#tick(true);
await Promise.all(promises);
}
};
return new RedisSocket(socketInitiator, this.#options?.socket)
.on('data', data => this.#queue.parseResponse(data))
.on('error', err => {
this.emit('error', err);
this.#queue.flushWaitingForReply(err);
})
.on('connect', () => this.emit('connect'))
.on('ready', () => {
this.emit('ready');
this.#tick();
})
.on('reconnecting', () => this.emit('reconnecting'))
.on('drain', () => this.#tick())
.on('end', () => this.emit('end'));
}
#initiateQueue(): RedisCommandsQueue {
return new RedisCommandsQueue(this.#options?.commandsQueueMaxLength);
}
#legacyMode(): void {
if (!this.#options?.legacyMode) return;
(this as any).#v4.sendCommand = this.#sendCommand.bind(this);
(this as any).sendCommand = (...args: ClientLegacyCommandArguments): void => {
let callback: ClientLegacyCallback;
if (typeof args[args.length - 1] === 'function') {
callback = args.pop() as ClientLegacyCallback;
}
this.#sendCommand(transformLegacyCommandArguments(args as LegacyCommandArguments))
.then((reply: RedisCommandRawReply) => {
if (!callback) return;
// https://github.com/NodeRedis/node-redis#commands:~:text=minimal%20parsing
callback(null, reply);
})
.catch((err: Error) => {
if (!callback) {
this.emit('error', err);
return;
}
callback(err);
});
};
for (const name of Object.keys(COMMANDS)) {
this.#defineLegacyCommand(name);
}
// hard coded commands
this.#defineLegacyCommand('SELECT');
this.#defineLegacyCommand('select');
this.#defineLegacyCommand('SUBSCRIBE');
this.#defineLegacyCommand('subscribe');
this.#defineLegacyCommand('PSUBSCRIBE');
this.#defineLegacyCommand('pSubscribe');
this.#defineLegacyCommand('UNSUBSCRIBE');
this.#defineLegacyCommand('unsubscribe');
this.#defineLegacyCommand('PUNSUBSCRIBE');
this.#defineLegacyCommand('pUnsubscribe');
this.#defineLegacyCommand('QUIT');
this.#defineLegacyCommand('quit');
}
#defineLegacyCommand(name: string): void {
(this as any).#v4[name] = (this as any)[name].bind(this);
(this as any)[name] = (...args: Array<unknown>): void => {
(this as any).sendCommand(name, ...args);
};
}
duplicate(overrides?: Partial<RedisClientOptions<M, S>>): RedisClientType<M, S> {
return new (Object.getPrototypeOf(this).constructor)({
...this.#options,
...overrides
});
}
async connect(): Promise<void> {
await this.#socket.connect();
}
async commandsExecutor(command: RedisCommand, args: Array<unknown>): Promise<RedisCommandReply<typeof command>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
return transformCommandReply(
command,
await this.#sendCommand(redisArgs, options, command.BUFFER_MODE),
redisArgs.preserve
);
}
sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
return this.#sendCommand(args, options, bufferMode);
}
// using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
#sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
if (!this.#socket.isOpen) {
return Promise.reject(new ClientClosedError());
}
if (options?.isolated) {
return this.executeIsolated(isolatedClient =>
isolatedClient.sendCommand(args, {
...options,
isolated: false
})
);
}
const promise = this.#queue.addCommand<T>(args, options, bufferMode);
this.#tick();
return promise;
}
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
return transformCommandReply(
script,
await this.executeScript(script, redisArgs, options, script.BUFFER_MODE),
redisArgs.preserve
);
}
async executeScript(script: RedisScript, args: RedisCommandArguments, options?: ClientCommandOptions, bufferMode?: boolean): Promise<RedisCommandReply<typeof script>> {
try {
return await this.#sendCommand([
'EVALSHA',
script.SHA1,
script.NUMBER_OF_KEYS.toString(),
...args
], options, bufferMode);
} catch (err: any) {
if (!err?.message?.startsWith?.('NOSCRIPT')) {
throw err;
}
return await this.#sendCommand([
'EVAL',
script.SCRIPT,
script.NUMBER_OF_KEYS.toString(),
...args
], options, bufferMode);
}
}
async SELECT(db: number): Promise<void>;
async SELECT(options: CommandOptions<ClientCommandOptions>, db: number): Promise<void>;
async SELECT(options?: any, db?: any): Promise<void> {
if (!isCommandOptions(options)) {
db = options;
options = null;
}
await this.#sendCommand(['SELECT', db.toString()], options);
this.#selectedDB = db;
}
select = this.SELECT;
SUBSCRIBE(channels: string | Array<string>, listener: PubSubListener): Promise<void> {
return this.#subscribe(PubSubSubscribeCommands.SUBSCRIBE, channels, listener);
}
subscribe = this.SUBSCRIBE;
PSUBSCRIBE(patterns: string | Array<string>, listener: PubSubListener): Promise<void> {
return this.#subscribe(PubSubSubscribeCommands.PSUBSCRIBE, patterns, listener);
}
pSubscribe = this.PSUBSCRIBE;
#subscribe(command: PubSubSubscribeCommands, channels: string | Array<string>, listener: PubSubListener): Promise<void> {
const promise = this.#queue.subscribe(command, channels, listener);
this.#tick();
return promise;
}
UNSUBSCRIBE(channels?: string | Array<string>, listener?: PubSubListener): Promise<void> {
return this.#unsubscribe(PubSubUnsubscribeCommands.UNSUBSCRIBE, channels, listener);
}
unsubscribe = this.UNSUBSCRIBE;
PUNSUBSCRIBE(patterns?: string | Array<string>, listener?: PubSubListener): Promise<void> {
return this.#unsubscribe(PubSubUnsubscribeCommands.PUNSUBSCRIBE, patterns, listener);
}
pUnsubscribe = this.PUNSUBSCRIBE;
#unsubscribe(command: PubSubUnsubscribeCommands, channels?: string | Array<string>, listener?: PubSubListener): Promise<void> {
const promise = this.#queue.unsubscribe(command, channels, listener);
this.#tick();
return promise;
}
QUIT(): Promise<void> {
return this.#socket.quit(() => {
const quitPromise = this.#queue.addCommand(['QUIT']);
this.#tick();
return Promise.all([
quitPromise,
this.#destroyIsolationPool()
]);
});
}
quit = this.QUIT;
#tick(force = false): void {
if (this.#socket.writableNeedDrain || (!force && !this.#socket.isReady)) {
return;
}
this.#socket.cork();
while (!this.#socket.writableNeedDrain) {
const args = this.#queue.getCommandToSend();
if (args === undefined) break;
this.#socket.writeCommand(args);
}
}
executeIsolated<T>(fn: (client: RedisClientType<M, S>) => T | Promise<T>): Promise<T> {
return this.#isolationPool.use(fn);
}
multi(): RedisClientMultiCommandType<M, S> {
return new (this as any).Multi(
this.multiExecutor.bind(this),
this.#options?.legacyMode
);
}
multiExecutor(commands: Array<RedisMultiQueuedCommand>, chainId?: symbol): Promise<Array<RedisCommandRawReply>> {
const promise = Promise.all(
commands.map(({ args }) => {
return this.#queue.addCommand(args, RedisClient.commandOptions({
chainId
}));
})
);
this.#tick();
return promise;
}
async* scanIterator(options?: ScanCommandOptions): AsyncIterable<string> {
let cursor = 0;
do {
const reply = await (this as any).scan(cursor, options);
cursor = reply.cursor;
for (const key of reply.keys) {
yield key;
}
} while (cursor !== 0);
}
async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<HScanTuple> {
let cursor = 0;
do {
const reply = await (this as any).hScan(key, cursor, options);
cursor = reply.cursor;
for (const tuple of reply.tuples) {
yield tuple;
}
} while (cursor !== 0);
}
async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable<string> {
let cursor = 0;
do {
const reply = await (this as any).sScan(key, cursor, options);
cursor = reply.cursor;
for (const member of reply.members) {
yield member;
}
} while (cursor !== 0);
}
async* zScanIterator(key: string, options?: ScanOptions): AsyncIterable<ZMember> {
let cursor = 0;
do {
const reply = await (this as any).zScan(key, cursor, options);
cursor = reply.cursor;
for (const member of reply.members) {
yield member;
}
} while (cursor !== 0);
}
async disconnect(): Promise<void> {
this.#queue.flushAll(new DisconnectsClientError());
this.#socket.disconnect();
await this.#destroyIsolationPool();
}
async #destroyIsolationPool(): Promise<void> {
await this.#isolationPool.drain();
await this.#isolationPool.clear();
}
}
extendWithCommands({
BaseClass: RedisClient,
commands: COMMANDS,
executor: RedisClient.prototype.commandsExecutor
});
(RedisClient.prototype as any).Multi = RedisClientMultiCommand;

View File

@@ -0,0 +1,132 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
import { extendWithCommands, extendWithModulesAndScripts, LegacyCommandArguments, transformLegacyCommandArguments } from '../commander';
type RedisClientMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
(...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, S>;
type WithCommands<M extends RedisModules, S extends RedisScripts> = {
[P in keyof typeof COMMANDS]: RedisClientMultiCommandSignature<(typeof COMMANDS)[P], M, S>
};
type WithModules<M extends RedisModules, S extends RedisScripts> = {
[P in keyof M as M[P] extends never ? never : P]: {
[C in keyof M[P]]: RedisClientMultiCommandSignature<M[P][C], M, S>;
};
};
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
[P in keyof S as S[P] extends never ? never : P]: RedisClientMultiCommandSignature<S[P], M, S>
};
export type RedisClientMultiCommandType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
RedisClientMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
export type RedisClientMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
export default class RedisClientMultiCommand {
readonly #multi = new RedisMultiCommand();
readonly #executor: RedisClientMultiExecutor;
static extend<M extends RedisModules, S extends RedisScripts>(
plugins?: RedisPlugins<M, S>
): new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisClientMultiCommandType<M, S> {
return <any>extendWithModulesAndScripts({
BaseClass: RedisClientMultiCommand,
modules: plugins?.modules,
modulesCommandsExecutor: RedisClientMultiCommand.prototype.commandsExecutor,
scripts: plugins?.scripts,
scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor
});
}
readonly v4: Record<string, any> = {};
constructor(executor: RedisClientMultiExecutor, legacyMode = false) {
this.#executor = executor;
if (legacyMode) {
this.#legacyMode();
}
}
#legacyMode(): void {
this.v4.addCommand = this.addCommand.bind(this);
(this as any).addCommand = (...args: LegacyCommandArguments): this => {
this.#multi.addCommand(transformLegacyCommandArguments(args));
return this;
};
this.v4.exec = this.exec.bind(this);
(this as any).exec = (callback?: (err: Error | null, replies?: Array<unknown>) => unknown): void => {
this.v4.exec()
.then((reply: Array<unknown>) => {
if (!callback) return;
callback(null, reply);
})
.catch((err: Error) => {
if (!callback) {
// this.emit('error', err);
return;
}
callback(err);
});
};
for (const name of Object.keys(COMMANDS)) {
this.#defineLegacyCommand(name);
}
}
#defineLegacyCommand(name: string): void {
(this as any).v4[name] = (this as any)[name].bind(this.v4);
(this as any)[name] = (...args: Array<unknown>): void => (this as any).addCommand(name, args);
}
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
return this.addCommand(
command.transformArguments(...args),
command.transformReply
);
}
addCommand(args: RedisCommandArguments, transformReply?: RedisCommand['transformReply']): this {
this.#multi.addCommand(args, transformReply);
return this;
}
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
this.#multi.addScript(script, args);
return this;
}
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
if (execAsPipeline) {
return this.execAsPipeline();
}
const commands = this.#multi.exec();
if (!commands) return [];
return this.#multi.handleExecReplies(
await this.#executor(commands, RedisMultiCommand.generateChainId())
);
}
EXEC = this.exec;
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
if (!this.#multi.queue.length) return [];
return this.#multi.transformReplies(
await this.#executor(this.#multi.queue)
);
}
}
extendWithCommands({
BaseClass: RedisClientMultiCommand,
commands: COMMANDS,
executor: RedisClientMultiCommand.prototype.commandsExecutor
});

View File

@@ -0,0 +1,38 @@
import { strict as assert } from 'assert';
import { SinonFakeTimers, useFakeTimers, spy } from 'sinon';
import RedisSocket from './socket';
describe('Socket', () => {
describe('reconnectStrategy', () => {
let clock: SinonFakeTimers;
beforeEach(() => clock = useFakeTimers());
afterEach(() => clock.uninstall());
it('custom strategy', () => {
const reconnectStrategy = spy((retries: number): number | Error => {
assert.equal(retries + 1, reconnectStrategy.callCount);
if (retries === 50) {
return Error('50');
}
const time = retries * 2;
queueMicrotask(() => clock.tick(time));
return time;
});
const socket = new RedisSocket(undefined, {
host: 'error',
reconnectStrategy
});
socket.on('error', () => {
// ignore errors
});
return assert.rejects(socket.connect(), {
message: '50'
});
});
});
});

View File

@@ -0,0 +1,267 @@
import { EventEmitter } from 'events';
import * as net from 'net';
import * as tls from 'tls';
import { encodeCommand } from '../commander';
import { RedisCommandArguments } from '../commands';
import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError } from '../errors';
import { promiseTimeout } from '../utils';
export interface RedisSocketCommonOptions {
connectTimeout?: number;
noDelay?: boolean;
keepAlive?: number | false;
reconnectStrategy?(retries: number): number | Error;
}
export interface RedisNetSocketOptions extends RedisSocketCommonOptions {
port?: number;
host?: string;
}
export interface RedisUnixSocketOptions extends RedisSocketCommonOptions {
path: string;
}
export interface RedisTlsSocketOptions extends RedisNetSocketOptions, tls.SecureContextOptions, tls.CommonConnectionOptions {
tls: true;
}
export type RedisSocketOptions = RedisNetSocketOptions | RedisUnixSocketOptions | RedisTlsSocketOptions;
interface CreateSocketReturn<T> {
connectEvent: string;
socket: T;
}
export type RedisSocketInitiator = () => Promise<void>;
export default class RedisSocket extends EventEmitter {
static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions {
options ??= {};
if (!RedisSocket.#isUnixSocket(options)) {
(options as RedisNetSocketOptions).port ??= 6379;
(options as RedisNetSocketOptions).host ??= '127.0.0.1';
}
options.connectTimeout ??= 5000;
options.keepAlive ??= 5000;
options.noDelay ??= true;
return options;
}
static #defaultReconnectStrategy(retries: number): number {
return Math.min(retries * 50, 500);
}
static #isUnixSocket(options: RedisSocketOptions): options is RedisUnixSocketOptions {
return Object.prototype.hasOwnProperty.call(options, 'path');
}
static #isTlsSocket(options: RedisSocketOptions): options is RedisTlsSocketOptions {
return (options as RedisTlsSocketOptions).tls === true;
}
readonly #initiator?: RedisSocketInitiator;
readonly #options: RedisSocketOptions;
#socket?: net.Socket | tls.TLSSocket;
#isOpen = false;
get isOpen(): boolean {
return this.#isOpen;
}
#isReady = false;
get isReady(): boolean {
return this.#isReady;
}
// `writable.writableNeedDrain` was added in v15.2.0 and therefore can't be used
// https://nodejs.org/api/stream.html#stream_writable_writableneeddrain
#writableNeedDrain = false;
get writableNeedDrain(): boolean {
return this.#writableNeedDrain;
}
constructor(initiator?: RedisSocketInitiator, options?: RedisSocketOptions) {
super();
this.#initiator = initiator;
this.#options = RedisSocket.#initiateOptions(options);
}
async connect(): Promise<void> {
if (this.#isOpen) {
throw new Error('Socket already opened');
}
return this.#connect();
}
async #connect(hadError?: boolean): Promise<void> {
this.#isOpen = true;
this.#socket = await this.#retryConnection(0, hadError);
this.#writableNeedDrain = false;
if (!this.#isOpen) {
this.disconnect();
return;
}
this.emit('connect');
if (this.#initiator) {
try {
await this.#initiator();
} catch (err) {
this.#socket.destroy();
this.#socket = undefined;
this.#isOpen = false;
throw err;
}
if (!this.#isOpen) return;
}
this.#isReady = true;
this.emit('ready');
}
async #retryConnection(retries: number, hadError?: boolean): Promise<net.Socket | tls.TLSSocket> {
if (retries > 0 || hadError) {
this.emit('reconnecting');
}
try {
return await this.#createSocket();
} catch (err) {
this.emit('error', err);
if (!this.#isOpen) {
throw err;
}
const retryIn = (this.#options?.reconnectStrategy ?? RedisSocket.#defaultReconnectStrategy)(retries);
if (retryIn instanceof Error) {
throw retryIn;
}
await promiseTimeout(retryIn);
return this.#retryConnection(retries + 1);
}
}
#createSocket(): Promise<net.Socket | tls.TLSSocket> {
return new Promise((resolve, reject) => {
const {connectEvent, socket} = RedisSocket.#isTlsSocket(this.#options) ?
this.#createTlsSocket() :
this.#createNetSocket();
if (this.#options.connectTimeout) {
socket.setTimeout(this.#options.connectTimeout, () => socket.destroy(new ConnectionTimeoutError()));
}
socket
.setNoDelay(this.#options.noDelay)
.setKeepAlive(this.#options.keepAlive !== false, this.#options.keepAlive || 0)
.once('error', reject)
.once(connectEvent, () => {
socket
.setTimeout(0)
.off('error', reject)
.once('error', (err: Error) => this.#onSocketError(err))
.once('close', hadError => {
if (!hadError && this.#isOpen) {
this.#onSocketError(new SocketClosedUnexpectedlyError());
}
})
.on('drain', () => {
this.#writableNeedDrain = false;
this.emit('drain');
})
.on('data', (data: Buffer) => this.emit('data', data));
resolve(socket);
});
});
}
#createNetSocket(): CreateSocketReturn<net.Socket> {
return {
connectEvent: 'connect',
socket: net.connect(this.#options as net.NetConnectOpts) // TODO
};
}
#createTlsSocket(): CreateSocketReturn<tls.TLSSocket> {
return {
connectEvent: 'secureConnect',
socket: tls.connect(this.#options as tls.ConnectionOptions) // TODO
};
}
#onSocketError(err: Error): void {
this.#isReady = false;
this.emit('error', err);
this.#connect(true).catch(() => {
// the error was already emitted, silently ignore it
});
}
writeCommand(args: RedisCommandArguments): void {
if (!this.#socket) {
throw new ClientClosedError();
}
for (const toWrite of encodeCommand(args)) {
this.#writableNeedDrain = !this.#socket.write(toWrite);
}
}
disconnect(): void {
if (!this.#socket) {
throw new ClientClosedError();
} else {
this.#isOpen = this.#isReady = false;
}
this.#socket.destroy();
this.#socket = undefined;
this.emit('end');
}
async quit(fn: () => Promise<unknown>): Promise<void> {
if (!this.#isOpen) {
throw new ClientClosedError();
}
this.#isOpen = false;
await fn();
this.disconnect();
}
#isCorked = false;
cork(): void {
if (!this.#socket) {
return;
}
if (!this.#isCorked) {
this.#socket.cork();
this.#isCorked = true;
queueMicrotask(() => {
this.#socket?.uncork();
this.#isCorked = false;
});
}
}
}

View File

@@ -0,0 +1,228 @@
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES';
import { RedisClusterClientOptions, RedisClusterOptions } from '.';
import { RedisModules, RedisScripts } from '../commands';
// We need to use 'require', because it's not possible with Typescript to import
// function that are exported as 'module.exports = function`, without esModuleInterop
// set to true.
const calculateSlot = require('cluster-key-slot');
export interface ClusterNode<M extends RedisModules, S extends RedisScripts> {
id: string;
client: RedisClientType<M, S>;
}
interface SlotNodes<M extends RedisModules, S extends RedisScripts> {
master: ClusterNode<M, S>;
replicas: Array<ClusterNode<M, S>>;
clientIterator: IterableIterator<RedisClientType<M, S>> | undefined;
}
type OnError = (err: unknown) => void;
export default class RedisClusterSlots<M extends RedisModules, S extends RedisScripts> {
readonly #options: RedisClusterOptions<M, S>;
readonly #Client: InstantiableRedisClient<M, S>;
readonly #onError: OnError;
readonly #nodeByUrl = new Map<string, ClusterNode<M, S>>();
readonly #slots: Array<SlotNodes<M, S>> = [];
constructor(options: RedisClusterOptions<M, S>, onError: OnError) {
this.#options = options;
this.#Client = RedisClient.extend(options);
this.#onError = onError;
}
async connect(): Promise<void> {
for (const rootNode of this.#options.rootNodes) {
if (await this.#discoverNodes(this.#clientOptionsDefaults(rootNode))) return;
}
throw new Error('None of the root nodes is available');
}
async discover(startWith: RedisClientType<M, S>): Promise<void> {
if (await this.#discoverNodes(startWith.options)) return;
for (const { client } of this.#nodeByUrl.values()) {
if (client === startWith) continue;
if (await this.#discoverNodes(client.options)) return;
}
throw new Error('None of the cluster nodes is available');
}
async #discoverNodes(clientOptions?: RedisClusterClientOptions): Promise<boolean> {
const client = new this.#Client(clientOptions);
await client.connect();
try {
await this.#reset(await client.clusterNodes());
return true;
} catch (err) {
this.#onError(err);
return false;
} finally {
if (client.isOpen) {
await client.disconnect();
}
}
}
async #reset(masters: Array<RedisClusterMasterNode>): Promise<void> {
// Override this.#slots and add not existing clients to this.#nodeByUrl
const promises: Array<Promise<void>> = [],
clientsInUse = new Set<string>();
for (const master of masters) {
const slot = {
master: this.#initiateClientForNode(master, false, clientsInUse, promises),
replicas: this.#options.useReplicas ?
master.replicas.map(replica => this.#initiateClientForNode(replica, true, clientsInUse, promises)) :
[],
clientIterator: undefined // will be initiated in use
};
for (const { from, to } of master.slots) {
for (let i = from; i <= to; i++) {
this.#slots[i] = slot;
}
}
}
// Remove unused clients from this.#nodeByUrl using clientsInUse
for (const [url, { client }] of this.#nodeByUrl.entries()) {
if (clientsInUse.has(url)) continue;
promises.push(client.disconnect());
this.#nodeByUrl.delete(url);
}
await Promise.all(promises);
}
#clientOptionsDefaults(options: RedisClusterClientOptions): RedisClusterClientOptions {
if (!this.#options.defaults) return options;
const merged = Object.assign({}, this.#options.defaults, options);
if (options.socket && this.#options.defaults.socket) {
Object.assign({}, this.#options.defaults.socket, options.socket);
}
return merged;
}
#initiateClientForNode(nodeData: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set<string>, promises: Array<Promise<void>>): ClusterNode<M, S> {
const url = `${nodeData.host}:${nodeData.port}`;
clientsInUse.add(url);
let node = this.#nodeByUrl.get(url);
if (!node) {
node = {
id: nodeData.id,
client: new this.#Client(
this.#clientOptionsDefaults({
socket: {
host: nodeData.host,
port: nodeData.port
},
readonly
})
)
};
promises.push(node.client.connect());
this.#nodeByUrl.set(url, node);
}
return node;
}
getSlotMaster(slot: number): ClusterNode<M, S> {
return this.#slots[slot].master;
}
*#slotClientIterator(slotNumber: number): IterableIterator<RedisClientType<M, S>> {
const slot = this.#slots[slotNumber];
yield slot.master.client;
for (const replica of slot.replicas) {
yield replica.client;
}
}
#getSlotClient(slotNumber: number): RedisClientType<M, S> {
const slot = this.#slots[slotNumber];
if (!slot.clientIterator) {
slot.clientIterator = this.#slotClientIterator(slotNumber);
}
const {done, value} = slot.clientIterator.next();
if (done) {
slot.clientIterator = undefined;
return this.#getSlotClient(slotNumber);
}
return value;
}
#randomClientIterator?: IterableIterator<ClusterNode<M, S>>;
#getRandomClient(): RedisClientType<M, S> {
if (!this.#nodeByUrl.size) {
throw new Error('Cluster is not connected');
}
if (!this.#randomClientIterator) {
this.#randomClientIterator = this.#nodeByUrl.values();
}
const {done, value} = this.#randomClientIterator.next();
if (done) {
this.#randomClientIterator = undefined;
return this.#getRandomClient();
}
return value.client;
}
getClient(firstKey?: string | Buffer, isReadonly?: boolean): RedisClientType<M, S> {
if (!firstKey) {
return this.#getRandomClient();
}
const slot = calculateSlot(firstKey);
if (!isReadonly || !this.#options.useReplicas) {
return this.getSlotMaster(slot).client;
}
return this.#getSlotClient(slot);
}
getMasters(): Array<ClusterNode<M, S>> {
const masters = [];
for (const node of this.#nodeByUrl.values()) {
if (node.client.options?.readonly) continue;
masters.push(node);
}
return masters;
}
getNodeByUrl(url: string): ClusterNode<M, S> | undefined {
return this.#nodeByUrl.get(url);
}
async disconnect(): Promise<void> {
await Promise.all(
[...this.#nodeByUrl.values()].map(({ client }) => client.disconnect())
);
this.#nodeByUrl.clear();
this.#slots.splice(0);
}
}

View File

@@ -0,0 +1,535 @@
import * as APPEND from '../commands/APPEND';
import * as BITCOUNT from '../commands/BITCOUNT';
import * as BITFIELD from '../commands/BITFIELD';
import * as BITOP from '../commands/BITOP';
import * as BITPOS from '../commands/BITPOS';
import * as BLMOVE from '../commands/BLMOVE';
import * as BLPOP from '../commands/BLPOP';
import * as BRPOP from '../commands/BRPOP';
import * as BRPOPLPUSH from '../commands/BRPOPLPUSH';
import * as BZPOPMAX from '../commands/BZPOPMAX';
import * as BZPOPMIN from '../commands/BZPOPMIN';
import * as COPY from '../commands/COPY';
import * as DECR from '../commands/DECR';
import * as DECRBY from '../commands/DECRBY';
import * as DEL from '../commands/DEL';
import * as DUMP from '../commands/DUMP';
import * as EVAL from '../commands/EVAL';
import * as EVALSHA from '../commands/EVALSHA';
import * as EXISTS from '../commands/EXISTS';
import * as EXPIRE from '../commands/EXPIRE';
import * as EXPIREAT from '../commands/EXPIREAT';
import * as GEOADD from '../commands/GEOADD';
import * as GEODIST from '../commands/GEODIST';
import * as GEOHASH from '../commands/GEOHASH';
import * as GEOPOS from '../commands/GEOPOS';
import * as GEOSEARCH_WITH from '../commands/GEOSEARCH_WITH';
import * as GEOSEARCH from '../commands/GEOSEARCH';
import * as GEOSEARCHSTORE from '../commands/GEOSEARCHSTORE';
import * as GET_BUFFER from '../commands/GET_BUFFER';
import * as GET from '../commands/GET';
import * as GETBIT from '../commands/GETBIT';
import * as GETDEL from '../commands/GETDEL';
import * as GETEX from '../commands/GETEX';
import * as GETRANGE from '../commands/GETRANGE';
import * as GETSET from '../commands/GETSET';
import * as HDEL from '../commands/HDEL';
import * as HEXISTS from '../commands/HEXISTS';
import * as HGET from '../commands/HGET';
import * as HGETALL from '../commands/HGETALL';
import * as HINCRBY from '../commands/HINCRBY';
import * as HINCRBYFLOAT from '../commands/HINCRBYFLOAT';
import * as HKEYS from '../commands/HKEYS';
import * as HLEN from '../commands/HLEN';
import * as HMGET from '../commands/HMGET';
import * as HRANDFIELD_COUNT_WITHVALUES from '../commands/HRANDFIELD_COUNT_WITHVALUES';
import * as HRANDFIELD_COUNT from '../commands/HRANDFIELD_COUNT';
import * as HRANDFIELD from '../commands/HRANDFIELD';
import * as HSCAN from '../commands/HSCAN';
import * as HSET from '../commands/HSET';
import * as HSETNX from '../commands/HSETNX';
import * as HSTRLEN from '../commands/HSTRLEN';
import * as HVALS from '../commands/HVALS';
import * as INCR from '../commands/INCR';
import * as INCRBY from '../commands/INCRBY';
import * as INCRBYFLOAT from '../commands/INCRBYFLOAT';
import * as LINDEX from '../commands/LINDEX';
import * as LINSERT from '../commands/LINSERT';
import * as LLEN from '../commands/LLEN';
import * as LMOVE from '../commands/LMOVE';
import * as LPOP_COUNT from '../commands/LPOP_COUNT';
import * as LPOP from '../commands/LPOP';
import * as LPOS_COUNT from '../commands/LPOS_COUNT';
import * as LPOS from '../commands/LPOS';
import * as LPUSH from '../commands/LPUSH';
import * as LPUSHX from '../commands/LPUSHX';
import * as LRANGE from '../commands/LRANGE';
import * as LREM from '../commands/LREM';
import * as LSET from '../commands/LSET';
import * as LTRIM from '../commands/LTRIM';
import * as MGET from '../commands/MGET';
import * as MIGRATE from '../commands/MIGRATE';
import * as MSET from '../commands/MSET';
import * as MSETNX from '../commands/MSETNX';
import * as PERSIST from '../commands/PERSIST';
import * as PEXPIRE from '../commands/PEXPIRE';
import * as PEXPIREAT from '../commands/PEXPIREAT';
import * as PFADD from '../commands/PFADD';
import * as PFCOUNT from '../commands/PFCOUNT';
import * as PFMERGE from '../commands/PFMERGE';
import * as PSETEX from '../commands/PSETEX';
import * as PTTL from '../commands/PTTL';
import * as PUBLISH from '../commands/PUBLISH';
import * as RENAME from '../commands/RENAME';
import * as RENAMENX from '../commands/RENAMENX';
import * as RPOP_COUNT from '../commands/RPOP_COUNT';
import * as RPOP from '../commands/RPOP';
import * as RPOPLPUSH from '../commands/RPOPLPUSH';
import * as RPUSH from '../commands/RPUSH';
import * as RPUSHX from '../commands/RPUSHX';
import * as SADD from '../commands/SADD';
import * as SCARD from '../commands/SCARD';
import * as SDIFF from '../commands/SDIFF';
import * as SDIFFSTORE from '../commands/SDIFFSTORE';
import * as SET from '../commands/SET';
import * as SETBIT from '../commands/SETBIT';
import * as SETEX from '../commands/SETEX';
import * as SETNX from '../commands/SETNX';
import * as SETRANGE from '../commands/SETRANGE';
import * as SINTER from '../commands/SINTER';
import * as SINTERSTORE from '../commands/SINTERSTORE';
import * as SISMEMBER from '../commands/SISMEMBER';
import * as SMEMBERS from '../commands/SMEMBERS';
import * as SMISMEMBER from '../commands/SMISMEMBER';
import * as SMOVE from '../commands/SMOVE';
import * as SORT from '../commands/SORT';
import * as SPOP from '../commands/SPOP';
import * as SRANDMEMBER_COUNT from '../commands/SRANDMEMBER_COUNT';
import * as SRANDMEMBER from '../commands/SRANDMEMBER';
import * as SREM from '../commands/SREM';
import * as SSCAN from '../commands/SSCAN';
import * as STRLEN from '../commands/STRLEN';
import * as SUNION from '../commands/SUNION';
import * as SUNIONSTORE from '../commands/SUNIONSTORE';
import * as TOUCH from '../commands/TOUCH';
import * as TTL from '../commands/TTL';
import * as TYPE from '../commands/TYPE';
import * as UNLINK from '../commands/UNLINK';
import * as WATCH from '../commands/WATCH';
import * as XACK from '../commands/XACK';
import * as XADD from '../commands/XADD';
import * as XAUTOCLAIM_JUSTID from '../commands/XAUTOCLAIM_JUSTID';
import * as XAUTOCLAIM from '../commands/XAUTOCLAIM';
import * as XCLAIM from '../commands/XCLAIM';
import * as XCLAIM_JUSTID from '../commands/XCLAIM_JUSTID';
import * as XDEL from '../commands/XDEL';
import * as XGROUP_CREATE from '../commands/XGROUP_CREATE';
import * as XGROUP_CREATECONSUMER from '../commands/XGROUP_CREATECONSUMER';
import * as XGROUP_DELCONSUMER from '../commands/XGROUP_DELCONSUMER';
import * as XGROUP_DESTROY from '../commands/XGROUP_DESTROY';
import * as XGROUP_SETID from '../commands/XGROUP_SETID';
import * as XINFO_CONSUMERS from '../commands/XINFO_CONSUMERS';
import * as XINFO_GROUPS from '../commands/XINFO_GROUPS';
import * as XINFO_STREAM from '../commands/XINFO_STREAM';
import * as XLEN from '../commands/XLEN';
import * as XPENDING_RANGE from '../commands/XPENDING_RANGE';
import * as XPENDING from '../commands/XPENDING';
import * as XRANGE from '../commands/XRANGE';
import * as XREAD from '../commands/XREAD';
import * as XREADGROUP from '../commands/XREADGROUP';
import * as XREVRANGE from '../commands/XREVRANGE';
import * as XTRIM from '../commands/XTRIM';
import * as ZADD from '../commands/ZADD';
import * as ZCARD from '../commands/ZCARD';
import * as ZCOUNT from '../commands/ZCOUNT';
import * as ZDIFF_WITHSCORES from '../commands/ZDIFF_WITHSCORES';
import * as ZDIFF from '../commands/ZDIFF';
import * as ZDIFFSTORE from '../commands/ZDIFFSTORE';
import * as ZINCRBY from '../commands/ZINCRBY';
import * as ZINTER_WITHSCORES from '../commands/ZINTER_WITHSCORES';
import * as ZINTER from '../commands/ZINTER';
import * as ZINTERSTORE from '../commands/ZINTERSTORE';
import * as ZLEXCOUNT from '../commands/ZLEXCOUNT';
import * as ZMSCORE from '../commands/ZMSCORE';
import * as ZPOPMAX_COUNT from '../commands/ZPOPMAX_COUNT';
import * as ZPOPMAX from '../commands/ZPOPMAX';
import * as ZPOPMIN_COUNT from '../commands/ZPOPMIN_COUNT';
import * as ZPOPMIN from '../commands/ZPOPMIN';
import * as ZRANDMEMBER_COUNT_WITHSCORES from '../commands/ZRANDMEMBER_COUNT_WITHSCORES';
import * as ZRANDMEMBER_COUNT from '../commands/ZRANDMEMBER_COUNT';
import * as ZRANDMEMBER from '../commands/ZRANDMEMBER';
import * as ZRANGE_WITHSCORES from '../commands/ZRANGE_WITHSCORES';
import * as ZRANGE from '../commands/ZRANGE';
import * as ZRANGEBYLEX from '../commands/ZRANGEBYLEX';
import * as ZRANGEBYSCORE_WITHSCORES from '../commands/ZRANGEBYSCORE_WITHSCORES';
import * as ZRANGEBYSCORE from '../commands/ZRANGEBYSCORE';
import * as ZRANGESTORE from '../commands/ZRANGESTORE';
import * as ZRANK from '../commands/ZRANK';
import * as ZREM from '../commands/ZREM';
import * as ZREMRANGEBYLEX from '../commands/ZREMRANGEBYLEX';
import * as ZREMRANGEBYRANK from '../commands/ZREMRANGEBYRANK';
import * as ZREMRANGEBYSCORE from '../commands/ZREMRANGEBYSCORE';
import * as ZREVRANK from '../commands/ZREVRANK';
import * as ZSCAN from '../commands/ZSCAN';
import * as ZSCORE from '../commands/ZSCORE';
import * as ZUNION_WITHSCORES from '../commands/ZUNION_WITHSCORES';
import * as ZUNION from '../commands/ZUNION';
import * as ZUNIONSTORE from '../commands/ZUNIONSTORE';
export default {
APPEND,
append: APPEND,
BITCOUNT,
bitCount: BITCOUNT,
BITFIELD,
bitField: BITFIELD,
BITOP,
bitOp: BITOP,
BITPOS,
bitPos: BITPOS,
BLMOVE,
blMove: BLMOVE,
BLPOP,
blPop: BLPOP,
BRPOP,
brPop: BRPOP,
BRPOPLPUSH,
brPopLPush: BRPOPLPUSH,
BZPOPMAX,
bzPopMax: BZPOPMAX,
BZPOPMIN,
bzPopMin: BZPOPMIN,
COPY,
copy: COPY,
DECR,
decr: DECR,
DECRBY,
decrBy: DECRBY,
DEL,
del: DEL,
DUMP,
dump: DUMP,
EVAL,
eval: EVAL,
EVALSHA,
evalSha: EVALSHA,
EXISTS,
exists: EXISTS,
EXPIRE,
expire: EXPIRE,
EXPIREAT,
expireAt: EXPIREAT,
GEOADD,
geoAdd: GEOADD,
GEODIST,
geoDist: GEODIST,
GEOHASH,
geoHash: GEOHASH,
GEOPOS,
geoPos: GEOPOS,
GEOSEARCH_WITH,
geoSearchWith: GEOSEARCH_WITH,
GEOSEARCH,
geoSearch: GEOSEARCH,
GEOSEARCHSTORE,
geoSearchStore: GEOSEARCHSTORE,
GET_BUFFER,
getBuffer: GET_BUFFER,
GET,
get: GET,
GETBIT,
getBit: GETBIT,
GETDEL,
getDel: GETDEL,
GETEX,
getEx: GETEX,
GETRANGE,
getRange: GETRANGE,
GETSET,
getSet: GETSET,
HDEL,
hDel: HDEL,
HEXISTS,
hExists: HEXISTS,
HGET,
hGet: HGET,
HGETALL,
hGetAll: HGETALL,
HINCRBY,
hIncrBy: HINCRBY,
HINCRBYFLOAT,
hIncrByFloat: HINCRBYFLOAT,
HKEYS,
hKeys: HKEYS,
HLEN,
hLen: HLEN,
HMGET,
hmGet: HMGET,
HRANDFIELD_COUNT_WITHVALUES,
hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES,
HRANDFIELD_COUNT,
hRandFieldCount: HRANDFIELD_COUNT,
HRANDFIELD,
hRandField: HRANDFIELD,
HSCAN,
hScan: HSCAN,
HSET,
hSet: HSET,
HSETNX,
hSetNX: HSETNX,
HSTRLEN,
hStrLen: HSTRLEN,
HVALS,
hVals: HVALS,
INCR,
incr: INCR,
INCRBY,
incrBy: INCRBY,
INCRBYFLOAT,
incrByFloat: INCRBYFLOAT,
LINDEX,
lIndex: LINDEX,
LINSERT,
lInsert: LINSERT,
LLEN,
lLen: LLEN,
LMOVE,
lMove: LMOVE,
LPOP_COUNT,
lPopCount: LPOP_COUNT,
LPOP,
lPop: LPOP,
LPOS_COUNT,
lPosCount: LPOS_COUNT,
LPOS,
lPos: LPOS,
LPUSH,
lPush: LPUSH,
LPUSHX,
lPushX: LPUSHX,
LRANGE,
lRange: LRANGE,
LREM,
lRem: LREM,
LSET,
lSet: LSET,
LTRIM,
lTrim: LTRIM,
MGET,
mGet: MGET,
MIGRATE,
migrate: MIGRATE,
MSET,
mSet: MSET,
MSETNX,
mSetNX: MSETNX,
PERSIST,
persist: PERSIST,
PEXPIRE,
pExpire: PEXPIRE,
PEXPIREAT,
pExpireAt: PEXPIREAT,
PFADD,
pfAdd: PFADD,
PFCOUNT,
pfCount: PFCOUNT,
PFMERGE,
pfMerge: PFMERGE,
PSETEX,
pSetEx: PSETEX,
PTTL,
pTTL: PTTL,
PUBLISH,
publish: PUBLISH,
RENAME,
rename: RENAME,
RENAMENX,
renameNX: RENAMENX,
RPOP_COUNT,
rPopCount: RPOP_COUNT,
RPOP,
rPop: RPOP,
RPOPLPUSH,
rPopLPush: RPOPLPUSH,
RPUSH,
rPush: RPUSH,
RPUSHX,
rPushX: RPUSHX,
SADD,
sAdd: SADD,
SCARD,
sCard: SCARD,
SDIFF,
sDiff: SDIFF,
SDIFFSTORE,
sDiffStore: SDIFFSTORE,
SINTER,
sInter: SINTER,
SINTERSTORE,
sInterStore: SINTERSTORE,
SET,
set: SET,
SETBIT,
setBit: SETBIT,
SETEX,
setEx: SETEX,
SETNX,
setNX: SETNX,
SETRANGE,
setRange: SETRANGE,
SISMEMBER,
sIsMember: SISMEMBER,
SMEMBERS,
sMembers: SMEMBERS,
SMISMEMBER,
smIsMember: SMISMEMBER,
SMOVE,
sMove: SMOVE,
SORT,
sort: SORT,
SPOP,
sPop: SPOP,
SRANDMEMBER_COUNT,
sRandMemberCount: SRANDMEMBER_COUNT,
SRANDMEMBER,
sRandMember: SRANDMEMBER,
SREM,
sRem: SREM,
SSCAN,
sScan: SSCAN,
STRLEN,
strLen: STRLEN,
SUNION,
sUnion: SUNION,
SUNIONSTORE,
sUnionStore: SUNIONSTORE,
TOUCH,
touch: TOUCH,
TTL,
ttl: TTL,
TYPE,
type: TYPE,
UNLINK,
unlink: UNLINK,
WATCH,
watch: WATCH,
XACK,
xAck: XACK,
XADD,
xAdd: XADD,
XAUTOCLAIM_JUSTID,
xAutoClaimJustId: XAUTOCLAIM_JUSTID,
XAUTOCLAIM,
xAutoClaim: XAUTOCLAIM,
XCLAIM,
xClaim: XCLAIM,
XCLAIM_JUSTID,
xClaimJustId: XCLAIM_JUSTID,
XDEL,
xDel: XDEL,
XGROUP_CREATE,
xGroupCreate: XGROUP_CREATE,
XGROUP_CREATECONSUMER,
xGroupCreateConsumer: XGROUP_CREATECONSUMER,
XGROUP_DELCONSUMER,
xGroupDelConsumer: XGROUP_DELCONSUMER,
XGROUP_DESTROY,
xGroupDestroy: XGROUP_DESTROY,
XGROUP_SETID,
xGroupSetId: XGROUP_SETID,
XINFO_CONSUMERS,
xInfoConsumers: XINFO_CONSUMERS,
XINFO_GROUPS,
xInfoGroups: XINFO_GROUPS,
XINFO_STREAM,
xInfoStream: XINFO_STREAM,
XLEN,
xLen: XLEN,
XPENDING_RANGE,
xPendingRange: XPENDING_RANGE,
XPENDING,
xPending: XPENDING,
XRANGE,
xRange: XRANGE,
XREAD,
xRead: XREAD,
XREADGROUP,
xReadGroup: XREADGROUP,
XREVRANGE,
xRevRange: XREVRANGE,
XTRIM,
xTrim: XTRIM,
ZADD,
zAdd: ZADD,
ZCARD,
zCard: ZCARD,
ZCOUNT,
zCount: ZCOUNT,
ZDIFF_WITHSCORES,
zDiffWithScores: ZDIFF_WITHSCORES,
ZDIFF,
zDiff: ZDIFF,
ZDIFFSTORE,
zDiffStore: ZDIFFSTORE,
ZINCRBY,
zIncrBy: ZINCRBY,
ZINTER_WITHSCORES,
zInterWithScores: ZINTER_WITHSCORES,
ZINTER,
zInter: ZINTER,
ZINTERSTORE,
zInterStore: ZINTERSTORE,
ZLEXCOUNT,
zLexCount: ZLEXCOUNT,
ZMSCORE,
zmScore: ZMSCORE,
ZPOPMAX_COUNT,
zPopMaxCount: ZPOPMAX_COUNT,
ZPOPMAX,
zPopMax: ZPOPMAX,
ZPOPMIN_COUNT,
zPopMinCount: ZPOPMIN_COUNT,
ZPOPMIN,
zPopMin: ZPOPMIN,
ZRANDMEMBER_COUNT_WITHSCORES,
zRandMemberCountWithScores: ZRANDMEMBER_COUNT_WITHSCORES,
ZRANDMEMBER_COUNT,
zRandMemberCount: ZRANDMEMBER_COUNT,
ZRANDMEMBER,
zRandMember: ZRANDMEMBER,
ZRANGE_WITHSCORES,
zRangeWithScores: ZRANGE_WITHSCORES,
ZRANGE,
zRange: ZRANGE,
ZRANGEBYLEX,
zRangeByLex: ZRANGEBYLEX,
ZRANGEBYSCORE_WITHSCORES,
zRangeByScoreWithScores: ZRANGEBYSCORE_WITHSCORES,
ZRANGEBYSCORE,
zRangeByScore: ZRANGEBYSCORE,
ZRANGESTORE,
zRangeStore: ZRANGESTORE,
ZRANK,
zRank: ZRANK,
ZREM,
zRem: ZREM,
ZREMRANGEBYLEX,
zRemRangeByLex: ZREMRANGEBYLEX,
ZREMRANGEBYRANK,
zRemRangeByRank: ZREMRANGEBYRANK,
ZREMRANGEBYSCORE,
zRemRangeByScore: ZREMRANGEBYSCORE,
ZREVRANK,
zRevRank: ZREVRANK,
ZSCAN,
zScan: ZSCAN,
ZSCORE,
zScore: ZSCORE,
ZUNION_WITHSCORES,
zUnionWithScores: ZUNION_WITHSCORES,
ZUNION,
zUnion: ZUNION,
ZUNIONSTORE,
zUnionStore: ZUNIONSTORE
};

View File

@@ -0,0 +1,105 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
import { SQUARE_SCRIPT } from '../client/index.spec';
// We need to use 'require', because it's not possible with Typescript to import
// function that are exported as 'module.exports = function`, without esModuleInterop
// set to true.
const calculateSlot = require('cluster-key-slot');
describe('Cluster', () => {
testUtils.testWithCluster('sendCommand', async cluster => {
await cluster.connect();
try {
await cluster.publish('channel', 'message');
await cluster.set('a', 'b');
await cluster.set('a{a}', 'bb');
await cluster.set('aa', 'bb');
await cluster.get('aa');
await cluster.get('aa');
await cluster.get('aa');
await cluster.get('aa');
} finally {
await cluster.disconnect();
}
}, GLOBAL.CLUSTERS.OPEN);
testUtils.testWithCluster('multi', async cluster => {
const key = 'key';
assert.deepEqual(
await cluster.multi()
.set(key, 'value')
.get(key)
.exec(),
['OK', 'value']
);
}, GLOBAL.CLUSTERS.OPEN);
testUtils.testWithCluster('scripts', async cluster => {
assert.equal(
await cluster.square(2),
4
);
}, {
...GLOBAL.CLUSTERS.OPEN,
clusterConfiguration: {
scripts: {
square: SQUARE_SCRIPT
}
}
});
testUtils.testWithCluster('should handle live resharding', async cluster => {
const key = 'key',
value = 'value';
await cluster.set(key, value);
const slot = calculateSlot(key),
source = cluster.getSlotMaster(slot),
destination = cluster.getMasters().find(node => node.id !== source.id)!;
await Promise.all([
source.client.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, destination.id),
destination.client.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, destination.id)
]);
// should be able to get the key from the source node using "ASKING"
assert.equal(
await cluster.get(key),
value
);
await Promise.all([
source.client.migrate(
'127.0.0.1',
(<any>destination.client.options).socket.port,
key,
0,
10
)
]);
// should be able to get the key from the destination node using the "ASKING" command
assert.equal(
await cluster.get(key),
value
);
await Promise.all(
cluster.getMasters().map(({ client }) => {
return client.clusterSetSlot(slot, ClusterSlotStates.NODE, destination.id);
})
);
// should handle "MOVED" errors
assert.equal(
await cluster.get(key),
value
);
}, {
serverArguments: [],
numberOfNodes: 2
});
});

View File

@@ -0,0 +1,206 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
import { ClientCommandOptions, RedisClientCommandSignature, RedisClientOptions, RedisClientType, WithModules, WithScripts } from '../client';
import RedisClusterSlots, { ClusterNode } from './cluster-slots';
import { extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, extendWithCommands } from '../commander';
import { EventEmitter } from 'events';
import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command';
export type RedisClusterClientOptions = Omit<RedisClientOptions<Record<string, never>, Record<string, never>>, 'modules' | 'scripts'>;
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
rootNodes: Array<RedisClusterClientOptions>;
defaults?: Partial<RedisClusterClientOptions>;
useReplicas?: boolean;
maxCommandRedirections?: number;
}
type WithCommands = {
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
};
export type RedisClusterType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
RedisCluster<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
export default class RedisCluster<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> extends EventEmitter {
static extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: RedisCommandArguments): string | Buffer | undefined {
if (command.FIRST_KEY_INDEX === undefined) {
return undefined;
} else if (typeof command.FIRST_KEY_INDEX === 'number') {
return redisArgs[command.FIRST_KEY_INDEX];
}
return command.FIRST_KEY_INDEX(...originalArgs);
}
static create<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
return new (<any>extendWithModulesAndScripts({
BaseClass: RedisCluster,
modules: options?.modules,
modulesCommandsExecutor: RedisCluster.prototype.commandsExecutor,
scripts: options?.scripts,
scriptsExecutor: RedisCluster.prototype.scriptsExecutor
}))(options);
}
readonly #options: RedisClusterOptions<M, S>;
readonly #slots: RedisClusterSlots<M, S>;
readonly #Multi: new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<M, S>;
constructor(options: RedisClusterOptions<M, S>) {
super();
this.#options = options;
this.#slots = new RedisClusterSlots(options, err => this.emit('error', err));
this.#Multi = RedisClusterMultiCommand.extend(options);
}
duplicate(overrides?: Partial<RedisClusterOptions<M, S>>): RedisClusterType<M, S> {
return new (Object.getPrototypeOf(this).constructor)({
...this.#options,
...overrides
});
}
async connect(): Promise<void> {
return this.#slots.connect();
}
async commandsExecutor(command: RedisCommand, args: Array<unknown>): Promise<RedisCommandReply<typeof command>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
return transformCommandReply(
command,
await this.sendCommand(
RedisCluster.extractFirstKey(command, args, redisArgs),
command.IS_READ_ONLY,
redisArgs,
options,
command.BUFFER_MODE
),
redisArgs.preserve
);
}
async sendCommand<C extends RedisCommand>(
firstKey: string | Buffer | undefined,
isReadonly: boolean | undefined,
args: RedisCommandArguments,
options?: ClientCommandOptions,
bufferMode?: boolean,
redirections = 0
): Promise<RedisCommandReply<C>> {
const client = this.#slots.getClient(firstKey, isReadonly);
try {
return await client.sendCommand(args, options, bufferMode);
} catch (err: any) {
const shouldRetry = await this.#handleCommandError(err, client, redirections);
if (shouldRetry === true) {
return this.sendCommand(firstKey, isReadonly, args, options, bufferMode, redirections + 1);
} else if (shouldRetry) {
return shouldRetry.sendCommand(args, options, bufferMode);
}
throw err;
}
}
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
return transformCommandReply(
script,
await this.executeScript(
script,
args,
redisArgs,
options
),
redisArgs.preserve
);
}
async executeScript(
script: RedisScript,
originalArgs: Array<unknown>,
redisArgs: RedisCommandArguments,
options?: ClientCommandOptions,
redirections = 0
): Promise<RedisCommandReply<typeof script>> {
const client = this.#slots.getClient(
RedisCluster.extractFirstKey(script, originalArgs, redisArgs),
script.IS_READ_ONLY
);
try {
return await client.executeScript(script, redisArgs, options, script.BUFFER_MODE);
} catch (err: any) {
const shouldRetry = await this.#handleCommandError(err, client, redirections);
if (shouldRetry === true) {
return this.executeScript(script, originalArgs, redisArgs, options, redirections + 1);
} else if (shouldRetry) {
return shouldRetry.executeScript(script, redisArgs, options, script.BUFFER_MODE);
}
throw err;
}
}
async #handleCommandError(err: Error, client: RedisClientType<M, S>, redirections: number): Promise<boolean | RedisClientType<M, S>> {
if (redirections > (this.#options.maxCommandRedirections ?? 16)) {
throw err;
}
if (err.message.startsWith('ASK')) {
const url = err.message.substring(err.message.lastIndexOf(' ') + 1);
let node = this.#slots.getNodeByUrl(url);
if (!node) {
await this.#slots.discover(client);
node = this.#slots.getNodeByUrl(url);
if (!node) {
throw new Error(`Cannot find node ${url}`);
}
}
await node.client.asking();
return node.client;
} else if (err.message.startsWith('MOVED')) {
await this.#slots.discover(client);
return true;
}
throw err;
}
multi(routing?: string | Buffer): RedisClusterMultiCommandType<M, S> {
return new this.#Multi(
async (commands: Array<RedisMultiQueuedCommand>, firstKey?: string | Buffer, chainId?: symbol) => {
return this.#slots
.getClient(firstKey)
.multiExecutor(commands, chainId);
},
routing
);
}
getMasters(): Array<ClusterNode<M, S>> {
return this.#slots.getMasters();
}
getSlotMaster(slot: number): ClusterNode<M, S> {
return this.#slots.getSlotMaster(slot);
}
disconnect(): Promise<void> {
return this.#slots.disconnect();
}
}
extendWithCommands({
BaseClass: RedisCluster,
commands: COMMANDS,
executor: RedisCluster.prototype.commandsExecutor
});

View File

@@ -0,0 +1,112 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
import { extendWithCommands, extendWithModulesAndScripts } from '../commander';
import RedisCluster from '.';
type RedisClusterMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
(...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<M, S>;
type WithCommands<M extends RedisModules, S extends RedisScripts> = {
[P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, S>
};
type WithModules<M extends RedisModules, S extends RedisScripts> = {
[P in keyof M as M[P] extends never ? never : P]: {
[C in keyof M[P]]: RedisClusterMultiCommandSignature<M[P][C], M, S>;
};
};
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
[P in keyof S as S[P] extends never ? never : P]: RedisClusterMultiCommandSignature<S[P], M, S>
};
export type RedisClusterMultiCommandType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
RedisClusterMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: string | Buffer, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
export default class RedisClusterMultiCommand {
readonly #multi = new RedisMultiCommand();
readonly #executor: RedisClusterMultiExecutor;
#firstKey: string | Buffer | undefined;
static extend<M extends RedisModules, S extends RedisScripts>(
plugins?: RedisPlugins<M, S>
): new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisClusterMultiCommandType<M, S> {
return <any>extendWithModulesAndScripts({
BaseClass: RedisClusterMultiCommand,
modules: plugins?.modules,
modulesCommandsExecutor: RedisClusterMultiCommand.prototype.commandsExecutor,
scripts: plugins?.scripts,
scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor
});
}
constructor(executor: RedisClusterMultiExecutor, firstKey?: string | Buffer) {
this.#executor = executor;
this.#firstKey = firstKey;
}
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
const transformedArguments = command.transformArguments(...args);
if (!this.#firstKey) {
this.#firstKey = RedisCluster.extractFirstKey(command, args, transformedArguments);
}
return this.addCommand(
undefined,
transformedArguments,
command.transformReply
);
}
addCommand(
firstKey: string | Buffer | undefined,
args: RedisCommandArguments,
transformReply?: RedisCommand['transformReply']
): this {
if (!this.#firstKey) {
this.#firstKey = firstKey;
}
this.#multi.addCommand(args, transformReply);
return this;
}
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
const transformedArguments = this.#multi.addScript(script, args);
if (!this.#firstKey) {
this.#firstKey = RedisCluster.extractFirstKey(script, args, transformedArguments);
}
return this.addCommand(undefined, transformedArguments);
}
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
if (execAsPipeline) {
return this.execAsPipeline();
}
const commands = this.#multi.exec();
if (!commands) return [];
return this.#multi.handleExecReplies(
await this.#executor(commands, this.#firstKey, RedisMultiCommand.generateChainId())
);
}
EXEC = this.exec;
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
return this.#multi.transformReplies(
await this.#executor(this.#multi.queue, this.#firstKey)
);
}
}
extendWithCommands({
BaseClass: RedisClusterMultiCommand,
commands: COMMANDS,
executor: RedisClusterMultiCommand.prototype.commandsExecutor
});

View File

@@ -0,0 +1,14 @@
const symbol = Symbol('Command Options');
export type CommandOptions<T> = T & {
readonly [symbol]: true;
};
export function commandOptions<T>(options: T): CommandOptions<T> {
(options as any)[symbol] = true;
return options as CommandOptions<T>;
}
export function isCommandOptions<T>(options: any): options is CommandOptions<T> {
return options && options[symbol] === true;
}

View File

@@ -0,0 +1,44 @@
import { strict as assert } from 'assert';
import { describe } from 'mocha';
import { encodeCommand } from './commander';
function encodeCommandToString(...args: Parameters<typeof encodeCommand>): string {
const arr = [];
for (const item of encodeCommand(...args)) {
arr.push(item.toString());
}
return arr.join('');
}
describe('Commander', () => {
describe('encodeCommand (see #1628)', () => {
it('1 byte', () => {
assert.equal(
encodeCommandToString(['a', 'z']),
'*2\r\n$1\r\na\r\n$1\r\nz\r\n'
);
});
it('2 bytes', () => {
assert.equal(
encodeCommandToString(['א', 'ת']),
'*2\r\n$2\r\nא\r\n$2\r\nת\r\n'
);
});
it('4 bytes', () => {
assert.equal(
encodeCommandToString(['🐣', '🐤']),
'*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n'
);
});
it('with a buffer', () => {
assert.equal(
encodeCommandToString([Buffer.from('string')]),
'*1\r\n$6\r\nstring\r\n'
);
});
});
});

View File

@@ -0,0 +1,130 @@
import { CommandOptions, isCommandOptions } from './command-options';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisCommands, RedisModules, RedisScript, RedisScripts } from './commands';
type Instantiable<T = any> = new(...args: Array<any>) => T;
interface ExtendWithCommandsConfig<T extends Instantiable> {
BaseClass: T;
commands: RedisCommands;
executor(command: RedisCommand, args: Array<unknown>): unknown;
}
export function extendWithCommands<T extends Instantiable>({ BaseClass, commands, executor }: ExtendWithCommandsConfig<T>): void {
for (const [name, command] of Object.entries(commands)) {
BaseClass.prototype[name] = function (...args: Array<unknown>): unknown {
return executor.call(this, command, args);
};
}
}
interface ExtendWithModulesAndScriptsConfig<T extends Instantiable> {
BaseClass: T;
modules?: RedisModules;
modulesCommandsExecutor(this: InstanceType<T>, command: RedisCommand, args: Array<unknown>): unknown;
scripts?: RedisScripts;
scriptsExecutor(this: InstanceType<T>, script: RedisScript, args: Array<unknown>): unknown;
}
export function extendWithModulesAndScripts<T extends Instantiable>(config: ExtendWithModulesAndScriptsConfig<T>): T {
let Commander: T | undefined;
if (config.modules) {
Commander = class extends config.BaseClass {
constructor(...args: Array<any>) {
super(...args);
for (const module of Object.keys(config.modules!)) {
this[module] = new this[module](this);
}
}
};
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) {
Commander ??= class extends config.BaseClass {};
for (const [name, script] of Object.entries(config.scripts)) {
Commander.prototype[name] = function (...args: Array<unknown>): unknown {
return config.scriptsExecutor.call(this, script, args);
};
}
}
return (Commander ?? config.BaseClass) as any;
}
export function transformCommandArguments<T = unknown>(
command: RedisCommand,
args: Array<unknown>
): {
args: RedisCommandArguments;
options: CommandOptions<T> | undefined;
} {
let options;
if (isCommandOptions<T>(args[0])) {
options = args[0];
args = args.slice(1);
}
return {
args: command.transformArguments(...args),
options
};
}
const DELIMITER = '\r\n';
export function* encodeCommand(args: RedisCommandArguments): IterableIterator<string | Buffer> {
yield `*${args.length}${DELIMITER}`;
for (const arg of args) {
const byteLength = typeof arg === 'string' ? Buffer.byteLength(arg): arg.length;
yield `$${byteLength.toString()}${DELIMITER}`;
yield arg;
yield DELIMITER;
}
}
export function transformCommandReply(
command: RedisCommand,
rawReply: RedisCommandRawReply,
preserved: unknown
): RedisCommandReply<typeof command> {
if (!command.transformReply) {
return rawReply;
}
return command.transformReply(rawReply, preserved);
}
export type LegacyCommandArguments = Array<string | number | Buffer | LegacyCommandArguments>;
export function transformLegacyCommandArguments(args: LegacyCommandArguments, flat: RedisCommandArguments = []): RedisCommandArguments {
for (const arg of args) {
if (Array.isArray(arg)) {
transformLegacyCommandArguments(arg, flat);
continue;
}
flat.push(typeof arg === 'number' ? arg.toString() : arg);
}
return flat;
}

View File

@@ -0,0 +1,23 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_CAT';
describe('ACL CAT', () => {
testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'CAT']
);
});
it('with categoryName', () => {
assert.deepEqual(
transformArguments('dangerous'),
['ACL', 'CAT', 'dangerous']
);
});
});
});

View File

@@ -0,0 +1,11 @@
export function transformArguments(categoryName?: string): Array<string> {
const args = ['ACL', 'CAT'];
if (categoryName) {
args.push(categoryName);
}
return args;
}
export declare function transformReply(): Array<string>;

View File

@@ -0,0 +1,30 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './ACL_DELUSER';
describe('ACL DELUSER', () => {
testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('username'),
['ACL', 'DELUSER', 'username']
);
});
it('array', () => {
assert.deepEqual(
transformArguments(['1', '2']),
['ACL', 'DELUSER', '1', '2']
);
});
});
testUtils.testWithClient('client.aclDelUser', async client => {
assert.equal(
await client.aclDelUser('dosenotexists'),
0
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,8 @@
import { RedisCommandArguments } from '.';
import { pushVerdictArguments } from './generic-transformers';
export function transformArguments(username: string | Array<string>): RedisCommandArguments {
return pushVerdictArguments(['ACL', 'DELUSER'], username);
}
export declare const transformReply: (reply: number) => number;

View File

@@ -0,0 +1,23 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_GENPASS';
describe('ACL GENPASS', () => {
testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'GENPASS']
);
});
it('with bits', () => {
assert.deepEqual(
transformArguments(128),
['ACL', 'GENPASS', '128']
);
});
});
});

View File

@@ -0,0 +1,11 @@
export function transformArguments(bits?: number): Array<string> {
const args = ['ACL', 'GENPASS'];
if (bits) {
args.push(bits.toString());
}
return args;
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,32 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './ACL_GETUSER';
describe('ACL GETUSER', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('username'),
['ACL', 'GETUSER', 'username']
);
});
testUtils.testWithClient('client.aclGetUser', async client => {
assert.deepEqual(
await client.aclGetUser('default'),
{
passwords: [],
commands: '+@all',
keys: ['*'],
...(testUtils.isVersionGreaterThan([6, 2]) ? {
flags: ['on', 'allkeys', 'allchannels', 'allcommands', 'nopass'],
channels: ['*']
} : {
flags: ['on', 'allkeys', 'allcommands', 'nopass'],
channels: undefined
})
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,34 @@
export function transformArguments(username: string): Array<string> {
return ['ACL', 'GETUSER', username];
}
type AclGetUserRawReply = [
_: string,
flags: Array<string>,
_: string,
passwords: Array<string>,
_: string,
commands: string,
_: string,
keys: Array<string>,
_: string,
channels: Array<string>
];
interface AclUser {
flags: Array<string>;
passwords: Array<string>;
commands: string;
keys: Array<string>;
channels: Array<string>
}
export function transformReply(reply: AclGetUserRawReply): AclUser {
return {
flags: reply[1],
passwords: reply[3],
commands: reply[5],
keys: reply[7],
channels: reply[9]
};
}

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_LIST';
describe('ACL LIST', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'LIST']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(): Array<string> {
return ['ACL', 'LIST'];
}
export declare function transformReply(): Array<string>;

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_SAVE';
describe('ACL SAVE', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'SAVE']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(): Array<string> {
return ['ACL', 'LOAD'];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,53 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments, transformReply } from './ACL_LOG';
describe('ACL LOG', () => {
testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'LOG']
);
});
it('with count', () => {
assert.deepEqual(
transformArguments(10),
['ACL', 'LOG', '10']
);
});
});
it('transformReply', () => {
assert.deepEqual(
transformReply([[
'count',
1,
'reason',
'auth',
'context',
'toplevel',
'object',
'AUTH',
'username',
'someuser',
'age-seconds',
'4.096',
'client-info',
'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
]]),
[{
count: 1,
reason: 'auth',
context: 'toplevel',
object: 'AUTH',
username: 'someuser',
ageSeconds: 4.096,
clientInfo: 'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
}]
);
});
});

View File

@@ -0,0 +1,48 @@
export function transformArguments(count?: number): Array<string> {
const args = ['ACL', 'LOG'];
if (count) {
args.push(count.toString());
}
return args;
}
type AclLogRawReply = [
_: string,
count: number,
_: string,
reason: string,
_: string,
context: string,
_: string,
object: string,
_: string,
username: string,
_: string,
ageSeconds: string,
_: string,
clientInfo: string
];
interface AclLog {
count: number;
reason: string;
context: string;
object: string;
username: string;
ageSeconds: number;
clientInfo: string;
}
export function transformReply(reply: Array<AclLogRawReply>): Array<AclLog> {
return reply.map(log => ({
count: log[1],
reason: log[3],
context: log[5],
object: log[7],
username: log[9],
ageSeconds: Number(log[11]),
clientInfo: log[13]
}));
}

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_LOG_RESET';
describe('ACL LOG RESET', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'LOG', 'RESET']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(): Array<string> {
return ['ACL', 'LOG', 'RESET'];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_LOAD';
describe('ACL LOAD', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'LOAD']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(): Array<string> {
return ['ACL', 'SAVE'];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,23 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_SETUSER';
describe('ACL SETUSER', () => {
testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('username', 'allkeys'),
['ACL', 'SETUSER', 'username', 'allkeys']
);
});
it('array', () => {
assert.deepEqual(
transformArguments('username', ['allkeys', 'allchannels']),
['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels']
);
});
});
});

View File

@@ -0,0 +1,8 @@
import { RedisCommandArguments } from '.';
import { pushVerdictArguments } from './generic-transformers';
export function transformArguments(username: string, rule: string | Array<string>): RedisCommandArguments {
return pushVerdictArguments(['ACL', 'SETUSER', username], rule);
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_USERS';
describe('ACL USERS', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'USERS']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(): Array<string> {
return ['ACL', 'USERS'];
}
export declare function transformReply(): Array<string>;

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_WHOAMI';
describe('ACL WHOAMI', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'WHOAMI']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(): Array<string> {
return ['ACL', 'WHOAMI'];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './APPEND';
describe('APPEND', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'value'),
['APPEND', 'key', 'value']
);
});
});

View File

@@ -0,0 +1,7 @@
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, value: string): Array<string> {
return ['APPEND', key, value];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './ASKING';
describe('ASKING', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ASKING']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(): Array<string> {
return ['ASKING'];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,25 @@
import { strict as assert } from 'assert';
import { transformArguments } from './AUTH';
describe('AUTH', () => {
describe('transformArguments', () => {
it('password only', () => {
assert.deepEqual(
transformArguments({
password: 'password'
}),
['AUTH', 'password']
);
});
it('username & password', () => {
assert.deepEqual(
transformArguments({
username: 'username',
password: 'password'
}),
['AUTH', 'username', 'password']
);
});
});
});

View File

@@ -0,0 +1,14 @@
export interface AuthOptions {
username?: string;
password: string;
}
export function transformArguments({username, password}: AuthOptions): Array<string> {
if (!username) {
return ['AUTH', password];
}
return ['AUTH', username, password];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './BGREWRITEAOF';
describe('BGREWRITEAOF', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['BGREWRITEAOF']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(): Array<string> {
return ['BGREWRITEAOF'];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,23 @@
import { strict as assert } from 'assert';
import { describe } from 'mocha';
import { transformArguments } from './BGSAVE';
describe('BGSAVE', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['BGSAVE']
);
});
it('with SCHEDULE', () => {
assert.deepEqual(
transformArguments({
SCHEDULE: true
}),
['BGSAVE', 'SCHEDULE']
);
});
});
});

View File

@@ -0,0 +1,15 @@
interface BgSaveOptions {
SCHEDULE?: true;
}
export function transformArguments(options?: BgSaveOptions): Array<string> {
const args = ['BGSAVE'];
if (options?.SCHEDULE) {
args.push('SCHEDULE');
}
return args;
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,31 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BITCOUNT';
describe('BITCOUNT', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments('key'),
['BITCOUNT', 'key']
);
});
it('with range', () => {
assert.deepEqual(
transformArguments('key', {
start: 0,
end: 1
}),
['BITCOUNT', 'key', '0', '1']
);
});
});
testUtils.testWithClient('client.bitCount', async client => {
assert.equal(
await client.bitCount('key'),
0
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,23 @@
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
interface BitCountRange {
start: number;
end: number;
}
export function transformArguments(key: string, range?: BitCountRange): Array<string> {
const args = ['BITCOUNT', key];
if (range) {
args.push(
range.start.toString(),
range.end.toString()
);
}
return args;
}
export declare function transformReply(): number;

View File

@@ -0,0 +1,42 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BITFIELD';
describe('BITFIELD', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', [{
operation: 'OVERFLOW',
behavior: 'WRAP'
}, {
operation: 'GET',
type: 'i8',
offset: 0
}, {
operation: 'OVERFLOW',
behavior: 'SAT'
}, {
operation: 'SET',
type: 'i16',
offset: 1,
value: 0
}, {
operation: 'OVERFLOW',
behavior: 'FAIL'
}, {
operation: 'INCRBY',
type: 'i32',
offset: 2,
increment: 1
}]),
['BITFIELD', 'key', 'OVERFLOW', 'WRAP', 'GET', 'i8', '0', 'OVERFLOW', 'SAT', 'SET', 'i16', '1', '0', 'OVERFLOW', 'FAIL', 'INCRBY', 'i32', '2', '1']
);
});
testUtils.testWithClient('client.bitField', async client => {
assert.deepEqual(
await client.bitField('key', []),
[]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,82 @@
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
type BitFieldType = string; // TODO 'i[1-64]' | 'u[1-63]'
interface BitFieldOperation<S extends string> {
operation: S;
}
interface BitFieldGetOperation extends BitFieldOperation<'GET'> {
type: BitFieldType;
offset: number | string;
}
interface BitFieldSetOperation extends BitFieldOperation<'SET'> {
type: BitFieldType;
offset: number | string;
value: number;
}
interface BitFieldIncrByOperation extends BitFieldOperation<'INCRBY'> {
type: BitFieldType;
offset: number | string;
increment: number;
}
interface BitFieldOverflowOperation extends BitFieldOperation<'OVERFLOW'> {
behavior: string;
}
type BitFieldOperations = Array<
BitFieldGetOperation |
BitFieldSetOperation |
BitFieldIncrByOperation |
BitFieldOverflowOperation
>;
export function transformArguments(key: string, operations: BitFieldOperations): Array<string> {
const args = ['BITFIELD', key];
for (const options of operations) {
switch (options.operation) {
case 'GET':
args.push(
'GET',
options.type,
options.offset.toString()
);
break;
case 'SET':
args.push(
'SET',
options.type,
options.offset.toString(),
options.value.toString()
);
break;
case 'INCRBY':
args.push(
'INCRBY',
options.type,
options.offset.toString(),
options.increment.toString()
);
break;
case 'OVERFLOW':
args.push(
'OVERFLOW',
options.behavior
);
break;
}
}
return args;
}
export declare function transformReply(): Array<number | null>;

View File

@@ -0,0 +1,35 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BITOP';
describe('BITOP', () => {
describe('transformArguments', () => {
it('single key', () => {
assert.deepEqual(
transformArguments('AND', 'destKey', 'key'),
['BITOP', 'AND', 'destKey', 'key']
);
});
it('multiple keys', () => {
assert.deepEqual(
transformArguments('AND', 'destKey', ['1', '2']),
['BITOP', 'AND', 'destKey', '1', '2']
);
});
});
testUtils.testWithClient('client.bitOp', async client => {
assert.equal(
await client.bitOp('AND', 'destKey', 'key'),
0
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithCluster('cluster.bitOp', async cluster => {
assert.equal(
await cluster.bitOp('AND', '{tag}destKey', '{tag}key'),
0
);
}, GLOBAL.CLUSTERS.OPEN);
});

View File

@@ -0,0 +1,12 @@
import { RedisCommandArguments } from '.';
import { pushVerdictArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = 2;
type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT';
export function transformArguments(operation: BitOperations, destKey: string, key: string | Array<string>): RedisCommandArguments {
return pushVerdictArguments(['BITOP', operation, destKey], key);
}
export declare function transformReply(): number;

View File

@@ -0,0 +1,42 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BITPOS';
describe('BITPOS', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments('key', 1),
['BITPOS', 'key', '1']
);
});
it('with start', () => {
assert.deepEqual(
transformArguments('key', 1, 1),
['BITPOS', 'key', '1', '1']
);
});
it('with start, end', () => {
assert.deepEqual(
transformArguments('key', 1, 1, -1),
['BITPOS', 'key', '1', '1', '-1']
);
});
});
testUtils.testWithClient('client.bitPos', async client => {
assert.equal(
await client.bitPos('key', 1, 1),
-1
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithCluster('cluster.bitPos', async cluster => {
assert.equal(
await cluster.bitPos('key', 1, 1),
-1
);
}, GLOBAL.CLUSTERS.OPEN);
});

View File

@@ -0,0 +1,21 @@
import { BitValue } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(key: string, bit: BitValue, start?: number, end?: number): Array<string> {
const args = ['BITPOS', key, bit.toString()];
if (typeof start === 'number') {
args.push(start.toString());
}
if (typeof end === 'number') {
args.push(end.toString());
}
return args;
}
export declare function transformReply(): number;

View File

@@ -0,0 +1,43 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BLMOVE';
import { commandOptions } from '../../index';
describe('BLMOVE', () => {
testUtils.isVersionGreaterThanHook([6, 2]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('source', 'destination', 'LEFT', 'RIGHT', 0),
['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0']
);
});
testUtils.testWithClient('client.blMove', async client => {
const [blMoveReply] = await Promise.all([
client.blMove(commandOptions({
isolated: true
}), 'source', 'destination', 'LEFT', 'RIGHT', 0),
client.lPush('source', 'element')
]);
assert.equal(
blMoveReply,
'element'
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithCluster('cluster.blMove', async cluster => {
const [blMoveReply] = await Promise.all([
cluster.blMove(commandOptions({
isolated: true
}), '{tag}source', '{tag}destination', 'LEFT', 'RIGHT', 0),
cluster.lPush('{tag}source', 'element')
]);
assert.equal(
blMoveReply,
'element'
);
}, GLOBAL.CLUSTERS.OPEN);
});

View File

@@ -0,0 +1,22 @@
import { LMoveSide } from './LMOVE';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(
source: string,
destination: string,
sourceDirection: LMoveSide,
destinationDirection: LMoveSide,
timeout: number
): Array<string> {
return [
'BLMOVE',
source,
destination,
sourceDirection,
destinationDirection,
timeout.toString()
];
}
export declare function transformReply(): string | null;

View File

@@ -0,0 +1,79 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments, transformReply } from './BLPOP';
import { commandOptions } from '../../index';
describe('BLPOP', () => {
describe('transformArguments', () => {
it('single', () => {
assert.deepEqual(
transformArguments('key', 0),
['BLPOP', 'key', '0']
);
});
it('multiple', () => {
assert.deepEqual(
transformArguments(['key1', 'key2'], 0),
['BLPOP', 'key1', 'key2', '0']
);
});
});
describe('transformReply', () => {
it('null', () => {
assert.equal(
transformReply(null),
null
);
});
it('member', () => {
assert.deepEqual(
transformReply(['key', 'element']),
{
key: 'key',
element: 'element'
}
);
});
});
testUtils.testWithClient('client.blPop', async client => {
const [ blPopReply ] = await Promise.all([
client.blPop(
commandOptions({ isolated: true }),
'key',
1
),
client.lPush('key', 'element'),
]);
assert.deepEqual(
blPopReply,
{
key: 'key',
element: 'element'
}
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithCluster('cluster.blPop', async cluster => {
const [ blPopReply ] = await Promise.all([
cluster.blPop(
commandOptions({ isolated: true }),
'key',
1
),
cluster.lPush('key', 'element'),
]);
assert.deepEqual(
blPopReply,
{
key: 'key',
element: 'element'
}
);
}, GLOBAL.CLUSTERS.OPEN);
});

View File

@@ -0,0 +1,26 @@
import { RedisCommandArguments } from '.';
import { pushVerdictArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(keys: string | Buffer | Array<string | Buffer>, timeout: number): RedisCommandArguments {
const args = pushVerdictArguments(['BLPOP'], keys);
args.push(timeout.toString());
return args;
}
type BLPOPReply = null | {
key: string;
element: string;
};
export function transformReply(reply: null | [string, string]): BLPOPReply {
if (reply === null) return null;
return {
key: reply[0],
element: reply[1]
};
}

View File

@@ -0,0 +1,79 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments, transformReply } from './BRPOP';
import { commandOptions } from '../../index';
describe('BRPOP', () => {
describe('transformArguments', () => {
it('single', () => {
assert.deepEqual(
transformArguments('key', 0),
['BRPOP', 'key', '0']
);
});
it('multiple', () => {
assert.deepEqual(
transformArguments(['key1', 'key2'], 0),
['BRPOP', 'key1', 'key2', '0']
);
});
});
describe('transformReply', () => {
it('null', () => {
assert.equal(
transformReply(null),
null
);
});
it('member', () => {
assert.deepEqual(
transformReply(['key', 'element']),
{
key: 'key',
element: 'element'
}
);
});
});
testUtils.testWithClient('client.brPop', async client => {
const [ brPopReply ] = await Promise.all([
client.brPop(
commandOptions({ isolated: true }),
'key',
1
),
client.lPush('key', 'element'),
]);
assert.deepEqual(
brPopReply,
{
key: 'key',
element: 'element'
}
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithCluster('cluster.brPop', async cluster => {
const [ brPopReply ] = await Promise.all([
cluster.brPop(
commandOptions({ isolated: true }),
'key',
1
),
cluster.lPush('key', 'element'),
]);
assert.deepEqual(
brPopReply,
{
key: 'key',
element: 'element'
}
);
}, GLOBAL.CLUSTERS.OPEN);
});

View File

@@ -0,0 +1,26 @@
import { RedisCommandArguments } from '.';
import { pushVerdictArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string | Array<string>, timeout: number): RedisCommandArguments {
const args = pushVerdictArguments(['BRPOP'], key);
args.push(timeout.toString());
return args;
}
type BRPOPReply = null | {
key: string;
element: string;
};
export function transformReply(reply: null | [string, string]): BRPOPReply {
if (reply === null) return null;
return {
key: reply[0],
element: reply[1]
};
}

View File

@@ -0,0 +1,47 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BRPOPLPUSH';
import { commandOptions } from '../../index';
describe('BRPOPLPUSH', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('source', 'destination', 0),
['BRPOPLPUSH', 'source', 'destination', '0']
);
});
testUtils.testWithClient('client.brPopLPush', async client => {
const [ popReply ] = await Promise.all([
client.brPopLPush(
commandOptions({ isolated: true }),
'source',
'destination',
0
),
client.lPush('source', 'element')
]);
assert.equal(
popReply,
'element'
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithCluster('cluster.brPopLPush', async cluster => {
const [ popReply ] = await Promise.all([
cluster.brPopLPush(
commandOptions({ isolated: true }),
'{tag}source',
'{tag}destination',
0
),
cluster.lPush('{tag}source', 'element')
]);
assert.equal(
popReply,
'element'
);
}, GLOBAL.CLUSTERS.OPEN);
});

View File

@@ -0,0 +1,7 @@
export const FIRST_KEY_INDEX = 1;
export function transformArguments(source: string, destination: string, timeout: number): Array<string> {
return ['BRPOPLPUSH', source, destination, timeout.toString()];
}
export declare function transformReply(): number | null;

View File

@@ -0,0 +1,65 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments, transformReply } from './BZPOPMAX';
import { commandOptions } from '../../index';
describe('BZPOPMAX', () => {
describe('transformArguments', () => {
it('single', () => {
assert.deepEqual(
transformArguments('key', 0),
['BZPOPMAX', 'key', '0']
);
});
it('multiple', () => {
assert.deepEqual(
transformArguments(['1', '2'], 0),
['BZPOPMAX', '1', '2', '0']
);
});
});
describe('transformReply', () => {
it('null', () => {
assert.equal(
transformReply(null),
null
);
});
it('member', () => {
assert.deepEqual(
transformReply(['key', 'value', '1']),
{
key: 'key',
value: 'value',
score: 1
}
);
});
});
testUtils.testWithClient('client.bzPopMax', async client => {
const [ bzPopMaxReply ] = await Promise.all([
client.bzPopMax(
commandOptions({ isolated: true }),
'key',
0
),
client.zAdd('key', [{
value: '1',
score: 1
}])
]);
assert.deepEqual(
bzPopMaxReply,
{
key: 'key',
value: '1',
score: 1
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,28 @@
import { RedisCommandArguments } from '.';
import { pushVerdictArguments, transformReplyNumberInfinity, ZMember } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string | Array<string>, timeout: number): RedisCommandArguments {
const args = pushVerdictArguments(['BZPOPMAX'], key);
args.push(timeout.toString());
return args;
}
interface ZMemberWithKey extends ZMember {
key: string;
}
type BZPopMaxReply = ZMemberWithKey | null;
export function transformReply(reply: [key: string, value: string, score: string] | null): BZPopMaxReply | null {
if (!reply) return null;
return {
key: reply[0],
value: reply[1],
score: transformReplyNumberInfinity(reply[2])
};
}

View File

@@ -0,0 +1,65 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments, transformReply } from './BZPOPMIN';
import { commandOptions } from '../../index';
describe('BZPOPMIN', () => {
describe('transformArguments', () => {
it('single', () => {
assert.deepEqual(
transformArguments('key', 0),
['BZPOPMIN', 'key', '0']
);
});
it('multiple', () => {
assert.deepEqual(
transformArguments(['1', '2'], 0),
['BZPOPMIN', '1', '2', '0']
);
});
});
describe('transformReply', () => {
it('null', () => {
assert.equal(
transformReply(null),
null
);
});
it('member', () => {
assert.deepEqual(
transformReply(['key', 'value', '1']),
{
key: 'key',
value: 'value',
score: 1
}
);
});
});
testUtils.testWithClient('client.bzPopMin', async client => {
const [ bzPopMinReply ] = await Promise.all([
client.bzPopMin(
commandOptions({ isolated: true }),
'key',
0
),
client.zAdd('key', [{
value: '1',
score: 1
}])
]);
assert.deepEqual(
bzPopMinReply,
{
key: 'key',
value: '1',
score: 1
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,28 @@
import { RedisCommandArguments } from '.';
import { pushVerdictArguments, transformReplyNumberInfinity, ZMember } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string | Array<string>, timeout: number): RedisCommandArguments {
const args = pushVerdictArguments(['BZPOPMIN'], key);
args.push(timeout.toString());
return args;
}
interface ZMemberWithKey extends ZMember {
key: string;
}
type BZPopMinReply = ZMemberWithKey | null;
export function transformReply(reply: [key: string, value: string, score: string] | null): BZPopMinReply | null {
if (!reply) return null;
return {
key: reply[0],
value: reply[1],
score: transformReplyNumberInfinity(reply[2])
};
}

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CLIENT_ID';
describe('CLIENT ID', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['CLIENT', 'ID']
);
});
testUtils.testWithClient('client.clientId', async client => {
assert.equal(
typeof (await client.clientId()),
'number'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,7 @@
export const IS_READ_ONLY = true;
export function transformArguments(): Array<string> {
return ['CLIENT', 'ID'];
}
export declare function transformReply(): number;

View File

@@ -0,0 +1,42 @@
import { strict as assert } from 'assert';
import { transformArguments, transformReply } from './CLIENT_INFO';
describe('CLIENT INFO', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['CLIENT', 'INFO']
);
});
it('transformReply', () => {
assert.deepEqual(
transformReply('id=526512 addr=127.0.0.1:36244 laddr=127.0.0.1:6379 fd=8 name= age=11213 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default redir=-1\n'),
{
id: 526512,
addr: '127.0.0.1:36244',
laddr: '127.0.0.1:6379',
fd: 8,
name: '',
age: 11213,
idle: 0,
flags: 'N',
db: 0,
sub: 0,
psub: 0,
multi: -1,
qbuf: 26,
qbufFree: 40928,
argvMem: 10,
obl: 0,
oll: 0,
omem: 0,
totMem: 61466,
events: 'r',
cmd: 'client',
user: 'default',
redir: -1
}
);
});
});

View File

@@ -0,0 +1,85 @@
export function transformArguments(): Array<string> {
return ['CLIENT', 'INFO'];
}
interface ClientInfoReply {
id: number;
addr: string;
laddr: string;
fd: number;
name: string;
age: number;
idle: number;
flags: string;
db: number;
sub: number;
psub: number;
multi: number;
qbuf: number;
qbufFree: number;
argvMem: number;
obl: number;
oll: number;
omem: number;
totMem: number;
events: string;
cmd: string;
user: string;
redir: number;
}
const REGEX = /=([^\s]*)/g;
export function transformReply(reply: string): ClientInfoReply {
const [
[, id],
[, addr],
[, laddr],
[, fd],
[, name],
[, age],
[, idle],
[, flags],
[, db],
[, sub],
[, psub],
[, multi],
[, qbuf],
[, qbufFree],
[, argvMem],
[, obl],
[, oll],
[, omem],
[, totMem],
[, events],
[, cmd],
[, user],
[, redir]
] = [...reply.matchAll(REGEX)];
return {
id: Number(id),
addr,
laddr,
fd: Number(fd),
name,
age: Number(age),
idle: Number(idle),
flags,
db: Number(db),
sub: Number(sub),
psub: Number(psub),
multi: Number(multi),
qbuf: Number(qbuf),
qbufFree: Number(qbufFree),
argvMem: Number(argvMem),
obl: Number(obl),
oll: Number(oll),
omem: Number(omem),
totMem: Number(totMem),
events,
cmd,
user,
redir: Number(redir)
};
}

View File

@@ -0,0 +1,20 @@
import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_ADDSLOTS';
describe('CLUSTER ADDSLOTS', () => {
describe('transformArguments', () => {
it('single', () => {
assert.deepEqual(
transformArguments(0),
['CLUSTER', 'ADDSLOTS', '0']
);
});
it('multiple', () => {
assert.deepEqual(
transformArguments([0, 1]),
['CLUSTER', 'ADDSLOTS', '0', '1']
);
});
});
});

View File

@@ -0,0 +1,13 @@
export function transformArguments(slots: number | Array<number>): Array<string> {
const args = ['CLUSTER', 'ADDSLOTS'];
if (typeof slots === 'number') {
args.push(slots.toString());
} else {
args.push(...slots.map(String));
}
return args;
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_FLUSHSLOTS';
describe('CLUSTER FLUSHSLOTS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['CLUSTER', 'FLUSHSLOTS']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(): Array<string> {
return ['CLUSTER', 'FLUSHSLOTS'];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_GETKEYSINSLOT';
describe('CLUSTER GETKEYSINSLOT', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(0, 10),
['CLUSTER', 'GETKEYSINSLOT', '0', '10']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(slot: number, count: number): Array<string> {
return ['CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,46 @@
import { strict as assert } from 'assert';
import { transformArguments, transformReply } from './CLUSTER_INFO';
describe('CLUSTER INFO', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['CLUSTER', 'INFO']
);
});
it('transformReply', () => {
assert.deepEqual(
transformReply([
'cluster_state:ok',
'cluster_slots_assigned:16384',
'cluster_slots_ok:16384',
'cluster_slots_pfail:0',
'cluster_slots_fail:0',
'cluster_known_nodes:6',
'cluster_size:3',
'cluster_current_epoch:6',
'cluster_my_epoch:2',
'cluster_stats_messages_sent:1483972',
'cluster_stats_messages_received:1483968'
].join('\r\n')),
{
state: 'ok',
slots: {
assigned: 16384,
ok: 16384,
pfail: 0,
fail: 0
},
knownNodes: 6,
size: 3,
currentEpoch: 6,
myEpoch: 2,
stats: {
messagesSent: 1483972,
messagesReceived: 1483968
}
}
);
});
});

View File

@@ -0,0 +1,47 @@
export function transformArguments(): Array<string> {
return ['CLUSTER', 'INFO'];
}
interface ClusterInfoReply {
state: string;
slots: {
assigned: number;
ok: number;
pfail: number;
fail: number;
};
knownNodes: number;
size: number;
currentEpoch: number;
myEpoch: number;
stats: {
messagesSent: number;
messagesReceived: number;
};
}
export function transformReply(reply: string): ClusterInfoReply {
const lines = reply.split('\r\n');
return {
state: extractLineValue(lines[0]),
slots: {
assigned: Number(extractLineValue(lines[1])),
ok: Number(extractLineValue(lines[2])),
pfail: Number(extractLineValue(lines[3])),
fail: Number(extractLineValue(lines[4]))
},
knownNodes: Number(extractLineValue(lines[5])),
size: Number(extractLineValue(lines[6])),
currentEpoch: Number(extractLineValue(lines[7])),
myEpoch: Number(extractLineValue(lines[8])),
stats: {
messagesSent: Number(extractLineValue(lines[9])),
messagesReceived: Number(extractLineValue(lines[10]))
}
};
}
export function extractLineValue(line: string): string {
return line.substring(line.indexOf(':') + 1);
}

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_MEET';
describe('CLUSTER MEET', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('127.0.0.1', 6379),
['CLUSTER', 'MEET', '127.0.0.1', '6379']
);
});
});

View File

@@ -0,0 +1,5 @@
export function transformArguments(ip: string, port: number): Array<string> {
return ['CLUSTER', 'MEET', ip, port.toString()];
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,95 @@
import { strict as assert } from 'assert';
import { RedisClusterNodeLinkStates, transformArguments, transformReply } from './CLUSTER_NODES';
describe('CLUSTER NODES', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['CLUSTER', 'NODES']
);
});
describe('transformReply', () => {
it('simple', () => {
assert.deepEqual(
transformReply([
'master 127.0.0.1:30001@31001 myself,master - 0 0 1 connected 0-16384',
'slave 127.0.0.1:30002@31002 slave master 0 0 1 connected',
''
].join('\n')),
[{
id: 'master',
url: '127.0.0.1:30001@31001',
host: '127.0.0.1',
port: 30001,
cport: 31001,
flags: ['myself', 'master'],
pingSent: 0,
pongRecv: 0,
configEpoch: 1,
linkState: RedisClusterNodeLinkStates.CONNECTED,
slots: [{
from: 0,
to: 16384
}],
replicas: [{
id: 'slave',
url: '127.0.0.1:30002@31002',
host: '127.0.0.1',
port: 30002,
cport: 31002,
flags: ['slave'],
pingSent: 0,
pongRecv: 0,
configEpoch: 1,
linkState: RedisClusterNodeLinkStates.CONNECTED
}]
}]
);
});
it.skip('with importing slots', () => {
assert.deepEqual(
transformReply(
'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0-<-16384\n'
),
[{
id: 'id',
url: '127.0.0.1:30001@31001',
host: '127.0.0.1',
port: 30001,
cport: 31001,
flags: ['master'],
pingSent: 0,
pongRecv: 0,
configEpoch: 0,
linkState: RedisClusterNodeLinkStates.CONNECTED,
slots: [], // TODO
replicas: []
}]
);
});
it.skip('with migrating slots', () => {
assert.deepEqual(
transformReply(
'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0->-16384\n'
),
[{
id: 'id',
url: '127.0.0.1:30001@31001',
host: '127.0.0.1',
port: 30001,
cport: 31001,
flags: ['master'],
pingSent: 0,
pongRecv: 0,
configEpoch: 0,
linkState: RedisClusterNodeLinkStates.CONNECTED,
slots: [], // TODO
replicas: []
}]
);
});
});
});

View File

@@ -0,0 +1,96 @@
export function transformArguments(): Array<string> {
return ['CLUSTER', 'NODES'];
}
export enum RedisClusterNodeLinkStates {
CONNECTED = 'connected',
DISCONNECTED = 'disconnected'
}
interface RedisClusterNodeTransformedUrl {
host: string;
port: number;
cport: number;
}
export interface RedisClusterReplicaNode extends RedisClusterNodeTransformedUrl {
id: string;
url: string;
flags: Array<string>;
pingSent: number;
pongRecv: number;
configEpoch: number;
linkState: RedisClusterNodeLinkStates;
}
export interface RedisClusterMasterNode extends RedisClusterReplicaNode {
slots: Array<{
from: number;
to: number;
}>;
replicas: Array<RedisClusterReplicaNode>;
}
export function transformReply(reply: string): Array<RedisClusterMasterNode> {
const lines = reply.split('\n');
lines.pop(); // last line is empty
const mastersMap = new Map<string, RedisClusterMasterNode>(),
replicasMap = new Map<string, Array<RedisClusterReplicaNode>>();
for (const line of lines) {
const [id, url, flags, masterId, pingSent, pongRecv, configEpoch, linkState, ...slots] = line.split(' '),
node = {
id,
url,
...transformNodeUrl(url),
flags: flags.split(','),
pingSent: Number(pingSent),
pongRecv: Number(pongRecv),
configEpoch: Number(configEpoch),
linkState: (linkState as RedisClusterNodeLinkStates)
};
if (masterId === '-') {
let replicas = replicasMap.get(id);
if (!replicas) {
replicas = [];
replicasMap.set(id, replicas);
}
mastersMap.set(id, {
...node,
slots: slots.map(slot => {
// TODO: importing & exporting (https://redis.io/commands/cluster-nodes#special-slot-entries)
const [fromString, toString] = slot.split('-', 2),
from = Number(fromString);
return {
from,
to: toString ? Number(toString) : from
};
}),
replicas
});
} else {
const replicas = replicasMap.get(masterId);
if (!replicas) {
replicasMap.set(masterId, [node]);
} else {
replicas.push(node);
}
}
}
return [...mastersMap.values()];
}
function transformNodeUrl(url: string): RedisClusterNodeTransformedUrl {
const indexOfColon = url.indexOf(':'),
indexOfAt = url.indexOf('@', indexOfColon);
return {
host: url.substring(0, indexOfColon),
port: Number(url.substring(indexOfColon + 1, indexOfAt)),
cport: Number(url.substring(indexOfAt + 1))
};
}

View File

@@ -0,0 +1,27 @@
import { strict as assert } from 'assert';
import { transformArguments } from './CLUSTER_RESET';
describe('CLUSTER RESET', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['CLUSTER', 'RESET']
);
});
it('HARD', () => {
assert.deepEqual(
transformArguments('HARD'),
['CLUSTER', 'RESET', 'HARD']
);
});
it('SOFT', () => {
assert.deepEqual(
transformArguments('SOFT'),
['CLUSTER', 'RESET', 'SOFT']
);
});
});
});

View File

@@ -0,0 +1,13 @@
export type ClusterResetModes = 'HARD' | 'SOFT';
export function transformArguments(mode?: ClusterResetModes): Array<string> {
const args = ['CLUSTER', 'RESET'];
if (mode) {
args.push(mode);
}
return args;
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,20 @@
import { strict as assert } from 'assert';
import { ClusterSlotStates, transformArguments } from './CLUSTER_SETSLOT';
describe('CLUSTER SETSLOT', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(0, ClusterSlotStates.IMPORTING),
['CLUSTER', 'SETSLOT', '0', 'IMPORTING']
);
});
it('with nodeId', () => {
assert.deepEqual(
transformArguments(0, ClusterSlotStates.IMPORTING, 'nodeId'),
['CLUSTER', 'SETSLOT', '0', 'IMPORTING', 'nodeId']
);
});
});
});

View File

@@ -0,0 +1,18 @@
export enum ClusterSlotStates {
IMPORTING = 'IMPORTING',
MIGRATING = 'MIGRATING',
STABLE = 'STABLE',
NODE = 'NODE'
}
export function transformArguments(slot: number, state: ClusterSlotStates, nodeId?: string): Array<string> {
const args = ['CLUSTER', 'SETSLOT', slot.toString(), state];
if (nodeId) {
args.push(nodeId);
}
return args;
}
export declare function transformReply(): string;

View File

@@ -0,0 +1,76 @@
import { strict as assert } from 'assert';
import { transformArguments, transformReply } from './CLUSTER_SLOTS';
describe('CLUSTER SLOTS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['CLUSTER', 'SLOTS']
);
});
it('transformReply', () => {
assert.deepEqual(
transformReply([
[
0,
5460,
['127.0.0.1', 30001, '09dbe9720cda62f7865eabc5fd8857c5d2678366'],
['127.0.0.1', 30004, '821d8ca00d7ccf931ed3ffc7e3db0599d2271abf']
],
[
5461,
10922,
['127.0.0.1', 30002, 'c9d93d9f2c0c524ff34cc11838c2003d8c29e013'],
['127.0.0.1', 30005, 'faadb3eb99009de4ab72ad6b6ed87634c7ee410f']
],
[
10923,
16383,
['127.0.0.1', 30003, '044ec91f325b7595e76dbcb18cc688b6a5b434a1'],
['127.0.0.1', 30006, '58e6e48d41228013e5d9c1c37c5060693925e97e']
]
]),
[{
from: 0,
to: 5460,
master: {
ip: '127.0.0.1',
port: 30001,
id: '09dbe9720cda62f7865eabc5fd8857c5d2678366'
},
replicas: [{
ip: '127.0.0.1',
port: 30004,
id: '821d8ca00d7ccf931ed3ffc7e3db0599d2271abf'
}]
}, {
from: 5461,
to: 10922,
master: {
ip: '127.0.0.1',
port: 30002,
id: 'c9d93d9f2c0c524ff34cc11838c2003d8c29e013'
},
replicas: [{
ip: '127.0.0.1',
port: 30005,
id: 'faadb3eb99009de4ab72ad6b6ed87634c7ee410f'
}]
}, {
from: 10923,
to: 16383,
master: {
ip: '127.0.0.1',
port: 30003,
id: '044ec91f325b7595e76dbcb18cc688b6a5b434a1'
},
replicas: [{
ip: '127.0.0.1',
port: 30006,
id: '58e6e48d41228013e5d9c1c37c5060693925e97e'
}]
}]
);
});
});

View File

@@ -0,0 +1,41 @@
import { RedisCommandArguments } from '.';
export function transformArguments(): RedisCommandArguments {
return ['CLUSTER', 'SLOTS'];
}
type ClusterSlotsRawNode = [ip: string, port: number, id: string];
type ClusterSlotsRawReply = Array<[from: number, to: number, master: ClusterSlotsRawNode, ...replicas: Array<ClusterSlotsRawNode>]>;
type ClusterSlotsNode = {
ip: string;
port: number;
id: string;
};
export type ClusterSlotsReply = Array<{
from: number;
to: number;
master: ClusterSlotsNode;
replicas: Array<ClusterSlotsNode>;
}>;
export function transformReply(reply: ClusterSlotsRawReply): ClusterSlotsReply {
return reply.map(([from, to, master, ...replicas]) => {
return {
from,
to,
master: transformNode(master),
replicas: replicas.map(transformNode)
};
});
}
function transformNode([ip, port, id]: ClusterSlotsRawNode): ClusterSlotsNode {
return {
ip,
port,
id
};
}

View File

@@ -0,0 +1,17 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './COMMAND';
import { assertPingCommand } from './COMMAND_INFO.spec';
describe('COMMAND', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['COMMAND']
);
});
testUtils.testWithClient('client.command', async client => {
assertPingCommand((await client.command()).find(command => command.name === 'ping'));
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,12 @@
import { RedisCommandArguments } from '.';
import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers';
export const IS_READ_ONLY = true;
export function transformArguments(): RedisCommandArguments {
return ['COMMAND'];
}
export function transformReply(reply: Array<CommandRawReply>): Array<CommandReply> {
return reply.map(transformCommandReply);
}

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './COMMAND_COUNT';
describe('COMMAND COUNT', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['COMMAND', 'COUNT']
);
});
testUtils.testWithClient('client.commandCount', async client => {
assert.equal(
typeof await client.commandCount(),
'number'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,9 @@
import { RedisCommandArguments } from '.';
export const IS_READ_ONLY = true;
export function transformArguments(): RedisCommandArguments {
return ['COMMAND', 'COUNT'];
}
export declare function transformReply(): number;

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './COMMAND_GETKEYS';
describe('COMMAND GETKEYS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(['GET', 'key']),
['COMMAND', 'GETKEYS', 'GET', 'key']
);
});
testUtils.testWithClient('client.commandGetKeys', async client => {
assert.deepEqual(
await client.commandGetKeys(['GET', 'key']),
['key']
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -0,0 +1,9 @@
import { RedisCommandArguments } from '.';
export const IS_READ_ONLY = true;
export function transformArguments(args: Array<string>): RedisCommandArguments {
return ['COMMAND', 'GETKEYS', ...args];
}
export declare function transformReply(): Array<string>;

View File

@@ -0,0 +1,45 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './COMMAND_INFO';
import { CommandCategories, CommandFlags, CommandReply } from './generic-transformers';
export function assertPingCommand(commandInfo: CommandReply | null | undefined): void {
assert.deepEqual(
commandInfo,
{
name: 'ping',
arity: -1,
flags: new Set([CommandFlags.STALE, CommandFlags.FAST]),
firstKeyIndex: 0,
lastKeyIndex: 0,
step: 0,
categories: new Set(
testUtils.isVersionGreaterThan([6]) ?
[CommandCategories.FAST, CommandCategories.CONNECTION] :
[]
)
}
);
}
describe('COMMAND INFO', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(['PING']),
['COMMAND', 'INFO', 'PING']
);
});
describe('client.commandInfo', () => {
testUtils.testWithClient('PING', async client => {
assertPingCommand((await client.commandInfo(['PING']))[0]);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('DOSE_NOT_EXISTS', async client => {
assert.deepEqual(
await client.commandInfo(['DOSE_NOT_EXISTS']),
[null]
);
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -0,0 +1,12 @@
import { RedisCommandArguments } from '.';
import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers';
export const IS_READ_ONLY = true;
export function transformArguments(commands: Array<string>): RedisCommandArguments {
return ['COMMAND', 'INFO', ...commands];
}
export function transformReply(reply: Array<CommandRawReply | null>): Array<CommandReply | null> {
return reply.map(command => command ? transformCommandReply(command) : null);
}

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './CONFIG_GET';
describe('CONFIG GET', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('*'),
['CONFIG', 'GET', '*']
);
});
});

Some files were not shown because too many files have changed in this diff Show More