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

V5 bringing RESP3, Sentinel and TypeMapping to node-redis

RESP3 Support
   - Some commands responses in RESP3 aren't stable yet and therefore return an "untyped" ReplyUnion.
 
Sentinel

TypeMapping

Correctly types Multi commands

Note: some API changes to be further documented in v4-to-v5.md
This commit is contained in:
Shaya Potter
2024-10-15 17:46:52 +03:00
committed by GitHub
parent 2fc79bdfb3
commit b2d35c5286
1174 changed files with 45931 additions and 36274 deletions

46
docs/RESP.md Normal file
View File

@@ -0,0 +1,46 @@
# Mapping RESP types
RESP, which stands for **R**edis **SE**rialization **P**rotocol, is the protocol used by Redis to communicate with clients. This document shows how RESP types can be mapped to JavaScript types. You can learn more about RESP itself in the [offical documentation](https://redis.io/docs/reference/protocol-spec/).
By default, each type is mapped to the first option in the lists below. To change this, configure a [`typeMapping`](.).
## RESP2
- Integer (`:`) => `number`
- Simple String (`+`) => `string | Buffer`
- Blob String (`$`) => `string | Buffer`
- Simple Error (`-`) => `ErrorReply`
- Array (`*`) => `Array`
> NOTE: the first type is the default type
## RESP3
- Null (`_`) => `null`
- Boolean (`#`) => `boolean`
- Number (`:`) => `number | string`
- Big Number (`(`) => `BigInt | string`
- Double (`,`) => `number | string`
- Simple String (`+`) => `string | Buffer`
- Blob String (`$`) => `string | Buffer`
- Verbatim String (`=`) => `string | Buffer | VerbatimString` (TODO: `VerbatimString` typedoc link)
- Simple Error (`-`) => `ErrorReply`
- Blob Error (`!`) => `ErrorReply`
- Array (`*`) => `Array`
- Set (`~`) => `Array | Set`
- Map (`%`) => `object | Map | Array`
- Push (`>`) => `Array` => PubSub push/`'push'` event
> NOTE: the first type is the default type
### Map keys and Set members
When decoding a Map to `Map | object` or a Set to `Set`, keys and members of type "Simple String" or "Blob String" will be decoded as `string`s which enables lookups by value, ignoring type mapping. If you want them as `Buffer`s, decode them as `Array`s instead.
### Not Implemented
These parts of RESP3 are not yet implemented in Redis itself (at the time of writing), so are not yet implemented in the Node-Redis client either:
- [Attribute type](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#attribute-type)
- [Streamed strings](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#streamed-strings)
- [Streamed aggregated data types](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#streamed-aggregated-data-types)

View File

@@ -1,31 +1,32 @@
# `createClient` configuration
| Property | Default | Description |
|--------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| url | | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) |
| socket | | Socket connection properties. Unlisted [`net.connect`](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) properties (and [`tls.connect`](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)) are also supported |
| socket.port | `6379` | Redis server port |
| socket.host | `'localhost'` | Redis server hostname |
| socket.family | `0` | IP Stack version (one of `4 \| 6 \| 0`) |
| socket.path | | Path to the UNIX Socket |
| socket.connectTimeout | `5000` | Connection Timeout (in milliseconds) |
| socket.noDelay | `true` | Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
| socket.keepAlive | `5000` | Toggle [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality |
| socket.tls | | See explanation and examples [below](#TLS) |
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
| password | | ACL password or the old "--requirepass" password |
| name | | Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) |
| database | | Redis database number (see [`SELECT`](https://redis.io/commands/select) command) |
| modules | | Included [Redis Modules](../README.md#packages) |
| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) |
| functions | | Function definitions (see [Functions](../README.md#functions)) |
| commandsQueueMaxLength | | Maximum length of the client's internal command queue |
| disableOfflineQueue | `false` | Disables offline queuing, see [FAQ](./FAQ.md#what-happens-when-the-network-goes-down) |
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](./v3-to-v4.md)) |
| isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) |
| pingInterval | | Send `PING` command at interval (in ms). Useful with ["Azure Cache for Redis"](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#idle-timeout) |
| Property | Default | Description |
|------------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| url | | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) |
| socket | | Socket connection properties. Unlisted [`net.connect`](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) properties (and [`tls.connect`](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)) are also supported |
| socket.port | `6379` | Redis server port |
| socket.host | `'localhost'` | Redis server hostname |
| socket.family | `0` | IP Stack version (one of `4 \| 6 \| 0`) |
| socket.path | | Path to the UNIX Socket |
| socket.connectTimeout | `5000` | Connection timeout (in milliseconds) |
| socket.noDelay | `true` | Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
| socket.keepAlive | `true` | Toggle [`keep-alive`](https://nodejs.org/api/net.html#socketsetkeepaliveenable-initialdelay) functionality |
| socket.keepAliveInitialDelay | `5000` | If set to a positive number, it sets the initial delay before the first keepalive probe is sent on an idle socket |
| socket.tls | | See explanation and examples [below](#TLS) |
| socket.reconnectStrategy | Exponential backoff with a maximum of 2000 ms; plus 0-200 ms random jitter. | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
| password | | ACL password or the old "--requirepass" password |
| name | | Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) |
| database | | Redis database number (see [`SELECT`](https://redis.io/commands/select) command) |
| modules | | Included [Redis Modules](../README.md#packages) |
| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) |
| functions | | Function definitions (see [Functions](../README.md#functions)) |
| commandsQueueMaxLength | | Maximum length of the client's internal command queue |
| disableOfflineQueue | `false` | Disables offline queuing, see [FAQ](./FAQ.md#what-happens-when-the-network-goes-down) |
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](./v3-to-v4.md)) |
| isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) |
| pingInterval | | Send `PING` command at interval (in ms). Useful with ["Azure Cache for Redis"](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#idle-timeout) |
## Reconnect Strategy
@@ -34,12 +35,19 @@ When the socket closes unexpectedly (without calling `.quit()`/`.disconnect()`),
2. `number` -> wait for `X` milliseconds before reconnecting.
3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
By default the strategy is `Math.min(retries * 50, 500)`, but it can be overwritten like so:
By default the strategy uses exponential backoff, but it can be overwritten like so:
```javascript
createClient({
socket: {
reconnectStrategy: retries => Math.min(retries * 50, 1000)
reconnectStrategy: retries => {
// Generate a random jitter between 0 200 ms:
const jitter = Math.floor(Math.random() * 200);
// Delay is an exponential back off, (times^2) * 50 ms, with a maximum value of 2000 ms:
const delay = Math.min(Math.pow(2, retries) * 50, 2000);
return delay + jitter;
}
}
});
```

View File

@@ -4,26 +4,22 @@
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
```javascript
import { createCluster } from 'redis';
const cluster = createCluster({
rootNodes: [
{
const cluster = await 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();
}]
})
.on('error', err => console.log('Redis Cluster Error', err))
.connect();
await cluster.set('key', 'value');
const value = await cluster.get('key');
await cluster.close();
```
## `createCluster` configuration
@@ -32,7 +28,7 @@ const value = await cluster.get('key');
| 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 |
| 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 |
| minimizeConnections | `false` | When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes. Useful for short-term or Pub/Sub-only connections. |
@@ -41,9 +37,11 @@ const value = await cluster.get('key');
| modules | | Included [Redis Modules](../README.md#packages) |
| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) |
| functions | | Function definitions (see [Functions](../README.md#functions)) |
## Auth with password and username
Specifying the password in the URL or a root node will only affect the connection to that specific node. In case you want to set the password for all the connections being created from a cluster instance, use the `defaults` option.
```javascript
createCluster({
rootNodes: [{
@@ -107,7 +105,7 @@ createCluster({
### Commands that operate on Redis Keys
Commands such as `GET`, `SET`, etc. are routed by the first key, for instance `MGET 1 2 3` will be routed by the key `1`.
Commands such as `GET`, `SET`, etc. are routed by the first key specified. For example `MGET 1 2 3` will be routed by the key `1`.
### [Server Commands](https://redis.io/commands#server)
@@ -115,4 +113,4 @@ Admin commands such as `MEMORY STATS`, `FLUSHALL`, etc. are not attached to the
### "Forwarded Commands"
Certain commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. This client sends these commands to a random node in order to spread the load across the cluster.
Certain commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. The client sends these commands to a random node in order to spread the load across the cluster.

68
docs/command-options.md Normal file
View File

@@ -0,0 +1,68 @@
# Command Options
> :warning: The command options API in v5 has breaking changes from the previous version. For more details, refer to the [v4-to-v5 guide](./v4-to-v5.md#command-options).
Command Options are used to create "proxy clients" that change the behavior of executed commands. See the sections below for details.
## Type Mapping
Some [RESP types](./RESP.md) can be mapped to more than one JavaScript type. For example, "Blob String" can be mapped to `string` or `Buffer`. You can override the default type mapping using the `withTypeMapping` function:
```javascript
await client.get('key'); // `string | null`
const proxyClient = client.withTypeMapping({
[TYPES.BLOB_STRING]: Buffer
});
await proxyClient.get('key'); // `Buffer | null`
```
See [RESP](./RESP.md) for a full list of types.
## Abort Signal
The client [batches commands](./FAQ.md#how-are-commands-batched) before sending them to Redis. Commands that haven't been written to the socket yet can be aborted using the [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) API:
```javascript
const controller = new AbortController(),
client = client.withAbortSignal(controller.signal);
try {
const promise = client.get('key');
controller.abort();
await promise;
} catch (err) {
// AbortError
}
```
## ASAP
Commands that are executed in the "asap" mode are added to the beginning of the "to sent" queue.
```javascript
const asapClient = client.asap();
await asapClient.ping();
```
## `withCommandOptions`
You can set all of the above command options in a single call with the `withCommandOptions` function:
```javascript
client.withCommandOptions({
typeMapping: ...,
abortSignal: ...,
asap: ...
});
```
If any of the above options are omitted, the default value will be used. For example, the following client would **not** be in ASAP mode:
```javascript
client.asap().withCommandOptions({
typeMapping: ...,
abortSignal: ...
});
```

View File

@@ -1,67 +0,0 @@
# Isolated Execution
Sometimes you want to run your commands on an exclusive connection. There are a few reasons to do this:
- You're using [transactions]() and need to `WATCH` a key or keys for changes.
- You want to run a blocking command that will take over the connection, such as `BLPOP` or `BLMOVE`.
- You're using the `MONITOR` command which also takes over a connection.
Below are several examples of how to use isolated execution.
> NOTE: Behind the scenes we're using [`generic-pool`](https://www.npmjs.com/package/generic-pool) to provide a pool of connections that can be isolated. Go there to learn more.
## The Simple Scenario
This just isolates execution on a single connection. Do what you want with that connection:
```typescript
await client.executeIsolated(async isolatedClient => {
await isolatedClient.set('key', 'value');
await isolatedClient.get('key');
});
```
## Transactions
Things get a little more complex with transactions. Here we are `.watch()`ing some keys. If the keys change during the transaction, a `WatchError` is thrown when `.exec()` is called:
```typescript
try {
await client.executeIsolated(async isolatedClient => {
await isolatedClient.watch('key');
const multi = isolatedClient.multi()
.ping()
.get('key');
if (Math.random() > 0.5) {
await isolatedClient.watch('another-key');
multi.set('another-key', await isolatedClient.get('another-key') / 2);
}
return multi.exec();
});
} catch (err) {
if (err instanceof WatchError) {
// the transaction aborted
}
}
```
## Blocking Commands
For blocking commands, you can execute a tidy little one-liner:
```typescript
await client.executeIsolated(isolatedClient => isolatedClient.blPop('key'));
```
Or, you can just run the command directly, and provide the `isolated` option:
```typescript
await client.blPop(
commandOptions({ isolated: true }),
'key'
);
```

74
docs/pool.md Normal file
View File

@@ -0,0 +1,74 @@
# `RedisClientPool`
Sometimes you want to run your commands on an exclusive connection. There are a few reasons to do this:
- You want to run a blocking command that will take over the connection, such as `BLPOP` or `BLMOVE`.
- You're using [transactions](https://redis.io/docs/interact/transactions/) and need to `WATCH` a key or keys for changes.
- Some more...
For those use cases you'll need to create a connection pool.
## Creating a pool
You can create a pool using the `createClientPool` function:
```javascript
import { createClientPool } from 'redis';
const pool = await createClientPool()
.on('error', err => console.error('Redis Client Pool Error', err));
```
the function accepts two arguments, the client configuration (see [here](./client-configuration.md) for more details), and the pool configuration:
| Property | Default | Description |
|----------------|---------|--------------------------------------------------------------------------------------------------------------------------------|
| minimum | 1 | The minimum clients the pool should hold to. The pool won't close clients if the pool size is less than the minimum. |
| maximum | 100 | The maximum clients the pool will have at once. The pool won't create any more resources and queue requests in memory. |
| acquireTimeout | 3000 | The maximum time (in ms) a task can wait in the queue. The pool will reject the task with `TimeoutError` in case of a timeout. |
| cleanupDelay | 3000 | The time to wait before cleaning up unused clients. |
You can also create a pool from a client (reusing it's configuration):
```javascript
const pool = await client.createPool()
.on('error', err => console.error('Redis Client Pool Error', err));
```
## The Simple Scenario
All the client APIs are exposed on the pool instance directly, and will execute the commands using one of the available clients.
```javascript
await pool.sendCommand(['PING']); // 'PONG'
await client.ping(); // 'PONG'
await client.withTypeMapping({
[RESP_TYPES.SIMPLE_STRING]: Buffer
}).ping(); // Buffer
```
## Transactions
Things get a little more complex with transactions. Here we are `.watch()`ing some keys. If the keys change during the transaction, a `WatchError` is thrown when `.exec()` is called:
```javascript
try {
await pool.execute(async client => {
await client.watch('key');
const multi = client.multi()
.ping()
.get('key');
if (Math.random() > 0.5) {
await client.watch('another-key');
multi.set('another-key', await client.get('another-key') / 2);
}
return multi.exec();
});
} catch (err) {
if (err instanceof WatchError) {
// the transaction aborted
}
}
```

76
docs/programmability.md Normal file
View File

@@ -0,0 +1,76 @@
# [Programmability](https://redis.io/docs/manual/programmability/)
Redis provides a programming interface allowing code execution on the redis server.
## [Functions](https://redis.io/docs/manual/programmability/functions-intro/)
The following example retrieves a key in redis, returning the value of the key, incremented by an integer. For example, if your key _foo_ has the value _17_ and we run `add('foo', 25)`, it returns the answer to Life, the Universe and Everything.
```lua
#!lua name=library
redis.register_function {
function_name = 'add',
callback = function(keys, args) return redis.call('GET', keys[1]) + args[1] end,
flags = { 'no-writes' }
}
```
Here is the same example, but in a format that can be pasted into the `redis-cli`.
```
FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name='add', callback=function(keys, args) return redis.call('GET', keys[1])+args[1] end, flags={'no-writes'}}"
```
Load the prior redis function on the _redis server_ before running the example below.
```typescript
import { createClient } from 'redis';
const client = createClient({
functions: {
library: {
add: {
NUMBER_OF_KEYS: 1,
FIRST_KEY_INDEX: 1,
transformArguments(key: string, toAdd: number): Array<string> {
return [key, toAdd.toString()];
},
transformReply: undefined as unknown as () => NumberReply
}
}
}
});
await client.connect();
await client.set('key', '1');
await client.library.add('key', 2); // 3
```
## [Lua Scripts](https://redis.io/docs/manual/programmability/eval-intro/)
The following is an end-to-end example of the prior concept.
```typescript
import { createClient, defineScript, NumberReply } from 'redis';
const client = createClient({
scripts: {
add: defineScript({
SCRIPT: 'return redis.call("GET", KEYS[1]) + ARGV[1];',
NUMBER_OF_KEYS: 1,
FIRST_KEY_INDEX: 1,
transformArguments(key: string, toAdd: number): Array<string> {
return [key, toAdd.toString()];
},
transformReply: undefined as unknown as () => NumberReply
})
}
});
await client.connect();
await client.set('key', '1');
await client.add('key', 2); // 3
```

View File

@@ -1,18 +1,20 @@
# Pub/Sub
The Pub/Sub API is implemented by `RedisClient` and `RedisCluster`.
The Pub/Sub API is implemented by `RedisClient`, `RedisCluster`, and `RedisSentinel`.
## Pub/Sub with `RedisClient`
Pub/Sub requires a dedicated stand-alone client. You can easily get one by `.duplicate()`ing an existing `RedisClient`:
### RESP2
```typescript
Using RESP2, Pub/Sub "takes over" the connection (a client with subscriptions will not execute commands), therefore it requires a dedicated connection. You can easily get one by `.duplicate()`ing an existing `RedisClient`:
```javascript
const subscriber = client.duplicate();
subscriber.on('error', err => console.error(err));
await subscriber.connect();
```
When working with a `RedisCluster`, this is handled automatically for you.
> When working with either `RedisCluster` or `RedisSentinel`, this is handled automatically for you.
### `sharded-channel-moved` event
@@ -29,6 +31,8 @@ The event listener signature is as follows:
)
```
> When working with `RedisCluster`, this is handled automatically for you.
## Subscribing
```javascript
@@ -39,7 +43,7 @@ await client.pSubscribe('channe*', listener);
await client.sSubscribe('channel', listener);
```
> ⚠️ Subscribing to the same channel more than once will create multiple listeners which will each be called when a message is recieved.
> ⚠️ Subscribing to the same channel more than once will create multiple listeners, each of which will be called when a message is received.
## Publishing

30
docs/scan-iterators.md Normal file
View File

@@ -0,0 +1,30 @@
# Scan Iterators
> :warning: The scan iterators API in v5 has breaking changes from the previous version. For more details, refer to the [v4-to-v5 guide](./v4-to-v5.md#scan-iterators).
[`SCAN`](https://redis.io/commands/scan) results can be looped over using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator):
```javascript
for await (const keys of client.scanIterator()) {
const values = await client.mGet(keys);
}
```
This works with `HSCAN`, `SSCAN`, and `ZSCAN` too:
```javascript
for await (const entries of client.hScanIterator('hash')) {}
for await (const members of client.sScanIterator('set')) {}
for await (const membersWithScores of client.zScanIterator('sorted-set')) {}
```
You can override the default options by providing a configuration object:
```javascript
client.scanIterator({
cursor: '0', // optional, defaults to '0'
TYPE: 'string', // `SCAN` only
MATCH: 'patter*',
COUNT: 100
});
```

100
docs/sentinel.md Normal file
View File

@@ -0,0 +1,100 @@
# Redis Sentinel
The [Redis Sentinel](https://redis.io/docs/management/sentinel/) object of node-redis provides a high level object that provides access to a high availability redis installation managed by Redis Sentinel to provide enumeration of master and replica nodes belonging to an installation as well as reconfigure itself on demand for failover and topology changes.
## Basic Example
```javascript
import { createSentinel } from 'redis';
const sentinel = await createSentinel({
name: 'sentinel-db',
sentinelRootNodes: [{
host: 'example',
port: 1234
}]
})
.on('error', err => console.error('Redis Sentinel Error', err));
.connect();
await sentinel.set('key', 'value');
const value = await sentinel.get('key');
await sentinel.close();
```
In the above example, we configure the sentinel object to fetch the configuration for the database Redis Sentinel is monitoring as "sentinel-db" with one of the sentinels being located at `example:1234`, then using it like a regular Redis client.
## `createSentinel` configuration
| Property | Default | Description |
|-----------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | | The sentinel identifier for a particular database cluster |
| sentinelRootNodes | | An array of root nodes that are part of the sentinel cluster, which will be used to get the 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 sentinel configuration from the server |
| maxCommandRediscovers | `16` | The maximum number of times a command will retry due to topology changes. |
| nodeClientOptions | | The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with |
| sentinelClientOptions | | The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with |
| masterPoolSize | `1` | The number of clients connected to the master node |
| replicaPoolSize | `0` | The number of clients connected to each replica node. When greater than 0, the client will distribute the load by executing read-only commands (such as `GET`, `GEOSEARCH`, etc.) across all the cluster nodes. |
| reserveClient | `false` | When `true`, one client will be reserved for the sentinel object. When `false`, the sentinel object will wait for the first available client from the pool. |
## PubSub
It supports PubSub via the normal mechanisms, including migrating the listeners if the node they are connected to goes down.
```javascript
await sentinel.subscribe('channel', message => {
// ...
});
await sentinel.unsubscribe('channel');
```
see [the PubSub guide](./pub-sub.md) for more details.
## Sentinel as a pool
The sentinel object provides the ability to manage a pool of clients for the master node:
```javascript
createSentinel({
// ...
masterPoolSize: 10
});
```
In addition, it also provides the ability have a pool of clients connected to the replica nodes, and to direct all read-only commands to them:
```javascript
createSentinel({
// ...
replicaPoolSize: 10
});
```
## Master client lease
Sometimes multiple commands needs to run on an exclusive client (for example, using `WATCH/MULTI/EXEC`).
There are 2 ways to get a client lease:
`.use()`
```javascript
const result = await sentinel.use(async client => {
await client.watch('key');
return client.multi()
.get('key')
.exec();
});
```
`.getMasterClientLease()`
```javascript
const clientLease = await sentinel.getMasterClientLease();
try {
await clientLease.watch('key');
const resp = await clientLease.multi()
.get('key')
.exec();
} finally {
clientLease.release();
}
```

6
docs/todo.md Normal file
View File

@@ -0,0 +1,6 @@
- "Isolation Pool" -> pool
- Cluster request response policies (either implement, or block "server" commands in cluster)
Docs:
- [Command Options](./command-options.md)
- [RESP](./RESP.md)

53
docs/transactions.md Normal file
View File

@@ -0,0 +1,53 @@
# [Transactions](https://redis.io/docs/interact/transactions/) ([`MULTI`](https://redis.io/commands/multi/)/[`EXEC`](https://redis.io/commands/exec/))
Start a [transaction](https://redis.io/docs/interact/transactions/) by calling `.multi()`, then chaining your commands. When you're done, call `.exec()` and you'll get an array back with your results:
```javascript
const [setReply, getReply] = await client.multi()
.set('key', 'value')
.get('another-key')
.exec();
```
## `exec<'typed'>()`/`execTyped()`
A transaction invoked with `.exec<'typed'>`/`execTyped()` will return types appropriate to the commands in the transaction:
```javascript
const multi = client.multi().ping();
await multi.exec(); // Array<ReplyUnion>
await multi.exec<'typed'>(); // [string]
await multi.execTyped(); // [string]
```
> :warning: this only works when all the commands are invoked in a single "call chain"
## [`WATCH`](https://redis.io/commands/watch/)
You can also [watch](https://redis.io/docs/interact/transactions/#optimistic-locking-using-check-and-set) keys by calling `.watch()`. Your transaction will abort if any of the watched keys change or if the client reconnected between the `watch` and `exec` calls.
The `WATCH` state is stored on the connection (by the server). In case you need to run multiple `WATCH` & `MULTI` in parallel you'll need to use a [pool](./pool.md).
## `execAsPipeline`
`execAsPipeline` will execute the commands without "wrapping" it with `MULTI` & `EXEC` (and lose the transactional semantics).
```javascript
await client.multi()
.get('a')
.get('b')
.execAsPipeline();
```
the diffrence between the above pipeline and `Promise.all`:
```javascript
await Promise.all([
client.get('a'),
client.get('b')
]);
```
is that if the socket disconnects during the pipeline, any unwritten commands will be discarded. i.e. if the socket disconnects after `GET a` is written to the socket, but before `GET b` is:
- using `Promise.all` - the client will try to execute `GET b` when the socket reconnects
- using `execAsPipeline` - `GET b` promise will be rejected as well

240
docs/v4-to-v5.md Normal file
View File

@@ -0,0 +1,240 @@
# v4 to v5 migration guide
## Client Configuration
### Keep Alive
To better align with Node.js build-in [`net`](https://nodejs.org/api/net.html) and [`tls`](https://nodejs.org/api/tls.html) modules, the `keepAlive` option has been split into 2 options: `keepAlive` (`boolean`) and `keepAliveInitialDelay` (`number`). The defaults remain `true` and `5000`.
### Legacy Mode
In the previous version, you could access "legacy" mode by creating a client and passing in `{ legacyMode: true }`. Now, you can create one off of an existing client by calling the `.legacy()` function. This allows easier access to both APIs and enables better TypeScript support.
```javascript
// use `client` for the current API
const client = createClient();
await client.set('key', 'value');
// use `legacyClient` for the "legacy" API
const legacyClient = client.legacy();
legacyClient.set('key', 'value', (err, reply) => {
// ...
});
```
## Command Options
In v4, command options are passed as a first optional argument:
```javascript
await client.get('key'); // `string | null`
await client.get(client.commandOptions({ returnBuffers: true }), 'key'); // `Buffer | null`
```
This has a couple of flaws:
1. The argument types are checked in runtime, which is a performance hit.
2. Code suggestions are less readable/usable, due to "function overloading".
3. Overall, "user code" is not as readable as it could be.
### The new API for v5
With the new API, instead of passing the options directly to the commands we use a "proxy client" to store them:
```javascript
await client.get('key'); // `string | null`
const proxyClient = client.withCommandOptions({
typeMapping: {
[TYPES.BLOB_STRING]: Buffer
}
});
await proxyClient.get('key'); // `Buffer | null`
```
for more information, see the [Command Options guide](./command-options.md).
## Quit VS Disconnect
The `QUIT` command has been deprecated in Redis 7.2 and should now also be considered deprecated in Node-Redis. Instead of sending a `QUIT` command to the server, the client can simply close the network connection.
`client.QUIT/quit()` is replaced by `client.close()`. and, to avoid confusion, `client.disconnect()` has been renamed to `client.destroy()`.
## Scan Iterators
Iterator commands like `SCAN`, `HSCAN`, `SSCAN`, and `ZSCAN` return collections of elements (depending on the data type). However, v4 iterators loop over these collections and yield individual items:
```javascript
for await (const key of client.scanIterator()) {
console.log(key, await client.get(key));
}
```
This mismatch can be awkward and makes "multi-key" commands like `MGET`, `UNLINK`, etc. pointless. So, in v5 the iterators now yield a collection instead of an element:
```javascript
for await (const keys of client.scanIterator()) {
// we can now meaningfully utilize "multi-key" commands
console.log(keys, await client.mGet(keys));
}
```
for more information, see the [Scan Iterators guide](./scan-iterators.md).
## Isolation Pool
In v4, `RedisClient` had the ability to create a pool of connections using an "Isolation Pool" on top of the "main" connection. However, there was no way to use the pool without a "main" connection:
```javascript
const client = await createClient()
.on('error', err => console.error(err))
.connect();
await client.ping(
client.commandOptions({ isolated: true })
);
```
In v5 we've extracted this pool logic into its own class—`RedisClientPool`:
```javascript
const pool = await createClientPool()
.on('error', err => console.error(err))
.connect();
await pool.ping();
```
See the [pool guide](./pool.md) for more information.
## Cluster `MULTI`
In v4, `cluster.multi()` did not support executing commands on replicas, even if they were readonly.
```javascript
// this might execute on a replica, depending on configuration
await cluster.sendCommand('key', true, ['GET', 'key']);
// this always executes on a master
await cluster.multi()
.addCommand('key', ['GET', 'key'])
.exec();
```
To support executing commands on replicas, `cluster.multi().addCommand` now requires `isReadonly` as the second argument, which matches the signature of `cluster.sendCommand`:
```javascript
await cluster.multi()
.addCommand('key', true, ['GET', 'key'])
.exec();
```
## `MULTI.execAsPipeline()`
```javascript
await client.multi()
.set('a', 'a')
.set('b', 'b')
.execAsPipeline();
```
In older versions, if the socket disconnects during the pipeline execution, i.e. after writing `SET a a` and before `SET b b`, the returned promise is rejected, but `SET b b` will still be executed on the server.
In v5, any unwritten commands (in the same pipeline) will be discarded.
## Commands
### Redis
- `ACL GETUSER`: `selectors`
- `COPY`: `destinationDb` -> `DB`, `replace` -> `REPLACE`, `boolean` -> `number` [^boolean-to-number]
- `CLIENT KILL`: `enum ClientKillFilters` -> `const CLIENT_KILL_FILTERS` [^enum-to-constants]
- `CLUSTER FAILOVER`: `enum FailoverModes` -> `const FAILOVER_MODES` [^enum-to-constants]
- `CLIENT TRACKINGINFO`: `flags` in RESP2 - `Set<string>` -> `Array<string>` (to match RESP3 default type mapping)
- `CLUSTER INFO`:
- `CLUSTER SETSLOT`: `ClusterSlotStates` -> `CLUSTER_SLOT_STATES` [^enum-to-constants]
- `CLUSTER RESET`: the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
- `CLUSTER FAILOVER`: `enum FailoverModes` -> `const FAILOVER_MODES` [^enum-to-constants], the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
- `CLUSTER LINKS`: `createTime` -> `create-time`, `sendBufferAllocated` -> `send-buffer-allocated`, `sendBufferUsed` -> `send-buffer-used` [^map-keys]
- `CLUSTER NODES`, `CLUSTER REPLICAS`, `CLUSTER INFO`: returning the raw `VerbatimStringReply`
- `EXPIRE`: `boolean` -> `number` [^boolean-to-number]
- `EXPIREAT`: `boolean` -> `number` [^boolean-to-number]
- `HSCAN`: `tuples` has been renamed to `entries`
- `HEXISTS`: `boolean` -> `number` [^boolean-to-number]
- `HRANDFIELD_COUNT_WITHVALUES`: `Record<BlobString, BlobString>` -> `Array<{ field: BlobString; value: BlobString; }>` (it can return duplicates).
- `HSETNX`: `boolean` -> `number` [^boolean-to-number]
- `INFO`:
- `LCS IDX`: `length` has been changed to `len`, `matches` has been changed from `Array<{ key1: RangeReply; key2: RangeReply; }>` to `Array<[key1: RangeReply, key2: RangeReply]>`
- `ZINTER`: instead of `client.ZINTER('key', { WEIGHTS: [1] })` use `client.ZINTER({ key: 'key', weight: 1 }])`
- `ZINTER_WITHSCORES`: instead of `client.ZINTER_WITHSCORES('key', { WEIGHTS: [1] })` use `client.ZINTER_WITHSCORES({ key: 'key', weight: 1 }])`
- `ZUNION`: instead of `client.ZUNION('key', { WEIGHTS: [1] })` use `client.ZUNION({ key: 'key', weight: 1 }])`
- `ZUNION_WITHSCORES`: instead of `client.ZUNION_WITHSCORES('key', { WEIGHTS: [1] })` use `client.ZUNION_WITHSCORES({ key: 'key', weight: 1 }])`
- `ZMPOP`: `{ elements: Array<{ member: string; score: number; }>; }` -> `{ members: Array<{ value: string; score: number; }>; }` to match other sorted set commands (e.g. `ZRANGE`, `ZSCAN`)
- `MOVE`: `boolean` -> `number` [^boolean-to-number]
- `PEXPIRE`: `boolean` -> `number` [^boolean-to-number]
- `PEXPIREAT`: `boolean` -> `number` [^boolean-to-number]
- `PFADD`: `boolean` -> `number` [^boolean-to-number]
- `RENAMENX`: `boolean` -> `number` [^boolean-to-number]
- `SETNX`: `boolean` -> `number` [^boolean-to-number]
- `SCAN`, `HSCAN`, `SSCAN`, and `ZSCAN`: `reply.cursor` will not be converted to number to avoid issues when the number is bigger than `Number.MAX_SAFE_INTEGER`. See [here](https://github.com/redis/node-redis/issues/2561).
- `SCRIPT EXISTS`: `Array<boolean>` -> `Array<number>` [^boolean-to-number]
- `SISMEMBER`: `boolean` -> `number` [^boolean-to-number]
- `SMISMEMBER`: `Array<boolean>` -> `Array<number>` [^boolean-to-number]
- `SMOVE`: `boolean` -> `number` [^boolean-to-number]
- `GEOSEARCH_WITH`/`GEORADIUS_WITH`: `GeoReplyWith` -> `GEO_REPLY_WITH` [^enum-to-constants]
- `GEORADIUSSTORE` -> `GEORADIUS_STORE`
- `GEORADIUSBYMEMBERSTORE` -> `GEORADIUSBYMEMBER_STORE`
- `XACK`: `boolean` -> `number` [^boolean-to-number]
- `XADD`: the `INCR` option has been removed, use `XADD_INCR` instead
- `LASTSAVE`: `Date` -> `number` (unix timestamp)
- `HELLO`: `protover` moved from the options object to it's own argument, `auth` -> `AUTH`, `clientName` -> `SETNAME`
- `MODULE LIST`: `version` -> `ver` [^map-keys]
- `MEMORY STATS`: [^map-keys]
- `FUNCTION RESTORE`: the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
- `FUNCTION STATS`: `runningScript` -> `running_script`, `durationMs` -> `duration_ms`, `librariesCount` -> `libraries_count`, `functionsCount` -> `functions_count` [^map-keys]
- `TIME`: `Date` -> `[unixTimestamp: string, microseconds: string]`
- `XGROUP_CREATECONSUMER`: [^boolean-to-number]
- `XGROUP_DESTROY`: [^boolean-to-number]
- `XINFO GROUPS`: `lastDeliveredId` -> `last-delivered-id` [^map-keys]
- `XINFO STREAM`: `radixTreeKeys` -> `radix-tree-keys`, `radixTreeNodes` -> `radix-tree-nodes`, `lastGeneratedId` -> `last-generated-id`, `maxDeletedEntryId` -> `max-deleted-entry-id`, `entriesAdded` -> `entries-added`, `recordedFirstEntryId` -> `recorded-first-entry-id`, `firstEntry` -> `first-entry`, `lastEntry` -> `last-entry`
- `XAUTOCLAIM`, `XCLAIM`, `XRANGE`, `XREVRANGE`: `Array<{ name: string; messages: Array<{ id: string; message: Record<string, string> }>; }>` -> `Record<string, Array<{ id: string; message: Record<string, string> }>>`
- `COMMAND LIST`: `enum FilterBy` -> `const COMMAND_LIST_FILTER_BY` [^enum-to-constants], the filter argument has been moved from a "top level argument" into ` { FILTERBY: { type: <MODULE|ACLCAT|PATTERN>; value: <value> } }`
### Bloom
- `TOPK.QUERY`: `Array<number>` -> `Array<boolean>`
### Graph
- `GRAPH.SLOWLOG`: `timestamp` has been changed from `Date` to `number`
### JSON
- `JSON.ARRINDEX`: `start` and `end` arguments moved to `{ range: { start: number; end: number; }; }` [^future-proofing]
- `JSON.ARRPOP`: `path` and `index` arguments moved to `{ path: string; index: number; }` [^future-proofing]
- `JSON.ARRLEN`, `JSON.CLEAR`, `JSON.DEBUG MEMORY`, `JSON.DEL`, `JSON.FORGET`, `JSON.OBJKEYS`, `JSON.OBJLEN`, `JSON.STRAPPEND`, `JSON.STRLEN`, `JSON.TYPE`: `path` argument moved to `{ path: string; }` [^future-proofing]
### Search
- `FT.SUGDEL`: [^boolean-to-number]
- `FT.CURSOR READ`: `cursor` type changed from `number` to `string` (in and out) to avoid issues when the number is bigger than `Number.MAX_SAFE_INTEGER`. See [here](https://github.com/redis/node-redis/issues/2561).
### Time Series
- `TS.ADD`: `boolean` -> `number` [^boolean-to-number]
- `TS.[M][REV]RANGE`: `enum TimeSeriesBucketTimestamp` -> `const TIME_SERIES_BUCKET_TIMESTAMP` [^enum-to-constants], `enum TimeSeriesReducers` -> `const TIME_SERIES_REDUCERS` [^enum-to-constants], the `ALIGN` argument has been moved into `AGGREGRATION`
- `TS.SYNUPDATE`: `Array<string | Array<string>>` -> `Record<string, Array<string>>`
- `TS.M[REV]RANGE[_WITHLABELS]`, `TS.MGET[_WITHLABELS]`: TODO
[^enum-to-constants]: TODO
[^map-keys]: To avoid unnecessary transformations and confusion, map keys will not be transformed to "js friendly" names (i.e. `number-of-keys` will not be renamed to `numberOfKeys`). See [here](https://github.com/redis/node-redis/discussions/2506).
[^future-proofing]: TODO

38
docs/v5.md Normal file
View File

@@ -0,0 +1,38 @@
# RESP3 Support
TODO
```javascript
const client = createClient({
RESP: 3
});
```
```javascript
// by default
await client.hGetAll('key'); // Record<string, string>
await client.withTypeMapping({
[TYPES.MAP]: Map
}).hGetAll('key'); // Map<string, string>
await client.withTypeMapping({
[TYPES.MAP]: Map,
[TYPES.BLOB_STRING]: Buffer
}).hGetAll('key'); // Map<string, Buffer>
```
# Sentinel Support
[TODO](./sentinel.md)
# `multi.exec<'typed'>` / `multi.execTyped`
We have introduced the ability to perform a "typed" `MULTI`/`EXEC` transaction. Rather than returning `Array<ReplyUnion>`, a transaction invoked with `.exec<'typed'>` will return types appropriate to the commands in the transaction where possible:
```javascript
const multi = client.multi().ping();
await multi.exec(); // Array<ReplyUnion>
await multi.exec<'typed'>(); // [string]
await multi.execTyped(); // [string]
```