1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-04 15:02:09 +03:00

v4.0.0-rc.4 (#1723)

* update workflows & README

* add .deepsource.toml

* fix client.quit, add error events on cluster, fix some "deepsource.io" warnings

* Release 4.0.0-rc.1

* add cluster.duplicate, add some tests

* fix #1650 - add support for Buffer in some commands, add GET_BUFFER command

* fix GET and GET_BUFFER return type

* update FAQ

* Update invalid code example in README.md (#1654)

* Update invalid code example in README.md

* Update README.md

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* fix #1652

* ref #1653 - better types

* better types

* fix 54124793ad

* Update GEOSEARCHSTORE.spec.ts

* fix #1660 - add support for client.HSET('key', 'field', 'value')

* upgrade dependencies, update README

* fix #1659 - add support for db-number in client options url

* fix README, remove unused import, downgrade typedoc & typedoc-plugin-markdown

* update client-configurations.md

* fix README

* add CLUSTER_SLOTS, add some tests

* fix "createClient with url" test with redis 5

* remove unused imports

* Release 4.0.0-rc.2

* add missing semicolon

* replace empty "transformReply" functions with typescript "declare"

* fix EVAL & EVALSHA, add some tests, npm update

* fix #1665 - add ZRANGEBYLEX, ZRANGEBYSCORE, ZRANGEBYSCORE_WITHSCORES

* new issue templates

* add all COMMAND commands

* run COMMAND & COMMAND INFO tests only on redis >6

* Create SECURITY.md

* fix #1671 - add support for all client configurations in cluster

* ref #1671 - add support for defaults

* remove some commands from cluster, npm update, clean code,

* lock benny version

* fix #1674 - remove `isolationPoolOptions` when creating isolated connection

* increase test coverage

* update .npmignore

* Release 4.0.0-rc.3

* fix README

* remove whitespace from LICENSE

* use "export { x as y }" instead of import & const

* move from "NodeRedis" to "Redis"

* fix #1676

* update comments

* Auth before select database (#1679)

* Auth before select database

* fix #1681

Co-authored-by: leibale <leibale1998@gmail.com>

* Adds connect-as-acl-user example. (#1684)

* Adds connect-as-acl-user example.

* Adds blank line at end.

* Set to private.

* Adds examples folder to npmignore.

* Adds Apple .DS_Store file to .gitignore (#1685)

* Adds Apple .DS_Store file.

* Add .DS_Store to .npmignore too

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* move examples

* clean some tests

* clean code

* Adds examples table of contents and contribution guidelines. (#1686)

* Updated examples to use named functions. (#1687)

* Updated examples to user named functions.

* Update README.md

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* update docs, add 6.0.x to the tests matrix, add eslint, npm update, fix some commands, fix some types

Co-authored-by: Simon Prickett <simon@crudworks.org>

* fix tests with redis 6.0.x

* fix ACL GETUSER test

* fix client.quit and client.disconnect

* fix ACL GETUSER

* Adds TypeScript note and corrects a typo.

* Fixes a bug in the Scan Iterator section. (#1694)

* Made examples use local version.

* Add `lua-multi-incr.js` example (#1692)

Also fix syntax error in the lua example in the README

Closes #1689.

* Add(examples): Create an example for blPop & lPush (#1696)

* Add(examples): Create an example for blPop & lPush

Signed-off-by: Aditya Rastogi <adit.rastogi2014@gmail.com>

* Update(examples): fix case, add timeout, update readme

Signed-off-by: Aditya Rastogi <adit.rastogi2014@gmail.com>

Closes #1693.

* Add command-with-modifiers.js example (#1695)

* Adds TypeScript note and corrects a typo.

* Adds command-with-modifiers example. (redis#1688)

* Adds command-with-modifiers example. (redis#1688)

* Adds command-with-modifiers example. (redis#1688)

* Removed callbacks.

Co-authored-by: Simon Prickett <simon@redislabs.com>

Closes #1688.

* Issue # 1697 FIX - creates an example script that shows how to use the SSCAN iterator (#1699)

* #1697 fix for set scan example

* adds the js file

* adds comment

* Minor layout and comment adjustment.

Co-authored-by: srawat2 <shashank19aug>
Co-authored-by: Simon Prickett <simon@redislabs.com>

Closes #1697.

* fix #1706 - HSET return type should be number

* use dockers for tests, fix some bugs

* increase dockers timeout to 30s

* release drafter (#1683)

* release drafter

* fixing contributors

* use dockers for tests, use npm workspaces, add rejson & redisearch modules, fix some bugs

* fix #1712 - fix LINDEX return type

* uncomment TIME tests

* use codecov

* fix tests.yml

* uncomment "should handle live resharding" test

* fix #1714 - update README(s)

* add package-lock.json

* update CONTRIBUTING.md

* update examples

* uncomment some tests

* fix test-utils

* move "all-in-one" to root folder

* fix tests workflow

* fix bug in cluster slots, enhance live resharding test

* fix live resharding test

* fix #1707 - handle number arguments in legacy mode

* Add rejectedUnauthorized and other TLS options (#1708)

* Update socket.ts

* fix #1716 - decode username and password from url

* fix some Z (sorted list) commands, increase commands test coverage

* remove empty lines

* fix 'Scenario' typo (#1720)

* update readmes, add createCluster to the `redis` package

* add .release-it.json files, update some md files

* run tests on pull requests too

* Support esModuleInterop set to false. (#1717)

* Support esModuleInterop set to false.

When testing the upcoming 4.x release, we got a bunch of typescript
errors emitted from this project.

We quickly realized this is because the library uses the esModuleInterop
flag. This makes some imports _slightly_ easier to write, but it comes
at a cost: it forces any application or library using this library to
*also* have esModuleInterop on.

The `esModuleInterop` flag is a bit of a holdover from an earlier time,
and I would not recommend using it in libraries. The main issue is that
if it's set to true, you are forcing any users of the library to also
have `esModuleInterop`, where if you keep have it set to `false` (the
default), you leave the decision to the user.

This change should have no rammifications to users with
`esModuleInterop` on, but it will enable support for those that have it
off.

This is especially good for library authors such as myself, because I
would also like to keep this flag off to not force *my* users into this
feature.

* All tests now pass!

* Move @types/redis-parser into client sub-package

and removed a comma

* npm update, remove html from readme

* add tests and licence badges

* update changelog.md

* update .npmignore and .release-it.json

* update .release-it.json

* Release client@1.0.0-rc.0

* revert d32f1edf8a

* fix .npmignore

* replace @redis with @node-redis

* Release client@1.0.0-rc.0

* update json & search version

* Release json@1.0.0-rc.0

* Release search@1.0.0-rc.0

* update dependencies

* Release redis@4.0.0-rc.4

Co-authored-by: Richard Samuelsson <noobtoothfairy@gmail.com>
Co-authored-by: mustard <mhqnwt@gmail.com>
Co-authored-by: Simon Prickett <simon@redislabs.com>
Co-authored-by: Simon Prickett <simon@crudworks.org>
Co-authored-by: Suze Shardlow <SuzeShardlow@users.noreply.github.com>
Co-authored-by: Joshua T <buildingsomethingfun@gmail.com>
Co-authored-by: Aditya Rastogi <adit.rastogi2014@gmail.com>
Co-authored-by: Rohan Kumar <rohan.kr20@gmail.com>
Co-authored-by: Kalki <shashank.kviit@gmail.com>
Co-authored-by: Chayim <chayim@users.noreply.github.com>
Co-authored-by: Da-Jin Chu <dajinchu@gmail.com>
Co-authored-by: Henrique Corrêa <75134774+HeCorr@users.noreply.github.com>
Co-authored-by: Evert Pot <me@evertpot.com>
This commit is contained in:
Leibale Eidelman
2021-11-16 02:48:20 -05:00
committed by GitHub
parent 199285aa71
commit eed479778f
705 changed files with 9959 additions and 4349 deletions

298
.github/README.md vendored Normal file
View File

@@ -0,0 +1,298 @@
# 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<string> {
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).

43
.github/release-drafter-config.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name-template: 'Version $NEXT_PATCH_VERSION'
tag-template: 'v$NEXT_PATCH_VERSION'
autolabeler:
- label: 'chore'
files:
- '*.md'
- '.github/*'
- label: 'bug'
branch:
- '/bug-.+'
- label: 'chore'
branch:
- '/chore-.+'
- label: 'feature'
branch:
- '/feature-.+'
categories:
- title: 'Breaking Changes'
labels:
- 'breakingchange'
- title: '🚀 New Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Maintenance'
label: 'chore'
change-template: '- $TITLE (#$NUMBER)'
exclude-labels:
- 'skip-changelog'
template: |
## Changes
$CHANGES
## Contributors
We'd like to thank all the contributors who worked on this release!
$CONTRIBUTORS

View File

@@ -1,46 +0,0 @@
name: Benchmark
on:
push:
branches:
- master
- v4.0
jobs:
benchmark:
name: Benchmark
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [16.x]
redis-version: [6.x]
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.3.0
with:
node-version: ${{ matrix.node-version }}
- name: Setup Redis
uses: shogo82148/actions-setup-redis@v1.12.0
with:
redis-version: ${{ matrix.redis-version }}
- name: Install Packages
run: npm ci
- name: Build
run: npm run build
- name: Install Benchmark Packages
run: npm ci
working-directory: ./benchmark
- name: Benchmark
run: npm run start
working-directory: ./benchmark

View File

@@ -9,21 +9,16 @@ on:
jobs:
documentation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 1
- name: Use Node.js
uses: actions/setup-node@v2.3.0
- name: Install Packages
run: npm ci
- name: Generate Documentation
run: npm run documentation
- name: Upload Documentation to Wiki
uses: SwiftDocOrg/github-wiki-publish-action@v1
with:

19
.github/workflows/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Release Drafter
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- master
jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
# Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v5
with:
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
config-name: release-drafter-config.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -5,6 +5,10 @@ on:
branches:
- master
- v4.0
pull_request:
branches:
- master
- v4.0
jobs:
tests:
@@ -12,47 +16,32 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [12.x, 14.x, 16.x]
redis-version: [5.x, 6.x]
node-version: [12, 14, 16]
redis-version: [5, 6.0, 6.2]
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.3.0
with:
node-version: ${{ matrix.node-version }}
- name: Setup Redis
uses: shogo82148/actions-setup-redis@v1.12.0
with:
redis-version: ${{ matrix.redis-version }}
auto-start: "false"
- name: Update npm
run: npm i -g npm
if: ${{ matrix.node-version <= 14 }}
- name: Install Packages
run: npm ci
- name: Build tests tools
run: npm run build:tests-tools
- name: Run Tests
run: npm run test
- name: Generate lcov
run: ./node_modules/.bin/nyc report -r lcov
- name: Coveralls
uses: coverallsapp/github-action@1.1.3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
flag-name: Node ${{ matrix.node-version }} Redis ${{ matrix.redis-version }}
parallel: true
finish:
needs: tests
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@1.1.3
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
run: npm run test -- --forbid-only --redis-version=${{ matrix.redis-version }}
- name: Upload to Codecov
run: |
curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import
curl -Os https://uploader.codecov.io/latest/linux/codecov
curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM
curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig
gpgv codecov.SHA256SUM.sig codecov.SHA256SUM
shasum -a 256 -c codecov.SHA256SUM
chmod +x codecov
./codecov

8
.gitignore vendored
View File

@@ -1,8 +1,8 @@
.vscode/
.idea/
node_modules/
dist/
.nyc_output/
.vscode/
coverage/
dist/
node_modules/
.DS_Store
dump.rdb
documentation/

View File

@@ -1,18 +1,12 @@
.vscode/
.idea/
node_modules/
.nyc_output/
coverage/
dump.rdb
documentation/
CONTRIBUTING.md
tsconfig.json
.deepsource.toml
.nycrc.json
benchmark/
.github/
scripts/
lib/
.vscode/
docs/
examples/
packages/
.deepsource.toml
.release-it.json
CONTRIBUTING.md
SECURITY.md
index.ts
*.spec.*
dist/lib/test-utils.*
tsconfig.base.json
tsconfig.json

7
.release-it.json Normal file
View File

@@ -0,0 +1,7 @@
{
"git": {
"tagName": "redis@${version}",
"commitMessage": "Release ${tagName}",
"tagAnnotation": "Release ${tagName}"
}
}

View File

@@ -45,12 +45,9 @@ A huge thank you to the original author of Node Redis, [Matthew Ranney](https://
Node Redis has a full test suite with coverage setup.
To run the tests, run `npm install` to install dependencies, then run `npm test`.
To run the tests, run `npm install` to install dependencies, then run `npm run build:tests-tools && npm test`.
Note that the test suite assumes that a few tools are installed in your environment, such as:
- redis (make sure redis-server is not running when starting the tests, it's part of the test-suite to start it and you'll end up with a "port already in use" error)
- stunnel (for TLS tests)
Note that the test suite assumes that [`docker`](https://www.docker.com/) is installed in your environment.
### Submitting Code for Review

View File

@@ -1,4 +1,4 @@
MIT License
MIT License
Copyright (c) 2016-present Node Redis contributors.

291
README.md
View File

@@ -1,289 +1,2 @@
<p align="center">
<a href="https://github.com/noderedis/node-redis">
<img width="128" src="https://static.invertase.io/assets/node_redis_logo.png" />
</a>
<h2 align="center">Node Redis</h2>
</p>
<div align="center">
<a href="https://coveralls.io/github/NodeRedis/node-redis">
<img src="https://coveralls.io/repos/github/NodeRedis/node-redis/badge.svg" alt="Coverage Status"/>
</a>
<a href="https://www.npmjs.com/package/redis/v/next">
<img src="https://img.shields.io/npm/dm/redis.svg" alt="Downloads"/>
</a>
<a href="https://www.npmjs.com/package/redis/v/next">
<img src="https://img.shields.io/npm/v/redis/next.svg" alt="Version"/>
</a>
<a href="https://discord.gg/XMMVgxUm">
<img src="https://img.shields.io/discord/697882427875393627" alt="Chat"/>
</a>
</div>
---
## 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 in the [Wiki](https://github.com/NodeRedis/node-redis/wiki/lib.socket#RedisSocketOptions).
### 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');
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 member of client.hScanIterator('hash')) {}
for await (const { field, value } of client.sScanIterator('set')) {}
for await (const { member, score } 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 } from 'redis';
import { defineScript } from 'redis/lua-script';
(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<string> {
return [key, number.toString()];
},
transformReply(reply: number): number {
return reply;
}
})
}
});
await client.connect();
await client.set('key', '1');
await client.add('key', 2); // 3
})();
```
### Cluster
Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a non-clustered client:
```typescript
import { createCluster } from 'redis';
(async () => {
const cluster = createCluster({
rootNodes: [
{
url: 'redis://10.0.0.1:30001'
},
{
url: 'redis://10.0.0.2:30002'
}
]
});
cluster.on('error', (err) => console.log('Redis Cluster Error', err));
await cluster.connect();
await cluster.set('key', 'value');
const value = await cluster.get('key');
})();
```
### 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==')
]);
```
## 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!
<a href="https://github.com/NodeRedis/node-redis/graphs/contributors">
<img src="https://contrib.rocks/image?repo=NodeRedis/node-redis"/>
</a>
## License
This repository is licensed under the "MIT" license. See [LICENSE](LICENSE).
# redis
The sources and docs for this package are in the main [node-redis](https://github.com/redis/node-redis) repo.

View File

@@ -5,9 +5,9 @@
Node Redis is generally backwards compatible with very few exceptions, so we recommend users to always use the latest version to experience stability, performance and security.
| Version | Supported |
| ------- | ------------------ |
| 4.0.x | :white_check_mark: |
| 3.1.x | :white_check_mark: |
|---------|--------------------|
| 4.0.z | :heavy_check_mark: |
| 3.1.z | :heavy_check_mark: |
| < 3.1 | :x: |
## Reporting a Vulnerability

View File

@@ -1 +0,0 @@
node_modules

View File

@@ -1,81 +0,0 @@
import { add, suite, cycle, complete } from 'benny';
import v4 from 'v4';
import v3 from 'v3';
import { once } from 'events';
const v4Client = v4.createClient(),
v4LegacyClient = v4.createClient({
legacyMode: true
}),
v3Client = v3.createClient();
await Promise.all([
v4Client.connect(),
v4LegacyClient.connect(),
once(v3Client, 'connect')
]);
const key = random(100),
value = random(100);
function random(size) {
const result = [];
for (let i = 0; i < size; i++) {
result.push(Math.floor(Math.random() * 10));
}
return result.join('');
}
suite(
'SET GET',
add('v4', async () => {
await Promise.all([
v4Client.set(key, value),
v4Client.get(key)
]);
}),
add('v4 - legacy mode', () => {
return new Promise((resolve, reject) => {
v4LegacyClient.set(key, value);
v4LegacyClient.get(key, (err, reply) => {
if (err) {
reject(err);
} else {
resolve(reply);
}
});
});
}),
add('v3', () => {
return new Promise((resolve, reject) => {
v3Client.set(key, value);
v3Client.get(key, (err, reply) => {
if (err) {
reject(err);
} else {
resolve(reply);
}
});
});
}),
cycle(),
complete(),
complete(() => {
return Promise.all([
v4Client.disconnect(),
v4LegacyClient.disconnect(),
new Promise((resolve, reject) => {
v3Client.quit((err) => {
if (err) {
reject(err);
} else {
resolve(err);
}
});
})
]);
})
);

View File

@@ -1,849 +0,0 @@
{
"name": "benchmark",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "benchmark",
"license": "ISC",
"dependencies": {
"benny": "3.6.15",
"v3": "npm:redis@3.1.2",
"v4": "file:../"
}
},
"..": {
"name": "redis",
"version": "4.0.0-rc.2",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.0",
"generic-pool": "3.8.2",
"redis-parser": "3.0.0",
"yallist": "4.0.0"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@tsconfig/node12": "^1.0.9",
"@types/mocha": "^9.0.0",
"@types/node": "^16.10.3",
"@types/sinon": "^10.0.4",
"@types/which": "^2.0.1",
"@types/yallist": "^4.0.1",
"mocha": "^9.1.2",
"nyc": "^15.1.0",
"release-it": "^14.11.6",
"sinon": "^11.1.2",
"source-map-support": "^0.5.20",
"ts-node": "^10.3.0",
"typedoc": "^0.22.5",
"typedoc-github-wiki-theme": "^0.6.0",
"typedoc-plugin-markdown": "^3.11.3",
"typescript": "^4.4.3",
"which": "^2.0.2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@arrows/array": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
"integrity": "sha512-MGYS8xi3c4tTy1ivhrVntFvufoNzje0PchjEz6G/SsWRgUKxL4tKwS6iPdO8vsaJYldagAeWMd5KRD0aX3Q39g==",
"dependencies": {
"@arrows/composition": "^1.2.2"
}
},
"node_modules/@arrows/composition": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@arrows/composition/-/composition-1.2.2.tgz",
"integrity": "sha512-9fh1yHwrx32lundiB3SlZ/VwuStPB4QakPsSLrGJFH6rCXvdrd060ivAZ7/2vlqPnEjBkPRRXOcG1YOu19p2GQ=="
},
"node_modules/@arrows/dispatch": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@arrows/dispatch/-/dispatch-1.0.3.tgz",
"integrity": "sha512-v/HwvrFonitYZM2PmBlAlCqVqxrkIIoiEuy5bQgn0BdfvlL0ooSBzcPzTMrtzY8eYktPyYcHg8fLbSgyybXEqw==",
"dependencies": {
"@arrows/composition": "^1.2.2"
}
},
"node_modules/@arrows/error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@arrows/error/-/error-1.0.2.tgz",
"integrity": "sha512-yvkiv1ay4Z3+Z6oQsUkedsQm5aFdyPpkBUQs8vejazU/RmANABx6bMMcBPPHI4aW43VPQmXFfBzr/4FExwWTEA=="
},
"node_modules/@arrows/multimethod": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@arrows/multimethod/-/multimethod-1.1.7.tgz",
"integrity": "sha512-EjHD3XuGAV4G28rm7mu8k7zQJh/EOizh104/p9i2ofGcnL5mgKONFH/Bq6H3SJjM+WDAlKcR9WBpNhaAKCnH2g==",
"dependencies": {
"@arrows/array": "^1.4.0",
"@arrows/composition": "^1.2.2",
"@arrows/error": "^1.0.2",
"fast-deep-equal": "^3.1.1"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dependencies": {
"type-fest": "^0.21.3"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/benchmark": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
"integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
"dependencies": {
"lodash": "^4.17.4",
"platform": "^1.3.3"
}
},
"node_modules/benny": {
"version": "3.6.15",
"resolved": "https://registry.npmjs.org/benny/-/benny-3.6.15.tgz",
"integrity": "sha512-kq6XVGGYVou3Y8KNPs3SEF881vi5fJ8sIf9w69D2rreiNfRicWVWK6u6/mObMw6BiexoHHumtipn5gcu0Tngng==",
"dependencies": {
"@arrows/composition": "^1.0.0",
"@arrows/dispatch": "^1.0.2",
"@arrows/multimethod": "^1.1.6",
"benchmark": "^2.1.4",
"fs-extra": "^9.0.1",
"json2csv": "^5.0.4",
"kleur": "^4.1.3",
"log-update": "^4.0.0",
"prettier": "^2.1.2",
"stats-median": "^1.0.1"
}
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"dependencies": {
"restore-cursor": "^3.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
"engines": {
"node": ">= 6"
}
},
"node_modules/denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/graceful-fs": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/json2csv": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.6.tgz",
"integrity": "sha512-0/4Lv6IenJV0qj2oBdgPIAmFiKKnh8qh7bmLFJ+/ZZHLjSeiL3fKKGX3UryvKPbxFbhV+JcYo9KUC19GJ/Z/4A==",
"dependencies": {
"commander": "^6.1.0",
"jsonparse": "^1.3.1",
"lodash.get": "^4.4.2"
},
"bin": {
"json2csv": "bin/json2csv.js"
},
"engines": {
"node": ">= 10",
"npm": ">= 6.13.0"
}
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
"engines": [
"node >= 0.2.0"
]
},
"node_modules/kleur": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==",
"engines": {
"node": ">=6"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"node_modules/log-update": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
"dependencies": {
"ansi-escapes": "^4.3.0",
"cli-cursor": "^3.1.0",
"slice-ansi": "^4.0.0",
"wrap-ansi": "^6.2.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"engines": {
"node": ">=6"
}
},
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"node_modules/prettier": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz",
"integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==",
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
"integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ=="
},
"node_modules/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/stats-median": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/stats-median/-/stats-median-1.0.1.tgz",
"integrity": "sha512-IYsheLg6dasD3zT/w9+8Iq9tcIQqqu91ZIpJOnIEM25C3X/g4Tl8mhXwW2ZQpbrsJISr9+wizEYgsibN5/b32Q=="
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/v3": {
"name": "redis",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
"dependencies": {
"denque": "^1.5.0",
"redis-commands": "^1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-redis"
}
},
"node_modules/v4": {
"resolved": "..",
"link": true
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
}
},
"dependencies": {
"@arrows/array": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
"integrity": "sha512-MGYS8xi3c4tTy1ivhrVntFvufoNzje0PchjEz6G/SsWRgUKxL4tKwS6iPdO8vsaJYldagAeWMd5KRD0aX3Q39g==",
"requires": {
"@arrows/composition": "^1.2.2"
}
},
"@arrows/composition": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@arrows/composition/-/composition-1.2.2.tgz",
"integrity": "sha512-9fh1yHwrx32lundiB3SlZ/VwuStPB4QakPsSLrGJFH6rCXvdrd060ivAZ7/2vlqPnEjBkPRRXOcG1YOu19p2GQ=="
},
"@arrows/dispatch": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@arrows/dispatch/-/dispatch-1.0.3.tgz",
"integrity": "sha512-v/HwvrFonitYZM2PmBlAlCqVqxrkIIoiEuy5bQgn0BdfvlL0ooSBzcPzTMrtzY8eYktPyYcHg8fLbSgyybXEqw==",
"requires": {
"@arrows/composition": "^1.2.2"
}
},
"@arrows/error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@arrows/error/-/error-1.0.2.tgz",
"integrity": "sha512-yvkiv1ay4Z3+Z6oQsUkedsQm5aFdyPpkBUQs8vejazU/RmANABx6bMMcBPPHI4aW43VPQmXFfBzr/4FExwWTEA=="
},
"@arrows/multimethod": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@arrows/multimethod/-/multimethod-1.1.7.tgz",
"integrity": "sha512-EjHD3XuGAV4G28rm7mu8k7zQJh/EOizh104/p9i2ofGcnL5mgKONFH/Bq6H3SJjM+WDAlKcR9WBpNhaAKCnH2g==",
"requires": {
"@arrows/array": "^1.4.0",
"@arrows/composition": "^1.2.2",
"@arrows/error": "^1.0.2",
"fast-deep-equal": "^3.1.1"
}
},
"ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"requires": {
"type-fest": "^0.21.3"
}
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="
},
"benchmark": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
"integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
"requires": {
"lodash": "^4.17.4",
"platform": "^1.3.3"
}
},
"benny": {
"version": "3.6.15",
"resolved": "https://registry.npmjs.org/benny/-/benny-3.6.15.tgz",
"integrity": "sha512-kq6XVGGYVou3Y8KNPs3SEF881vi5fJ8sIf9w69D2rreiNfRicWVWK6u6/mObMw6BiexoHHumtipn5gcu0Tngng==",
"requires": {
"@arrows/composition": "^1.0.0",
"@arrows/dispatch": "^1.0.2",
"@arrows/multimethod": "^1.1.6",
"benchmark": "^2.1.4",
"fs-extra": "^9.0.1",
"json2csv": "^5.0.4",
"kleur": "^4.1.3",
"log-update": "^4.0.0",
"prettier": "^2.1.2",
"stats-median": "^1.0.1"
}
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"requires": {
"restore-cursor": "^3.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="
},
"denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"graceful-fs": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"json2csv": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.6.tgz",
"integrity": "sha512-0/4Lv6IenJV0qj2oBdgPIAmFiKKnh8qh7bmLFJ+/ZZHLjSeiL3fKKGX3UryvKPbxFbhV+JcYo9KUC19GJ/Z/4A==",
"requires": {
"commander": "^6.1.0",
"jsonparse": "^1.3.1",
"lodash.get": "^4.4.2"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
}
},
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
},
"kleur": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA=="
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"log-update": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
"requires": {
"ansi-escapes": "^4.3.0",
"cli-cursor": "^3.1.0",
"slice-ansi": "^4.0.0",
"wrap-ansi": "^6.2.0"
}
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"requires": {
"mimic-fn": "^2.1.0"
}
},
"platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"prettier": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz",
"integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA=="
},
"redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
},
"redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"requires": {
"redis-errors": "^1.0.0"
}
},
"restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"requires": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
}
},
"signal-exit": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
"integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ=="
},
"slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"requires": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
}
},
"stats-median": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/stats-median/-/stats-median-1.0.1.tgz",
"integrity": "sha512-IYsheLg6dasD3zT/w9+8Iq9tcIQqqu91ZIpJOnIEM25C3X/g4Tl8mhXwW2ZQpbrsJISr9+wizEYgsibN5/b32Q=="
},
"string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
}
},
"strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"requires": {
"ansi-regex": "^5.0.1"
}
},
"type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
},
"v3": {
"version": "npm:redis@3.1.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
"requires": {
"denque": "^1.5.0",
"redis-commands": "^1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
}
},
"v4": {
"version": "file:..",
"requires": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@tsconfig/node12": "^1.0.9",
"@types/mocha": "^9.0.0",
"@types/node": "^16.10.3",
"@types/sinon": "^10.0.4",
"@types/which": "^2.0.1",
"@types/yallist": "^4.0.1",
"cluster-key-slot": "1.1.0",
"generic-pool": "3.8.2",
"mocha": "^9.1.2",
"nyc": "^15.1.0",
"redis-parser": "3.0.0",
"release-it": "^14.11.6",
"sinon": "^11.1.2",
"source-map-support": "^0.5.20",
"ts-node": "^10.3.0",
"typedoc": "^0.22.5",
"typedoc-github-wiki-theme": "^0.6.0",
"typedoc-plugin-markdown": "^3.11.3",
"typescript": "^4.4.3",
"which": "^2.0.2",
"yallist": "4.0.0"
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
}
}
}

View File

@@ -1,17 +0,0 @@
{
"name": "benchmark",
"private": true,
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node ./"
},
"author": "",
"license": "ISC",
"dependencies": {
"benny": "3.6.15",
"v3": "npm:redis@3.1.2",
"v4": "file:../"
}
}

View File

@@ -15,7 +15,7 @@
| 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](https://redis.io/modules) to include (TODO - document) |
| modules | | Object defining which [Redis Modules](../.github/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 |

56
docs/clustering.md Normal file
View File

@@ -0,0 +1,56 @@
# Clustering
## Basic Example
Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a regular client instance:
```typescript
import { createCluster } from 'redis';
(async () => {
const cluster = createCluster({
rootNodes: [
{
url: 'redis://10.0.0.1:30001'
},
{
url: 'redis://10.0.0.2:30002'
}
]
});
cluster.on('error', (err) => console.log('Redis Cluster Error', err));
await cluster.connect();
await cluster.set('key', 'value');
const value = await cluster.get('key');
})();
```
## `createCluster` configuration
> See the [client configuration](./client-configuration.md) page for the `rootNodes` and `defaults` configuration schemas.
| Property | Default | Description |
|------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| rootNodes | | An array of root nodes that are part of the cluster, which will be used to get the cluster topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster, 3 should be enough to reliably connect and obtain the cluster configuration from the server |
| defaults | | The default configuration values for every client in the cluster. Use this for example when specifying an ACL user to connect with |
| useReplicas | `false` | When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes |
| maxCommandRedirections | `16` | The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors |
| 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
### Commands that operate on Redis Keys
Commands such as `GET`, `SET`, etc. will be routed by the first key, for instance `MGET 1 2 3` will be routed by the key `1`.
### [Server Commands](https://redis.io/commands#server)
Admin commands such as `MEMORY STATS`, `FLUSHALL`, etc. are not attached to the cluster, and should be executed on a specific node using `.getSlot()` or `.getAllMasters()`.
### "Forwarded Commands"
Some commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. The client will send these commands to a random node in order to spread the load across the cluster.

View File

@@ -10,7 +10,7 @@ Below are several examples of how to use isolated execution.
> NOTE: Behind the scences we're using [`generic-pool`](https://www.npmjs.com/package/generic-pool) to provide a pool of connections that can be isolated. Go there to learn more.
## The Simple Secnario
## The Simple Scenario
This just isolates execution on a single connection. Do what you want with that connection:

1
examples/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
package-lock.json

76
examples/README.md Normal file
View File

@@ -0,0 +1,76 @@
# Node Redis: Examples
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 |
## Contributing
We'd love to see more examples here. If you have an idea that you'd like to see included here, submit a Pull Request and we'll be sure to review it! Don't forget to check out our [contributing guide](../CONTRIBUTING.md).
## Setup
To set up the examples folder so that you can run an example / develop one of your own:
```
$ git clone https://github.com/redis/node-redis.git
$ cd node-redis
$ npm install -ws && npm run build
$ cd examples
$ npm install
```
### Coding Guidelines for Examples
When adding a new example, please follow these guidelines:
* Add your code in a single JavaScript or TypeScript file per example, directly in the `examples` folder
* Do not introduce other dependencies in your example
* Give your `.js` file a meaningful name using `-` separators e.g. `adding-to-a-stream.js` / `adding-to-a-stream.ts`
* Indent your code using 2 spaces
* Use the single line `//` comment style and comment your code
* Add a comment at the top of your `.js` / `.ts` file describing what your example does
* Add a comment at the top of your `.js` / `.ts` file describing any Redis commands that need to be run to set up data for your example (try and keep this minimal)
* Use semicolons
* Use `async` and `await`
* Use single quotes, `'hello'` not `"hello"`
* Place your example code in a single `async` function where possible, named according to the file name e.g. `add-to-stream.js` would contain `const addtoStream = async () => { ... };`, and call this function at the end of the file e.g. `addToStream();`
* Unless your example requires a connection string, assume Redis is on the default localhost port 6379 with no password
* Use meaningful example data, let's not use `foo`, `bar`, `baz` etc!
* Leave an empty line at the end of your `.js` file
* Update this `README.md` file to add your example to the table
Use [connect-as-acl-user.js](./connect-as-acl-user.js) as a guide to develop a well formatted example script.
### Example Template
Here's a starter template for adding a new example, imagine this is stored in `do-something.js`:
```javascript
// This comment should describe what the example does
// and can extend to multiple lines.
// Set up the data in redis-cli using these commands:
// <add your command(s) here, one per line>
import { createClient } from 'redis';
async function doSomething() {
const client = createClient();
await client.connect();
// Add your example code here...
await client.quit();
}
doSomething();
```

View File

@@ -0,0 +1,32 @@
// This example shows how to use the blocking LPUSH command.
// This code shows how to run with isolation the blPop Command to block the script while waiting for a value to be pushed to the list.
// The script will be blocked until the LPUSH command is executed.
// After which we log the list and quit the client.
import { createClient, commandOptions } from 'redis';
async function blockingListPop() {
const client = createClient();
await client.connect();
const keyName = 'keyName';
const blpopPromise = client.blPop(
commandOptions({ isolated: true }),
keyName,
0
);
await client.lPush(keyName, 'value');
await blpopPromise;
console.log('blpopPromise resolved');
console.log(keyName);
await client.quit();
}
blockingListPop();

View File

@@ -0,0 +1,31 @@
// Define a custom script that shows example of SET command
// with several modifiers.
import { createClient } from 'redis';
async function commandWithModifiers() {
const client = createClient();
await client.connect();
await client.del('mykey');
let result = await client.set('mykey', 'myvalue', {
EX: 60,
GET: true
});
console.log(result); //nil
result = await client.set('mykey', 'newvalue', {
EX: 60,
GET: true
}
);
console.log(result); //myvalue
await client.quit();
}
commandWithModifiers();

View File

@@ -0,0 +1,30 @@
// Connect to Redis 6.x as an ACL user. Attempt to run a command
// that the user is allowed to execute, and a command that the
// user is not allowed to execute.
// Create the test user in redis-cli with this command:
// acl setuser testuser on >testpassword +ping
import { createClient } from 'redis';
async function connectWithACLUser() {
const client = createClient({
url: 'redis://testuser:testpassword@127.0.0.1:6379'
});
await client.connect();
// Returns PONG
console.log(`Response from PING command: ${await client.ping()}`);
try {
// This will error as this user is not allowed to run this command...
console.log(`Response from GET command: ${await client.get('somekey')}`);
} catch (e) {
console.log(`GET command failed: ${e.message}`);
}
await client.quit();
}
connectWithACLUser();

View File

@@ -0,0 +1,31 @@
// Define a custome lua script that accepts two keys and an amount to
// increment each of them by
import { createClient, defineScript } from 'redis';
async function luaMultiIncr() {
const client = createClient({
scripts: {
mincr: defineScript({
NUMBER_OF_KEYS: 2,
SCRIPT:
'return {' +
'redis.pcall("INCRBY", KEYS[1], ARGV[1]),' +
'redis.pcall("INCRBY", KEYS[2], ARGV[1])' +
'}',
transformArguments(key1, key2, increment) {
return [key1, key2, increment.toString()];
},
}),
},
});
await client.connect();
await client.set('mykey', '5');
console.log(await client.mincr('mykey', 'myotherkey', 10)); // [ 15, 10 ]
await client.quit();
}
luaMultiIncr();

12
examples/package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "node-redis-examples",
"version": "1.0.0",
"description": "node-redis 4 example script",
"main": "index.js",
"private": true,
"type": "module",
"dependencies": {
"redis": "../"
}
}

74
examples/search+json.js Normal file
View File

@@ -0,0 +1,74 @@
// 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();

19
examples/set-scan.js Normal file
View File

@@ -0,0 +1,19 @@
// An example script that shows how to use the SSCAN iterator functionality to retrieve the contents of a Redis set.
// Create the set in redis-cli with this command:
// sadd setName a b c d e f g h i j k l m n o p q
import { createClient } from 'redis';
async function setScan() {
const client = createClient();
await client.connect();
const setName = 'setName';
for await (const member of client.sScanIterator(setName)) {
console.log(member);
}
await client.quit();
}
setScan();

View File

@@ -1,8 +1,33 @@
import RedisClient from './lib/client';
import RedisCluster from './lib/cluster';
import { createClient as _createClient, createCluster as _createCluster } from '@node-redis/client';
import { RedisScripts } from '@node-redis/client/dist/lib/commands';
import { RedisClientOptions, RedisClientType } from '@node-redis/client/dist/lib/client';
import { RedisClusterOptions, RedisClusterType } from '@node-redis/client/dist/lib/cluster';
import RedisJSON from '@node-redis/json';
import RediSearch from '@node-redis/search';
export const createClient = RedisClient.create;
export * from '@node-redis/client';
export * from '@node-redis/json';
export * from '@node-redis/search';
export const commandOptions = RedisClient.commandOptions;
const modules = {
json: RedisJSON,
ft: RediSearch
};
export const createCluster = RedisCluster.create;
export function createClient<S extends RedisScripts = Record<string, never>>(
options?: Omit<RedisClientOptions<never, S>, 'modules'>
): RedisClientType<typeof modules, S> {
return _createClient({
...options,
modules
});
}
export function createCluster<S extends RedisScripts = Record<string, never>>(
options: Omit<RedisClusterOptions<never, S>, 'modules'>
): RedisClusterType<typeof modules, S> {
return _createCluster({
...options,
modules
});
}

View File

@@ -1,114 +0,0 @@
import { strict as assert } from 'assert';
import RedisCluster from '.';
import { defineScript } from '../lua-script';
import { itWithCluster, itWithDedicatedCluster, TestRedisClusters, TEST_REDIS_CLUSTERES } from '../test-utils';
import calculateSlot from 'cluster-key-slot';
import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
describe('Cluster', () => {
it('sendCommand', async () => {
const cluster = RedisCluster.create({
...TEST_REDIS_CLUSTERES[TestRedisClusters.OPEN],
useReplicas: true
});
await cluster.connect();
try {
await cluster.publish('channel', 'message');
await cluster.set('a', 'b');
await cluster.set('a{a}', 'bb');
await cluster.set('aa', 'bb');
await cluster.get('aa');
await cluster.get('aa');
await cluster.get('aa');
await cluster.get('aa');
} finally {
await cluster.disconnect();
}
});
itWithCluster(TestRedisClusters.OPEN, 'multi', async cluster => {
const key = 'key';
assert.deepEqual(
await cluster.multi()
.set(key, 'value')
.get(key)
.exec(),
['OK', 'value']
);
});
it('scripts', async () => {
const cluster = RedisCluster.create({
...TEST_REDIS_CLUSTERES[TestRedisClusters.OPEN],
scripts: {
add: defineScript({
NUMBER_OF_KEYS: 0,
SCRIPT: 'return ARGV[1] + 1;',
transformArguments(number: number): Array<string> {
assert.equal(number, 1);
return [number.toString()];
},
transformReply(reply: number): number {
assert.equal(reply, 2);
return reply;
}
})
}
});
await cluster.connect();
try {
assert.equal(
await cluster.add(1),
2
);
} finally {
await cluster.disconnect();
}
});
itWithDedicatedCluster('should handle live resharding', async cluster => {
const key = 'key',
value = 'value';
await cluster.set(key, value);
const slot = calculateSlot(key),
from = cluster.getSlotMaster(slot),
to = cluster.getMasters().find(node => node.id !== from.id);
await to!.client.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, from.id);
// should be able to get the key from the original node before it was migrated
assert.equal(
await cluster.get(key),
value
);
await from.client.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, to!.id);
// should be able to get the key from the original node using the "ASKING" command
assert.equal(
await cluster.get(key),
value
);
const { port: toPort } = <any>to!.client.options!.socket;
await from.client.migrate(
'127.0.0.1',
toPort,
key,
0,
10
);
// should be able to get the key from the new node
assert.equal(
await cluster.get(key),
value
);
});
});

View File

@@ -1,27 +0,0 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion, itWithClient, TestRedisServers } from '../test-utils';
import { transformArguments } from './ACL_GETUSER';
describe('ACL GETUSER', () => {
describeHandleMinimumRedisVersion([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('username'),
['ACL', 'GETUSER', 'username']
);
});
itWithClient(TestRedisServers.OPEN, 'client.aclGetUser', async client => {
assert.deepEqual(
await client.aclGetUser('default'),
{
flags: ['on', 'allkeys', 'allchannels', 'allcommands', 'nopass'],
passwords: [],
commands: '+@all',
keys: ['*'],
channels: ['*']
}
);
});
});

View File

@@ -1,30 +0,0 @@
import { strict as assert } from 'assert';
import { itWithClient, TestRedisServers } from '../test-utils';
import { transformArguments } from './COMMAND';
import { CommandCategories, CommandFlags } from './generic-transformers';
describe('COMMAND', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['COMMAND']
);
});
itWithClient(TestRedisServers.OPEN, 'client.command', async client => {
assert.deepEqual(
(await client.command()).find(command => command.name === 'ping'),
{
name: 'ping',
arity: -1,
flags: new Set([CommandFlags.STALE, CommandFlags.FAST]),
firstKeyIndex: 0,
lastKeyIndex: 0,
step: 0,
categories: new Set([CommandCategories.FAST, CommandCategories.CONNECTION])
}
);
}, {
minimumRedisVersion: [6]
});
});

View File

@@ -1,30 +0,0 @@
import { strict as assert } from 'assert';
import { itWithClient, TestRedisServers } from '../test-utils';
import { transformArguments } from './COMMAND_INFO';
import { CommandCategories, CommandFlags } from './generic-transformers';
describe('COMMAND INFO', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(['PING']),
['COMMAND', 'INFO', 'PING']
);
});
itWithClient(TestRedisServers.OPEN, 'client.commandInfo', async client => {
assert.deepEqual(
await client.commandInfo(['PING']),
[{
name: 'ping',
arity: -1,
flags: new Set([CommandFlags.STALE, CommandFlags.FAST]),
firstKeyIndex: 0,
lastKeyIndex: 0,
step: 0,
categories: new Set([CommandCategories.FAST, CommandCategories.CONNECTION])
}]
);
}, {
minimumRedisVersion: [6]
});
});

View File

@@ -1,7 +0,0 @@
import { transformReplyTuples } from './generic-transformers';
export function transformArguments(parameter: string): Array<string> {
return ['CONFIG', 'GET', parameter];
}
export const transformReply = transformReplyTuples;

View File

@@ -1,16 +0,0 @@
import { transformArgumentNumberInfinity } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(key: string, min: number, max: number): Array<string> {
return [
'ZCOUNT',
key,
transformArgumentNumberInfinity(min),
transformArgumentNumberInfinity(max)
];
}
export declare function transformReply(): number;

View File

@@ -1,377 +0,0 @@
import { strict as assert } from 'assert';
import RedisClient, { RedisClientOptions, RedisClientType } from './client';
import { execSync, spawn } from 'child_process';
import { once } from 'events';
import which from 'which';
import { SinonSpy } from 'sinon';
import RedisCluster, { RedisClusterOptions, RedisClusterType } from './cluster';
import { promises as fs } from 'fs';
import { Context as MochaContext } from 'mocha';
import { promiseTimeout } from './utils';
import { RedisModules, RedisScripts } from './commands';
type RedisVersion = [major: number, minor: number, patch: number];
type PartialRedisVersion = RedisVersion | [major: number, minor: number] | [major: number];
const REDIS_PATH = which.sync('redis-server');
export const REDIS_VERSION = getRedisVersion();
function getRedisVersion(): RedisVersion {
const raw = execSync(`${REDIS_PATH} -v`).toString(),
indexOfVersion = raw.indexOf('v=');
if (indexOfVersion === -1) {
throw new Error('Unknown redis version');
}
const start = indexOfVersion + 2;
return raw.substring(
start,
raw.indexOf(' ', start)
).split('.', 3).map(Number) as RedisVersion;
}
export function isRedisVersionGreaterThan(minimumVersion: PartialRedisVersion | undefined): boolean {
if (minimumVersion === undefined) return true;
const lastIndex = minimumVersion.length - 1;
for (let i = 0; i < lastIndex; i++) {
if (REDIS_VERSION[i] > minimumVersion[i]) {
return true;
} else if (minimumVersion[i] > REDIS_VERSION[i]) {
return false;
}
}
return REDIS_VERSION[lastIndex] >= minimumVersion[lastIndex];
}
export enum TestRedisServers {
OPEN,
PASSWORD
}
export const TEST_REDIS_SERVERS: Record<TestRedisServers, RedisClientOptions<RedisModules, RedisScripts>> = <any>{};
export enum TestRedisClusters {
OPEN
}
export const TEST_REDIS_CLUSTERES: Record<TestRedisClusters, RedisClusterOptions<RedisModules, RedisScripts>> = <any>{};
let port = 6379;
interface SpawnRedisServerResult {
port: number;
cleanup: () => Promise<void>;
}
async function spawnRedisServer(args?: Array<string>): Promise<SpawnRedisServerResult> {
const currentPort = port++,
process = spawn(REDIS_PATH, [
'--save',
'',
'--port',
currentPort.toString(),
...(args ?? [])
]);
process
.once('error', err => console.error('Redis process error', err))
.once('close', code => console.error(`Redis process closed unexpectedly with code ${code}`));
for await (const chunk of process.stdout) {
if (chunk.toString().includes('Ready to accept connections')) {
break;
}
}
if (process.exitCode !== null) {
throw new Error('Error while spawning redis server');
}
return {
port: currentPort,
async cleanup(): Promise<void> {
process.removeAllListeners('close');
assert.ok(process.kill());
await once(process, 'close');
}
};
}
async function spawnGlobalRedisServer(args?: Array<string>): Promise<number> {
const { port, cleanup } = await spawnRedisServer(args);
after(cleanup);
return port;
}
const SLOTS = 16384;
interface SpawnRedisClusterNodeResult extends SpawnRedisServerResult {
client: RedisClientType
}
async function spawnRedisClusterNode(
type: TestRedisClusters | null,
nodeIndex: number,
fromSlot: number,
toSlot: number,
args?: Array<string>
): Promise<SpawnRedisClusterNodeResult> {
const clusterConfigFile = `/tmp/${type}-${nodeIndex}.conf`,
{ port, cleanup: originalCleanup } = await spawnRedisServer([
'--cluster-enabled',
'yes',
'--cluster-node-timeout',
'5000',
'--cluster-config-file',
clusterConfigFile,
...(args ?? [])
]);
const client = RedisClient.create({
socket: {
port
}
});
await client.connect();
const range = [];
for (let i = fromSlot; i < toSlot; i++) {
range.push(i);
}
await Promise.all([
client.clusterFlushSlots(),
client.clusterAddSlots(range)
]);
return {
port,
async cleanup(): Promise<void> {
await originalCleanup();
try {
await fs.unlink(clusterConfigFile);
} catch (err: any) {
if (err.code === 'ENOENT') return;
throw err;
}
},
client
};
}
export async function spawnRedisCluster(type: TestRedisClusters | null, numberOfNodes: number, args?: Array<string>): Promise<Array<SpawnRedisServerResult>> {
const spawnPromises = [],
slotsPerNode = Math.floor(SLOTS / numberOfNodes);
for (let i = 0; i < numberOfNodes; i++) {
const fromSlot = i * slotsPerNode;
spawnPromises.push(
spawnRedisClusterNode(
type,
i,
fromSlot,
i === numberOfNodes - 1 ? SLOTS : fromSlot + slotsPerNode,
args
)
);
}
const spawnResults = await Promise.all(spawnPromises),
meetPromises = [];
for (let i = 1; i < spawnResults.length; i++) {
meetPromises.push(
spawnResults[i].client.clusterMeet(
'127.0.0.1',
spawnResults[i - 1].port
)
);
}
await Promise.all(meetPromises);
while (!(await clusterIsReady(spawnResults))) {
await promiseTimeout(100);
}
await Promise.all(
spawnResults.map(result => result.client.disconnect())
);
return spawnResults;
}
async function clusterIsReady(spawnResults: Array<SpawnRedisClusterNodeResult>): Promise<boolean> {
const nodesClusetrInfo = await Promise.all(
spawnResults.map(result => result.client.clusterInfo())
);
return nodesClusetrInfo.every(({ state }) => state === 'ok');
}
export async function spawnGlobalRedisCluster(type: TestRedisClusters | null, numberOfNodes: number, args?: Array<string>): Promise<Array<number>> {
const results = await spawnRedisCluster(type, numberOfNodes, args);
after(() => Promise.all(
results.map(({ cleanup }) => cleanup())
));
return results.map(({ port }) => port);
}
async function spawnOpenServer(): Promise<void> {
TEST_REDIS_SERVERS[TestRedisServers.OPEN] = {
socket: {
port: await spawnGlobalRedisServer()
}
};
}
async function spawnPasswordServer(): Promise<void> {
TEST_REDIS_SERVERS[TestRedisServers.PASSWORD] = {
socket: {
port: await spawnGlobalRedisServer(['--requirepass', 'password']),
},
password: 'password'
};
if (isRedisVersionGreaterThan([6])) {
TEST_REDIS_SERVERS[TestRedisServers.PASSWORD].username = 'default';
}
}
async function spawnOpenCluster(): Promise<void> {
TEST_REDIS_CLUSTERES[TestRedisClusters.OPEN] = {
rootNodes: (await spawnGlobalRedisCluster(TestRedisClusters.OPEN, 3)).map(port => ({
socket: {
port
}
}))
};
}
before(function () {
this.timeout(10000);
return Promise.all([
spawnOpenServer(),
spawnPasswordServer(),
spawnOpenCluster()
]);
});
interface RedisTestOptions {
minimumRedisVersion?: PartialRedisVersion;
}
export function handleMinimumRedisVersion(mochaContext: MochaContext, minimumVersion: PartialRedisVersion | undefined): boolean {
if (isRedisVersionGreaterThan(minimumVersion)) {
return false;
}
mochaContext.skip();
return true;
}
export function describeHandleMinimumRedisVersion(minimumVersion: PartialRedisVersion): void {
before(function () {
handleMinimumRedisVersion(this, minimumVersion);
});
}
export function itWithClient(
type: TestRedisServers,
title: string,
fn: (client: RedisClientType) => Promise<void>,
options?: RedisTestOptions
): void {
it(title, async function () {
if (handleMinimumRedisVersion(this, options?.minimumRedisVersion)) return;
const client = RedisClient.create(TEST_REDIS_SERVERS[type]);
await client.connect();
try {
await client.flushAll();
await fn(client);
} finally {
await client.flushAll();
await client.disconnect();
}
});
}
export function itWithCluster(
type: TestRedisClusters,
title: string,
fn: (cluster: RedisClusterType) => Promise<void>,
options?: RedisTestOptions
): void {
it(title, async function () {
if (handleMinimumRedisVersion(this, options?.minimumRedisVersion)) return;
const cluster = RedisCluster.create(TEST_REDIS_CLUSTERES[type]);
await cluster.connect();
try {
await clusterFlushAll(cluster);
await fn(cluster);
} finally {
await clusterFlushAll(cluster);
await cluster.disconnect();
}
});
}
export function itWithDedicatedCluster(title: string, fn: (cluster: RedisClusterType) => Promise<void>): void {
it(title, async function () {
this.timeout(10000);
const spawnResults = await spawnRedisCluster(null, 3),
cluster = RedisCluster.create({
rootNodes: [{
socket: {
port: spawnResults[0].port
}
}]
});
await cluster.connect();
try {
await fn(cluster);
} finally {
await cluster.disconnect();
for (const { cleanup } of spawnResults) {
await cleanup();
}
}
});
}
async function clusterFlushAll(cluster: RedisCluster): Promise<void> {
await Promise.all(
cluster.getMasters().map(({ client }) => client.flushAll())
);
}
export async function waitTillBeenCalled(spy: SinonSpy): Promise<void> {
const start = process.hrtime.bigint(),
calls = spy.callCount;
do {
if (process.hrtime.bigint() - start > 1_000_000_000) {
throw new Error('Waiting for more than 1 second');
}
await promiseTimeout(1);
} while (spy.callCount === calls)
}

View File

@@ -1,15 +0,0 @@
declare module 'redis-parser' {
interface RedisParserCallbacks {
returnReply(reply: unknown): void;
returnError(err: Error): void;
returnFatalError?(err: Error): void;
}
export default class RedisParser {
constructor(callbacks: RedisParserCallbacks);
setReturnBuffers(returnBuffers?: boolean): void;
execute(buffer: Buffer): void;
}
}

3399
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +1,37 @@
{
"name": "redis",
"version": "4.0.0-rc.3",
"description": "A high performance Redis client.",
"keywords": [
"database",
"redis",
"pubsub"
],
"author": "Matt Ranney <mjr@ranney.com>",
"contributors": [
{
"name": "Mike Diarmid (Salakar)",
"url": "https://github.com/salakar"
},
{
"name": "Ruben Bridgewater (BridgeAR)",
"url": "https://github.com/BridgeAR"
}
],
"version": "4.0.0-rc.4",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"workspaces": [
"./packages/*"
],
"scripts": {
"test": "nyc -r text-summary -r html mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
"test": "npm run test -ws --if-present",
"build:client": "npm run build -w ./packages/client",
"build:test-utils": "npm run build -w ./packages/test-utils",
"build:tests-tools": "npm run build:client && npm run build:test-utils",
"build:modules": "find ./packages -mindepth 1 -maxdepth 1 -type d ! -name 'client' ! -name 'test-utils' -exec npm run build -w {} \\;",
"build": "tsc",
"documentation": "typedoc"
"build-all": "npm run build:client && npm run build:test-utils && npm run build:modules && npm run build"
},
"dependencies": {
"cluster-key-slot": "1.1.0",
"generic-pool": "3.8.2",
"redis-parser": "3.0.0",
"yallist": "4.0.0"
"@node-redis/client": "^1.0.0-rc.0",
"@node-redis/json": "^1.0.0-rc.0",
"@node-redis/search": "^1.0.0-rc.0"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@tsconfig/node12": "^1.0.9",
"@types/mocha": "^9.0.0",
"@types/node": "^16.10.3",
"@types/sinon": "^10.0.4",
"@types/which": "^2.0.1",
"@types/yallist": "^4.0.1",
"mocha": "^9.1.2",
"nyc": "^15.1.0",
"release-it": "^14.11.6",
"sinon": "^11.1.2",
"source-map-support": "^0.5.20",
"ts-node": "^10.3.0",
"typedoc": "^0.22.5",
"typedoc-github-wiki-theme": "^0.6.0",
"typedoc-plugin-markdown": "^3.11.3",
"typescript": "^4.4.3",
"which": "^2.0.2"
},
"engines": {
"node": ">=12"
"release-it": "^14.11.7",
"typescript": "^4.4.4"
},
"repository": {
"type": "git",
"url": "git://github.com/NodeRedis/node-redis.git"
"url": "git://github.com/redis/node-redis.git"
},
"bugs": {
"url": "https://github.com/NodeRedis/node-redis/issues"
"url": "https://github.com/redis/node-redis/issues"
},
"homepage": "https://github.com/NodeRedis/node-redis"
"homepage": "https://github.com/redis/node-redis"
}

View File

@@ -0,0 +1,15 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"semi": [2, "always"]
}
}

1
packages/client/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
documentation/

View File

@@ -0,0 +1,10 @@
.nyc_output/
coverage/
documentation/
lib/
.eslintrc.json
.nycrc.json
.release-it.json
dump.rdb
index.ts
tsconfig.json

View File

@@ -0,0 +1,4 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"exclude": ["**/*.spec.ts", "lib/test-utils.ts", "examples/*"]
}

View File

@@ -0,0 +1,10 @@
{
"git": {
"tagName": "client@${version}",
"commitMessage": "Release ${tagName}",
"tagAnnotation": "Release ${tagName}"
},
"npm": {
"publishArgs": ["--access", "public"]
}
}

View File

@@ -2,7 +2,7 @@
## v4.0.0
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.
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.
### Breaking Changes
@@ -17,12 +17,35 @@ 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](./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 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 the ability to extend Node Redis with Redis Module commands
## v3.1.2
### Fixes
- Exclude unnecessary files from tarball
## v3.1.1
### Enhancements
- Upgrade node and dependencies
### Fixes
- Fix a potential exponential regex in monitor mode
## v3.1.0 - 31 Mar, 2021
### Enhancements
- Upgrade node and dependencies and redis-commands to support Redis 6
- Add support for Redis 6 `auth pass [user]`
## v3.0.0 - 09 Feb, 2020
This version is mainly a release to distribute all the unreleased changes on master since 2017 and additionally removes

View File

@@ -0,0 +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.

10
packages/client/index.ts Normal file
View File

@@ -0,0 +1,10 @@
import RedisClient from './lib/client';
import RedisCluster from './lib/cluster';
export const createClient = RedisClient.create;
export const commandOptions = RedisClient.commandOptions;
export const createCluster = RedisCluster.create;
export { defineScript } from './lib/lua-script';

View File

@@ -1,25 +1,29 @@
import LinkedList from 'yallist';
import RedisParser from 'redis-parser';
import * as LinkedList from 'yallist';
import { AbortError } from '../errors';
import { RedisCommandRawReply } from '../commands';
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?: any; // TODO: `AbortSignal` type is incorrect
signal?: AbortSignal;
}
interface CommandWaitingToBeSent extends CommandWaitingForReply {
args: Array<string | Buffer>;
args: RedisCommandArguments;
chainId?: symbol;
abort?: {
signal: any; // TODO: `AbortSignal` type is incorrect
signal: AbortSignal;
listener(): void;
};
}
interface CommandWaitingForReply {
resolve(reply?: any): void;
resolve(reply?: unknown): void;
reject(err: Error): void;
channelsCounter?: number;
bufferMode?: boolean;
@@ -107,7 +111,7 @@ export default class RedisCommandsQueue {
this.#maxLength = maxLength;
}
addCommand<T = RedisCommandRawReply>(args: Array<string | Buffer>, options?: QueueCommandOptions, bufferMode?: boolean): Promise<T> {
addCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: QueueCommandOptions, bufferMode?: boolean): Promise<T> {
if (this.#pubSubState.subscribing || this.#pubSubState.subscribed) {
return Promise.reject(new Error('Cannot send commands in PubSub mode'));
} else if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
@@ -135,7 +139,8 @@ export default class RedisCommandsQueue {
signal: options.signal,
listener
};
options.signal.addEventListener('abort', listener, {
// AbortSignal type is incorrent
(options.signal as any).addEventListener('abort', listener, {
once: true
});
}
@@ -246,7 +251,7 @@ export default class RedisCommandsQueue {
]);
}
getCommandToSend(): Array<string | Buffer> | undefined {
getCommandToSend(): RedisCommandArguments | undefined {
const toSend = this.#waitingToBeSent.shift();
if (toSend) {

View File

@@ -229,5 +229,5 @@ export default {
UNWATCH,
unwatch: UNWATCH,
WAIT,
wait: WAIT,
wait: WAIT
};

View File

@@ -1,11 +1,12 @@
import { strict as assert, AssertionError } from 'assert';
import { once } from 'events';
import { itWithClient, TEST_REDIS_SERVERS, TestRedisServers, waitTillBeenCalled, isRedisVersionGreaterThan } from '../test-utils';
import RedisClient from '.';
import { AbortError, ClientClosedError, ConnectionTimeoutError, WatchError } from '../errors';
import { strict as assert } from 'assert';
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
import RedisClient, { ClientLegacyCommandArguments, RedisClientType } from '.';
import { RedisClientMultiCommandType } from './multi-command';
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands';
import { AbortError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
import { defineScript } from '../lua-script';
import { spy } from 'sinon';
import { RedisNetSocketOptions } from '../client/socket';
import { once } from 'events';
export const SQUARE_SCRIPT = defineScript({
NUMBER_OF_KEYS: 0,
@@ -75,204 +76,242 @@ describe('Client', () => {
}
);
});
it('createClient with url', async () => {
const client = RedisClient.create({
url: `redis://localhost:${(TEST_REDIS_SERVERS[TestRedisServers.OPEN].socket as RedisNetSocketOptions)!.port!.toString()}/1`
});
await client.connect();
try {
assert.equal(
await client.ping(),
'PONG'
);
} finally {
await client.disconnect();
}
})
});
describe('authentication', () => {
itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => {
testUtils.testWithClient('Client should be authenticated', async client => {
assert.equal(
await client.ping(),
'PONG'
);
});
}, GLOBAL.SERVERS.PASSWORD);
it('should not retry connecting if failed due to wrong auth', async () => {
const client = RedisClient.create({
...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD],
password: 'wrongpassword'
});
testUtils.testWithClient('should not retry connecting if failed due to wrong auth', async client => {
let message;
if (testUtils.isVersionGreaterThan([6, 2])) {
message = 'WRONGPASS invalid username-password pair or user is disabled.';
} else if (testUtils.isVersionGreaterThan([6])) {
message = 'WRONGPASS invalid username-password pair';
} else {
message = 'ERR invalid password';
}
await assert.rejects(
client.connect(),
{
message: isRedisVersionGreaterThan([6]) ?
'WRONGPASS invalid username-password pair or user is disabled.' :
'ERR invalid password'
}
{ message }
);
assert.equal(client.isOpen, false);
}, {
...GLOBAL.SERVERS.PASSWORD,
clientOptions: {
password: 'wrongpassword'
},
disableClientSetup: true
});
testUtils.testWithClient('should execute AUTH before SELECT', async client => {
assert.equal(
(await client.clientInfo()).db,
2
);
}, {
...GLOBAL.SERVERS.PASSWORD,
clientOptions: {
...GLOBAL.SERVERS.PASSWORD.clientOptions,
database: 2
},
minimumDockerVersion: [6, 2]
});
});
describe('legacyMode', () => {
const client = RedisClient.create({
...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
scripts: {
square: SQUARE_SCRIPT
},
legacyMode: true
});
function sendCommandAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, args: RedisCommandArguments): Promise<RedisCommandRawReply> {
return new Promise((resolve, reject) => {
(client as any).sendCommand(args, (err: Error | undefined, reply: RedisCommandRawReply) => {
if (err) return reject(err);
before(() => client.connect());
afterEach(() => client.v4.flushAll());
after(() => client.disconnect());
it('client.sendCommand should call the callback', done => {
(client as any).sendCommand('PING', (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, 'PONG');
done();
} catch (err) {
done(err);
}
resolve(reply);
});
});
}
testUtils.testWithClient('client.sendCommand should call the callback', async client => {
assert.equal(
await sendCommandAsync(client, ['PING']),
'PONG'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
it('client.sendCommand should work without callback', async () => {
(client as any).sendCommand('PING');
testUtils.testWithClient('client.sendCommand should work without callback', async client => {
client.sendCommand(['PING']);
await client.v4.ping(); // make sure the first command was replied
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
it('client.v4.sendCommand should return a promise', async () => {
testUtils.testWithClient('client.v4.sendCommand should return a promise', async client => {
assert.equal(
await client.v4.sendCommand(['PING']),
'PONG'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
it('client.{command} should accept vardict arguments', done => {
(client as any).set('a', 'b', (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: ClientLegacyCommandArguments): Promise<RedisCommandRawReply> {
return new Promise((resolve, reject) => {
(client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
if (err) return reject(err);
try {
assert.equal(reply, 'OK');
done();
} catch (err) {
done(err);
}
});
});
it('client.{command} should accept arguments array', done => {
(client as any).set(['a', 'b'], (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, 'OK');
done();
} catch (err) {
done(err);
}
});
});
it('client.{command} should accept mix of strings and array of strings', done => {
(client as any).set(['a'], 'b', ['XX'], (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, null);
done();
} catch (err) {
done(err);
}
});
});
it('client.multi.ping.exec should call the callback', done => {
(client as any).multi()
.ping()
.exec((err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.deepEqual(reply, ['PONG']);
done();
} catch (err) {
done(err);
}
resolve(reply);
});
});
}
testUtils.testWithClient('client.{command} should accept vardict arguments', async client => {
assert.equal(
await setAsync(client, 'a', 'b'),
'OK'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
it('client.multi.ping.exec should work without callback', async () => {
(client as any).multi()
testUtils.testWithClient('client.{command} should accept arguments array', async client => {
assert.equal(
await setAsync(client, ['a', 'b']),
'OK'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
testUtils.testWithClient('client.{command} should accept mix of arrays and arguments', async client => {
assert.equal(
await setAsync(client, ['a'], 'b', ['EX', 1]),
'OK'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
function multiExecAsync<M extends RedisModules, S extends RedisScripts>(multi: RedisClientMultiCommandType<M, S>): Promise<Array<RedisCommandRawReply>> {
return new Promise((resolve, reject) => {
(multi as any).exec((err: Error | undefined, replies: Array<RedisCommandRawReply>) => {
if (err) return reject(err);
resolve(replies);
});
});
}
testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => {
assert.deepEqual(
await multiExecAsync(
client.multi().ping()
),
['PONG']
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => {
client.multi()
.ping()
.exec();
await client.v4.ping(); // make sure the first command was replied
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
it('client.multi.ping.v4.ping.v4.exec should return a promise', async () => {
testUtils.testWithClient('client.multi.ping.v4.ping.v4.exec should return a promise', async client => {
assert.deepEqual(
await ((client as any).multi()
await client.multi()
.ping()
.v4.ping()
.v4.exec()),
.v4.exec(),
['PONG', 'PONG']
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
});
it('client.{script} should return a promise', async () => {
assert.equal(await client.square(2), 4);
testUtils.testWithClient('client.{script} should return a promise', async client => {
assert.equal(
await client.square(2),
4
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true,
scripts: {
square: SQUARE_SCRIPT
}
}
});
});
describe('events', () => {
it('connect, ready, end', async () => {
const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
testUtils.testWithClient('connect, ready, end', async client => {
await Promise.all([
client.connect(),
once(client, 'connect'),
once(client, 'ready')
once(client, 'ready'),
client.connect()
]);
await Promise.all([
client.disconnect(),
once(client, 'end')
once(client, 'end'),
client.disconnect()
]);
}, {
...GLOBAL.SERVERS.OPEN,
disableClientSetup: true
});
});
describe('sendCommand', () => {
itWithClient(TestRedisServers.OPEN, 'PING', async client => {
testUtils.testWithClient('PING', async client => {
assert.equal(await client.sendCommand(['PING']), 'PONG');
});
}, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'bufferMode', async client => {
testUtils.testWithClient('bufferMode', async client => {
assert.deepEqual(
await client.sendCommand(['PING'], undefined, true),
Buffer.from('PONG')
);
});
}, GLOBAL.SERVERS.OPEN);
describe('AbortController', () => {
before(function () {
@@ -281,13 +320,13 @@ describe('Client', () => {
}
});
itWithClient(TestRedisServers.OPEN, 'success', async client => {
testUtils.testWithClient('success', async client => {
await client.sendCommand(['PING'], {
signal: new AbortController().signal
});
});
}, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'AbortError', client => {
testUtils.testWithClient('AbortError', client => {
const controller = new AbortController();
controller.abort();
@@ -297,12 +336,12 @@ describe('Client', () => {
}),
AbortError
);
});
}, GLOBAL.SERVERS.OPEN);
});
});
describe('multi', () => {
itWithClient(TestRedisServers.OPEN, 'simple', async client => {
testUtils.testWithClient('simple', async client => {
assert.deepEqual(
await client.multi()
.ping()
@@ -311,44 +350,35 @@ describe('Client', () => {
.exec(),
['PONG', 'OK', 'value']
);
});
itWithClient(TestRedisServers.OPEN, 'should reject the whole chain on error', client => {
client.on('error', () => {
// ignore errors
});
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('should reject the whole chain on error', client => {
return assert.rejects(
client.multi()
.ping()
.addCommand(['DEBUG', 'RESTART'])
.addCommand(['INVALID COMMAND'])
.ping()
.exec()
);
});
}, GLOBAL.SERVERS.OPEN);
it('with script', async () => {
const client = RedisClient.create({
testUtils.testWithClient('with script', async client => {
assert.deepEqual(
await client.multi()
.square(2)
.exec(),
[4]
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
scripts: {
square: SQUARE_SCRIPT
}
});
await client.connect();
try {
assert.deepEqual(
await client.multi()
.square(2)
.exec(),
[4]
);
} finally {
await client.disconnect();
}
});
itWithClient(TestRedisServers.OPEN, 'WatchError', async client => {
testUtils.testWithClient('WatchError', async client => {
await client.watch('key');
await client.set(
@@ -365,39 +395,40 @@ describe('Client', () => {
.exec(),
WatchError
);
});
}, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'execAsPipeline', async client => {
testUtils.testWithClient('execAsPipeline', async client => {
assert.deepEqual(
await client.multi()
.ping()
.exec(true),
['PONG']
);
});
}, GLOBAL.SERVERS.OPEN);
});
it('scripts', async () => {
const client = RedisClient.create({
testUtils.testWithClient('scripts', async client => {
assert.equal(
await client.square(2),
4
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
scripts: {
square: SQUARE_SCRIPT
}
});
await client.connect();
try {
assert.equal(
await client.square(2),
4
);
} finally {
await client.disconnect();
}
});
it('modules', async () => {
const client = RedisClient.create({
testUtils.testWithClient('modules', async client => {
assert.equal(
await client.module.echo('message'),
'message'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
modules: {
module: {
echo: {
@@ -410,21 +441,10 @@ describe('Client', () => {
}
}
}
});
await client.connect();
try {
assert.equal(
await client.module.echo('message'),
'message'
);
} finally {
await client.disconnect();
}
});
itWithClient(TestRedisServers.OPEN, 'executeIsolated', async client => {
testUtils.testWithClient('executeIsolated', async client => {
await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
assert.equal(
@@ -433,35 +453,35 @@ describe('Client', () => {
),
null
);
});
}, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'should reconnect after DEBUG RESTART', async client => {
client.on('error', () => {
// ignore errors
});
async function killClient<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>): Promise<void> {
const onceErrorPromise = once(client, 'error');
await client.sendCommand(['QUIT']);
await Promise.all([
onceErrorPromise,
assert.rejects(client.ping(), SocketClosedUnexpectedlyError)
]);
}
await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
await assert.rejects(client.sendCommand(['DEBUG', 'RESTART']));
assert.ok(await client.sendCommand(['CLIENT', 'GETNAME']) === null);
});
itWithClient(TestRedisServers.OPEN, 'should SELECT db after reconnection', async client => {
client.on('error', () => {
// ignore errors
});
testUtils.testWithClient('should reconnect when socket disconnects', async client => {
await killClient(client);
await assert.doesNotReject(client.ping());
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('should remember selected db', async client => {
await client.select(1);
await assert.rejects(client.sendCommand(['DEBUG', 'RESTART']));
await killClient(client);
assert.equal(
(await client.clientInfo()).db,
1
);
}, {
// because of CLIENT INFO
minimumRedisVersion: [6, 2]
...GLOBAL.SERVERS.OPEN,
minimumDockerVersion: [6, 2] // CLIENT INFO
});
itWithClient(TestRedisServers.OPEN, 'scanIterator', async client => {
testUtils.testWithClient('scanIterator', async client => {
const promises = [],
keys = new Set();
for (let i = 0; i < 100; i++) {
@@ -478,9 +498,9 @@ describe('Client', () => {
}
assert.deepEqual(keys, results);
});
}, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'hScanIterator', async client => {
testUtils.testWithClient('hScanIterator', async client => {
const hash: Record<string, string> = {};
for (let i = 0; i < 100; i++) {
hash[i.toString()] = i.toString();
@@ -494,9 +514,9 @@ describe('Client', () => {
}
assert.deepEqual(hash, results);
});
}, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'sScanIterator', async client => {
testUtils.testWithClient('sScanIterator', async client => {
const members = new Set<string>();
for (let i = 0; i < 100; i++) {
members.add(i.toString());
@@ -510,9 +530,9 @@ describe('Client', () => {
}
assert.deepEqual(members, results);
});
}, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'zScanIterator', async client => {
testUtils.testWithClient('zScanIterator', async client => {
const members = [];
for (let i = 0; i < 100; i++) {
members.push({
@@ -538,9 +558,9 @@ describe('Client', () => {
[...map.entries()].sort(sort),
members.map<MemberTuple>(member => [member.value, member.score]).sort(sort)
);
});
}, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'PubSub', async publisher => {
testUtils.testWithClient('PubSub', async publisher => {
const subscriber = publisher.duplicate();
await subscriber.connect();
@@ -603,49 +623,59 @@ describe('Client', () => {
} finally {
await subscriber.disconnect();
}
});
}, GLOBAL.SERVERS.OPEN);
it('ConnectionTimeoutError', async () => {
const client = RedisClient.create({
testUtils.testWithClient('ConnectionTimeoutError', async client => {
const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
start = process.hrtime.bigint();
while (process.hrtime.bigint() - start < 1_000_000) {
// block the event loop for 1ms, to make sure the connection will timeout
}
await promise;
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
socket: {
...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
connectTimeout: 1
}
});
try {
const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
start = process.hrtime.bigint();
// block the event loop for 1ms, to make sure the connection will timeout
while (process.hrtime.bigint() - start < 1_000_000) {}
await promise;
} catch (err) {
if (err instanceof AssertionError) {
await client.disconnect();
}
throw err;
}
},
disableClientSetup: true
});
it('client.quit', async () => {
const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
testUtils.testWithClient('client.quit', async client => {
await client.connect();
try {
const quitPromise = client.quit();
assert.equal(client.isOpen, false);
await Promise.all([
quitPromise,
assert.rejects(client.ping(), ClientClosedError)
]);
} finally {
if (client.isOpen) {
await client.disconnect();
}
}
const pingPromise = client.ping(),
quitPromise = client.quit();
assert.equal(client.isOpen, false);
const [ping] = await Promise.all([
pingPromise,
assert.doesNotReject(quitPromise),
assert.rejects(client.ping(), ClientClosedError)
]);
assert.equal(ping, 'PONG');
}, {
...GLOBAL.SERVERS.OPEN,
disableClientSetup: true
});
testUtils.testWithClient('client.disconnect', async client => {
await client.connect();
const pingPromise = client.ping(),
disconnectPromise = client.disconnect();
assert.equal(client.isOpen, false);
await Promise.all([
assert.rejects(pingPromise, DisconnectsClientError),
assert.doesNotReject(disconnectPromise),
assert.rejects(client.ping(), ClientClosedError)
]);
}, {
...GLOBAL.SERVERS.OPEN,
disableClientSetup: true
});
});

View File

@@ -4,14 +4,14 @@ import RedisSocket, { RedisSocketOptions, RedisNetSocketOptions, RedisTlsSocketO
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command';
import EventEmitter from 'events';
import { EventEmitter } from 'events';
import { CommandOptions, commandOptions, isCommandOptions } from '../command-options';
import { ScanOptions, ZMember } from '../commands/generic-transformers';
import { ScanCommandOptions } from '../commands/SCAN';
import { HScanTuple } from '../commands/HSCAN';
import { encodeCommand, extendWithCommands, extendWithModulesAndScripts, transformCommandArguments, transformCommandReply } from '../commander';
import { extendWithCommands, extendWithModulesAndScripts, LegacyCommandArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
import { ClientClosedError } from '../errors';
import { ClientClosedError, DisconnectsClientError } from '../errors';
import { URL } from 'url';
export interface RedisClientOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
@@ -34,16 +34,16 @@ type WithCommands = {
};
export type WithModules<M extends RedisModules> = {
[P in keyof M]: {
[P in keyof M as M[P] extends never ? never : P]: {
[C in keyof M[P]]: RedisClientCommandSignature<M[P][C]>;
};
};
export type WithScripts<S extends RedisScripts> = {
[P in keyof S]: RedisClientCommandSignature<S[P]>;
[P in keyof S as S[P] extends never ? never : P]: RedisClientCommandSignature<S[P]>;
};
export type RedisClientType<M extends RedisModules = {}, S extends RedisScripts = {}> =
export type RedisClientType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
RedisClient<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
export type InstantiableRedisClient<M extends RedisModules, S extends RedisScripts> =
@@ -53,12 +53,15 @@ export interface ClientCommandOptions extends QueueCommandOptions {
isolated?: boolean;
}
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
export type ClientLegacyCommandArguments = LegacyCommandArguments | [...LegacyCommandArguments, ClientLegacyCallback];
export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> {
return commandOptions(options);
}
static extend<M extends RedisModules = {}, S extends RedisScripts = {}>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
static extend<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
const Client = <any>extendWithModulesAndScripts({
BaseClass: RedisClient,
modules: plugins?.modules,
@@ -74,14 +77,14 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
return Client;
}
static create<M extends RedisModules = {}, S extends RedisScripts = {}>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
static create<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
return new (RedisClient.extend(options))(options);
}
static parseURL(url: string): RedisClientOptions<{}, {}> {
static parseURL(url: string): RedisClientOptions<Record<string, never>, Record<string, never>> {
// https://www.iana.org/assignments/uri-schemes/prov/redis
const { hostname, port, protocol, username, password, pathname } = new URL(url),
parsed: RedisClientOptions<{}, {}> = {
parsed: RedisClientOptions<Record<string, never>, Record<string, never>> = {
socket: {
host: hostname
}
@@ -98,11 +101,11 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
}
if (username) {
parsed.username = username;
parsed.username = decodeURIComponent(username);
}
if (password) {
parsed.password = password;
parsed.password = decodeURIComponent(password);
}
if (pathname.length > 1) {
@@ -177,28 +180,47 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
#initiateSocket(): RedisSocket {
const socketInitiator = async (): Promise<void> => {
const v4Commands = this.#options?.legacyMode ? this.#v4 : this,
promises = [];
const promises = [];
if (this.#selectedDB !== 0) {
promises.push(v4Commands.select(RedisClient.commandOptions({ asap: true }), this.#selectedDB));
promises.push(
this.#queue.addCommand(
['SELECT', this.#selectedDB.toString()],
{ asap: true }
)
);
}
if (this.#options?.readonly) {
promises.push(v4Commands.readonly(RedisClient.commandOptions({ asap: true })));
promises.push(
this.#queue.addCommand(
COMMANDS.READONLY.transformArguments(),
{ asap: true }
)
);
}
if (this.#options?.username || this.#options?.password) {
promises.push(v4Commands.auth(RedisClient.commandOptions({ asap: true }), this.#options));
promises.push(
this.#queue.addCommand(
COMMANDS.AUTH.transformArguments({
username: this.#options.username,
password: this.#options.password ?? ''
}),
{ asap: true }
)
);
}
const resubscribePromise = this.#queue.resubscribe();
if (resubscribePromise) {
promises.push(resubscribePromise);
this.#tick();
}
await Promise.all(promises);
if (promises.length) {
this.#tick(true);
await Promise.all(promises);
}
};
return new RedisSocket(socketInitiator, this.#options?.socket)
@@ -213,6 +235,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
this.#tick();
})
.on('reconnecting', () => this.emit('reconnecting'))
.on('drain', () => this.#tick())
.on('end', () => this.emit('end'));
}
@@ -224,11 +247,14 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
if (!this.#options?.legacyMode) return;
(this as any).#v4.sendCommand = this.#sendCommand.bind(this);
(this as any).sendCommand = (...args: Array<unknown>): void => {
const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] as Function : undefined,
actualArgs = !callback ? args : args.slice(0, -1);
this.#sendCommand(actualArgs.flat() as Array<string>)
.then((reply: unknown) => {
(this as any).sendCommand = (...args: ClientLegacyCommandArguments): void => {
let callback: ClientLegacyCallback;
if (typeof args[args.length - 1] === 'function') {
callback = args.pop() as ClientLegacyCallback;
}
this.#sendCommand(transformLegacyCommandArguments(args as LegacyCommandArguments))
.then((reply: RedisCommandRawReply) => {
if (!callback) return;
// https://github.com/NodeRedis/node-redis#commands:~:text=minimal%20parsing
@@ -297,9 +323,9 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
}
// using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
async #sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
#sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
if (!this.#socket.isOpen) {
throw new ClientClosedError();
return Promise.reject(new ClientClosedError());
}
if (options?.isolated) {
@@ -313,7 +339,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
const promise = this.#queue.addCommand<T>(args, options, bufferMode);
this.#tick();
return await promise;
return promise;
}
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> {
@@ -400,33 +426,29 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
QUIT(): Promise<void> {
return this.#socket.quit(() => {
const promise = this.#queue.addCommand(['QUIT']);
const quitPromise = this.#queue.addCommand(['QUIT']);
this.#tick();
return promise;
return Promise.all([
quitPromise,
this.#destroyIsolationPool()
]);
});
}
quit = this.QUIT;
#tick(): void {
if (!this.#socket.isSocketExists) {
#tick(force = false): void {
if (this.#socket.writableNeedDrain || (!force && !this.#socket.isReady)) {
return;
}
this.#socket.cork();
while (true) {
while (!this.#socket.writableNeedDrain) {
const args = this.#queue.getCommandToSend();
if (args === undefined) break;
let writeResult;
for (const toWrite of encodeCommand(args)) {
writeResult = this.#socket.write(toWrite);
}
if (!writeResult) {
break;
}
this.#socket.writeCommand(args);
}
}
@@ -463,7 +485,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
for (const key of reply.keys) {
yield key;
}
} while (cursor !== 0)
} while (cursor !== 0);
}
async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<HScanTuple> {
@@ -474,7 +496,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
for (const tuple of reply.tuples) {
yield tuple;
}
} while (cursor !== 0)
} while (cursor !== 0);
}
async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable<string> {
@@ -485,7 +507,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
for (const member of reply.members) {
yield member;
}
} while (cursor !== 0)
} while (cursor !== 0);
}
async* zScanIterator(key: string, options?: ScanOptions): AsyncIterable<ZMember> {
@@ -496,15 +518,13 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
for (const member of reply.members) {
yield member;
}
} while (cursor !== 0)
} while (cursor !== 0);
}
async disconnect(): Promise<void> {
this.#queue.flushAll(new Error('Disconnecting'));
await Promise.all([
this.#socket.disconnect(),
this.#destroyIsolationPool()
]);
this.#queue.flushAll(new DisconnectsClientError());
this.#socket.disconnect();
await this.#destroyIsolationPool();
}
async #destroyIsolationPool(): Promise<void> {

View File

@@ -1,7 +1,7 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
import { extendWithCommands, extendWithModulesAndScripts } from '../commander';
import { extendWithCommands, extendWithModulesAndScripts, LegacyCommandArguments, transformLegacyCommandArguments } from '../commander';
type RedisClientMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
(...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, S>;
@@ -11,16 +11,16 @@ type WithCommands<M extends RedisModules, S extends RedisScripts> = {
};
type WithModules<M extends RedisModules, S extends RedisScripts> = {
[P in keyof M]: {
[P in keyof M as M[P] extends never ? never : P]: {
[C in keyof M[P]]: RedisClientMultiCommandSignature<M[P][C], M, S>;
};
};
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
[P in keyof S]: RedisClientMultiCommandSignature<S[P], M, S>
[P in keyof S as S[P] extends never ? never : P]: RedisClientMultiCommandSignature<S[P], M, S>
};
export type RedisClientMultiCommandType<M extends RedisModules = {}, S extends RedisScripts = {}> =
export type RedisClientMultiCommandType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
RedisClientMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
export type RedisClientMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
@@ -52,8 +52,8 @@ export default class RedisClientMultiCommand {
#legacyMode(): void {
this.v4.addCommand = this.addCommand.bind(this);
(this as any).addCommand = (...args: Array<string | Buffer | Array<string | Buffer>>): this => {
this.#multi.addCommand(args.flat());
(this as any).addCommand = (...args: LegacyCommandArguments): this => {
this.#multi.addCommand(transformLegacyCommandArguments(args));
return this;
};
this.v4.exec = this.exec.bind(this);

View File

@@ -33,6 +33,6 @@ describe('Socket', () => {
return assert.rejects(socket.connect(), {
message: '50'
});
})
});
});
});

View File

@@ -1,7 +1,9 @@
import EventEmitter from 'events';
import net from 'net';
import tls from 'tls';
import { ConnectionTimeoutError, ClientClosedError } from '../errors';
import { EventEmitter } from 'events';
import * as net from 'net';
import * as tls from 'tls';
import { encodeCommand } from '../commander';
import { RedisCommandArguments } from '../commands';
import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError } from '../errors';
import { promiseTimeout } from '../utils';
export interface RedisSocketCommonOptions {
@@ -20,7 +22,7 @@ export interface RedisUnixSocketOptions extends RedisSocketCommonOptions {
path: string;
}
export interface RedisTlsSocketOptions extends RedisNetSocketOptions, tls.SecureContextOptions {
export interface RedisTlsSocketOptions extends RedisNetSocketOptions, tls.SecureContextOptions, tls.CommonConnectionOptions {
tls: true;
}
@@ -72,8 +74,18 @@ export default class RedisSocket extends EventEmitter {
return this.#isOpen;
}
get isSocketExists(): boolean {
return !!this.#socket;
#isReady = false;
get isReady(): boolean {
return this.#isReady;
}
// `writable.writableNeedDrain` was added in v15.2.0 and therefore can't be used
// https://nodejs.org/api/stream.html#stream_writable_writableneeddrain
#writableNeedDrain = false;
get writableNeedDrain(): boolean {
return this.#writableNeedDrain;
}
constructor(initiator?: RedisSocketInitiator, options?: RedisSocketOptions) {
@@ -85,33 +97,39 @@ export default class RedisSocket extends EventEmitter {
async connect(): Promise<void> {
if (this.#isOpen) {
throw new Error('Socket is connection/connecting');
throw new Error('Socket already opened');
}
this.#isOpen = true;
try {
await this.#connect();
} catch (err) {
this.#isOpen = false;
throw err;
}
return this.#connect();
}
async #connect(hadError?: boolean): Promise<void> {
this.#isOpen = true;
this.#socket = await this.#retryConnection(0, hadError);
this.#writableNeedDrain = false;
if (!this.#isOpen) {
this.disconnect();
return;
}
this.emit('connect');
if (this.#initiator) {
try {
await this.#initiator();
} catch (err) {
this.#socket.end();
this.#socket.destroy();
this.#socket = undefined;
this.#isOpen = false;
throw err;
}
if (!this.#isOpen) return;
}
this.#isReady = true;
this.emit('ready');
}
@@ -160,10 +178,13 @@ export default class RedisSocket extends EventEmitter {
.once('error', (err: Error) => this.#onSocketError(err))
.once('close', hadError => {
if (!hadError && this.#isOpen) {
this.#onSocketError(new Error('Socket closed unexpectedly'));
this.#onSocketError(new SocketClosedUnexpectedlyError());
}
})
.on('drain', () => this.emit('drain'))
.on('drain', () => {
this.#writableNeedDrain = false;
this.emit('drain');
})
.on('data', (data: Buffer) => this.emit('data', data));
resolve(socket);
@@ -186,30 +207,32 @@ export default class RedisSocket extends EventEmitter {
}
#onSocketError(err: Error): void {
this.#socket = undefined;
this.#isReady = false;
this.emit('error', err);
this.#connect(true)
.catch(err => this.emit('error', err));
this.#connect(true).catch(() => {
// the error was already emitted, silently ignore it
});
}
write(toWrite: string | Buffer): boolean {
writeCommand(args: RedisCommandArguments): void {
if (!this.#socket) {
throw new ClientClosedError();
}
return this.#socket.write(toWrite);
for (const toWrite of encodeCommand(args)) {
this.#writableNeedDrain = !this.#socket.write(toWrite);
}
}
async disconnect(ignoreIsOpen = false): Promise<void> {
if ((!ignoreIsOpen && !this.#isOpen) || !this.#socket) {
disconnect(): void {
if (!this.#socket) {
throw new ClientClosedError();
} else {
this.#isOpen = false;
this.#isOpen = this.#isReady = false;
}
this.#socket.end();
await EventEmitter.once(this.#socket, 'end');
this.#socket.destroy();
this.#socket = undefined;
this.emit('end');
}
@@ -220,15 +243,8 @@ export default class RedisSocket extends EventEmitter {
}
this.#isOpen = false;
try {
await fn();
await this.disconnect(true);
} catch (err) {
this.#isOpen = true;
throw err;
}
await fn();
this.disconnect();
}
#isCorked = false;

View File

@@ -1,9 +1,13 @@
import calculateSlot from 'cluster-key-slot';
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES';
import { RedisClusterClientOptions, RedisClusterOptions } from '.';
import { RedisModules, RedisScripts } from '../commands';
// We need to use 'require', because it's not possible with Typescript to import
// function that are exported as 'module.exports = function`, without esModuleInterop
// set to true.
const calculateSlot = require('cluster-key-slot');
export interface ClusterNode<M extends RedisModules, S extends RedisScripts> {
id: string;
client: RedisClientType<M, S>;
@@ -69,7 +73,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
}
async #reset(masters: Array<RedisClusterMasterNode>): Promise<void> {
// Override this.#slots and add not existing clients to this.#clientByKey
// Override this.#slots and add not existing clients to this.#nodeByUrl
const promises: Array<Promise<void>> = [],
clientsInUse = new Set<string>();
for (const master of masters) {
@@ -82,13 +86,13 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
};
for (const { from, to } of master.slots) {
for (let i = from; i < to; i++) {
for (let i = from; i <= to; i++) {
this.#slots[i] = slot;
}
}
}
// Remove unused clients from this.#clientBykey using clientsInUse
// Remove unused clients from this.#nodeByUrl using clientsInUse
for (const [url, { client }] of this.#nodeByUrl.entries()) {
if (clientsInUse.has(url)) continue;

View File

@@ -0,0 +1,105 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
import { SQUARE_SCRIPT } from '../client/index.spec';
// We need to use 'require', because it's not possible with Typescript to import
// function that are exported as 'module.exports = function`, without esModuleInterop
// set to true.
const calculateSlot = require('cluster-key-slot');
describe('Cluster', () => {
testUtils.testWithCluster('sendCommand', async cluster => {
await cluster.connect();
try {
await cluster.publish('channel', 'message');
await cluster.set('a', 'b');
await cluster.set('a{a}', 'bb');
await cluster.set('aa', 'bb');
await cluster.get('aa');
await cluster.get('aa');
await cluster.get('aa');
await cluster.get('aa');
} finally {
await cluster.disconnect();
}
}, GLOBAL.CLUSTERS.OPEN);
testUtils.testWithCluster('multi', async cluster => {
const key = 'key';
assert.deepEqual(
await cluster.multi()
.set(key, 'value')
.get(key)
.exec(),
['OK', 'value']
);
}, GLOBAL.CLUSTERS.OPEN);
testUtils.testWithCluster('scripts', async cluster => {
assert.equal(
await cluster.square(2),
4
);
}, {
...GLOBAL.CLUSTERS.OPEN,
clusterConfiguration: {
scripts: {
square: SQUARE_SCRIPT
}
}
});
testUtils.testWithCluster('should handle live resharding', async cluster => {
const key = 'key',
value = 'value';
await cluster.set(key, value);
const slot = calculateSlot(key),
source = cluster.getSlotMaster(slot),
destination = cluster.getMasters().find(node => node.id !== source.id)!;
await Promise.all([
source.client.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, destination.id),
destination.client.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, destination.id)
]);
// should be able to get the key from the source node using "ASKING"
assert.equal(
await cluster.get(key),
value
);
await Promise.all([
source.client.migrate(
'127.0.0.1',
(<any>destination.client.options).socket.port,
key,
0,
10
)
]);
// should be able to get the key from the destination node using the "ASKING" command
assert.equal(
await cluster.get(key),
value
);
await Promise.all(
cluster.getMasters().map(({ client }) => {
return client.clusterSetSlot(slot, ClusterSlotStates.NODE, destination.id);
})
);
// should handle "MOVED" errors
assert.equal(
await cluster.get(key),
value
);
}, {
serverArguments: [],
numberOfNodes: 2
});
});

View File

@@ -1,5 +1,5 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisModules, RedisScript, RedisScripts } from '../commands';
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
import { ClientCommandOptions, RedisClientCommandSignature, RedisClientOptions, RedisClientType, WithModules, WithScripts } from '../client';
import RedisClusterSlots, { ClusterNode } from './cluster-slots';
import { extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, extendWithCommands } from '../commander';
@@ -7,14 +7,9 @@ import { EventEmitter } from 'events';
import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command';
export type RedisClusterClientOptions = Omit<RedisClientOptions<{}, {}>, 'modules' | 'scripts'>;
export type RedisClusterClientOptions = Omit<RedisClientOptions<Record<string, never>, Record<string, never>>, 'modules' | 'scripts'>;
export interface RedisClusterPlugins<M extends RedisModules, S extends RedisScripts> {
modules?: M;
scripts?: S;
}
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisClusterPlugins<M, S> {
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
rootNodes: Array<RedisClusterClientOptions>;
defaults?: Partial<RedisClusterClientOptions>;
useReplicas?: boolean;
@@ -25,10 +20,10 @@ type WithCommands = {
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
};
export type RedisClusterType<M extends RedisModules = {}, S extends RedisScripts = {}> =
export type RedisClusterType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
RedisCluster<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
export default class RedisCluster<M extends RedisModules = {}, S extends RedisScripts = {}> extends EventEmitter {
export default class RedisCluster<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> extends EventEmitter {
static extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: RedisCommandArguments): string | Buffer | undefined {
if (command.FIRST_KEY_INDEX === undefined) {
return undefined;
@@ -39,7 +34,7 @@ export default class RedisCluster<M extends RedisModules = {}, S extends RedisSc
return command.FIRST_KEY_INDEX(...originalArgs);
}
static create<M extends RedisModules = {}, S extends RedisScripts = {}>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
static create<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
return new (<any>extendWithModulesAndScripts({
BaseClass: RedisCluster,
modules: options?.modules,

View File

@@ -12,16 +12,16 @@ type WithCommands<M extends RedisModules, S extends RedisScripts> = {
};
type WithModules<M extends RedisModules, S extends RedisScripts> = {
[P in keyof M]: {
[P in keyof M as M[P] extends never ? never : P]: {
[C in keyof M[P]]: RedisClusterMultiCommandSignature<M[P][C], M, S>;
};
};
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
[P in keyof S]: RedisClusterMultiCommandSignature<S[P], M, S>
[P in keyof S as S[P] extends never ? never : P]: RedisClusterMultiCommandSignature<S[P], M, S>
};
export type RedisClusterMultiCommandType<M extends RedisModules = {}, S extends RedisScripts = {}> =
export type RedisClusterMultiCommandType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
RedisClusterMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: string | Buffer, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;

View File

@@ -113,3 +113,18 @@ export function transformCommandReply(
return command.transformReply(rawReply, preserved);
}
export type LegacyCommandArguments = Array<string | number | Buffer | LegacyCommandArguments>;
export function transformLegacyCommandArguments(args: LegacyCommandArguments, flat: RedisCommandArguments = []): RedisCommandArguments {
for (const arg of args) {
if (Array.isArray(arg)) {
transformLegacyCommandArguments(arg, flat);
continue;
}
flat.push(typeof arg === 'number' ? arg.toString() : arg);
}
return flat;
}

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_CAT';
describe('ACL CAT', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => {
it('simple', () => {

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion, itWithClient, TestRedisServers } from '../test-utils';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './ACL_DELUSER';
describe('ACL DELUSER', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => {
it('string', () => {
@@ -21,10 +21,10 @@ describe('ACL DELUSER', () => {
});
});
itWithClient(TestRedisServers.OPEN, 'client.aclDelUser', async client => {
testUtils.testWithClient('client.aclDelUser', async client => {
assert.equal(
await client.aclDelUser('dosenotexists'),
0
);
});
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_GENPASS';
describe('ACL GENPASS', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => {
it('simple', () => {

View File

@@ -0,0 +1,32 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './ACL_GETUSER';
describe('ACL GETUSER', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('username'),
['ACL', 'GETUSER', 'username']
);
});
testUtils.testWithClient('client.aclGetUser', async client => {
assert.deepEqual(
await client.aclGetUser('default'),
{
passwords: [],
commands: '+@all',
keys: ['*'],
...(testUtils.isVersionGreaterThan([6, 2]) ? {
flags: ['on', 'allkeys', 'allchannels', 'allcommands', 'nopass'],
channels: ['*']
} : {
flags: ['on', 'allkeys', 'allcommands', 'nopass'],
channels: undefined
})
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_LIST';
describe('ACL LIST', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_SAVE';
describe('ACL SAVE', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import testUtils from '../test-utils';
import { transformArguments, transformReply } from './ACL_LOG';
describe('ACL LOG', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => {
it('simple', () => {

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_LOG_RESET';
describe('ACL LOG RESET', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_LOAD';
describe('ACL LOAD', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_SETUSER';
describe('ACL SETUSER', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => {
it('string', () => {

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_USERS';
describe('ACL USERS', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(

View File

@@ -1,9 +1,9 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_WHOAMI';
describe('ACL WHOAMI', () => {
describeHandleMinimumRedisVersion([6]);
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(

View File

@@ -1,7 +1,7 @@
import { strict as assert } from 'assert';
import { transformArguments } from './APPEND';
describe('AUTH', () => {
describe('APPEND', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'value'),

View File

@@ -1,5 +1,5 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BITCOUNT';
describe('BITCOUNT', () => {
@@ -22,10 +22,10 @@ describe('BITCOUNT', () => {
});
});
itWithClient(TestRedisServers.OPEN, 'client.bitCount', async client => {
testUtils.testWithClient('client.bitCount', async client => {
assert.equal(
await client.bitCount('key'),
0
);
});
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,5 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BITFIELD';
describe('BITFIELD', () => {
@@ -33,10 +33,10 @@ describe('BITFIELD', () => {
);
});
itWithClient(TestRedisServers.OPEN, 'client.bitField', async client => {
testUtils.testWithClient('client.bitField', async client => {
assert.deepEqual(
await client.bitField('key', []),
[]
);
});
}, GLOBAL.SERVERS.OPEN);
});

Some files were not shown because too many files have changed in this diff Show More