Skip to content

Commit 2a7a7c1

Browse files
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 <[email protected]>
1 parent 46aad78 commit 2a7a7c1

18 files changed

+1894
-491
lines changed

.eslintrc.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"root": true,
3+
"parser": "@typescript-eslint/parser",
4+
"plugins": [
5+
"@typescript-eslint"
6+
],
7+
"extends": [
8+
"eslint:recommended",
9+
"plugin:@typescript-eslint/eslint-recommended",
10+
"plugin:@typescript-eslint/recommended"
11+
]
12+
}

.github/workflows/tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
fail-fast: false
1414
matrix:
1515
node-version: [12.x, 14.x, 16.x]
16-
redis-version: [5.x, 6.x]
16+
redis-version: [5.x, 6.0.x, 6.2.x]
1717

1818
steps:
1919
- uses: actions/[email protected]

README.md

+40-21
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ createClient({
5757
});
5858
```
5959

60-
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).
60+
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).
6161

6262
### Redis Commands
6363

@@ -227,32 +227,34 @@ import { createClient, defineScript } from 'redis';
227227
})();
228228
```
229229

230-
### Cluster
230+
### Disconnecting
231231

232-
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:
232+
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.
233233

234-
```typescript
235-
import { createCluster } from 'redis';
234+
#### `.QUIT()`/`.quit()`
236235

237-
(async () => {
238-
const cluster = createCluster({
239-
rootNodes: [
240-
{
241-
url: 'redis://10.0.0.1:30001'
242-
},
243-
{
244-
url: 'redis://10.0.0.2:30002'
245-
}
246-
]
247-
});
236+
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.
237+
238+
```typescript
239+
const [ping, get, quit] = await Promise.all([
240+
client.ping(),
241+
client.get('key'),
242+
client.quit()
243+
]); // ['PONG', null, 'OK']
244+
245+
try {
246+
await client.get('key');
247+
} catch (err) {
248+
// ClosedClient Error
249+
}
250+
```
248251

249-
cluster.on('error', (err) => console.log('Redis Cluster Error', err));
252+
#### `.disconnect()`
250253

251-
await cluster.connect();
254+
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.
252255

253-
await cluster.set('key', 'value');
254-
const value = await cluster.get('key');
255-
})();
256+
```typescript
257+
await client.disconnect();
256258
```
257259

258260
### Auto-Pipelining
@@ -273,6 +275,23 @@ await Promise.all([
273275
]);
274276
```
275277

278+
### Clustering
279+
280+
Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to connect to a Redis Cluster.
281+
282+
## Supported Redis versions
283+
284+
Node Redis is supported with the following versions of Redis:
285+
286+
| Version | Supported |
287+
|---------|--------------------|
288+
| 6.2.z | :heavy_check_mark: |
289+
| 6.0.z | :heavy_check_mark: |
290+
| 5.y.z | :heavy_check_mark: |
291+
| < 5.0 | :x: |
292+
293+
> Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support.
294+
276295
## Contributing
277296

278297
If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md).

SECURITY.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
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.
66

77
| Version | Supported |
8-
| ------- | ------------------ |
9-
| 4.0.x | :white_check_mark: |
10-
| 3.1.x | :white_check_mark: |
8+
|---------|--------------------|
9+
| 4.0.z | :heavy_check_mark: |
10+
| 3.1.z | :heavy_check_mark: |
1111
| < 3.1 | :x: |
1212

1313
## Reporting a Vulnerability

docs/clustering.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Clustering
2+
3+
## Basic Example
4+
5+
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:
6+
7+
```typescript
8+
import { createCluster } from 'redis';
9+
10+
(async () => {
11+
const cluster = createCluster({
12+
rootNodes: [
13+
{
14+
url: 'redis://10.0.0.1:30001'
15+
},
16+
{
17+
url: 'redis://10.0.0.2:30002'
18+
}
19+
]
20+
});
21+
22+
cluster.on('error', (err) => console.log('Redis Cluster Error', err));
23+
24+
await cluster.connect();
25+
26+
await cluster.set('key', 'value');
27+
const value = await cluster.get('key');
28+
})();
29+
```
30+
31+
## `createCluster` configuration
32+
33+
> See the [client configuration](./client-configuration.md) page for the `rootNodes` and `defaults` configuration schemas.
34+
35+
| Property | Default | Description |
36+
|------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
37+
| 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 |
38+
| defaults | | The default configuration values for every client in the cluster. Use this for example when specifying an ACL user to connect with |
39+
| 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 |
40+
| maxCommandRedirections | `16` | The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors | |
41+
42+
## Command Routing
43+
44+
### Commands that operate on Redis Keys
45+
46+
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`.
47+
48+
### [Server Commands][https://redis.io/commands#server]
49+
50+
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()`.
51+
52+
### "Forwarded Commands"
53+
54+
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.

lib/client/commands-queue.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ import { RedisCommandRawReply } from '../commands';
66
export interface QueueCommandOptions {
77
asap?: boolean;
88
chainId?: symbol;
9-
signal?: any; // TODO: `AbortSignal` type is incorrect
9+
signal?: AbortSignal;
1010
}
1111

1212
interface CommandWaitingToBeSent extends CommandWaitingForReply {
1313
args: Array<string | Buffer>;
1414
chainId?: symbol;
1515
abort?: {
16-
signal: any; // TODO: `AbortSignal` type is incorrect
16+
signal: AbortSignal;
1717
listener(): void;
1818
};
1919
}
2020

2121
interface CommandWaitingForReply {
22-
resolve(reply?: any): void;
22+
resolve(reply?: unknown): void;
2323
reject(err: Error): void;
2424
channelsCounter?: number;
2525
bufferMode?: boolean;
@@ -135,7 +135,8 @@ export default class RedisCommandsQueue {
135135
signal: options.signal,
136136
listener
137137
};
138-
options.signal.addEventListener('abort', listener, {
138+
// AbortSignal type is incorrent
139+
(options.signal as any).addEventListener('abort', listener, {
139140
once: true
140141
});
141142
}

lib/client/commands.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -229,5 +229,5 @@ export default {
229229
UNWATCH,
230230
unwatch: UNWATCH,
231231
WAIT,
232-
wait: WAIT,
232+
wait: WAIT
233233
};

lib/client/index.spec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -611,8 +611,9 @@ describe('Client', () => {
611611
const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
612612
start = process.hrtime.bigint();
613613

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

617618
await promise;
618619
} catch (err) {

lib/client/index.ts

+15-16
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,16 @@ type WithCommands = {
3434
};
3535

3636
export type WithModules<M extends RedisModules> = {
37-
[P in keyof M]: {
37+
[P in keyof M as M[P] extends never ? never : P]: {
3838
[C in keyof M[P]]: RedisClientCommandSignature<M[P][C]>;
3939
};
4040
};
4141

4242
export type WithScripts<S extends RedisScripts> = {
43-
[P in keyof S]: RedisClientCommandSignature<S[P]>;
43+
[P in keyof S as S[P] extends never ? never : P]: RedisClientCommandSignature<S[P]>;
4444
};
4545

46-
export type RedisClientType<M extends RedisModules = {}, S extends RedisScripts = {}> =
46+
export type RedisClientType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
4747
RedisClient<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
4848

4949
export type InstantiableRedisClient<M extends RedisModules, S extends RedisScripts> =
@@ -53,12 +53,14 @@ export interface ClientCommandOptions extends QueueCommandOptions {
5353
isolated?: boolean;
5454
}
5555

56+
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
57+
5658
export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
5759
static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> {
5860
return commandOptions(options);
5961
}
6062

61-
static extend<M extends RedisModules = {}, S extends RedisScripts = {}>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
63+
static extend<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
6264
const Client = <any>extendWithModulesAndScripts({
6365
BaseClass: RedisClient,
6466
modules: plugins?.modules,
@@ -74,14 +76,14 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
7476
return Client;
7577
}
7678

77-
static create<M extends RedisModules = {}, S extends RedisScripts = {}>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
79+
static create<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
7880
return new (RedisClient.extend(options))(options);
7981
}
8082

81-
static parseURL(url: string): RedisClientOptions<{}, {}> {
83+
static parseURL(url: string): RedisClientOptions<Record<string, never>, Record<string, never>> {
8284
// https://www.iana.org/assignments/uri-schemes/prov/redis
8385
const { hostname, port, protocol, username, password, pathname } = new URL(url),
84-
parsed: RedisClientOptions<{}, {}> = {
86+
parsed: RedisClientOptions<Record<string, never>, Record<string, never>> = {
8587
socket: {
8688
host: hostname
8789
}
@@ -245,10 +247,12 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
245247

246248
(this as any).#v4.sendCommand = this.#sendCommand.bind(this);
247249
(this as any).sendCommand = (...args: Array<unknown>): void => {
248-
const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] as Function : undefined,
250+
const callback = typeof args[args.length - 1] === 'function' ?
251+
args[args.length - 1] as ClientLegacyCallback :
252+
undefined,
249253
actualArgs = !callback ? args : args.slice(0, -1);
250254
this.#sendCommand(actualArgs.flat() as Array<string>)
251-
.then((reply: unknown) => {
255+
.then((reply: RedisCommandRawReply) => {
252256
if (!callback) return;
253257

254258
// https://github.com/NodeRedis/node-redis#commands:~:text=minimal%20parsing
@@ -435,17 +439,12 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
435439

436440
this.#socket.cork();
437441

438-
while (true) {
442+
while (!this.#socket.writableNeedDrain) {
439443
const args = this.#queue.getCommandToSend();
440444
if (args === undefined) break;
441445

442-
let writeResult;
443446
for (const toWrite of encodeCommand(args)) {
444-
writeResult = this.#socket.write(toWrite);
445-
}
446-
447-
if (!writeResult) {
448-
break;
447+
this.#socket.write(toWrite);
449448
}
450449
}
451450
}

lib/client/multi-command.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ type WithCommands<M extends RedisModules, S extends RedisScripts> = {
1111
};
1212

1313
type WithModules<M extends RedisModules, S extends RedisScripts> = {
14-
[P in keyof M]: {
14+
[P in keyof M as M[P] extends never ? never : P]: {
1515
[C in keyof M[P]]: RedisClientMultiCommandSignature<M[P][C], M, S>;
1616
};
1717
};
1818

1919
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
20-
[P in keyof S]: RedisClientMultiCommandSignature<S[P], M, S>
20+
[P in keyof S as S[P] extends never ? never : P]: RedisClientMultiCommandSignature<S[P], M, S>
2121
};
2222

23-
export type RedisClientMultiCommandType<M extends RedisModules = {}, S extends RedisScripts = {}> =
23+
export type RedisClientMultiCommandType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
2424
RedisClientMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
2525

2626
export type RedisClientMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;

lib/client/socket.ts

-1
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ export default class RedisSocket extends EventEmitter {
234234

235235
this.#isOpen = false;
236236

237-
238237
try {
239238
await fn();
240239
await this.disconnect(true);

lib/cluster/index.ts

+6-11
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
import COMMANDS from './commands';
2-
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisModules, RedisScript, RedisScripts } from '../commands';
2+
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
33
import { ClientCommandOptions, RedisClientCommandSignature, RedisClientOptions, RedisClientType, WithModules, WithScripts } from '../client';
44
import RedisClusterSlots, { ClusterNode } from './cluster-slots';
55
import { extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, extendWithCommands } from '../commander';
66
import { EventEmitter } from 'events';
77
import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
88
import { RedisMultiQueuedCommand } from '../multi-command';
99

10-
export type RedisClusterClientOptions = Omit<RedisClientOptions<{}, {}>, 'modules' | 'scripts'>;
10+
export type RedisClusterClientOptions = Omit<RedisClientOptions<Record<string, never>, Record<string, never>>, 'modules' | 'scripts'>;
1111

12-
export interface RedisClusterPlugins<M extends RedisModules, S extends RedisScripts> {
13-
modules?: M;
14-
scripts?: S;
15-
}
16-
17-
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisClusterPlugins<M, S> {
12+
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
1813
rootNodes: Array<RedisClusterClientOptions>;
1914
defaults?: Partial<RedisClusterClientOptions>;
2015
useReplicas?: boolean;
@@ -25,10 +20,10 @@ type WithCommands = {
2520
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
2621
};
2722

28-
export type RedisClusterType<M extends RedisModules = {}, S extends RedisScripts = {}> =
23+
export type RedisClusterType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
2924
RedisCluster<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
3025

31-
export default class RedisCluster<M extends RedisModules = {}, S extends RedisScripts = {}> extends EventEmitter {
26+
export default class RedisCluster<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> extends EventEmitter {
3227
static extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: RedisCommandArguments): string | Buffer | undefined {
3328
if (command.FIRST_KEY_INDEX === undefined) {
3429
return undefined;
@@ -39,7 +34,7 @@ export default class RedisCluster<M extends RedisModules = {}, S extends RedisSc
3934
return command.FIRST_KEY_INDEX(...originalArgs);
4035
}
4136

42-
static create<M extends RedisModules = {}, S extends RedisScripts = {}>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
37+
static create<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
4338
return new (<any>extendWithModulesAndScripts({
4439
BaseClass: RedisCluster,
4540
modules: options?.modules,

0 commit comments

Comments
 (0)