Skip to content

Commit

Permalink
update docs, add 6.0.x to the tests matrix, add eslint, npm update, f…
Browse files Browse the repository at this point in the history
…ix some commands, fix some types

Co-authored-by: Simon Prickett <[email protected]>
  • Loading branch information
leibale and simonprickett committed Oct 19, 2021
1 parent 46aad78 commit 2a7a7c1
Show file tree
Hide file tree
Showing 18 changed files with 1,894 additions and 491 deletions.
12 changes: 12 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]
Expand Down
61 changes: 40 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

```typescript
import { createCluster } from 'redis';
#### `.QUIT()`/`.quit()`

(async () => {
const cluster = createCluster({
rootNodes: [
{
url: 'redis://10.0.0.1:30001'
},
{
url: 'redis://10.0.0.2:30002'
}
]
});
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
const [ping, get, quit] = await Promise.all([
client.ping(),
client.get('key'),
client.quit()
]); // ['PONG', null, 'OK']

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
Expand All @@ -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).
Expand Down
6 changes: 3 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions docs/clustering.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 5 additions & 4 deletions lib/client/commands-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
});
}
Expand Down
2 changes: 1 addition & 1 deletion lib/client/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,5 +229,5 @@ export default {
UNWATCH,
unwatch: UNWATCH,
WAIT,
wait: WAIT,
wait: WAIT
};
5 changes: 3 additions & 2 deletions lib/client/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,8 +611,9 @@ describe('Client', () => {
const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
start = process.hrtime.bigint();

// block the event loop for 1ms, to make sure the connection will timeout
while (process.hrtime.bigint() - start < 1_000_000) {}
while (process.hrtime.bigint() - start < 1_000_000) {
// block the event loop for 1ms, to make sure the connection will timeout
}

await promise;
} catch (err) {
Expand Down
31 changes: 15 additions & 16 deletions lib/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> =
Expand All @@ -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,
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions lib/client/multi-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>>;
Expand Down
1 change: 0 additions & 1 deletion lib/client/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ export default class RedisSocket extends EventEmitter {

this.#isOpen = false;


try {
await fn();
await this.disconnect(true);
Expand Down
17 changes: 6 additions & 11 deletions lib/cluster/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
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';
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;
Expand All @@ -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;
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 2a7a7c1

Please sign in to comment.