You've already forked node-redis
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 * fix54124793ad
* 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 * revertd32f1edf8a
* 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:
302
packages/client/lib/client/commands-queue.ts
Normal file
302
packages/client/lib/client/commands-queue.ts
Normal 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);
|
||||
}
|
||||
}
|
233
packages/client/lib/client/commands.ts
Normal file
233
packages/client/lib/client/commands.ts
Normal 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
|
||||
};
|
681
packages/client/lib/client/index.spec.ts
Normal file
681
packages/client/lib/client/index.spec.ts
Normal 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
|
||||
});
|
||||
});
|
541
packages/client/lib/client/index.ts
Normal file
541
packages/client/lib/client/index.ts
Normal 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;
|
132
packages/client/lib/client/multi-command.ts
Normal file
132
packages/client/lib/client/multi-command.ts
Normal 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
|
||||
});
|
38
packages/client/lib/client/socket.spec.ts
Normal file
38
packages/client/lib/client/socket.spec.ts
Normal 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'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
267
packages/client/lib/client/socket.ts
Normal file
267
packages/client/lib/client/socket.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
228
packages/client/lib/cluster/cluster-slots.ts
Normal file
228
packages/client/lib/cluster/cluster-slots.ts
Normal 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);
|
||||
}
|
||||
}
|
535
packages/client/lib/cluster/commands.ts
Normal file
535
packages/client/lib/cluster/commands.ts
Normal 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
|
||||
};
|
105
packages/client/lib/cluster/index.spec.ts
Normal file
105
packages/client/lib/cluster/index.spec.ts
Normal 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
|
||||
});
|
||||
});
|
206
packages/client/lib/cluster/index.ts
Normal file
206
packages/client/lib/cluster/index.ts
Normal 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
|
||||
});
|
112
packages/client/lib/cluster/multi-command.ts
Normal file
112
packages/client/lib/cluster/multi-command.ts
Normal 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
|
||||
});
|
14
packages/client/lib/command-options.ts
Normal file
14
packages/client/lib/command-options.ts
Normal 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;
|
||||
}
|
44
packages/client/lib/commander.spec.ts
Normal file
44
packages/client/lib/commander.spec.ts
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
130
packages/client/lib/commander.ts
Normal file
130
packages/client/lib/commander.ts
Normal 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;
|
||||
}
|
23
packages/client/lib/commands/ACL_CAT.spec.ts
Normal file
23
packages/client/lib/commands/ACL_CAT.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
11
packages/client/lib/commands/ACL_CAT.ts
Normal file
11
packages/client/lib/commands/ACL_CAT.ts
Normal 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>;
|
30
packages/client/lib/commands/ACL_DELUSER.spec.ts
Normal file
30
packages/client/lib/commands/ACL_DELUSER.spec.ts
Normal 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);
|
||||
});
|
8
packages/client/lib/commands/ACL_DELUSER.ts
Normal file
8
packages/client/lib/commands/ACL_DELUSER.ts
Normal 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;
|
23
packages/client/lib/commands/ACL_GENPASS.spec.ts
Normal file
23
packages/client/lib/commands/ACL_GENPASS.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
11
packages/client/lib/commands/ACL_GENPASS.ts
Normal file
11
packages/client/lib/commands/ACL_GENPASS.ts
Normal 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;
|
32
packages/client/lib/commands/ACL_GETUSER.spec.ts
Normal file
32
packages/client/lib/commands/ACL_GETUSER.spec.ts
Normal 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);
|
||||
});
|
34
packages/client/lib/commands/ACL_GETUSER.ts
Normal file
34
packages/client/lib/commands/ACL_GETUSER.ts
Normal 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]
|
||||
};
|
||||
}
|
14
packages/client/lib/commands/ACL_LIST.spec.ts
Normal file
14
packages/client/lib/commands/ACL_LIST.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_LIST.ts
Normal file
5
packages/client/lib/commands/ACL_LIST.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'LIST'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<string>;
|
14
packages/client/lib/commands/ACL_LOAD.spec.ts
Normal file
14
packages/client/lib/commands/ACL_LOAD.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_LOAD.ts
Normal file
5
packages/client/lib/commands/ACL_LOAD.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'LOAD'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
53
packages/client/lib/commands/ACL_LOG.spec.ts
Normal file
53
packages/client/lib/commands/ACL_LOG.spec.ts
Normal 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'
|
||||
}]
|
||||
);
|
||||
});
|
||||
});
|
48
packages/client/lib/commands/ACL_LOG.ts
Normal file
48
packages/client/lib/commands/ACL_LOG.ts
Normal 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]
|
||||
}));
|
||||
}
|
14
packages/client/lib/commands/ACL_LOG_RESET.spec.ts
Normal file
14
packages/client/lib/commands/ACL_LOG_RESET.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_LOG_RESET.ts
Normal file
5
packages/client/lib/commands/ACL_LOG_RESET.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'LOG', 'RESET'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
14
packages/client/lib/commands/ACL_SAVE.spec.ts
Normal file
14
packages/client/lib/commands/ACL_SAVE.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_SAVE.ts
Normal file
5
packages/client/lib/commands/ACL_SAVE.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'SAVE'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
23
packages/client/lib/commands/ACL_SETUSER.spec.ts
Normal file
23
packages/client/lib/commands/ACL_SETUSER.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
8
packages/client/lib/commands/ACL_SETUSER.ts
Normal file
8
packages/client/lib/commands/ACL_SETUSER.ts
Normal 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;
|
14
packages/client/lib/commands/ACL_USERS.spec.ts
Normal file
14
packages/client/lib/commands/ACL_USERS.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_USERS.ts
Normal file
5
packages/client/lib/commands/ACL_USERS.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'USERS'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<string>;
|
14
packages/client/lib/commands/ACL_WHOAMI.spec.ts
Normal file
14
packages/client/lib/commands/ACL_WHOAMI.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_WHOAMI.ts
Normal file
5
packages/client/lib/commands/ACL_WHOAMI.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'WHOAMI'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
11
packages/client/lib/commands/APPEND.spec.ts
Normal file
11
packages/client/lib/commands/APPEND.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
7
packages/client/lib/commands/APPEND.ts
Normal file
7
packages/client/lib/commands/APPEND.ts
Normal 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;
|
11
packages/client/lib/commands/ASKING.spec.ts
Normal file
11
packages/client/lib/commands/ASKING.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './ASKING';
|
||||
|
||||
describe('ASKING', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ASKING']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ASKING.ts
Normal file
5
packages/client/lib/commands/ASKING.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ASKING'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
25
packages/client/lib/commands/AUTH.spec.ts
Normal file
25
packages/client/lib/commands/AUTH.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
14
packages/client/lib/commands/AUTH.ts
Normal file
14
packages/client/lib/commands/AUTH.ts
Normal 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;
|
11
packages/client/lib/commands/BGREWRITEAOF.spec.ts
Normal file
11
packages/client/lib/commands/BGREWRITEAOF.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './BGREWRITEAOF';
|
||||
|
||||
describe('BGREWRITEAOF', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['BGREWRITEAOF']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/BGREWRITEAOF.ts
Normal file
5
packages/client/lib/commands/BGREWRITEAOF.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['BGREWRITEAOF'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
23
packages/client/lib/commands/BGSAVE.spec.ts
Normal file
23
packages/client/lib/commands/BGSAVE.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
15
packages/client/lib/commands/BGSAVE.ts
Normal file
15
packages/client/lib/commands/BGSAVE.ts
Normal 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;
|
31
packages/client/lib/commands/BITCOUNT.spec.ts
Normal file
31
packages/client/lib/commands/BITCOUNT.spec.ts
Normal 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);
|
||||
});
|
23
packages/client/lib/commands/BITCOUNT.ts
Normal file
23
packages/client/lib/commands/BITCOUNT.ts
Normal 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;
|
42
packages/client/lib/commands/BITFIELD.spec.ts
Normal file
42
packages/client/lib/commands/BITFIELD.spec.ts
Normal 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);
|
||||
});
|
82
packages/client/lib/commands/BITFIELD.ts
Normal file
82
packages/client/lib/commands/BITFIELD.ts
Normal 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>;
|
35
packages/client/lib/commands/BITOP.spec.ts
Normal file
35
packages/client/lib/commands/BITOP.spec.ts
Normal 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);
|
||||
});
|
12
packages/client/lib/commands/BITOP.ts
Normal file
12
packages/client/lib/commands/BITOP.ts
Normal 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;
|
42
packages/client/lib/commands/BITPOS.spec.ts
Normal file
42
packages/client/lib/commands/BITPOS.spec.ts
Normal 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);
|
||||
});
|
21
packages/client/lib/commands/BITPOS.ts
Normal file
21
packages/client/lib/commands/BITPOS.ts
Normal 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;
|
43
packages/client/lib/commands/BLMOVE.spec.ts
Normal file
43
packages/client/lib/commands/BLMOVE.spec.ts
Normal 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);
|
||||
});
|
22
packages/client/lib/commands/BLMOVE.ts
Normal file
22
packages/client/lib/commands/BLMOVE.ts
Normal 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;
|
79
packages/client/lib/commands/BLPOP.spec.ts
Normal file
79
packages/client/lib/commands/BLPOP.spec.ts
Normal 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);
|
||||
});
|
26
packages/client/lib/commands/BLPOP.ts
Normal file
26
packages/client/lib/commands/BLPOP.ts
Normal 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]
|
||||
};
|
||||
}
|
79
packages/client/lib/commands/BRPOP.spec.ts
Normal file
79
packages/client/lib/commands/BRPOP.spec.ts
Normal 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);
|
||||
});
|
26
packages/client/lib/commands/BRPOP.ts
Normal file
26
packages/client/lib/commands/BRPOP.ts
Normal 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]
|
||||
};
|
||||
}
|
47
packages/client/lib/commands/BRPOPLPUSH.spec.ts
Normal file
47
packages/client/lib/commands/BRPOPLPUSH.spec.ts
Normal 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);
|
||||
});
|
7
packages/client/lib/commands/BRPOPLPUSH.ts
Normal file
7
packages/client/lib/commands/BRPOPLPUSH.ts
Normal 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;
|
65
packages/client/lib/commands/BZPOPMAX.spec.ts
Normal file
65
packages/client/lib/commands/BZPOPMAX.spec.ts
Normal 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);
|
||||
});
|
28
packages/client/lib/commands/BZPOPMAX.ts
Normal file
28
packages/client/lib/commands/BZPOPMAX.ts
Normal 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])
|
||||
};
|
||||
}
|
65
packages/client/lib/commands/BZPOPMIN.spec.ts
Normal file
65
packages/client/lib/commands/BZPOPMIN.spec.ts
Normal 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);
|
||||
});
|
28
packages/client/lib/commands/BZPOPMIN.ts
Normal file
28
packages/client/lib/commands/BZPOPMIN.ts
Normal 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])
|
||||
};
|
||||
}
|
19
packages/client/lib/commands/CLIENT_ID.spec.ts
Normal file
19
packages/client/lib/commands/CLIENT_ID.spec.ts
Normal 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);
|
||||
});
|
7
packages/client/lib/commands/CLIENT_ID.ts
Normal file
7
packages/client/lib/commands/CLIENT_ID.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const IS_READ_ONLY = true;
|
||||
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['CLIENT', 'ID'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): number;
|
42
packages/client/lib/commands/CLIENT_INFO.spec.ts
Normal file
42
packages/client/lib/commands/CLIENT_INFO.spec.ts
Normal 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
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
85
packages/client/lib/commands/CLIENT_INFO.ts
Normal file
85
packages/client/lib/commands/CLIENT_INFO.ts
Normal 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)
|
||||
};
|
||||
}
|
20
packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts
Normal file
20
packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
13
packages/client/lib/commands/CLUSTER_ADDSLOTS.ts
Normal file
13
packages/client/lib/commands/CLUSTER_ADDSLOTS.ts
Normal 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;
|
11
packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts
Normal file
11
packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts
Normal file
5
packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['CLUSTER', 'FLUSHSLOTS'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
11
packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts
Normal file
11
packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts
Normal file
5
packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts
Normal 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;
|
46
packages/client/lib/commands/CLUSTER_INFO.spec.ts
Normal file
46
packages/client/lib/commands/CLUSTER_INFO.spec.ts
Normal 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
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
47
packages/client/lib/commands/CLUSTER_INFO.ts
Normal file
47
packages/client/lib/commands/CLUSTER_INFO.ts
Normal 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);
|
||||
}
|
11
packages/client/lib/commands/CLUSTER_MEET.spec.ts
Normal file
11
packages/client/lib/commands/CLUSTER_MEET.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/CLUSTER_MEET.ts
Normal file
5
packages/client/lib/commands/CLUSTER_MEET.ts
Normal 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;
|
95
packages/client/lib/commands/CLUSTER_NODES.spec.ts
Normal file
95
packages/client/lib/commands/CLUSTER_NODES.spec.ts
Normal 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: []
|
||||
}]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
96
packages/client/lib/commands/CLUSTER_NODES.ts
Normal file
96
packages/client/lib/commands/CLUSTER_NODES.ts
Normal 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))
|
||||
};
|
||||
}
|
27
packages/client/lib/commands/CLUSTER_RESET.spec.ts
Normal file
27
packages/client/lib/commands/CLUSTER_RESET.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
13
packages/client/lib/commands/CLUSTER_RESET.ts
Normal file
13
packages/client/lib/commands/CLUSTER_RESET.ts
Normal 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;
|
20
packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts
Normal file
20
packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
18
packages/client/lib/commands/CLUSTER_SETSLOT.ts
Normal file
18
packages/client/lib/commands/CLUSTER_SETSLOT.ts
Normal 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;
|
76
packages/client/lib/commands/CLUSTER_SLOTS.spec.ts
Normal file
76
packages/client/lib/commands/CLUSTER_SLOTS.spec.ts
Normal 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'
|
||||
}]
|
||||
}]
|
||||
);
|
||||
});
|
||||
});
|
41
packages/client/lib/commands/CLUSTER_SLOTS.ts
Normal file
41
packages/client/lib/commands/CLUSTER_SLOTS.ts
Normal 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
|
||||
};
|
||||
}
|
17
packages/client/lib/commands/COMMAND.spec.ts
Normal file
17
packages/client/lib/commands/COMMAND.spec.ts
Normal 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);
|
||||
});
|
12
packages/client/lib/commands/COMMAND.ts
Normal file
12
packages/client/lib/commands/COMMAND.ts
Normal 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);
|
||||
}
|
19
packages/client/lib/commands/COMMAND_COUNT.spec.ts
Normal file
19
packages/client/lib/commands/COMMAND_COUNT.spec.ts
Normal 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);
|
||||
});
|
9
packages/client/lib/commands/COMMAND_COUNT.ts
Normal file
9
packages/client/lib/commands/COMMAND_COUNT.ts
Normal 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;
|
19
packages/client/lib/commands/COMMAND_GETKEYS.spec.ts
Normal file
19
packages/client/lib/commands/COMMAND_GETKEYS.spec.ts
Normal 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);
|
||||
});
|
9
packages/client/lib/commands/COMMAND_GETKEYS.ts
Normal file
9
packages/client/lib/commands/COMMAND_GETKEYS.ts
Normal 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>;
|
45
packages/client/lib/commands/COMMAND_INFO.spec.ts
Normal file
45
packages/client/lib/commands/COMMAND_INFO.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
12
packages/client/lib/commands/COMMAND_INFO.ts
Normal file
12
packages/client/lib/commands/COMMAND_INFO.ts
Normal 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);
|
||||
}
|
11
packages/client/lib/commands/CONFIG_GET.spec.ts
Normal file
11
packages/client/lib/commands/CONFIG_GET.spec.ts
Normal 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
Reference in New Issue
Block a user