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

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>
This commit is contained in:
leibale
2021-10-19 16:20:02 -04:00
parent 46aad787eb
commit 2a7a7c1c2e
18 changed files with 1883 additions and 480 deletions

12
.eslintrc.json Normal file
View File

@@ -0,0 +1,12 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}

View File

@@ -13,7 +13,7 @@ jobs:
fail-fast: false
matrix:
node-version: [12.x, 14.x, 16.x]
redis-version: [5.x, 6.x]
redis-version: [5.x, 6.0.x, 6.2.x]
steps:
- uses: actions/checkout@v2.3.4

View File

@@ -57,7 +57,7 @@ createClient({
});
```
You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in in the [Wiki](https://github.com/redis/node-redis/wiki/lib.socket#RedisSocketOptions).
You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in the [client configuration guide](./docs/client-configuration.md).
### Redis Commands
@@ -227,32 +227,34 @@ import { createClient, defineScript } from 'redis';
})();
```
### Cluster
### Disconnecting
Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a non-clustered client:
There are two functions that disconnect a client from the Redis server. In most scenarios you should use `.quit()` to ensure that pending commands are sent to Redis before closing a connection.
#### `.QUIT()`/`.quit()`
Gracefully close a client's connection to Redis, by sending the [`QUIT`](https://redis.io/commands/quit) command to the server. Before quitting, the client executes any remaining commands in its queue, and will receive replies from Redis for each of them.
```typescript
import { createCluster } from 'redis';
const [ping, get, quit] = await Promise.all([
client.ping(),
client.get('key'),
client.quit()
]); // ['PONG', null, 'OK']
(async () => {
const cluster = createCluster({
rootNodes: [
{
url: 'redis://10.0.0.1:30001'
},
{
url: 'redis://10.0.0.2:30002'
}
]
});
try {
await client.get('key');
} catch (err) {
// ClosedClient Error
}
```
cluster.on('error', (err) => console.log('Redis Cluster Error', err));
#### `.disconnect()`
await cluster.connect();
Forcibly close a client's connection to Redis immediately. Calling `disconnect` will not send further pending commands to the Redis server, or wait for or parse outstanding responses.
await cluster.set('key', 'value');
const value = await cluster.get('key');
})();
```typescript
await client.disconnect();
```
### Auto-Pipelining
@@ -273,6 +275,23 @@ await Promise.all([
]);
```
### Clustering
Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to connect to a Redis Cluster.
## Supported Redis versions
Node Redis is supported with the following versions of Redis:
| Version | Supported |
|---------|--------------------|
| 6.2.z | :heavy_check_mark: |
| 6.0.z | :heavy_check_mark: |
| 5.y.z | :heavy_check_mark: |
| < 5.0 | :x: |
> Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support.
## Contributing
If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md).

View File

@@ -5,9 +5,9 @@
Node Redis is generally backwards compatible with very few exceptions, so we recommend users to always use the latest version to experience stability, performance and security.
| Version | Supported |
| ------- | ------------------ |
| 4.0.x | :white_check_mark: |
| 3.1.x | :white_check_mark: |
|---------|--------------------|
| 4.0.z | :heavy_check_mark: |
| 3.1.z | :heavy_check_mark: |
| < 3.1 | :x: |
## Reporting a Vulnerability

54
docs/clustering.md Normal file
View File

@@ -0,0 +1,54 @@
# Clustering
## Basic Example
Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a regular client instance:
```typescript
import { createCluster } from 'redis';
(async () => {
const cluster = createCluster({
rootNodes: [
{
url: 'redis://10.0.0.1:30001'
},
{
url: 'redis://10.0.0.2:30002'
}
]
});
cluster.on('error', (err) => console.log('Redis Cluster Error', err));
await cluster.connect();
await cluster.set('key', 'value');
const value = await cluster.get('key');
})();
```
## `createCluster` configuration
> See the [client configuration](./client-configuration.md) page for the `rootNodes` and `defaults` configuration schemas.
| Property | Default | Description |
|------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| rootNodes | | An array of root nodes that are part of the cluster, which will be used to get the cluster topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster, 3 should be enough to reliably connect and obtain the cluster configuration from the server |
| defaults | | The default configuration values for every client in the cluster. Use this for example when specifying an ACL user to connect with |
| useReplicas | `false` | When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes |
| maxCommandRedirections | `16` | The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors | |
## Command Routing
### Commands that operate on Redis Keys
Commands such as `GET`, `SET`, etc. will be routed by the first key, for instance `MGET 1 2 3` will be routed by the key `1`.
### [Server Commands][https://redis.io/commands#server]
Admin commands such as `MEMORY STATS`, `FLUSHALL`, etc. are not attached to the cluster, and should be executed on a specific node using `.getSlot()` or `.getAllMasters()`.
### "Forwarded Commands"
Some commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. The client will send these commands to a random node in order to spread the load across the cluster.

View File

@@ -6,20 +6,20 @@ import { RedisCommandRawReply } from '../commands';
export interface QueueCommandOptions {
asap?: boolean;
chainId?: symbol;
signal?: any; // TODO: `AbortSignal` type is incorrect
signal?: AbortSignal;
}
interface CommandWaitingToBeSent extends CommandWaitingForReply {
args: Array<string | Buffer>;
chainId?: symbol;
abort?: {
signal: any; // TODO: `AbortSignal` type is incorrect
signal: AbortSignal;
listener(): void;
};
}
interface CommandWaitingForReply {
resolve(reply?: any): void;
resolve(reply?: unknown): void;
reject(err: Error): void;
channelsCounter?: number;
bufferMode?: boolean;
@@ -135,7 +135,8 @@ export default class RedisCommandsQueue {
signal: options.signal,
listener
};
options.signal.addEventListener('abort', listener, {
// AbortSignal type is incorrent
(options.signal as any).addEventListener('abort', listener, {
once: true
});
}

View File

@@ -229,5 +229,5 @@ export default {
UNWATCH,
unwatch: UNWATCH,
WAIT,
wait: WAIT,
wait: WAIT
};

View File

@@ -611,8 +611,9 @@ describe('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
while (process.hrtime.bigint() - start < 1_000_000) {}
}
await promise;
} catch (err) {

View File

@@ -34,16 +34,16 @@ type WithCommands = {
};
export type WithModules<M extends RedisModules> = {
[P in keyof M]: {
[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]: RedisClientCommandSignature<S[P]>;
[P in keyof S as S[P] extends never ? never : P]: RedisClientCommandSignature<S[P]>;
};
export type RedisClientType<M extends RedisModules = {}, S extends RedisScripts = {}> =
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> =
@@ -53,12 +53,14 @@ export interface ClientCommandOptions extends QueueCommandOptions {
isolated?: boolean;
}
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
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 = {}, S extends RedisScripts = {}>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
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,
@@ -74,14 +76,14 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
return Client;
}
static create<M extends RedisModules = {}, S extends RedisScripts = {}>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
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<{}, {}> {
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<{}, {}> = {
parsed: RedisClientOptions<Record<string, never>, Record<string, never>> = {
socket: {
host: hostname
}
@@ -245,10 +247,12 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
(this as any).#v4.sendCommand = this.#sendCommand.bind(this);
(this as any).sendCommand = (...args: Array<unknown>): void => {
const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] as Function : undefined,
const callback = typeof args[args.length - 1] === 'function' ?
args[args.length - 1] as ClientLegacyCallback :
undefined,
actualArgs = !callback ? args : args.slice(0, -1);
this.#sendCommand(actualArgs.flat() as Array<string>)
.then((reply: unknown) => {
.then((reply: RedisCommandRawReply) => {
if (!callback) return;
// https://github.com/NodeRedis/node-redis#commands:~:text=minimal%20parsing
@@ -435,17 +439,12 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
this.#socket.cork();
while (true) {
while (!this.#socket.writableNeedDrain) {
const args = this.#queue.getCommandToSend();
if (args === undefined) break;
let writeResult;
for (const toWrite of encodeCommand(args)) {
writeResult = this.#socket.write(toWrite);
}
if (!writeResult) {
break;
this.#socket.write(toWrite);
}
}
}

View File

@@ -11,16 +11,16 @@ type WithCommands<M extends RedisModules, S extends RedisScripts> = {
};
type WithModules<M extends RedisModules, S extends RedisScripts> = {
[P in keyof M]: {
[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]: RedisClientMultiCommandSignature<S[P], M, S>
[P in keyof S as S[P] extends never ? never : P]: RedisClientMultiCommandSignature<S[P], M, S>
};
export type RedisClientMultiCommandType<M extends RedisModules = {}, S extends RedisScripts = {}> =
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>>;

View File

@@ -234,7 +234,6 @@ export default class RedisSocket extends EventEmitter {
this.#isOpen = false;
try {
await fn();
await this.disconnect(true);

View File

@@ -1,5 +1,5 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisModules, RedisScript, RedisScripts } 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';
@@ -7,14 +7,9 @@ import { EventEmitter } from 'events';
import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command';
export type RedisClusterClientOptions = Omit<RedisClientOptions<{}, {}>, 'modules' | 'scripts'>;
export type RedisClusterClientOptions = Omit<RedisClientOptions<Record<string, never>, Record<string, never>>, 'modules' | 'scripts'>;
export interface RedisClusterPlugins<M extends RedisModules, S extends RedisScripts> {
modules?: M;
scripts?: S;
}
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisClusterPlugins<M, S> {
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
rootNodes: Array<RedisClusterClientOptions>;
defaults?: Partial<RedisClusterClientOptions>;
useReplicas?: boolean;
@@ -25,10 +20,10 @@ type WithCommands = {
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
};
export type RedisClusterType<M extends RedisModules = {}, S extends RedisScripts = {}> =
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 = {}, S extends RedisScripts = {}> extends EventEmitter {
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;
@@ -39,7 +34,7 @@ export default class RedisCluster<M extends RedisModules = {}, S extends RedisSc
return command.FIRST_KEY_INDEX(...originalArgs);
}
static create<M extends RedisModules = {}, S extends RedisScripts = {}>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
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,

View File

@@ -12,16 +12,16 @@ type WithCommands<M extends RedisModules, S extends RedisScripts> = {
};
type WithModules<M extends RedisModules, S extends RedisScripts> = {
[P in keyof M]: {
[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]: RedisClusterMultiCommandSignature<S[P], M, S>
[P in keyof S as S[P] extends never ? never : P]: RedisClusterMultiCommandSignature<S[P], M, S>
};
export type RedisClusterMultiCommandType<M extends RedisModules = {}, S extends RedisScripts = {}> =
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>>;

View File

@@ -6,4 +6,4 @@ export function transformArguments(): RedisCommandArguments {
return ['COMMAND', 'COUNT'];
}
declare function transformReply(): number;
export declare function transformReply(): number;

View File

@@ -6,4 +6,4 @@ export function transformArguments(args: Array<string>): RedisCommandArguments {
return ['COMMAND', 'GETKEYS', ...args];
}
declare function transformReply(): Array<string>;
export declare function transformReply(): Array<string>;

View File

@@ -2,43 +2,27 @@ import { RedisCommandArguments } from '.';
export const FIRST_KEY_INDEX = 1;
interface EX {
type MaximumOneOf<T, K extends keyof T = keyof T> =
K extends keyof T ? { [P in K]?: T[K] } & Partial<Record<Exclude<keyof T, K>, never>> : never;
type SetTTL = MaximumOneOf<{
EX: number;
}
interface PX {
PX: number
}
interface EXAT {
PX: number;
EXAT: number;
}
interface PXAT {
PXAT: number;
}
interface KEEPTTL {
KEEPTTL: true;
}
}>;
type SetTTL = EX | PX | EXAT | PXAT | KEEPTTL | {};
interface NX {
type SetGuards = MaximumOneOf<{
NX: true;
}
interface XX {
XX: true;
}
type SetGuards = NX | XX | {};
}>;
interface SetCommonOptions {
GET: true
GET?: true;
}
type SetOptions = SetTTL & SetGuards & (SetCommonOptions | {});
type SetOptions = SetTTL & SetGuards & SetCommonOptions;
export function transformArguments(key: string | Buffer, value: string | Buffer, options?: SetOptions): RedisCommandArguments {
const args = ['SET', key, value];
@@ -47,25 +31,25 @@ export function transformArguments(key: string | Buffer, value: string | Buffer,
return args;
}
if ('EX' in options) {
if (options.EX) {
args.push('EX', options.EX.toString());
} else if ('PX' in options) {
} else if (options.PX) {
args.push('PX', options.PX.toString());
} else if ('EXAT' in options) {
} else if (options.EXAT) {
args.push('EXAT', options.EXAT.toString());
} else if ('PXAT' in options) {
} else if (options.PXAT) {
args.push('PXAT', options.PXAT.toString());
} else if ((<KEEPTTL>options).KEEPTTL) {
} else if (options.KEEPTTL) {
args.push('KEEPTTL');
}
if ((<NX>options).NX) {
if (options.NX) {
args.push('NX');
} else if ((<XX>options).XX) {
} else if (options.XX) {
args.push('XX');
}
if ((<SetCommonOptions>options).GET) {
if (options.GET) {
args.push('GET');
}

2075
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"scripts": {
"test": "nyc -r text-summary -r html mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
"build": "tsc",
"lint": "eslint ./*.ts ./lib/**/*.ts",
"documentation": "typedoc"
},
"dependencies": {
@@ -26,20 +27,23 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@tsconfig/node12": "^1.0.9",
"@types/mocha": "^9.0.0",
"@types/node": "^16.10.3",
"@types/node": "^16.11.1",
"@types/sinon": "^10.0.4",
"@types/which": "^2.0.1",
"@types/yallist": "^4.0.1",
"mocha": "^9.1.2",
"@typescript-eslint/eslint-plugin": "^5.1.0",
"@typescript-eslint/parser": "^5.1.0",
"eslint": "^8.0.1",
"mocha": "^9.1.3",
"nyc": "^15.1.0",
"release-it": "^14.11.6",
"sinon": "^11.1.2",
"source-map-support": "^0.5.20",
"ts-node": "^10.3.0",
"typedoc": "^0.22.5",
"typedoc": "^0.22.6",
"typedoc-github-wiki-theme": "^0.6.0",
"typedoc-plugin-markdown": "^3.11.3",
"typescript": "^4.4.3",
"typescript": "^4.4.4",
"which": "^2.0.2"
},
"engines": {