diff --git a/.github/README.md b/.github/README.md deleted file mode 100644 index c7046805107..00000000000 --- a/.github/README.md +++ /dev/null @@ -1,298 +0,0 @@ -# Node-Redis - -[![Tests](https://img.shields.io/github/workflow/status/redis/node-redis/Tests/master.svg?label=tests)](https://codecov.io/gh/redis/node-redis) -[![Coverage](https://codecov.io/gh/redis/node-redis/branch/master/graph/badge.svg?token=xcfqHhJC37)](https://codecov.io/gh/redis/node-redis) -[![License](https://img.shields.io/github/license/redis/node-redis.svg)](https://codecov.io/gh/redis/node-redis) -[![Chat](https://img.shields.io/discord/697882427875393627.svg)](https://discord.gg/XMMVgxUm) - -## Installation - -```bash -npm install redis@next -``` - -> :warning: The new interface is clean and cool, but if you have an existing code base, you'll want to read the [migration guide](../docs/v3-to-v4.md). - -## Usage - -### Basic Example - -```typescript -import { createClient } from 'redis'; - -(async () => { - const client = createClient(); - - client.on('error', (err) => console.log('Redis Client Error', err)); - - await client.connect(); - - await client.set('key', 'value'); - const value = await client.get('key'); -})(); -``` - -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). - -### 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'] -``` - -### 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. - -To dig deeper into transactions, check out the [Isolated Execution Guide](../docs/isolated-execution.md). - -### Blocking Commands - -Any command can be run on a new connection by specifying the `isolated` option. The newly created connection is closed when the command's `Promise` is fulfilled. - -This pattern works especially well for blocking commands—such as `BLPOP` and `BLMOVE`: - -```typescript -import { commandOptions } from 'redis'; - -const blPopPromise = client.blPop(commandOptions({ isolated: true }), 'key', 0); - -await client.lPush('key', ['1', '2']); - -await blPopPromise; // '2' -``` - -To learn more about isolated execution, check out the [guide](../docs/isolated-execution.md). - -### Pub/Sub - -Subscribing to a channel requires a dedicated stand-alone connection. You can easily get one by `.duplicate()`ing an existing Redis connection. - -```typescript -const subscriber = client.duplicate(); - -await subscriber.connect(); -``` - -Once you have one, simply subscribe and unsubscribe as needed: - -```typescript -await subscriber.subscribe('channel', (message) => { - console.log(message); // 'message' -}); - -await subscriber.pSubscribe('channe*', (message, channel) => { - console.log(message, channel); // 'message', 'channel' -}); - -await subscriber.unsubscribe('channel'); - -await subscriber.pUnsubscribe('channe*'); -``` - -Publish a message on a channel: - -```typescript -await publisher.publish('channel', 'message'); -``` - -### 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, member } 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 -}); -``` - -### Lua Scripts - -Define new functions using [Lua scripts](https://redis.io/commands/eval) which execute on the Redis server: - -```typescript -import { createClient, defineScript } from 'redis'; - -(async () => { - const client = createClient({ - scripts: { - add: defineScript({ - NUMBER_OF_KEYS: 1, - SCRIPT: - 'local val = redis.pcall("GET", KEYS[1]);' + - 'return val + ARGV[1];', - transformArguments(key: string, toAdd: number): Array { - return [key, toAdd.toString()]; - }, - transformReply(reply: number): number { - return reply; - } - }) - } - }); - - await client.connect(); - - await client.set('key', '1'); - await client.add('key', 2); // 3 -})(); -``` - -### Disconnecting - -There are two functions that disconnect a client from the Redis server. In most scenarios you should use `.quit()` to ensure that pending commands are sent to Redis before closing a connection. - -#### `.QUIT()`/`.quit()` - -Gracefully close a client's connection to Redis, by sending the [`QUIT`](https://redis.io/commands/quit) command to the server. Before quitting, the client executes any remaining commands in its queue, and will receive replies from Redis for each of them. - -```typescript -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 -} -``` - -#### `.disconnect()` - -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. - -```typescript -await client.disconnect(); -``` - -### 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==') -]); -``` - -### 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. - -## Packages - -| Name | Description | -|-------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [redis](../) | [![Downloads](https://img.shields.io/npm/dm/redis.svg)](https://www.npmjs.com/package/redis/v/next) [![Version](https://img.shields.io/npm/v/redis/next.svg)](https://www.npmjs.com/package/redis/v/next) | -| [@node-redis/client](../packages/client) | [![Downloads](https://img.shields.io/npm/dm/@node-redis/client.svg)](https://www.npmjs.com/package/@node-redis/client/v/next) [![Version](https://img.shields.io/npm/v/@node-redis/client/next.svg)](https://www.npmjs.com/package/@node-redis/client/v/next) | -| [@node-redis/json](../packages/json) | [![Downloads](https://img.shields.io/npm/dm/@node-redis/json.svg)](https://www.npmjs.com/package/@node-redis/json/v/next) [![Version](https://img.shields.io/npm/v/@node-redis/json/next.svg)](https://www.npmjs.com/package/@node-redis/json/v/next) [Redis JSON](https://oss.redis.com/redisjson/) commands | -| [@node-redis/search](../packages/search) | [![Downloads](https://img.shields.io/npm/dm/@node-redis/search.svg)](https://www.npmjs.com/package/@node-redis/search/v/next) [![Version](https://img.shields.io/npm/v/@node-redis/search/next.svg)](https://www.npmjs.com/package/@node-redis/search/v/next) [Redis Search](https://oss.redis.com/redisearch/) commands | - -## Contributing - -If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md). - -Thank you to all the people who already contributed to Node Redis! - -[![Contributors](https://contrib.rocks/image?repo=redis/node-redis)](https://github.com/redis/node-redis/graphs/contributors) - -## License - -This repository is licensed under the "MIT" license. See [LICENSE](LICENSE). diff --git a/README.md b/README.md index a98e6a261b2..d89219214cd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,312 @@ -# redis -The sources and docs for this package are in the main [node-redis](https://github.com/redis/node-redis) repo. +# Node-Redis + +[![Tests](https://img.shields.io/github/workflow/status/redis/node-redis/Tests/master.svg?label=tests)](https://codecov.io/gh/redis/node-redis) +[![Coverage](https://codecov.io/gh/redis/node-redis/branch/master/graph/badge.svg?token=xcfqHhJC37)](https://codecov.io/gh/redis/node-redis) +[![License](https://img.shields.io/github/license/redis/node-redis.svg)](https://codecov.io/gh/redis/node-redis) +[![Chat](https://img.shields.io/discord/697882427875393627.svg)](https://discord.gg/XMMVgxUm) + +node-redis is a modern, high performance [Redis](https://redis.io) client for Node.js with built-in support for Redis 6.2 commands and modules including [RediSearch](https://redisearch.io) and [RedisJSON](https://redisjson.io). + +## Installation + +```bash +npm install redis +``` + +> :warning: The new interface is clean and cool, but if you have an existing codebase, you'll want to read the [migration guide](./docs/v3-to-v4.md). + +## Usage + +### Basic Example + +```typescript +import { createClient } from 'redis'; + +(async () => { + const client = createClient(); + + client.on('error', (err) => console.log('Redis Client Error', err)); + + await client.connect(); + + await client.set('key', 'value'); + const value = await client.get('key'); +})(); +``` + +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). + +### 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'] +``` + +### 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. + +To dig deeper into transactions, check out the [Isolated Execution Guide](./docs/isolated-execution.md). + +### Blocking Commands + +Any command can be run on a new connection by specifying the `isolated` option. The newly created connection is closed when the command's `Promise` is fulfilled. + +This pattern works especially well for blocking commands—such as `BLPOP` and `BLMOVE`: + +```typescript +import { commandOptions } from 'redis'; + +const blPopPromise = client.blPop(commandOptions({ isolated: true }), 'key', 0); + +await client.lPush('key', ['1', '2']); + +await blPopPromise; // '2' +``` + +To learn more about isolated execution, check out the [guide](./docs/isolated-execution.md). + +### Pub/Sub + +Subscribing to a channel requires a dedicated stand-alone connection. You can easily get one by `.duplicate()`ing an existing Redis connection. + +```typescript +const subscriber = client.duplicate(); + +await subscriber.connect(); +``` + +Once you have one, simply subscribe and unsubscribe as needed: + +```typescript +await subscriber.subscribe('channel', (message) => { + console.log(message); // 'message' +}); + +await subscriber.pSubscribe('channe*', (message, channel) => { + console.log(message, channel); // 'message', 'channel' +}); + +await subscriber.unsubscribe('channel'); + +await subscriber.pUnsubscribe('channe*'); +``` + +Publish a message on a channel: + +```typescript +await publisher.publish('channel', 'message'); +``` + +There is support for buffers as well: + +```typescript +await subscriber.subscribe('channel', (message) => { + console.log(message); // +}, true); + +await subscriber.pSubscribe('channe*', (message, channel) => { + console.log(message, channel); // , +}, true); +``` + +### 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, member } 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 +}); +``` + +### Lua Scripts + +Define new functions using [Lua scripts](https://redis.io/commands/eval) which execute on the Redis server: + +```typescript +import { createClient, defineScript } from 'redis'; + +(async () => { + const client = createClient({ + scripts: { + add: defineScript({ + NUMBER_OF_KEYS: 1, + SCRIPT: + 'local val = redis.pcall("GET", KEYS[1]);' + + 'return val + ARGV[1];', + transformArguments(key: string, toAdd: number): Array { + return [key, toAdd.toString()]; + }, + transformReply(reply: number): number { + return reply; + } + }) + } + }); + + await client.connect(); + + await client.set('key', '1'); + await client.add('key', 2); // 3 +})(); +``` + +### Disconnecting + +There are two functions that disconnect a client from the Redis server. In most scenarios you should use `.quit()` to ensure that pending commands are sent to Redis before closing a connection. + +#### `.QUIT()`/`.quit()` + +Gracefully close a client's connection to Redis, by sending the [`QUIT`](https://redis.io/commands/quit) command to the server. Before quitting, the client executes any remaining commands in its queue, and will receive replies from Redis for each of them. + +```typescript +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 +} +``` + +#### `.disconnect()` + +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. + +```typescript +await client.disconnect(); +``` + +### 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==') +]); +``` + +### 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. + +## Packages + +| Name | Description | +|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [redis](./) | [![Downloads](https://img.shields.io/npm/dm/redis.svg)](https://www.npmjs.com/package/redis/v/next) [![Version](https://img.shields.io/npm/v/redis/next.svg)](https://www.npmjs.com/package/redis/v/next) | +| [@node-redis/client](./packages/client) | [![Downloads](https://img.shields.io/npm/dm/@node-redis/client.svg)](https://www.npmjs.com/package/@node-redis/client/v/next) [![Version](https://img.shields.io/npm/v/@node-redis/client/next.svg)](https://www.npmjs.com/package/@node-redis/client/v/next) | +| [@node-redis/json](./packages/json) | [![Downloads](https://img.shields.io/npm/dm/@node-redis/json.svg)](https://www.npmjs.com/package/@node-redis/json/v/next) [![Version](https://img.shields.io/npm/v/@node-redis/json/next.svg)](https://www.npmjs.com/package/@node-redis/json/v/next) [Redis JSON](https://oss.redis.com/redisjson/) commands | +| [@node-redis/search](./packages/search) | [![Downloads](https://img.shields.io/npm/dm/@node-redis/search.svg)](https://www.npmjs.com/package/@node-redis/search/v/next) [![Version](https://img.shields.io/npm/v/@node-redis/search/next.svg)](https://www.npmjs.com/package/@node-redis/search/v/next) [Redis Search](https://oss.redis.com/redisearch/) commands | + +## Contributing + +If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md). + +Thank you to all the people who already contributed to Node Redis! + +[![Contributors](https://contrib.rocks/image?repo=redis/node-redis)](https://github.com/redis/node-redis/graphs/contributors) + +## License + +This repository is licensed under the "MIT" license. See [LICENSE](LICENSE). diff --git a/docs/client-configuration.md b/docs/client-configuration.md index 1dbbdd8cba2..1b0194615af 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -15,11 +15,11 @@ | username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) | | password | | ACL password or the old "--requirepass" password | | database | | Database number to connect to (see [`SELECT`](https://redis.io/commands/select) command) | -| modules | | Object defining which [Redis Modules](../.github/README.md#packages) to include | +| modules | | Object defining which [Redis Modules](../README.md#packages) to include | | scripts | | Object defining Lua Scripts to use with this client (see [Lua Scripts](../README.md#lua-scripts)) | | commandsQueueMaxLength | | Maximum length of the client's internal command queue | | 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)) | +| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](./v3-to-v4.md)) | | isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) | ## Reconnect Strategy diff --git a/docs/clustering.md b/docs/clustering.md index 3b5ef94a5c7..f5ef9a9612d 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -38,7 +38,7 @@ import { createCluster } from 'redis'; | 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 | -| modules | | Object defining which [Redis Modules](../../README.md#modules) to include | +| modules | | Object defining which [Redis Modules](../README.md#modules) to include | | scripts | | Object defining Lua Scripts to use with this client (see [Lua Scripts](../README.md#lua-scripts)) | ## Command Routing diff --git a/docs/v3-to-v4.md b/docs/v3-to-v4.md index 7c3e9880431..90267d8245c 100644 --- a/docs/v3-to-v4.md +++ b/docs/v3-to-v4.md @@ -4,7 +4,7 @@ Version 4 of Node Redis is a major refactor. While we have tried to maintain bac ## Breaking Changes -See the [Change Log](../CHANGELOG.md). +See the [Change Log](../packages/client/CHANGELOG.md). ## Promises diff --git a/examples/README.md b/examples/README.md index aef0b38bdbb..94b043ae483 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,14 +2,16 @@ This folder contains example scripts showing how to use Node Redis in different scenarios. -| File Name | Description | -|-----------------------------|------------------------------------------------------------------------------------| -| `blocking-list-pop.js` | Block until an element is pushed to a list | -| `command-with-modifiers.js` | Define a script that allows to run a command with several modifiers | -| `connect-as-acl-user.js` | Connect to Redis 6 using an ACL user | -| `lua-multi-incr.js` | Define a custom lua script that allows you to perform INCRBY on multiple keys | -| `search+json.js` | Use [Redis Search](https://redisearch.io/) and [Redis JSON](https://redisjson.io/) | -| `set-scan.js` | An example script that shows how to use the SSCAN iterator functionality | +| File Name | Description | +|-----------------------------|----------------------------------------------------------------------------------------------------------------| +| `blocking-list-pop.js` | Block until an element is pushed to a list | +| `command-with-modifiers.js` | Define a script that allows to run a command with several modifiers | +| `connect-as-acl-user.js` | Connect to Redis 6 using an ACL user | +| `lua-multi-incr.js` | Define a custom lua script that allows you to perform INCRBY on multiple keys | +| `managing-json.js` | Store, retrieve and manipulate JSON data atomically with [RedisJSON](https://redisjson.io/) | +| `search-hashes.js` | Uses [RediSearch](https://redisearch.io) to index and search data in hashes | +| `search-json.js` | Uses [RediSearch](https://redisearch.io/) and [RedisJSON](https://redisjson.io/) to index and search JSON data | +| `set-scan.js` | An example script that shows how to use the SSCAN iterator functionality | ## Contributing diff --git a/examples/managing-json.js b/examples/managing-json.js new file mode 100644 index 00000000000..44978a94f05 --- /dev/null +++ b/examples/managing-json.js @@ -0,0 +1,81 @@ +// Store, retrieve and manipulate JSON data atomically with RedisJSON. + +import { createClient } from 'redis'; + +async function managingJSON() { + const client = createClient(); + + await client.connect(); + await client.del('noderedis:jsondata'); + + // Store a JSON object... + await client.json.set('noderedis:jsondata', '$', { + name: 'Roberta McDonald', + pets: [ + { + name: 'Fluffy', + species: 'dog', + age: 5, + isMammal: true + }, + { + name: 'Rex', + species: 'dog', + age: 3, + isMammal: true + }, + { + name: 'Goldie', + species: 'fish', + age: 2, + isMammal: false + } + ], + address: { + number: 99, + street: 'Main Street', + city: 'Springfield', + state: 'OH', + country: 'USA' + } + }); + + // Retrieve the name and age of the second pet in the pets array. + let results = await client.json.get('noderedis:jsondata', { + path: [ + '.pets[1].name', + '.pets[1].age' + ] + }); + + // { '.pets[1].name': 'Rex', '.pets[1].age': 3 } + console.log(results); + + // Goldie had a birthday, increment the age... + await client.json.numIncrBy('noderedis:jsondata', '.pets[2].age', 1); + results = await client.json.get('noderedis:jsondata', { + path: '.pets[2].age' + }); + + // Goldie is 3 years old now. + console.log(`Goldie is ${JSON.stringify(results)} years old now.`); + + // Add a new pet... + await client.json.arrAppend('noderedis:jsondata', '.pets', { + name: 'Robin', + species: 'bird', + isMammal: false, + age: 1 + }); + + // How many pets do we have now? + const numPets = await client.json.arrLen('noderedis:jsondata', '.pets'); + + // We now have 4 pets. + console.log(`We now have ${numPets} pets.`); + + await client.quit(); +} + +managingJSON(); + diff --git a/examples/search+json.js b/examples/search+json.js deleted file mode 100644 index adc298289cd..00000000000 --- a/examples/search+json.js +++ /dev/null @@ -1,74 +0,0 @@ -// Use Redis Search and Redis JSON - -import { createClient, SchemaFieldTypes, AggregateGroupByReducers, AggregateSteps } from 'redis'; - -async function searchPlusJson() { - const client = createClient(); - - await client.connect(); - - // Create an index - await client.ft.create('users', { - '$.name': { - type: SchemaFieldTypes.TEXT, - SORTABLE: 'UNF' - }, - '$.age': SchemaFieldTypes.NUMERIC, - '$.coins': SchemaFieldTypes.NUMERIC - }, { - ON: 'JSON' - }); - - // Add some users - await Promise.all([ - client.json.set('users:1', '$', { - name: 'Alice', - age: 32, - coins: 100 - }), - client.json.set('users:2', '$', { - name: 'Bob', - age: 23, - coins: 15 - }) - ]); - - // Search all users under 30 - // TODO: why "$.age:[-inf, 30]" does not work? - console.log( - await client.ft.search('users', '*') - ); - // { - // total: 1, - // documents: [...] - // } - - // Some aggrigrations - console.log( - await client.ft.aggregate('users', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: [{ - type: AggregateGroupByReducers.AVG, - property: '$.age', - AS: 'avarageAge' - }, { - type: AggregateGroupByReducers.SUM, - property: '$.coins', - AS: 'totalCoins' - }] - }] - }) - ); - // { - // total: 2, - // results: [{ - // avarageAvg: '27.5', - // totalCoins: '115' - // }] - // } - - await client.quit(); -} - -searchPlusJson(); diff --git a/examples/search-hashes.js b/examples/search-hashes.js new file mode 100644 index 00000000000..ded4b0edb95 --- /dev/null +++ b/examples/search-hashes.js @@ -0,0 +1,82 @@ +// This example demonstrates how to use RediSearch to index and query data +// stored in Redis hashes. + +import { createClient, SchemaFieldTypes } from 'redis'; + +async function searchHashes() { + const client = createClient(); + + await client.connect(); + + // Create an index... + try { + // Documentation: https://oss.redis.com/redisearch/Commands/#ftcreate + await client.ft.create('idx:animals', { + name: { + type: SchemaFieldTypes.TEXT, + sortable: true + }, + species: SchemaFieldTypes.TAG, + age: SchemaFieldTypes.NUMERIC + }, { + ON: 'HASH', + PREFIX: 'noderedis:animals' + }); + } catch (e) { + if (e.message === 'Index already exists') { + console.log('Index exists already, skipped creation.'); + } else { + // Something went wrong, perhaps RediSearch isn't installed... + console.error(e); + process.exit(1); + } + } + + // Add some sample data... + await Promise.all([ + client.hSet('noderedis:animals:1', {name: 'Fluffy', species: 'cat', age: 3}), + client.hSet('noderedis:animals:2', {name: 'Ginger', species: 'cat', age: 4}), + client.hSet('noderedis:animals:3', {name: 'Rover', species: 'dog', age: 9}), + client.hSet('noderedis:animals:4', {name: 'Fido', species: 'dog', age: 7}) + ]); + + // Perform a search query, find all the dogs... + // Documentation: https://oss.redis.com/redisearch/Commands/#ftsearch + // Query synatax: https://oss.redis.com/redisearch/Query_Syntax/ + const results = await client.ft.search('idx:animals', '@species:{dog}'); + + // results: + // { + // total: 2, + // documents: [ + // { + // id: 'noderedis:animals:4', + // value: { + // name: 'Fido', + // species: 'dog', + // age: '7' + // } + // }, + // { + // id: 'noderedis:animals:3', + // value: { + // name: 'Rover', + // species: 'dog', + // age: '9' + // } + // } + // ] + // } + + console.log(`Results found: ${results.total}.`); + + for (const doc of results.documents) { + // noderedis:animals:4: Fido + // noderedis:animals:3: Rover + console.log(`${doc.id}: ${doc.value.name}`); + } + + await client.quit(); +} + +searchHashes(); \ No newline at end of file diff --git a/examples/search-json.js b/examples/search-json.js new file mode 100644 index 00000000000..a608d3aefa5 --- /dev/null +++ b/examples/search-json.js @@ -0,0 +1,93 @@ +// This example demonstrates how to use RediSearch and RedisJSON together. + +import { createClient, SchemaFieldTypes, AggregateGroupByReducers, AggregateSteps } from 'redis'; + +async function searchJSON() { + const client = createClient(); + + await client.connect(); + + // Create an index. + try { + await client.ft.create('idx:users', { + '$.name': { + type: SchemaFieldTypes.TEXT, + SORTABLE: 'UNF' + }, + '$.age': { + type: SchemaFieldTypes.NUMERIC, + AS: 'age' + }, + '$.coins': { + type: SchemaFieldTypes.NUMERIC, + AS: 'coins' + } + }, { + ON: 'JSON', + PREFIX: 'noderedis:users' + }); + } catch (e) { + if (e.message === 'Index already exists') { + console.log('Index exists already, skipped creation.'); + } else { + // Something went wrong, perhaps RediSearch isn't installed... + console.error(e); + process.exit(1); + } + } + + // Add some users. + await Promise.all([ + client.json.set('noderedis:users:1', '$', { + name: 'Alice', + age: 32, + coins: 100 + }), + client.json.set('noderedis:users:2', '$', { + name: 'Bob', + age: 23, + coins: 15 + }) + ]); + + // Search all users under 30 + console.log('Users under 30 years old:'); + console.log( + // https://oss.redis.com/redisearch/Commands/#ftsearch + await client.ft.search('idx:users', '@age:[0 30]') + ); + // { + // total: 1, + // documents: [ { id: 'noderedis:users:2', value: [Object] } ] + // } + + // Some aggregrations, what's the average age and total number of coins... + // https://oss.redis.com/redisearch/Commands/#ftaggregate + console.log( + await client.ft.aggregate('idx:users', '*', { + STEPS: [{ + type: AggregateSteps.GROUPBY, + REDUCE: [{ + type: AggregateGroupByReducers.AVG, + property: 'age', + AS: 'averageAge' + }, { + type: AggregateGroupByReducers.SUM, + property: 'coins', + AS: 'totalCoins' + }] + }] + }) + ); + // { + // total: 2, + // results: [{ + // averageAge: '27.5', + // totalCoins: '115' + // }] + // } + + await client.quit(); +} + +searchJSON(); diff --git a/package-lock.json b/package-lock.json index 5c3ebb5c208..69d569e7337 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,25 @@ { "name": "redis", - "version": "4.0.0-rc.4", + "version": "4.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "redis", - "version": "4.0.0-rc.4", + "version": "4.0.0", "license": "MIT", "workspaces": [ "./packages/*" ], "dependencies": { - "@node-redis/client": "^1.0.0-rc", - "@node-redis/json": "^1.0.0-rc", - "@node-redis/search": "^1.0.0-rc" + "@node-redis/client": "^1.0.0", + "@node-redis/json": "^1.0.0", + "@node-redis/search": "^1.0.0" }, "devDependencies": { "@tsconfig/node12": "^1.0.9", - "release-it": "^14.11.7", - "typescript": "^4.4.4" + "release-it": "^14.11.8", + "typescript": "^4.5.2" } }, "node_modules/@babel/code-frame": { @@ -35,9 +35,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.0.tgz", - "integrity": "sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.4.tgz", + "integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==", "dev": true, "engines": { "node": ">=6.9.0" @@ -364,9 +364,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.3.tgz", - "integrity": "sha512-dcNwU1O4sx57ClvLBVFbEgx0UZWfd0JQX5X6fxFRCLHelFBGXFfSz6Y0FAq2PEwUqlqLkdVjVr4VASEOuUnLJw==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.4.tgz", + "integrity": "sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -669,6 +669,10 @@ "resolved": "packages/test-utils", "link": true }, + "node_modules/@node-redis/time-series": { + "resolved": "packages/time-series", + "link": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -816,15 +820,15 @@ } }, "node_modules/@octokit/rest": { - "version": "18.10.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.10.0.tgz", - "integrity": "sha512-esHR5OKy38bccL/sajHqZudZCvmv4yjovMJzyXlphaUo7xykmtOdILGJ3aAm0mFHmMLmPFmDMJXf39cAjNJsrw==", + "version": "18.12.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", + "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", "dev": true, "dependencies": { "@octokit/core": "^3.5.1", - "@octokit/plugin-paginate-rest": "^2.16.0", + "@octokit/plugin-paginate-rest": "^2.16.8", "@octokit/plugin-request-log": "^1.0.4", - "@octokit/plugin-rest-endpoint-methods": "^5.9.0" + "@octokit/plugin-rest-endpoint-methods": "^5.12.0" } }, "node_modules/@octokit/types": { @@ -959,9 +963,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.11.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", - "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==", + "version": "16.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz", + "integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==", "dev": true }, "node_modules/@types/parse-json": { @@ -1011,9 +1015,9 @@ "dev": true }, "node_modules/@types/yargs": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.5.tgz", - "integrity": "sha512-4HNq144yhaVjJs+ON6A07NEoi9Hh0Rhl/jI9Nt/l/YRjt+T6St/QK3meFARWZ8IgkzoD1LC0PdTdJenlQQi2WQ==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.7.tgz", + "integrity": "sha512-OvLKmpKdea1aWtqHv9bxVVcMoT6syAeK+198dfETIFkAevYRGwqh4H+KFxfjUETZuUuE5sQCAFwdOdoHUdo8eg==", "dev": true, "dependencies": { "@types/yargs-parser": "*" @@ -1189,9 +1193,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", - "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1630,9 +1634,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001280", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001280.tgz", - "integrity": "sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA==", + "version": "1.0.30001282", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz", + "integrity": "sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==", "dev": true, "funding": { "type": "opencollective", @@ -1683,9 +1687,9 @@ } }, "node_modules/ci-info": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", - "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", + "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", "dev": true }, "node_modules/clean-stack": { @@ -2085,9 +2089,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.3.897", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.897.tgz", - "integrity": "sha512-nRNZhAZ7hVCe75jrCUG7xLOqHMwloJMj6GEXEzY4OMahRGgwerAo+ls/qbqUwFH+E20eaSncKkQ4W8KP5SOiAg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.0.tgz", + "integrity": "sha512-+oXCt6SaIu8EmFTPx8wNGSB0tHQ5biDscnlf6Uxuz17e9CjzMRtGk9B8705aMPnj0iWr3iC74WuIkngCsLElmA==", "dev": true }, "node_modules/emoji-regex": { @@ -2160,9 +2164,9 @@ } }, "node_modules/eslint": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.2.0.tgz", - "integrity": "sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.3.0.tgz", + "integrity": "sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.0.4", @@ -2174,10 +2178,10 @@ "doctrine": "^3.0.0", "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^6.0.0", + "eslint-scope": "^7.1.0", "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.0.0", - "espree": "^9.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.1.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2276,9 +2280,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-6.0.0.tgz", - "integrity": "sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -2331,14 +2335,14 @@ } }, "node_modules/espree": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.0.0.tgz", - "integrity": "sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.1.0.tgz", + "integrity": "sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==", "dev": true, "dependencies": { - "acorn": "^8.5.0", + "acorn": "^8.6.0", "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.0.0" + "eslint-visitor-keys": "^3.1.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2860,9 +2864,9 @@ } }, "node_modules/got": { - "version": "11.8.2", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", - "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "version": "11.8.3", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz", + "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==", "dev": true, "dependencies": { "@sindresorhus/is": "^4.0.0", @@ -2870,7 +2874,7 @@ "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.1", + "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", @@ -3182,9 +3186,9 @@ } }, "node_modules/inquirer": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.1.5.tgz", - "integrity": "sha512-G6/9xUqmt/r+UvufSyrPpt84NYwhKZ9jLsgMbQzlx804XErNupor8WQdBnBRrXmBfTPpuwf1sV+ss2ovjgdXIg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz", + "integrity": "sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==", "dev": true, "dependencies": { "ansi-escapes": "^4.2.1", @@ -3234,12 +3238,12 @@ } }, "node_modules/is-ci": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz", - "integrity": "sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", "dev": true, "dependencies": { - "ci-info": "^3.1.1" + "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" @@ -3701,9 +3705,9 @@ } }, "node_modules/lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, "node_modules/locate-path": { @@ -3871,21 +3875,21 @@ } }, "node_modules/mime-db": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "dev": true, "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", - "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dev": true, "dependencies": { - "mime-db": "1.49.0" + "mime-db": "1.51.0" }, "engines": { "node": ">= 0.6" @@ -5227,13 +5231,13 @@ } }, "node_modules/release-it": { - "version": "14.11.7", - "resolved": "https://registry.npmjs.org/release-it/-/release-it-14.11.7.tgz", - "integrity": "sha512-m4p9+x6AEQPczc96Jyg6dGFeovpJVgRCtA1lxeIgTmQVt9dutYPkkjZeJngZgUJ17/Lb1bx6ZzW2qsKmopKnbQ==", + "version": "14.11.8", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-14.11.8.tgz", + "integrity": "sha512-951DJ0kwjwU7CwGU3BCvRBgLxuJsOPRrZkqx0AsugJdSyPpUdwY9nlU0RAoSKqgh+VTerzecXLIIwgsGIpNxlA==", "dev": true, "dependencies": { "@iarna/toml": "2.2.5", - "@octokit/rest": "18.10.0", + "@octokit/rest": "18.12.0", "async-retry": "1.3.3", "chalk": "4.1.2", "cosmiconfig": "7.0.1", @@ -5243,12 +5247,12 @@ "form-data": "4.0.0", "git-url-parse": "11.6.0", "globby": "11.0.4", - "got": "11.8.2", + "got": "11.8.3", "import-cwd": "3.0.0", - "inquirer": "8.1.5", - "is-ci": "3.0.0", + "inquirer": "8.2.0", + "is-ci": "3.0.1", "lodash": "4.17.21", - "mime-types": "2.1.32", + "mime-types": "2.1.34", "new-github-release-url": "1.0.0", "open": "7.4.2", "ora": "5.4.1", @@ -5562,9 +5566,9 @@ } }, "node_modules/signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", "dev": true }, "node_modules/sinon": { @@ -5613,9 +5617,9 @@ } }, "node_modules/source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "dependencies": { "buffer-from": "^1.0.0", @@ -5940,9 +5944,9 @@ } }, "node_modules/typedoc": { - "version": "0.22.9", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.9.tgz", - "integrity": "sha512-84PjudoXVcap6bwdZFbYIUWlgdz/iLV09ZHwrCzhtHWXaDQG6mlosJ8te6DSThuRkRvQjp46HO+qY/P7Gpm78g==", + "version": "0.22.10", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.10.tgz", + "integrity": "sha512-hQYZ4WtoMZ61wDC6w10kxA42+jclWngdmztNZsDvIz7BMJg7F2xnT+uYsUa7OluyKossdFj9E9Ye4QOZKTy8SA==", "dev": true, "dependencies": { "glob": "^7.2.0", @@ -5958,7 +5962,7 @@ "node": ">= 12.10.0" }, "peerDependencies": { - "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x" + "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x" } }, "node_modules/typedoc-github-wiki-theme": { @@ -5972,9 +5976,9 @@ } }, "node_modules/typedoc-plugin-markdown": { - "version": "3.11.6", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.11.6.tgz", - "integrity": "sha512-CV1BuxL7HR/EE1ctnPXOWzf4/Exl0FzkwtFVYaKTVWTnD/dkFLgABOfWuOL4lPmzLUOsAL85pmq+/PB6cdRppw==", + "version": "3.11.7", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.11.7.tgz", + "integrity": "sha512-Wm3HP5gcBOGOOTeDA8GLgw+BY+GAI31RP9Lyog21BvTaSeWUcdXls5TG1MK+XDatS2/0dup9gFO+emoyoQJm9Q==", "dev": true, "dependencies": { "handlebars": "^4.7.7" @@ -5984,9 +5988,9 @@ } }, "node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -6412,7 +6416,7 @@ }, "packages/client": { "name": "@node-redis/client", - "version": "1.0.0-rc", + "version": "1.0.0", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.0", @@ -6423,22 +6427,22 @@ "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@node-redis/test-utils": "*", - "@types/node": "^16.11.7", + "@types/node": "^16.11.10", "@types/redis-parser": "^3.0.0", "@types/sinon": "^10.0.6", "@types/yallist": "^4.0.1", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", - "eslint": "^8.2.0", + "eslint": "^8.3.0", "nyc": "^15.1.0", - "release-it": "^14.11.7", + "release-it": "^14.11.8", "sinon": "^12.0.1", - "source-map-support": "^0.5.20", + "source-map-support": "^0.5.21", "ts-node": "^10.4.0", - "typedoc": "^0.22.9", + "typedoc": "^0.22.10", "typedoc-github-wiki-theme": "^0.6.0", - "typedoc-plugin-markdown": "^3.11.6", - "typescript": "^4.4.4" + "typedoc-plugin-markdown": "^3.11.7", + "typescript": "^4.5.2" }, "engines": { "node": ">=12" @@ -6451,33 +6455,33 @@ "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@node-redis/test-utils": "*", - "@types/node": "^16.11.7", + "@types/node": "^16.11.10", "nyc": "^15.1.0", - "release-it": "^14.11.7", - "source-map-support": "^0.5.20", + "release-it": "^14.11.8", + "source-map-support": "^0.5.21", "ts-node": "^10.4.0", - "typescript": "^4.4.4" + "typescript": "^4.5.2" }, "peerDependencies": { - "@node-redis/client": "^1.0.0-rc" + "@node-redis/client": "^1.0.0" } }, "packages/search": { "name": "@node-redis/search", - "version": "1.0.0-rc.0", + "version": "1.0.0", "license": "MIT", "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@node-redis/test-utils": "*", - "@types/node": "^16.11.7", + "@types/node": "^16.11.10", "nyc": "^15.1.0", - "release-it": "^14.11.7", - "source-map-support": "^0.5.20", + "release-it": "^14.11.8", + "source-map-support": "^0.5.21", "ts-node": "^10.4.0", - "typescript": "^4.4.4" + "typescript": "^4.5.2" }, "peerDependencies": { - "@node-redis/client": "^1.0.0-rc" + "@node-redis/client": "^1.0.0" } }, "packages/test-utils": { @@ -6485,18 +6489,36 @@ "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@types/mocha": "^9.0.0", - "@types/node": "^16.11.7", - "@types/yargs": "^17.0.5", + "@types/node": "^16.11.10", + "@types/yargs": "^17.0.7", "mocha": "^9.1.3", "nyc": "^15.1.0", + "release-it": "^14.11.8", + "source-map-support": "^0.5.21", + "ts-node": "^10.4.0", + "typescript": "^4.5.2", + "yargs": "^17.2.1" + }, + "peerDependencies": { + "@node-redis/client": "^1.0.0" + } + }, + "packages/time-series": { + "name": "@node-redis/time-series", + "version": "1.0.0-rc.0", + "license": "MIT", + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.1", + "@node-redis/test-utils": "*", + "@types/node": "^16.11.7", + "nyc": "^15.1.0", "release-it": "^14.11.7", "source-map-support": "^0.5.20", "ts-node": "^10.4.0", - "typescript": "^4.4.4", - "yargs": "^17.2.1" + "typescript": "^4.4.4" }, "peerDependencies": { - "@node-redis/client": "^1.0.0-rc" + "@node-redis/client": "^1.0.0" } } }, @@ -6511,9 +6533,9 @@ } }, "@babel/compat-data": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.0.tgz", - "integrity": "sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.4.tgz", + "integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==", "dev": true }, "@babel/core": { @@ -6767,9 +6789,9 @@ } }, "@babel/parser": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.3.tgz", - "integrity": "sha512-dcNwU1O4sx57ClvLBVFbEgx0UZWfd0JQX5X6fxFRCLHelFBGXFfSz6Y0FAq2PEwUqlqLkdVjVr4VASEOuUnLJw==", + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.4.tgz", + "integrity": "sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==", "dev": true }, "@babel/template": { @@ -6990,25 +7012,25 @@ "requires": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@node-redis/test-utils": "*", - "@types/node": "^16.11.7", + "@types/node": "^16.11.10", "@types/redis-parser": "^3.0.0", "@types/sinon": "^10.0.6", "@types/yallist": "^4.0.1", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cluster-key-slot": "1.1.0", - "eslint": "^8.2.0", + "eslint": "^8.3.0", "generic-pool": "3.8.2", "nyc": "^15.1.0", "redis-parser": "3.0.0", - "release-it": "^14.11.7", + "release-it": "^14.11.8", "sinon": "^12.0.1", - "source-map-support": "^0.5.20", + "source-map-support": "^0.5.21", "ts-node": "^10.4.0", - "typedoc": "^0.22.9", + "typedoc": "^0.22.10", "typedoc-github-wiki-theme": "^0.6.0", - "typedoc-plugin-markdown": "^3.11.6", - "typescript": "^4.4.4", + "typedoc-plugin-markdown": "^3.11.7", + "typescript": "^4.5.2", "yallist": "4.0.0" } }, @@ -7017,12 +7039,12 @@ "requires": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@node-redis/test-utils": "*", - "@types/node": "^16.11.7", + "@types/node": "^16.11.10", "nyc": "^15.1.0", - "release-it": "^14.11.7", - "source-map-support": "^0.5.20", + "release-it": "^14.11.8", + "source-map-support": "^0.5.21", "ts-node": "^10.4.0", - "typescript": "^4.4.4" + "typescript": "^4.5.2" } }, "@node-redis/search": { @@ -7030,12 +7052,12 @@ "requires": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@node-redis/test-utils": "*", - "@types/node": "^16.11.7", + "@types/node": "^16.11.10", "nyc": "^15.1.0", - "release-it": "^14.11.7", - "source-map-support": "^0.5.20", + "release-it": "^14.11.8", + "source-map-support": "^0.5.21", "ts-node": "^10.4.0", - "typescript": "^4.4.4" + "typescript": "^4.5.2" } }, "@node-redis/test-utils": { @@ -7043,15 +7065,28 @@ "requires": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@types/mocha": "^9.0.0", - "@types/node": "^16.11.7", - "@types/yargs": "^17.0.5", + "@types/node": "^16.11.10", + "@types/yargs": "^17.0.7", "mocha": "^9.1.3", "nyc": "^15.1.0", + "release-it": "^14.11.8", + "source-map-support": "^0.5.21", + "ts-node": "^10.4.0", + "typescript": "^4.5.2", + "yargs": "^17.2.1" + } + }, + "@node-redis/time-series": { + "version": "file:packages/time-series", + "requires": { + "@istanbuljs/nyc-config-typescript": "^1.0.1", + "@node-redis/test-utils": "*", + "@types/node": "^16.11.7", + "nyc": "^15.1.0", "release-it": "^14.11.7", "source-map-support": "^0.5.20", "ts-node": "^10.4.0", - "typescript": "^4.4.4", - "yargs": "^17.2.1" + "typescript": "^4.4.4" } }, "@nodelib/fs.scandir": { @@ -7184,15 +7219,15 @@ } }, "@octokit/rest": { - "version": "18.10.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.10.0.tgz", - "integrity": "sha512-esHR5OKy38bccL/sajHqZudZCvmv4yjovMJzyXlphaUo7xykmtOdILGJ3aAm0mFHmMLmPFmDMJXf39cAjNJsrw==", + "version": "18.12.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", + "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", "dev": true, "requires": { "@octokit/core": "^3.5.1", - "@octokit/plugin-paginate-rest": "^2.16.0", + "@octokit/plugin-paginate-rest": "^2.16.8", "@octokit/plugin-request-log": "^1.0.4", - "@octokit/plugin-rest-endpoint-methods": "^5.9.0" + "@octokit/plugin-rest-endpoint-methods": "^5.12.0" } }, "@octokit/types": { @@ -7318,9 +7353,9 @@ "dev": true }, "@types/node": { - "version": "16.11.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", - "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==", + "version": "16.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz", + "integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==", "dev": true }, "@types/parse-json": { @@ -7370,9 +7405,9 @@ "dev": true }, "@types/yargs": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.5.tgz", - "integrity": "sha512-4HNq144yhaVjJs+ON6A07NEoi9Hh0Rhl/jI9Nt/l/YRjt+T6St/QK3meFARWZ8IgkzoD1LC0PdTdJenlQQi2WQ==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.7.tgz", + "integrity": "sha512-OvLKmpKdea1aWtqHv9bxVVcMoT6syAeK+198dfETIFkAevYRGwqh4H+KFxfjUETZuUuE5sQCAFwdOdoHUdo8eg==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -7474,9 +7509,9 @@ "dev": true }, "acorn": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", - "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", "dev": true }, "acorn-jsx": { @@ -7791,9 +7826,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001280", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001280.tgz", - "integrity": "sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA==", + "version": "1.0.30001282", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz", + "integrity": "sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==", "dev": true }, "chalk": { @@ -7829,9 +7864,9 @@ } }, "ci-info": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", - "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", + "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", "dev": true }, "clean-stack": { @@ -8137,9 +8172,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.897", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.897.tgz", - "integrity": "sha512-nRNZhAZ7hVCe75jrCUG7xLOqHMwloJMj6GEXEzY4OMahRGgwerAo+ls/qbqUwFH+E20eaSncKkQ4W8KP5SOiAg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.0.tgz", + "integrity": "sha512-+oXCt6SaIu8EmFTPx8wNGSB0tHQ5biDscnlf6Uxuz17e9CjzMRtGk9B8705aMPnj0iWr3iC74WuIkngCsLElmA==", "dev": true }, "emoji-regex": { @@ -8200,9 +8235,9 @@ "dev": true }, "eslint": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.2.0.tgz", - "integrity": "sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.3.0.tgz", + "integrity": "sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww==", "dev": true, "requires": { "@eslint/eslintrc": "^1.0.4", @@ -8214,10 +8249,10 @@ "doctrine": "^3.0.0", "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^6.0.0", + "eslint-scope": "^7.1.0", "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.0.0", - "espree": "^9.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.1.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -8252,9 +8287,9 @@ "dev": true }, "eslint-scope": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-6.0.0.tgz", - "integrity": "sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -8324,14 +8359,14 @@ "dev": true }, "espree": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.0.0.tgz", - "integrity": "sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.1.0.tgz", + "integrity": "sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==", "dev": true, "requires": { - "acorn": "^8.5.0", + "acorn": "^8.6.0", "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.0.0" + "eslint-visitor-keys": "^3.1.0" } }, "esprima": { @@ -8705,9 +8740,9 @@ } }, "got": { - "version": "11.8.2", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", - "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "version": "11.8.3", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz", + "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==", "dev": true, "requires": { "@sindresorhus/is": "^4.0.0", @@ -8715,7 +8750,7 @@ "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.1", + "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", @@ -8933,9 +8968,9 @@ "dev": true }, "inquirer": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.1.5.tgz", - "integrity": "sha512-G6/9xUqmt/r+UvufSyrPpt84NYwhKZ9jLsgMbQzlx804XErNupor8WQdBnBRrXmBfTPpuwf1sV+ss2ovjgdXIg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz", + "integrity": "sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", @@ -8976,12 +9011,12 @@ } }, "is-ci": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz", - "integrity": "sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", "dev": true, "requires": { - "ci-info": "^3.1.1" + "ci-info": "^3.2.0" } }, "is-core-module": { @@ -9325,9 +9360,9 @@ } }, "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, "locate-path": { @@ -9452,18 +9487,18 @@ } }, "mime-db": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "dev": true }, "mime-types": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", - "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dev": true, "requires": { - "mime-db": "1.49.0" + "mime-db": "1.51.0" } }, "mimic-fn": { @@ -10482,13 +10517,13 @@ } }, "release-it": { - "version": "14.11.7", - "resolved": "https://registry.npmjs.org/release-it/-/release-it-14.11.7.tgz", - "integrity": "sha512-m4p9+x6AEQPczc96Jyg6dGFeovpJVgRCtA1lxeIgTmQVt9dutYPkkjZeJngZgUJ17/Lb1bx6ZzW2qsKmopKnbQ==", + "version": "14.11.8", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-14.11.8.tgz", + "integrity": "sha512-951DJ0kwjwU7CwGU3BCvRBgLxuJsOPRrZkqx0AsugJdSyPpUdwY9nlU0RAoSKqgh+VTerzecXLIIwgsGIpNxlA==", "dev": true, "requires": { "@iarna/toml": "2.2.5", - "@octokit/rest": "18.10.0", + "@octokit/rest": "18.12.0", "async-retry": "1.3.3", "chalk": "4.1.2", "cosmiconfig": "7.0.1", @@ -10498,12 +10533,12 @@ "form-data": "4.0.0", "git-url-parse": "11.6.0", "globby": "11.0.4", - "got": "11.8.2", + "got": "11.8.3", "import-cwd": "3.0.0", - "inquirer": "8.1.5", - "is-ci": "3.0.0", + "inquirer": "8.2.0", + "is-ci": "3.0.1", "lodash": "4.17.21", - "mime-types": "2.1.32", + "mime-types": "2.1.34", "new-github-release-url": "1.0.0", "open": "7.4.2", "ora": "5.4.1", @@ -10727,9 +10762,9 @@ } }, "signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", "dev": true }, "sinon": { @@ -10770,9 +10805,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -11016,9 +11051,9 @@ } }, "typedoc": { - "version": "0.22.9", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.9.tgz", - "integrity": "sha512-84PjudoXVcap6bwdZFbYIUWlgdz/iLV09ZHwrCzhtHWXaDQG6mlosJ8te6DSThuRkRvQjp46HO+qY/P7Gpm78g==", + "version": "0.22.10", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.10.tgz", + "integrity": "sha512-hQYZ4WtoMZ61wDC6w10kxA42+jclWngdmztNZsDvIz7BMJg7F2xnT+uYsUa7OluyKossdFj9E9Ye4QOZKTy8SA==", "dev": true, "requires": { "glob": "^7.2.0", @@ -11036,18 +11071,18 @@ "requires": {} }, "typedoc-plugin-markdown": { - "version": "3.11.6", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.11.6.tgz", - "integrity": "sha512-CV1BuxL7HR/EE1ctnPXOWzf4/Exl0FzkwtFVYaKTVWTnD/dkFLgABOfWuOL4lPmzLUOsAL85pmq+/PB6cdRppw==", + "version": "3.11.7", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.11.7.tgz", + "integrity": "sha512-Wm3HP5gcBOGOOTeDA8GLgw+BY+GAI31RP9Lyog21BvTaSeWUcdXls5TG1MK+XDatS2/0dup9gFO+emoyoQJm9Q==", "dev": true, "requires": { "handlebars": "^4.7.7" } }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index 49339e44d26..7eee7f3958b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redis", - "version": "4.0.0-rc.4", + "version": "4.0.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -17,14 +17,14 @@ "build-all": "npm run build:client && npm run build:test-utils && npm run build:modules && npm run build" }, "dependencies": { - "@node-redis/client": "^1.0.0-rc.0", - "@node-redis/json": "^1.0.0-rc.0", - "@node-redis/search": "^1.0.0-rc.0" + "@node-redis/client": "^1.0.0", + "@node-redis/json": "^1.0.0", + "@node-redis/search": "^1.0.0" }, "devDependencies": { "@tsconfig/node12": "^1.0.9", - "release-it": "^14.11.7", - "typescript": "^4.4.4" + "release-it": "^14.11.8", + "typescript": "^4.5.2" }, "repository": { "type": "git", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 21b7177e8b6..39ea947b064 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## v4.0.0 +## v4.0.0 - 24 Nov, 2021 This version is a major change and refactor, adding modern JavaScript capabilities and multiple breaking changes. See the [migration guide](../../docs/v3-to-v4.md) for tips on how to upgrade. @@ -17,10 +17,10 @@ This version is a major change and refactor, adding modern JavaScript capabiliti - Added support for Promises - Added built-in TypeScript declaration files enabling code completion -- Added support for [clustering](../../.github/README.md#cluster) -- Added idiomatic arguments and responses to [Redis commands](../../.github/README.md#redis-commands) -- Added full support for [Lua Scripts](../../.github/README.md#lua-scripts) -- Added support for [SCAN iterators](../../.github/README.md#scan-iterator) +- Added support for [clustering](../../README.md#cluster) +- Added idiomatic arguments and responses to [Redis commands](../../README.md#redis-commands) +- Added full support for [Lua Scripts](../../README.md#lua-scripts) +- Added support for [SCAN iterators](../../README.md#scan-iterator) - Added the ability to extend Node Redis with Redis Module commands ## v3.1.2 diff --git a/packages/client/README.md b/packages/client/README.md index 37c326fc42e..6007608ea3b 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -1,2 +1,2 @@ # @node-redis/client -The sources and docs for this package are in the main [node-redis](https://github.com/redis/node-redis) repo. +The source code and documentation for this package are in the main [node-redis](https://github.com/redis/node-redis) repo. diff --git a/packages/client/lib/client/commands-queue.ts b/packages/client/lib/client/commands-queue.ts index 4fcae1e8b63..480d7d51408 100644 --- a/packages/client/lib/client/commands-queue.ts +++ b/packages/client/lib/client/commands-queue.ts @@ -1,18 +1,15 @@ import * as LinkedList from 'yallist'; import { AbortError } from '../errors'; import { RedisCommandArguments, RedisCommandRawReply } from '../commands'; - // We need to use 'require', because it's not possible with Typescript to import // classes that are exported as 'module.exports = class`, without esModuleInterop // set to true. const RedisParser = require('redis-parser'); - export interface QueueCommandOptions { asap?: boolean; chainId?: symbol; signal?: AbortSignal; } - interface CommandWaitingToBeSent extends CommandWaitingForReply { args: RedisCommandArguments; chainId?: symbol; @@ -21,27 +18,44 @@ interface CommandWaitingToBeSent extends CommandWaitingForReply { listener(): void; }; } - interface CommandWaitingForReply { resolve(reply?: unknown): void; reject(err: Error): void; channelsCounter?: number; bufferMode?: boolean; } - export enum PubSubSubscribeCommands { SUBSCRIBE = 'SUBSCRIBE', PSUBSCRIBE = 'PSUBSCRIBE' } - export enum PubSubUnsubscribeCommands { UNSUBSCRIBE = 'UNSUBSCRIBE', PUNSUBSCRIBE = 'PUNSUBSCRIBE' } -export type PubSubListener = (message: string, channel: string) => unknown; +type PubSubArgumentTypes = Buffer | string; -export type PubSubListenersMap = Map>; +export type PubSubListener< + BUFFER_MODE extends boolean = false, + T = BUFFER_MODE extends true ? Buffer : string +> = (message: T, channel: T) => unknown; + +interface PubSubListeners { + buffers: Set>; + strings: Set>; +} + +type PubSubListenersMap = Map; + +interface PubSubState { + subscribing: number; + subscribed: number; + unsubscribing: number; + listeners: { + channels: PubSubListenersMap; + patterns: PubSubListenersMap; + }; +} export default class RedisCommandsQueue { static #flushQueue(queue: LinkedList, err: Error): void { @@ -50,53 +64,64 @@ export default class RedisCommandsQueue { } } - static #emitPubSubMessage(listeners: Set, message: string, channel: string): void { - for (const listener of listeners) { + static #emitPubSubMessage(listenersMap: PubSubListenersMap, message: Buffer, channel: Buffer, pattern?: Buffer): void { + const keyString = (pattern || channel).toString(), + listeners = listenersMap.get(keyString)!; + for (const listener of listeners.buffers) { listener(message, channel); } + + if (!listeners.strings.size) return; + + const messageString = message.toString(), + channelString = pattern ? channel.toString() : keyString; + for (const listener of listeners.strings) { + listener(messageString, channelString); + } } readonly #maxLength: number | null | undefined; - readonly #waitingToBeSent = new LinkedList(); readonly #waitingForReply = new LinkedList(); - readonly #pubSubState = { - subscribing: 0, - subscribed: 0, - unsubscribing: 0 - }; + #pubSubState: PubSubState | undefined; - readonly #pubSubListeners = { - channels: new Map(), - patterns: new Map() + static readonly #PUB_SUB_MESSAGES = { + message: Buffer.from('message'), + pMessage: Buffer.from('pmessage'), + subscribe: Buffer.from('subscribe'), + pSubscribe: Buffer.from('psubscribe'), + unsubscribe: Buffer.from('unsunscribe'), + pUnsubscribe: Buffer.from('punsubscribe') }; readonly #parser = new RedisParser({ returnReply: (reply: unknown) => { - if ((this.#pubSubState.subscribing || this.#pubSubState.subscribed) && Array.isArray(reply)) { - switch (reply[0]) { - case 'message': - return RedisCommandsQueue.#emitPubSubMessage( - this.#pubSubListeners.channels.get(reply[1])!, - reply[2], - reply[1] - ); - - case 'pmessage': - return RedisCommandsQueue.#emitPubSubMessage( - this.#pubSubListeners.patterns.get(reply[1])!, - reply[3], - reply[2] - ); - - case 'subscribe': - case 'psubscribe': - if (--this.#waitingForReply.head!.value.channelsCounter! === 0) { - this.#shiftWaitingForReply().resolve(); - } - return; + if (this.#pubSubState && Array.isArray(reply)) { + if (RedisCommandsQueue.#PUB_SUB_MESSAGES.message.equals(reply[0])) { + return RedisCommandsQueue.#emitPubSubMessage( + this.#pubSubState.listeners.channels, + reply[2], + reply[1] + ); + } else if (RedisCommandsQueue.#PUB_SUB_MESSAGES.pMessage.equals(reply[0])) { + return RedisCommandsQueue.#emitPubSubMessage( + this.#pubSubState.listeners.patterns, + reply[3], + reply[2], + reply[1] + ); + } else if ( + RedisCommandsQueue.#PUB_SUB_MESSAGES.subscribe.equals(reply[0]) || + RedisCommandsQueue.#PUB_SUB_MESSAGES.pSubscribe.equals(reply[0]) || + RedisCommandsQueue.#PUB_SUB_MESSAGES.unsubscribe.equals(reply[0]) || + RedisCommandsQueue.#PUB_SUB_MESSAGES.pUnsubscribe.equals(reply[0]) + ) { + if (--this.#waitingForReply.head!.value.channelsCounter! === 0) { + this.#shiftWaitingForReply().resolve(); + } + return; } } @@ -104,29 +129,26 @@ export default class RedisCommandsQueue { }, returnError: (err: Error) => this.#shiftWaitingForReply().reject(err) }); - #chainInExecution: symbol | undefined; - constructor(maxLength: number | null | undefined) { this.#maxLength = maxLength; } addCommand(args: RedisCommandArguments, options?: QueueCommandOptions, bufferMode?: boolean): Promise { - if (this.#pubSubState.subscribing || this.#pubSubState.subscribed) { + if (this.#pubSubState) { return Promise.reject(new Error('Cannot send commands in PubSub mode')); } else if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) { return Promise.reject(new Error('The queue is full')); } else if (options?.signal?.aborted) { return Promise.reject(new AbortError()); } - return new Promise((resolve, reject) => { const node = new LinkedList.Node({ args, chainId: options?.chainId, bufferMode, resolve, - reject, + reject }); if (options?.signal) { @@ -134,7 +156,6 @@ export default class RedisCommandsQueue { this.#waitingToBeSent.removeNode(node); node.value.reject(new AbortError()); }; - node.value.abort = { signal: options.signal, listener @@ -144,7 +165,6 @@ export default class RedisCommandsQueue { once: true }); } - if (options?.asap) { this.#waitingToBeSent.unshiftNode(node); } else { @@ -153,28 +173,63 @@ export default class RedisCommandsQueue { }); } - subscribe(command: PubSubSubscribeCommands, channels: string | Array, listener: PubSubListener): Promise { - const channelsToSubscribe: Array = [], - listeners = command === PubSubSubscribeCommands.SUBSCRIBE ? this.#pubSubListeners.channels : this.#pubSubListeners.patterns; + #initiatePubSubState(): PubSubState { + return this.#pubSubState ??= { + subscribed: 0, + subscribing: 0, + unsubscribing: 0, + listeners: { + channels: new Map(), + patterns: new Map() + } + }; + } + + subscribe( + command: PubSubSubscribeCommands, + channels: PubSubArgumentTypes | Array, + listener: PubSubListener, + bufferMode?: T + ): Promise { + const pubSubState = this.#initiatePubSubState(), + channelsToSubscribe: Array = [], + listenersMap = command === PubSubSubscribeCommands.SUBSCRIBE ? pubSubState.listeners.channels : pubSubState.listeners.patterns; for (const channel of (Array.isArray(channels) ? channels : [channels])) { - if (listeners.has(channel)) { - listeners.get(channel)!.add(listener); - continue; + const channelString = typeof channel === 'string' ? channel : channel.toString(); + let listeners = listenersMap.get(channelString); + if (!listeners) { + listeners = { + buffers: new Set(), + strings: new Set() + }; + listenersMap.set(channelString, listeners); + channelsToSubscribe.push(channel); } - listeners.set(channel, new Set([listener])); - channelsToSubscribe.push(channel); + // https://github.com/microsoft/TypeScript/issues/23132 + (bufferMode ? listeners.buffers : listeners.strings).add(listener as any); } if (!channelsToSubscribe.length) { return Promise.resolve(); } - return this.#pushPubSubCommand(command, channelsToSubscribe); } - unsubscribe(command: PubSubUnsubscribeCommands, channels?: string | Array, listener?: PubSubListener): Promise { - const listeners = command === PubSubUnsubscribeCommands.UNSUBSCRIBE ? this.#pubSubListeners.channels : this.#pubSubListeners.patterns; + unsubscribe( + command: PubSubUnsubscribeCommands, + channels?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ): Promise { + if (!this.#pubSubState) { + return Promise.resolve(); + } + + const listeners = command === PubSubUnsubscribeCommands.UNSUBSCRIBE ? + this.#pubSubState.listeners.channels : + this.#pubSubState.listeners.patterns; + if (!channels) { const size = listeners.size; listeners.clear(); @@ -183,13 +238,16 @@ export default class RedisCommandsQueue { const channelsToUnsubscribe = []; for (const channel of (Array.isArray(channels) ? channels : [channels])) { - const set = listeners.get(channel); - if (!set) continue; + const sets = listeners.get(channel); + if (!sets) continue; - let shouldUnsubscribe = !listener; + let shouldUnsubscribe; if (listener) { - set.delete(listener); - shouldUnsubscribe = set.size === 0; + // https://github.com/microsoft/TypeScript/issues/23132 + (bufferMode ? sets.buffers : sets.strings).delete(listener as any); + shouldUnsubscribe = !sets.buffers.size && !sets.strings.size; + } else { + shouldUnsubscribe = true; } if (shouldUnsubscribe) { @@ -197,19 +255,18 @@ export default class RedisCommandsQueue { listeners.delete(channel); } } - if (!channelsToUnsubscribe.length) { return Promise.resolve(); } - return this.#pushPubSubCommand(command, channelsToUnsubscribe); } - #pushPubSubCommand(command: PubSubSubscribeCommands | PubSubUnsubscribeCommands, channels: number | Array): Promise { + #pushPubSubCommand(command: PubSubSubscribeCommands | PubSubUnsubscribeCommands, channels: number | Array): Promise { return new Promise((resolve, reject) => { - const isSubscribe = command === PubSubSubscribeCommands.SUBSCRIBE || command === PubSubSubscribeCommands.PSUBSCRIBE, + const pubSubState = this.#initiatePubSubState(), + isSubscribe = command === PubSubSubscribeCommands.SUBSCRIBE || command === PubSubSubscribeCommands.PSUBSCRIBE, inProgressKey = isSubscribe ? 'subscribing' : 'unsubscribing', - commandArgs: Array = [command]; + commandArgs: Array = [command]; let channelsCounter: number; if (typeof channels === 'number') { // unsubscribe only @@ -219,18 +276,26 @@ export default class RedisCommandsQueue { channelsCounter = channels.length; } - this.#pubSubState[inProgressKey] += channelsCounter; + pubSubState[inProgressKey] += channelsCounter; this.#waitingToBeSent.push({ args: commandArgs, channelsCounter, + bufferMode: true, resolve: () => { - this.#pubSubState[inProgressKey] -= channelsCounter; - this.#pubSubState.subscribed += channelsCounter * (isSubscribe ? 1 : -1); + pubSubState[inProgressKey] -= channelsCounter; + if (isSubscribe) { + pubSubState.subscribed += channelsCounter; + } else { + pubSubState.subscribed -= channelsCounter; + if (!pubSubState.subscribed && !pubSubState.subscribing && !pubSubState.subscribed) { + this.#pubSubState = undefined; + } + } resolve(); }, reject: () => { - this.#pubSubState[inProgressKey] -= channelsCounter; + pubSubState[inProgressKey] -= channelsCounter * (isSubscribe ? 1 : -1); reject(); } }); @@ -238,22 +303,19 @@ export default class RedisCommandsQueue { } resubscribe(): Promise | undefined { - if (!this.#pubSubState.subscribed && !this.#pubSubState.subscribing) { + if (!this.#pubSubState) { return; } - this.#pubSubState.subscribed = this.#pubSubState.subscribing = 0; - // TODO: acl error on one channel/pattern will reject the whole command return Promise.all([ - this.#pushPubSubCommand(PubSubSubscribeCommands.SUBSCRIBE, [...this.#pubSubListeners.channels.keys()]), - this.#pushPubSubCommand(PubSubSubscribeCommands.PSUBSCRIBE, [...this.#pubSubListeners.patterns.keys()]) + this.#pushPubSubCommand(PubSubSubscribeCommands.SUBSCRIBE, [...this.#pubSubState.listeners.channels.keys()]), + this.#pushPubSubCommand(PubSubSubscribeCommands.PSUBSCRIBE, [...this.#pubSubState.listeners.patterns.keys()]) ]); } getCommandToSend(): RedisCommandArguments | undefined { const toSend = this.#waitingToBeSent.shift(); - if (toSend) { this.#waitingForReply.push({ resolve: toSend.resolve, @@ -262,14 +324,15 @@ export default class RedisCommandsQueue { bufferMode: toSend.bufferMode }); } - this.#chainInExecution = toSend?.chainId; - return toSend?.args; } parseResponse(data: Buffer): void { - this.#parser.setReturnBuffers(!!this.#waitingForReply.head?.value.bufferMode); + this.#parser.setReturnBuffers( + !!this.#waitingForReply.head?.value.bufferMode || + !!this.#pubSubState?.subscribed + ); this.#parser.execute(data); } @@ -277,24 +340,18 @@ export default class RedisCommandsQueue { if (!this.#waitingForReply.length) { throw new Error('Got an unexpected reply from Redis'); } - return this.#waitingForReply.shift()!; } - flushWaitingForReply(err: Error): void { RedisCommandsQueue.#flushQueue(this.#waitingForReply, err); - if (!this.#chainInExecution) { return; } - while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) { this.#waitingToBeSent.shift(); } - this.#chainInExecution = undefined; } - flushAll(err: Error): void { RedisCommandsQueue.#flushQueue(this.#waitingForReply, err); RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err); diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 3f0bca45e27..679c7ae692a 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -561,63 +561,66 @@ describe('Client', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('PubSub', async publisher => { + function assertStringListener(message: string, channel: string) { + assert.ok(typeof message === 'string'); + assert.ok(typeof channel === 'string'); + } + + function assertBufferListener(message: Buffer, channel: Buffer) { + assert.ok(Buffer.isBuffer(message)); + assert.ok(Buffer.isBuffer(channel)); + } + const subscriber = publisher.duplicate(); await subscriber.connect(); try { - const channelListener1 = spy(), - channelListener2 = spy(), - patternListener = spy(); + const channelListener1 = spy(assertBufferListener), + channelListener2 = spy(assertStringListener), + patternListener = spy(assertStringListener); await Promise.all([ - subscriber.subscribe('channel', channelListener1), + subscriber.subscribe('channel', channelListener1, true), subscriber.subscribe('channel', channelListener2), subscriber.pSubscribe('channel*', patternListener) ]); - await Promise.all([ waitTillBeenCalled(channelListener1), waitTillBeenCalled(channelListener2), waitTillBeenCalled(patternListener), - publisher.publish('channel', 'message') + publisher.publish(Buffer.from('channel'), Buffer.from('message')) ]); - assert.ok(channelListener1.calledOnceWithExactly('message', 'channel')); + assert.ok(channelListener1.calledOnceWithExactly(Buffer.from('message'), Buffer.from('channel'))); assert.ok(channelListener2.calledOnceWithExactly('message', 'channel')); assert.ok(patternListener.calledOnceWithExactly('message', 'channel')); - await subscriber.unsubscribe('channel', channelListener1); + await subscriber.unsubscribe('channel', channelListener1, true); await Promise.all([ waitTillBeenCalled(channelListener2), waitTillBeenCalled(patternListener), publisher.publish('channel', 'message') ]); - assert.ok(channelListener1.calledOnce); assert.ok(channelListener2.calledTwice); assert.ok(channelListener2.secondCall.calledWithExactly('message', 'channel')); assert.ok(patternListener.calledTwice); assert.ok(patternListener.secondCall.calledWithExactly('message', 'channel')); - await subscriber.unsubscribe('channel'); await Promise.all([ waitTillBeenCalled(patternListener), publisher.publish('channel', 'message') ]); - assert.ok(channelListener1.calledOnce); assert.ok(channelListener2.calledTwice); assert.ok(patternListener.calledThrice); assert.ok(patternListener.thirdCall.calledWithExactly('message', 'channel')); - await subscriber.pUnsubscribe(); await publisher.publish('channel', 'message'); - assert.ok(channelListener1.calledOnce); assert.ok(channelListener2.calledTwice); assert.ok(patternListener.calledThrice); - // should be able to send commands when unsubsribed from all channels (see #1652) await assert.doesNotReject(subscriber.ping()); } finally { diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 8802631eda1..c520e36a08f 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -388,42 +388,93 @@ export default class RedisClient select = this.SELECT; - SUBSCRIBE(channels: string | Array, listener: PubSubListener): Promise { - return this.#subscribe(PubSubSubscribeCommands.SUBSCRIBE, channels, listener); + #subscribe( + command: PubSubSubscribeCommands, + channels: string | Array, + listener: PubSubListener, + bufferMode?: T + ): Promise { + const promise = this.#queue.subscribe( + command, + channels, + listener, + bufferMode + ); + this.#tick(); + return promise; + } + + SUBSCRIBE( + channels: string | Array, + listener: PubSubListener, + bufferMode?: T + ): Promise { + return this.#subscribe( + PubSubSubscribeCommands.SUBSCRIBE, + channels, + listener, + bufferMode + ); } subscribe = this.SUBSCRIBE; - PSUBSCRIBE(patterns: string | Array, listener: PubSubListener): Promise { - return this.#subscribe(PubSubSubscribeCommands.PSUBSCRIBE, patterns, listener); + PSUBSCRIBE( + patterns: string | Array, + listener: PubSubListener, + bufferMode?: T + ): Promise { + return this.#subscribe( + PubSubSubscribeCommands.PSUBSCRIBE, + patterns, + listener, + bufferMode + ); } pSubscribe = this.PSUBSCRIBE; - #subscribe(command: PubSubSubscribeCommands, channels: string | Array, listener: PubSubListener): Promise { - const promise = this.#queue.subscribe(command, channels, listener); + #unsubscribe( + command: PubSubUnsubscribeCommands, + channels?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ): Promise { + const promise = this.#queue.unsubscribe(command, channels, listener, bufferMode); this.#tick(); return promise; } - UNSUBSCRIBE(channels?: string | Array, listener?: PubSubListener): Promise { - return this.#unsubscribe(PubSubUnsubscribeCommands.UNSUBSCRIBE, channels, listener); + UNSUBSCRIBE( + channels?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ): Promise { + return this.#unsubscribe( + PubSubUnsubscribeCommands.UNSUBSCRIBE, + channels, + listener, + bufferMode + ); } unsubscribe = this.UNSUBSCRIBE; - PUNSUBSCRIBE(patterns?: string | Array, listener?: PubSubListener): Promise { - return this.#unsubscribe(PubSubUnsubscribeCommands.PUNSUBSCRIBE, patterns, listener); + PUNSUBSCRIBE( + patterns?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ): Promise { + return this.#unsubscribe( + PubSubUnsubscribeCommands.PUNSUBSCRIBE, + patterns, + listener, + bufferMode + ); } pUnsubscribe = this.PUNSUBSCRIBE; - #unsubscribe(command: PubSubUnsubscribeCommands, channels?: string | Array, listener?: PubSubListener): Promise { - const promise = this.#queue.unsubscribe(command, channels, listener); - this.#tick(); - return promise; - } - QUIT(): Promise { return this.#socket.quit(() => { const quitPromise = this.#queue.addCommand(['QUIT']); diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index ff4c79b4d36..f69449efa1a 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -42,20 +42,8 @@ export default class RedisClusterSlots): Promise { - if (await this.#discoverNodes(startWith.options)) return; - - for (const { client } of this.#nodeByUrl.values()) { - if (client === startWith) continue; - - if (await this.#discoverNodes(client.options)) return; - } - - throw new Error('None of the cluster nodes is available'); - } - async #discoverNodes(clientOptions?: RedisClusterClientOptions): Promise { - const client = new this.#Client(clientOptions); + const client = this.#initiateClient(clientOptions); await client.connect(); @@ -72,6 +60,29 @@ export default class RedisClusterSlots; + + async rediscover(startWith: RedisClientType): Promise { + if (!this.#runningRediscoverPromise) { + this.#runningRediscoverPromise = this.#rediscover(startWith) + .finally(() => this.#runningRediscoverPromise = undefined); + } + + return this.#runningRediscoverPromise; + } + + async #rediscover(startWith: RedisClientType): Promise { + if (await this.#discoverNodes(startWith.options)) return; + + for (const { client } of this.#nodeByUrl.values()) { + if (client === startWith) continue; + + if (await this.#discoverNodes(client.options)) return; + } + + throw new Error('None of the cluster nodes is available'); + } + async #reset(masters: Array): Promise { // Override this.#slots and add not existing clients to this.#nodeByUrl const promises: Array> = [], @@ -103,18 +114,23 @@ export default class RedisClusterSlots { + return new this.#Client(this.#clientOptionsDefaults(options)) + .on('error', this.#onError); + } + #initiateClientForNode(nodeData: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set, promises: Array>): ClusterNode { const url = `${nodeData.host}:${nodeData.port}`; clientsInUse.add(url); @@ -123,15 +139,13 @@ export default class RedisClusterSlots const url = err.message.substring(err.message.lastIndexOf(' ') + 1); let node = this.#slots.getNodeByUrl(url); if (!node) { - await this.#slots.discover(client); + await this.#slots.rediscover(client); node = this.#slots.getNodeByUrl(url); if (!node) { @@ -168,7 +168,7 @@ export default class RedisCluster await node.client.asking(); return node.client; } else if (err.message.startsWith('MOVED')) { - await this.#slots.discover(client); + await this.#slots.rediscover(client); return true; } diff --git a/packages/client/lib/commands/CLUSTER_NODES.spec.ts b/packages/client/lib/commands/CLUSTER_NODES.spec.ts index 2b3881d8cd0..d061c59e8ee 100644 --- a/packages/client/lib/commands/CLUSTER_NODES.spec.ts +++ b/packages/client/lib/commands/CLUSTER_NODES.spec.ts @@ -48,6 +48,31 @@ describe('CLUSTER NODES', () => { ); }); + it('should support urls without cport', () => { + assert.deepEqual( + transformReply( + 'id 127.0.0.1:30001 master - 0 0 0 connected 0-16384\n' + ), + [{ + id: 'id', + url: '127.0.0.1:30001', + host: '127.0.0.1', + port: 30001, + cport: null, + flags: ['master'], + pingSent: 0, + pongRecv: 0, + configEpoch: 0, + linkState: RedisClusterNodeLinkStates.CONNECTED, + slots: [{ + from: 0, + to: 16384 + }], + replicas: [] + }] + ); + }); + it.skip('with importing slots', () => { assert.deepEqual( transformReply( diff --git a/packages/client/lib/commands/CLUSTER_NODES.ts b/packages/client/lib/commands/CLUSTER_NODES.ts index d04ffc10a1d..ba4477cdd20 100644 --- a/packages/client/lib/commands/CLUSTER_NODES.ts +++ b/packages/client/lib/commands/CLUSTER_NODES.ts @@ -10,7 +10,7 @@ export enum RedisClusterNodeLinkStates { interface RedisClusterNodeTransformedUrl { host: string; port: number; - cport: number; + cport: number | null; } export interface RedisClusterReplicaNode extends RedisClusterNodeTransformedUrl { @@ -86,7 +86,16 @@ export function transformReply(reply: string): Array { function transformNodeUrl(url: string): RedisClusterNodeTransformedUrl { const indexOfColon = url.indexOf(':'), - indexOfAt = url.indexOf('@', indexOfColon); + indexOfAt = url.indexOf('@', indexOfColon), + host = url.substring(0, indexOfColon); + + if (indexOfAt === -1) { + return { + host, + port: Number(url.substring(indexOfColon + 1)), + cport: null + }; + } return { host: url.substring(0, indexOfColon), diff --git a/packages/client/lib/commands/LINDEX.spec.ts b/packages/client/lib/commands/LINDEX.spec.ts index 5e0b1473ec4..aa3aafa789b 100644 --- a/packages/client/lib/commands/LINDEX.spec.ts +++ b/packages/client/lib/commands/LINDEX.spec.ts @@ -1,26 +1,36 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import { transformArguments } from './LINDEX'; - describe('LINDEX', () => { it('transformArguments', () => { assert.deepEqual( - transformArguments('key', 'element'), - ['LINDEX', 'key', 'element'] + transformArguments('key', 0), + ['LINDEX', 'key', '0'] ); }); - testUtils.testWithClient('client.lIndex', async client => { - assert.equal( - await client.lIndex('key', 'element'), - null - ); - }, GLOBAL.SERVERS.OPEN); + describe('client.lIndex', () => { + testUtils.testWithClient('null', async client => { + assert.equal( + await client.lIndex('key', 0), + null + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('with value', async client => { + const [, lIndexReply] = await Promise.all([ + client.lPush('key', 'element'), + client.lIndex('key', 0) + ]); + + assert.equal(lIndexReply, 'element'); + }, GLOBAL.SERVERS.OPEN); + }); testUtils.testWithCluster('cluster.lIndex', async cluster => { assert.equal( - await cluster.lIndex('key', 'element'), + await cluster.lIndex('key', 0), null ); }, GLOBAL.CLUSTERS.OPEN); -}); +}); \ No newline at end of file diff --git a/packages/client/lib/commands/LINDEX.ts b/packages/client/lib/commands/LINDEX.ts index 4c283f0912c..d13bc0c2d02 100644 --- a/packages/client/lib/commands/LINDEX.ts +++ b/packages/client/lib/commands/LINDEX.ts @@ -1,9 +1,8 @@ -export const FIRST_KEY_INDEX = 1; export const IS_READ_ONLY = true; -export function transformArguments(key: string, element: string): Array { - return ['LINDEX', key, element]; +export function transformArguments(key: string, index: number): Array { + return ['LINDEX', key, index.toString()]; } -export declare function transformReply(): string | null; +export declare function transformReply(): string | null; \ No newline at end of file diff --git a/packages/client/lib/commands/PUBLISH.ts b/packages/client/lib/commands/PUBLISH.ts index eda5234df20..cbfcaabd1cd 100644 --- a/packages/client/lib/commands/PUBLISH.ts +++ b/packages/client/lib/commands/PUBLISH.ts @@ -1,4 +1,6 @@ -export function transformArguments(channel: string, message: string): Array { +import { RedisCommandArguments } from '.'; + +export function transformArguments(channel: string | Buffer, message: string | Buffer): RedisCommandArguments { return ['PUBLISH', channel, message]; } diff --git a/packages/client/package.json b/packages/client/package.json index 7a6d23f5ff9..d697d200bea 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@node-redis/client", - "version": "1.0.0-rc.0", + "version": "1.0.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -19,22 +19,22 @@ "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@node-redis/test-utils": "*", - "@types/node": "^16.11.7", + "@types/node": "^16.11.10", "@types/redis-parser": "^3.0.0", "@types/sinon": "^10.0.6", "@types/yallist": "^4.0.1", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", - "eslint": "^8.2.0", + "eslint": "^8.3.0", "nyc": "^15.1.0", - "release-it": "^14.11.7", + "release-it": "^14.11.8", "sinon": "^12.0.1", - "source-map-support": "^0.5.20", + "source-map-support": "^0.5.21", "ts-node": "^10.4.0", - "typedoc": "^0.22.9", + "typedoc": "^0.22.10", "typedoc-github-wiki-theme": "^0.6.0", - "typedoc-plugin-markdown": "^3.11.6", - "typescript": "^4.4.4" + "typedoc-plugin-markdown": "^3.11.7", + "typescript": "^4.5.2" }, "engines": { "node": ">=12" diff --git a/packages/json/README.md b/packages/json/README.md index 1cd599d5ea8..5b6d5ba8ce4 100644 --- a/packages/json/README.md +++ b/packages/json/README.md @@ -1,2 +1,80 @@ # @node-redis/json -The sources and docs for this package are in the main [node-redis](https://github.com/redis/node-redis) repo. + +This package provides support for the [RedisJSON](https://redisjson.io) module, which adds JSON as a native data type to Redis. It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RedisJSON commands. + +To use these extra commands, your Redis server must have the RedisJSON module installed. + +## Usage + +For a complete example, see [`managing-json.js`](https://github.com/redis/node-redis/blob/master/examples/managing-json.js) in the Node Redis examples folder. + +### Storing JSON Documents in Redis + +The [`JSON.SET`](https://oss.redis.com/redisjson/commands/#jsonset) command stores a JSON value at a given JSON Path in a Redis key. + +Here, we'll store a JSON document in the root of the Redis key "`mydoc`": + +```javascript +import { createClient } from 'redis'; + +... +await client.json.set('noderedis:jsondata', '$', { + name: 'Roberta McDonald', + pets: [ + { + name: 'Rex', + species: 'dog', + age: 3, + isMammal: true + }, + { + name: 'Goldie', + species: 'fish', + age: 2, + isMammal: false + } + ] +}); +``` + +For more information about RedisJSON's path syntax, [check out the documentation](https://oss.redis.com/redisjson/path/). + +### Retrieving JSON Documents from Redis + +With RedisJSON, we can retrieve all or part(s) of a JSON document using the [`JSON.GET`]() command and one or more JSON Paths. Let's get the name and age of one of the pets: + +```javascript +const results = await client.json.get('noderedis:jsondata', { + path: [ + '.pets[1].name', + '.pets[1].age' + ] +}); +``` + +`results` will contain the following: + +```javascript + { '.pets[1].name': 'Goldie', '.pets[1].age': 2 } +``` + +### Performing Atomic Updates on JSON Documents Stored in Redis + +RedisJSON includes commands that can atomically update values in a JSON document, in place in Redis without having to first retrieve the entire document. + +Using the [`JSON.NUMINCRBY`](https://oss.redis.com/redisjson/commands/#jsonnumincrby) command, we can update the age of one of the pets like this: + +```javascript +await client.json.numIncrBy('noderedis:jsondata', '.pets[1].age', 1); +``` + +And we can add a new object to the pets array with the [`JSON.ARRAPPEND`](https://oss.redis.com/redisjson/commands/#jsonarrappend) command: + +```javascript + await client.json.arrAppend('noderedis:jsondata', '.pets', { + name: 'Robin', + species: 'bird', + age: 1, + isMammal: false + }); +``` diff --git a/packages/json/lib/commands/ARRPOP.ts b/packages/json/lib/commands/ARRPOP.ts index 5d8785a8d94..932b3294d85 100644 --- a/packages/json/lib/commands/ARRPOP.ts +++ b/packages/json/lib/commands/ARRPOP.ts @@ -14,4 +14,4 @@ export function transformArguments(key: string, path?: string, index?: number): return args; } -export { transformRedisJsonNullArrayReply as transformReply } from '.'; +export { transformRedisJsonNullArrayNullReply as transformReply } from '.'; diff --git a/packages/json/lib/commands/index.ts b/packages/json/lib/commands/index.ts index 91b4f7dc4b5..a3c561addcc 100644 --- a/packages/json/lib/commands/index.ts +++ b/packages/json/lib/commands/index.ts @@ -84,8 +84,9 @@ export function transformRedisJsonNullReply(json: string | null): RedisJSON | nu return transformRedisJsonReply(json); } +export function transformRedisJsonNullArrayNullReply(jsons: Array | null): Array | null { + if (jsons === null) return null; -export function transformRedisJsonNullArrayReply(jsons: Array): Array { return jsons.map(transformRedisJsonNullReply); } diff --git a/packages/json/package.json b/packages/json/package.json index 7e5f6e10c1f..2db2c926248 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -9,16 +9,16 @@ "build": "tsc" }, "peerDependencies": { - "@node-redis/client": "^1.0.0-rc" + "@node-redis/client": "^1.0.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@node-redis/test-utils": "*", - "@types/node": "^16.11.7", + "@types/node": "^16.11.10", "nyc": "^15.1.0", - "release-it": "^14.11.7", - "source-map-support": "^0.5.20", + "release-it": "^14.11.8", + "source-map-support": "^0.5.21", "ts-node": "^10.4.0", - "typescript": "^4.4.4" + "typescript": "^4.5.2" } } diff --git a/packages/search/README.md b/packages/search/README.md index 856a75fbb5a..f54316d3c18 100644 --- a/packages/search/README.md +++ b/packages/search/README.md @@ -1,2 +1,120 @@ # @node-redis/search -The sources and docs for this package are in the main [node-redis](https://github.com/redis/node-redis) repo. + +This package provides support for the [RediSearch](https://redisearch.io) module, which adds indexing and querying support for data stored in Redis Hashes or as JSON documents with the RedisJSON module. It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RediSearch commands. + +To use these extra commands, your Redis server must have the RediSearch module installed. To index and query JSON documents, you'll also need to add the RedisJSON module. + +## Usage + +For complete examples, see [`search-hashes.js`](https://github.com/redis/node-redis/blob/master/examples/search-hashes.js) and [`search-json.js`](https://github.com/redis/node-redis/blob/master/examples/search-json.js) in the Node Redis examples folder. + +### Indexing and Querying Data in Redis Hashes + +#### Creating an Index + +Before we can perform any searches, we need to tell RediSearch how to index our data, and which Redis keys to find that data in. The [FT.CREATE](https://oss.redis.com/redisearch/Commands/#ftcreate) command creates a RediSearch index. Here's how to use it to create an index we'll call `idx:animals` where we want to index hashes containing `name`, `species` and `age` fields, and whose key names in Redis begin with the prefix `noderedis:animals`: + +```javascript +await client.ft.create('idx:animals', { + name: { + type: SchemaFieldTypes.TEXT, + sortable: true + }, + species: SchemaFieldTypes.TAG, + age: SchemaFieldTypes.NUMERIC + }, { + ON: 'HASH', + PREFIX: 'noderedis:animals' + } +); +``` + +See the [`FT.CREATE` documentation](https://oss.redis.com/redisearch/Commands/#ftcreate) for information about the different field types and additional options. + +#### Querying the Index + +Once we've created an index, and added some data to Redis hashes whose keys begin with the prefix `noderedis:animals`, we can start writing some search queries. RediSearch supports a rich query syntax for full-text search, faceted search, aggregation and more. Check out the [`FT.SEARCH` documentation](https://oss.redis.com/redisearch/Commands/#ftsearch) and the [query syntax reference](https://oss.redis.com/redisearch/Query_Syntax/) for more information. + +Let's write a query to find all the animals where the `species` field has the value `dog`: + +```javascript +const results = await client.ft.search('idx:animals', '@species:{dog}'); +``` + +`results` looks like this: + +```javascript +{ + total: 2, + documents: [ + { + id: 'noderedis:animals:4', + value: { + name: 'Fido', + species: 'dog', + age: '7' + } + }, + { + id: 'noderedis:animals:3', + value: { + name: 'Rover', + species: 'dog', + age: '9' + } + } + ] +} +``` + +### Indexing and Querying Data with RedisJSON + +RediSearch can also index and query JSON documents stored in Redis using the RedisJSON module. The approach is similar to that for indexing and searching data in hashes, but we can now use JSON Path like syntax and the data no longer has to be flat name/value pairs - it can contain nested objects and arrays. + +#### Creating an Index + +As before, we create an index with the `FT.CREATE` command, this time specifying we want to index JSON documents that look like this: + +```javascript +{ + name: 'Alice', + age: 32, + coins: 100 +} +``` + +Each document represents a user in some system, and users have name, age and coins properties. + +One way we might choose to index these documents is as follows: + +```javascript +await client.ft.create('idx:users', { + '$.name': { + type: SchemaFieldTypes.TEXT, + SORTABLE: 'UNF' + }, + '$.age': { + type: SchemaFieldTypes.NUMERIC, + AS: 'age' + }, + '$.coins': { + type: SchemaFieldTypes.NUMERIC, + AS: 'coins' + } +}, { + ON: 'JSON', + PREFIX: 'noderedis:users' +}); +``` + +Note that we're using JSON Path to specify where the fields to index are in our JSON documents, and the `AS` clause to define a name/alias for each field. We'll use these when writing queries. + +#### Querying the Index + +Now we have an index and some data stored as JSON documents in Redis (see the [JSON package documentation](https://github.com/redis/node-redis/tree/master/packages/json) for examples of how to store JSON), we can write some queries... + +We'll use the [RediSearch query language](https://oss.redis.com/redisearch/Query_Syntax/) and [`FT.SEARCH`](https://oss.redis.com/redisearch/Commands/#ftsearch) command. Here's a query to find users under the age of 30: + +```javascript +await client.ft.search('idx:users', '@age:[0 30]'); +``` diff --git a/packages/search/package.json b/packages/search/package.json index a72678c2add..e5730ab886e 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@node-redis/search", - "version": "1.0.0-rc.0", + "version": "1.0.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -9,16 +9,16 @@ "build": "tsc" }, "peerDependencies": { - "@node-redis/client": "^1.0.0-rc" + "@node-redis/client": "^1.0.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@node-redis/test-utils": "*", - "@types/node": "^16.11.7", + "@types/node": "^16.11.10", "nyc": "^15.1.0", - "release-it": "^14.11.7", - "source-map-support": "^0.5.20", + "release-it": "^14.11.8", + "source-map-support": "^0.5.21", "ts-node": "^10.4.0", - "typescript": "^4.4.4" + "typescript": "^4.5.2" } } diff --git a/packages/test-utils/docker/entrypoint.sh b/packages/test-utils/docker/entrypoint.sh index 244977e83c4..d4006f55622 100755 --- a/packages/test-utils/docker/entrypoint.sh +++ b/packages/test-utils/docker/entrypoint.sh @@ -1,7 +1,3 @@ #!/bin/bash -echo testststealkshdfklhasdf - -echo $REDIS_ARGUMENTS - redis-server $REDIS_ARGUMENTS diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 47ddc25acff..e46f82f0c01 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -8,19 +8,19 @@ "test": "echo \"TODO\"" }, "peerDependencies": { - "@node-redis/client": "^1.0.0-rc" + "@node-redis/client": "^1.0.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@types/mocha": "^9.0.0", - "@types/node": "^16.11.7", - "@types/yargs": "^17.0.5", + "@types/node": "^16.11.10", + "@types/yargs": "^17.0.7", "mocha": "^9.1.3", "nyc": "^15.1.0", - "release-it": "^14.11.7", - "source-map-support": "^0.5.20", + "release-it": "^14.11.8", + "source-map-support": "^0.5.21", "ts-node": "^10.4.0", - "typescript": "^4.4.4", + "typescript": "^4.5.2", "yargs": "^17.2.1" } } diff --git a/packages/time-series/.npmignore b/packages/time-series/.npmignore new file mode 100644 index 00000000000..bbef2b404fb --- /dev/null +++ b/packages/time-series/.npmignore @@ -0,0 +1,6 @@ +.nyc_output/ +coverage/ +lib/ +.nycrc.json +.release-it.json +tsconfig.json