diff --git a/README.md b/README.md index 8e7e384525..b9fe524c67 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ node-redis is a modern, high performance [Redis](https://redis.io) client for No ## Installation -Start a redis-server via docker (or any other method you prefer): +Start a redis via docker: ```bash -docker run -p 6379:6379 -it redis/redis-stack-server:latest +docker run -p 6379:6379 -d redis:8.0-rc1 ``` To install node-redis, simply: @@ -38,13 +38,13 @@ To install node-redis, simply: ```bash npm install redis ``` - -> "redis" is the "whole in one" package that includes all the other packages. If you only need a subset of the commands, you can install the individual packages. See the list below. +> "redis" is the "whole in one" package that includes all the other packages. If you only need a subset of the commands, +> you can install the individual packages. See the list below. ## Packages | Name | Description | -|------------------------------------------------|---------------------------------------------------------------------------------------------| +| ---------------------------------------------- | ------------------------------------------------------------------------------------------- | | [`redis`](./packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | | [`@redis/client`](./packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | | [`@redis/bloom`](./packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | @@ -53,7 +53,254 @@ npm install redis | [`@redis/time-series`](./packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | | [`@redis/entraid`](./packages/entraid) | Secure token-based authentication for Redis clients using Microsoft Entra ID | -> Looking for a high-level library to handle object mapping? See [redis-om-node](https://github.com/redis/redis-om-node)! +> Looking for a high-level library to handle object mapping? +> See [redis-om-node](https://github.com/redis/redis-om-node)! + + +## Usage + +### Basic Example + +```typescript +import { createClient } from "redis"; + +const client = await createClient() + .on("error", (err) => console.log("Redis Client Error", err)) + .connect(); + +await client.set("key", "value"); +const value = await client.get("key"); +client.destroy(); +``` + +The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in +the format `redis[s]://[[username][:password]@][host][:port][/db-number]`: + +```typescript +createClient({ + url: "redis://alice:foobared@awesome.redis.server:6380", +}); +``` + +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). + +To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean. +`client.isOpen` is also available. This returns `true` when the client's underlying socket is open, and `false` when it +isn't (for example when the client is still connecting or reconnecting after a network error). + +### Redis Commands + +There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed +using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`, +etc.): + +```typescript +// raw Redis commands +await client.HSET("key", "field", "value"); +await client.HGETALL("key"); + +// friendly JavaScript commands +await client.hSet("key", "field", "value"); +await client.hGetAll("key"); +``` + +Modifiers to commands are specified using a JavaScript object: + +```typescript +await client.set("key", "value", { + EX: 10, + NX: true, +}); +``` + +Replies will be transformed into useful data structures: + +```typescript +await client.hGetAll("key"); // { field1: 'value1', field2: 'value2' } +await client.hVals("key"); // ['value1', 'value2'] +``` + +`Buffer`s are supported as well: + +```typescript +const client = createClient().withTypeMapping({ + [RESP_TYPES.BLOB_STRING]: Buffer +}); + +await client.hSet("key", "field", Buffer.from("value")); // 'OK' +await client.hGet("key", "field"); // { field: } + +``` + +### Unsupported Redis Commands + +If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`: + +```typescript +await client.sendCommand(["SET", "key", "value", "NX"]); // 'OK' + +await client.sendCommand(["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2'] +``` + +### Transactions (Multi/Exec) + +Start a [transaction](https://redis.io/topics/transactions) by calling `.multi()`, then chaining your commands. When +you're done, call `.exec()` and you'll get an array back with your results: + +```typescript +await client.set("another-key", "another-value"); + +const [setKeyReply, otherKeyValue] = await client + .multi() + .set("key", "value") + .get("another-key") + .exec(); // ['OK', 'another-value'] +``` + +You can also [watch](https://redis.io/topics/transactions#optimistic-locking-using-check-and-set) keys by calling +`.watch()`. Your transaction will abort if any of the watched keys change. + + +### Blocking Commands + +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(); +``` + + +### Pub/Sub + +See the [Pub/Sub overview](./docs/pub-sub.md). + +### Scan Iterator + +[`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): + +```typescript +for await (const key of client.scanIterator()) { + // use the key! + await client.get(key); +} +``` + +This works with `HSCAN`, `SSCAN`, and `ZSCAN` too: + +```typescript +for await (const { field, value } of client.hScanIterator("hash")) { +} +for await (const member of client.sScanIterator("set")) { +} +for await (const { score, value } of client.zScanIterator("sorted-set")) { +} +``` + +You can override the default options by providing a configuration object: + +```typescript +client.scanIterator({ + TYPE: "string", // `SCAN` only + MATCH: "patter*", + COUNT: 100, +}); +``` + +### Disconnecting + +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()`. + +```typescript +client.destroy(); +``` + +### Auto-Pipelining + +Node Redis will automatically pipeline requests that are made during the same "tick". + +```typescript +client.set("Tm9kZSBSZWRpcw==", "users:1"); +client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw=="); +``` + +Of course, if you don't do something with your Promises you're certain to +get [unhandled Promise exceptions](https://nodejs.org/api/process.html#process_event_unhandledrejection). To take +advantage of auto-pipelining and handle your Promises, use `Promise.all()`. + +```typescript +await Promise.all([ + client.set("Tm9kZSBSZWRpcw==", "users:1"), + client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw=="), +]); +``` + +### Programmability + +See the [Programmability overview](./docs/programmability.md). + +### Clustering + +Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to connect to a Redis Cluster. + +### Events + +The Node Redis client class is an Nodejs EventEmitter and it emits an event each time the network status changes: + +| Name | When | Listener arguments | +| ----------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------- | +| `connect` | Initiating a connection to the server | _No arguments_ | +| `ready` | Client is ready to use | _No arguments_ | +| `end` | Connection has been closed (via `.disconnect()`) | _No arguments_ | +| `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` | +| `reconnecting` | Client is trying to reconnect to the server | _No arguments_ | +| `sharded-channel-moved` | See [here](./docs/pub-sub.md#sharded-channel-moved-event) | See [here](./docs/pub-sub.md#sharded-channel-moved-event) | + +> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and +> an `error` occurs, that error will be thrown and the Node.js process will exit. See the [ > `EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details. + +> The client will not emit [any other events](./docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. + +## Supported Redis versions + +Node Redis is supported with the following versions of Redis: + +| Version | Supported | +| ------- | ------------------ | +| 8.0.z | :heavy_check_mark: | +| 7.0.z | :heavy_check_mark: | +| 6.2.z | :heavy_check_mark: | +| 6.0.z | :heavy_check_mark: | +| 5.0.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. + +## Migration + +- [From V3 to V4](docs/v3-to-v4.md) +- [From V4 to V5](docs/v4-to-v5.md) +- [V5](docs/v5.md) ## Contributing diff --git a/docs/RESP.md b/docs/RESP.md index 5d634831e1..f8c2388226 100644 --- a/docs/RESP.md +++ b/docs/RESP.md @@ -23,7 +23,7 @@ By default, each type is mapped to the first option in the lists below. To chang - Double (`,`) => `number | string` - Simple String (`+`) => `string | Buffer` - Blob String (`$`) => `string | Buffer` -- Verbatim String (`=`) => `string | Buffer | VerbatimString` (TODO: `VerbatimString` typedoc link) +- Verbatim String (`=`) => `string | Buffer | VerbatimString` - Simple Error (`-`) => `ErrorReply` - Blob Error (`!`) => `ErrorReply` - Array (`*`) => `Array` diff --git a/docs/programmability.md b/docs/programmability.md index f6ae42033c..56eb048ca0 100644 --- a/docs/programmability.md +++ b/docs/programmability.md @@ -25,16 +25,22 @@ FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name='add', Load the prior redis function on the _redis server_ before running the example below. ```typescript -import { createClient } from 'redis'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { NumberReply } from '@redis/client/lib/RESP/types'; +import { createClient, RedisArgument } from 'redis'; const client = createClient({ functions: { library: { add: { NUMBER_OF_KEYS: 1, - FIRST_KEY_INDEX: 1, - transformArguments(key: string, toAdd: number): Array { - return [key, toAdd.toString()]; + parseCommand( + parser: CommandParser, + key: RedisArgument, + toAdd: RedisArgument + ) { + parser.pushKey(key) + parser.push(toAdd) }, transformReply: undefined as unknown as () => NumberReply } @@ -43,9 +49,8 @@ const client = createClient({ }); await client.connect(); - await client.set('key', '1'); -await client.library.add('key', 2); // 3 +await client.library.add('key', '2'); // 3 ``` ## [Lua Scripts](https://redis.io/docs/manual/programmability/eval-intro/) @@ -53,7 +58,9 @@ await client.library.add('key', 2); // 3 The following is an end-to-end example of the prior concept. ```typescript -import { createClient, defineScript, NumberReply } from 'redis'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { NumberReply } from '@redis/client/lib/RESP/types'; +import { createClient, defineScript, RedisArgument } from 'redis'; const client = createClient({ scripts: { @@ -61,8 +68,13 @@ const client = createClient({ SCRIPT: 'return redis.call("GET", KEYS[1]) + ARGV[1];', NUMBER_OF_KEYS: 1, FIRST_KEY_INDEX: 1, - transformArguments(key: string, toAdd: number): Array { - return [key, toAdd.toString()]; + parseCommand( + parser: CommandParser, + key: RedisArgument, + toAdd: RedisArgument + ) { + parser.pushKey(key) + parser.push(toAdd) }, transformReply: undefined as unknown as () => NumberReply }) @@ -70,7 +82,6 @@ const client = createClient({ }); await client.connect(); - await client.set('key', '1'); -await client.add('key', 2); // 3 +await client.add('key', '2'); // 3 ``` diff --git a/docs/v4-to-v5.md b/docs/v4-to-v5.md index 57ad3f9bbf..0f3dc96eaa 100644 --- a/docs/v4-to-v5.md +++ b/docs/v4-to-v5.md @@ -225,12 +225,12 @@ In v5, any unwritten commands (in the same pipeline) will be discarded. ### 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.[M][REV]RANGE`: the `ALIGN` argument has been moved into `AGGREGATION` - `TS.SYNUPDATE`: `Array>` -> `Record>` -- `TS.M[REV]RANGE[_WITHLABELS]`, `TS.MGET[_WITHLABELS]`: TODO - -[^enum-to-constants]: TODO +- `TimeSeriesDuplicatePolicies` -> `TIME_SERIES_DUPLICATE_POLICIES` [^enum-to-constants] +- `TimeSeriesEncoding` -> `TIME_SERIES_ENCODING` [^enum-to-constants] +- `TimeSeriesAggregationType` -> `TIME_SERIES_AGGREGATION_TYPE` [^enum-to-constants] +- `TimeSeriesReducers` -> `TIME_SERIES_REDUCERS` [^enum-to-constants] +- `TimeSeriesBucketTimestamp` -> `TIME_SERIES_BUCKET_TIMESTAMP` [^enum-to-constants] [^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 diff --git a/docs/v5.md b/docs/v5.md index 4a1bd817b9..a3d0ab6838 100644 --- a/docs/v5.md +++ b/docs/v5.md @@ -1,30 +1,83 @@ # RESP3 Support -TODO +Node Redis v5 adds support for [RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md), the new Redis serialization protocol. RESP3 offers richer data types and improved type handling compared to RESP2. + +To use RESP3, specify it when creating your client: ```javascript +import { createClient } from 'redis'; + const client = createClient({ RESP: 3 }); ``` +## Type Mapping + +With RESP3, you can leverage the protocol's richer type system. You can customize how different Redis types are represented in JavaScript using type mapping: + ```javascript -// by default +import { createClient, RESP_TYPES } from 'redis'; + +// By default await client.hGetAll('key'); // Record +// Use Map instead of plain object await client.withTypeMapping({ - [TYPES.MAP]: Map + [RESP_TYPES.MAP]: Map }).hGetAll('key'); // Map +// Use both Map and Buffer await client.withTypeMapping({ - [TYPES.MAP]: Map, - [TYPES.BLOB_STRING]: Buffer + [RESP_TYPES.MAP]: Map, + [RESP_TYPES.BLOB_STRING]: Buffer }).hGetAll('key'); // Map ``` +This replaces the previous approach of using `commandOptions({ returnBuffers: true })` in v4. + +## PubSub in RESP3 + +RESP3 uses a different mechanism for handling Pub/Sub messages. Instead of modifying the `onReply` handler as in RESP2, RESP3 provides a dedicated `onPush` handler. When using RESP3, the client automatically uses this more efficient push notification system. + +## Known Limitations + +### Unstable Module Commands + +Some Redis module commands have unstable RESP3 transformations. These commands will throw an error when used with RESP3 unless you explicitly opt in to using them by setting `unstableResp3: true` in your client configuration: + +```javascript +const client = createClient({ + RESP: 3, + unstableResp3: true +}); +``` + +The following commands have unstable RESP3 implementations: + +1. **Stream Commands**: + - `XREAD` and `XREADGROUP` - The response format differs between RESP2 and RESP3 + +2. **Search Commands (RediSearch)**: + - `FT.AGGREGATE` + - `FT.AGGREGATE_WITHCURSOR` + - `FT.CURSOR_READ` + - `FT.INFO` + - `FT.PROFILE_AGGREGATE` + - `FT.PROFILE_SEARCH` + - `FT.SEARCH` + - `FT.SEARCH_NOCONTENT` + - `FT.SPELLCHECK` + +3. **Time Series Commands**: + - `TS.INFO` + - `TS.INFO_DEBUG` + +If you need to use these commands with RESP3, be aware that the response format might change in future versions. + # Sentinel Support -[TODO](./sentinel.md) +[Sentinel](./sentinel.md) # `multi.exec<'typed'>` / `multi.execTyped`