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

8
.gitignore vendored
View File

@@ -1,8 +1,8 @@
.vscode/
.idea/ .idea/
node_modules/
dist/
.nyc_output/ .nyc_output/
.vscode/
coverage/ coverage/
dist/
node_modules/
.DS_Store
dump.rdb 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/ .github/
scripts/ .vscode/
lib/ docs/
examples/
packages/
.deepsource.toml
.release-it.json
CONTRIBUTING.md
SECURITY.md
index.ts index.ts
*.spec.* tsconfig.base.json
dist/lib/test-utils.* 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. 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: Note that the test suite assumes that [`docker`](https://www.docker.com/) is installed in your environment.
- 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)
### Submitting Code for Review ### Submitting Code for Review

View File

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

291
README.md
View File

@@ -1,289 +1,2 @@
<p align="center"> # redis
<a href="https://github.com/noderedis/node-redis"> The sources and docs for this package are in the main [node-redis](https://github.com/redis/node-redis) repo.
<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).

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. 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 | | Version | Supported |
| ------- | ------------------ | |---------|--------------------|
| 4.0.x | :white_check_mark: | | 4.0.z | :heavy_check_mark: |
| 3.1.x | :white_check_mark: | | 3.1.z | :heavy_check_mark: |
| < 3.1 | :x: | | < 3.1 | :x: |
## Reporting a Vulnerability ## 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)) | | username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
| password | | ACL password or the old "--requirepass" password | | password | | ACL password or the old "--requirepass" password |
| database | | Database number to connect to (see [`SELECT`](https://redis.io/commands/select) command) | | 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)) | | 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 | | commandsQueueMaxLength | | Maximum length of the client's internal command queue |
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode | | 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. > 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: 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 { createClient as _createClient, createCluster as _createCluster } from '@node-redis/client';
import RedisCluster from './lib/cluster'; 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", "name": "redis",
"version": "4.0.0-rc.3", "version": "4.0.0-rc.4",
"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"
}
],
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"workspaces": [
"./packages/*"
],
"scripts": { "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", "build": "tsc",
"documentation": "typedoc" "build-all": "npm run build:client && npm run build:test-utils && npm run build:modules && npm run build"
}, },
"dependencies": { "dependencies": {
"cluster-key-slot": "1.1.0", "@node-redis/client": "^1.0.0-rc.0",
"generic-pool": "3.8.2", "@node-redis/json": "^1.0.0-rc.0",
"redis-parser": "3.0.0", "@node-redis/search": "^1.0.0-rc.0"
"yallist": "4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@tsconfig/node12": "^1.0.9", "@tsconfig/node12": "^1.0.9",
"@types/mocha": "^9.0.0", "release-it": "^14.11.7",
"@types/node": "^16.10.3", "typescript": "^4.4.4"
"@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"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/NodeRedis/node-redis.git" "url": "git://github.com/redis/node-redis.git"
}, },
"bugs": { "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 ## 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 ### Breaking Changes
@@ -17,12 +17,35 @@ This version is a major change and refactor, adding modern JavaScript capabiliti
- Added support for Promises - Added support for Promises
- Added built-in TypeScript declaration files enabling code completion - Added built-in TypeScript declaration files enabling code completion
- Added support for [clustering](./README.md#cluster) - Added support for [clustering](../../.github/README.md#cluster)
- Added idiomatic arguments and responses to [Redis commands](./README.md#redis-commands) - Added idiomatic arguments and responses to [Redis commands](../../.github/README.md#redis-commands)
- Added full support for [Lua Scripts](./README.md#lua-scripts) - Added full support for [Lua Scripts](../../.github/README.md#lua-scripts)
- Added support for [SCAN iterators](./README.md#scan-iterator) - Added support for [SCAN iterators](../../.github/README.md#scan-iterator)
- Added the ability to extend Node Redis with Redis Module commands - 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 ## 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 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 * as LinkedList from 'yallist';
import RedisParser from 'redis-parser';
import { AbortError } from '../errors'; 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 { export interface QueueCommandOptions {
asap?: boolean; asap?: boolean;
chainId?: symbol; chainId?: symbol;
signal?: any; // TODO: `AbortSignal` type is incorrect signal?: AbortSignal;
} }
interface CommandWaitingToBeSent extends CommandWaitingForReply { interface CommandWaitingToBeSent extends CommandWaitingForReply {
args: Array<string | Buffer>; args: RedisCommandArguments;
chainId?: symbol; chainId?: symbol;
abort?: { abort?: {
signal: any; // TODO: `AbortSignal` type is incorrect signal: AbortSignal;
listener(): void; listener(): void;
}; };
} }
interface CommandWaitingForReply { interface CommandWaitingForReply {
resolve(reply?: any): void; resolve(reply?: unknown): void;
reject(err: Error): void; reject(err: Error): void;
channelsCounter?: number; channelsCounter?: number;
bufferMode?: boolean; bufferMode?: boolean;
@@ -107,7 +111,7 @@ export default class RedisCommandsQueue {
this.#maxLength = maxLength; 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) { if (this.#pubSubState.subscribing || this.#pubSubState.subscribed) {
return Promise.reject(new Error('Cannot send commands in PubSub mode')); return Promise.reject(new Error('Cannot send commands in PubSub mode'));
} else if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) { } else if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
@@ -135,7 +139,8 @@ export default class RedisCommandsQueue {
signal: options.signal, signal: options.signal,
listener listener
}; };
options.signal.addEventListener('abort', listener, { // AbortSignal type is incorrent
(options.signal as any).addEventListener('abort', listener, {
once: true once: true
}); });
} }
@@ -246,7 +251,7 @@ export default class RedisCommandsQueue {
]); ]);
} }
getCommandToSend(): Array<string | Buffer> | undefined { getCommandToSend(): RedisCommandArguments | undefined {
const toSend = this.#waitingToBeSent.shift(); const toSend = this.#waitingToBeSent.shift();
if (toSend) { if (toSend) {

View File

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

View File

@@ -1,11 +1,12 @@
import { strict as assert, AssertionError } from 'assert'; import { strict as assert } from 'assert';
import { once } from 'events'; import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
import { itWithClient, TEST_REDIS_SERVERS, TestRedisServers, waitTillBeenCalled, isRedisVersionGreaterThan } from '../test-utils'; import RedisClient, { ClientLegacyCommandArguments, RedisClientType } from '.';
import RedisClient from '.'; import { RedisClientMultiCommandType } from './multi-command';
import { AbortError, ClientClosedError, ConnectionTimeoutError, WatchError } from '../errors'; import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands';
import { AbortError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
import { defineScript } from '../lua-script'; import { defineScript } from '../lua-script';
import { spy } from 'sinon'; import { spy } from 'sinon';
import { RedisNetSocketOptions } from '../client/socket'; import { once } from 'events';
export const SQUARE_SCRIPT = defineScript({ export const SQUARE_SCRIPT = defineScript({
NUMBER_OF_KEYS: 0, 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', () => { describe('authentication', () => {
itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => { testUtils.testWithClient('Client should be authenticated', async client => {
assert.equal( assert.equal(
await client.ping(), await client.ping(),
'PONG' 'PONG'
); );
}); }, GLOBAL.SERVERS.PASSWORD);
it('should not retry connecting if failed due to wrong auth', async () => { testUtils.testWithClient('should not retry connecting if failed due to wrong auth', async client => {
const client = RedisClient.create({ let message;
...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD], if (testUtils.isVersionGreaterThan([6, 2])) {
password: 'wrongpassword' 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( await assert.rejects(
client.connect(), client.connect(),
{ { message }
message: isRedisVersionGreaterThan([6]) ?
'WRONGPASS invalid username-password pair or user is disabled.' :
'ERR invalid password'
}
); );
assert.equal(client.isOpen, false); 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', () => { describe('legacyMode', () => {
const client = RedisClient.create({ function sendCommandAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, args: RedisCommandArguments): Promise<RedisCommandRawReply> {
...TEST_REDIS_SERVERS[TestRedisServers.OPEN], return new Promise((resolve, reject) => {
scripts: { (client as any).sendCommand(args, (err: Error | undefined, reply: RedisCommandRawReply) => {
square: SQUARE_SCRIPT if (err) return reject(err);
},
legacyMode: true
});
before(() => client.connect()); resolve(reply);
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);
}
}); });
}
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 () => { testUtils.testWithClient('client.sendCommand should work without callback', async client => {
(client as any).sendCommand('PING'); client.sendCommand(['PING']);
await client.v4.ping(); // make sure the first command was replied 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( assert.equal(
await client.v4.sendCommand(['PING']), await client.v4.sendCommand(['PING']),
'PONG' 'PONG'
); );
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
}); });
it('client.{command} should accept vardict arguments', done => { function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: ClientLegacyCommandArguments): Promise<RedisCommandRawReply> {
(client as any).set('a', 'b', (err?: Error, reply?: string) => { return new Promise((resolve, reject) => {
if (err) { (client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
return done(err); if (err) return reject(err);
}
try { resolve(reply);
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);
}
}); });
});
}
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 () => { testUtils.testWithClient('client.{command} should accept arguments array', async client => {
(client as any).multi() 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() .ping()
.exec(); .exec();
await client.v4.ping(); // make sure the first command was replied 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( assert.deepEqual(
await ((client as any).multi() await client.multi()
.ping() .ping()
.v4.ping() .v4.ping()
.v4.exec()), .v4.exec(),
['PONG', 'PONG'] ['PONG', 'PONG']
); );
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true
}
}); });
it('client.{script} should return a promise', async () => { testUtils.testWithClient('client.{script} should return a promise', async client => {
assert.equal(await client.square(2), 4); assert.equal(
await client.square(2),
4
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
legacyMode: true,
scripts: {
square: SQUARE_SCRIPT
}
}
}); });
}); });
describe('events', () => { describe('events', () => {
it('connect, ready, end', async () => { testUtils.testWithClient('connect, ready, end', async client => {
const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
await Promise.all([ await Promise.all([
client.connect(),
once(client, 'connect'), once(client, 'connect'),
once(client, 'ready') once(client, 'ready'),
client.connect()
]); ]);
await Promise.all([ await Promise.all([
client.disconnect(), once(client, 'end'),
once(client, 'end') client.disconnect()
]); ]);
}, {
...GLOBAL.SERVERS.OPEN,
disableClientSetup: true
}); });
}); });
describe('sendCommand', () => { describe('sendCommand', () => {
itWithClient(TestRedisServers.OPEN, 'PING', async client => { testUtils.testWithClient('PING', async client => {
assert.equal(await client.sendCommand(['PING']), 'PONG'); assert.equal(await client.sendCommand(['PING']), 'PONG');
}); }, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'bufferMode', async client => { testUtils.testWithClient('bufferMode', async client => {
assert.deepEqual( assert.deepEqual(
await client.sendCommand(['PING'], undefined, true), await client.sendCommand(['PING'], undefined, true),
Buffer.from('PONG') Buffer.from('PONG')
); );
}); }, GLOBAL.SERVERS.OPEN);
describe('AbortController', () => { describe('AbortController', () => {
before(function () { before(function () {
@@ -281,13 +320,13 @@ describe('Client', () => {
} }
}); });
itWithClient(TestRedisServers.OPEN, 'success', async client => { testUtils.testWithClient('success', async client => {
await client.sendCommand(['PING'], { await client.sendCommand(['PING'], {
signal: new AbortController().signal signal: new AbortController().signal
}); });
}); }, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'AbortError', client => { testUtils.testWithClient('AbortError', client => {
const controller = new AbortController(); const controller = new AbortController();
controller.abort(); controller.abort();
@@ -297,12 +336,12 @@ describe('Client', () => {
}), }),
AbortError AbortError
); );
}); }, GLOBAL.SERVERS.OPEN);
}); });
}); });
describe('multi', () => { describe('multi', () => {
itWithClient(TestRedisServers.OPEN, 'simple', async client => { testUtils.testWithClient('simple', async client => {
assert.deepEqual( assert.deepEqual(
await client.multi() await client.multi()
.ping() .ping()
@@ -311,44 +350,35 @@ describe('Client', () => {
.exec(), .exec(),
['PONG', 'OK', 'value'] ['PONG', 'OK', 'value']
); );
}); }, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'should reject the whole chain on error', client => {
client.on('error', () => {
// ignore errors
});
testUtils.testWithClient('should reject the whole chain on error', client => {
return assert.rejects( return assert.rejects(
client.multi() client.multi()
.ping() .ping()
.addCommand(['DEBUG', 'RESTART']) .addCommand(['INVALID COMMAND'])
.ping() .ping()
.exec() .exec()
); );
}); }, GLOBAL.SERVERS.OPEN);
it('with script', async () => { testUtils.testWithClient('with script', async client => {
const client = RedisClient.create({ assert.deepEqual(
await client.multi()
.square(2)
.exec(),
[4]
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
scripts: { scripts: {
square: SQUARE_SCRIPT 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.watch('key');
await client.set( await client.set(
@@ -365,39 +395,40 @@ describe('Client', () => {
.exec(), .exec(),
WatchError WatchError
); );
}); }, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'execAsPipeline', async client => { testUtils.testWithClient('execAsPipeline', async client => {
assert.deepEqual( assert.deepEqual(
await client.multi() await client.multi()
.ping() .ping()
.exec(true), .exec(true),
['PONG'] ['PONG']
); );
}); }, GLOBAL.SERVERS.OPEN);
}); });
it('scripts', async () => { testUtils.testWithClient('scripts', async client => {
const client = RedisClient.create({ assert.equal(
await client.square(2),
4
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
scripts: { scripts: {
square: SQUARE_SCRIPT square: SQUARE_SCRIPT
} }
});
await client.connect();
try {
assert.equal(
await client.square(2),
4
);
} finally {
await client.disconnect();
} }
}); });
it('modules', async () => { testUtils.testWithClient('modules', async client => {
const client = RedisClient.create({ assert.equal(
await client.module.echo('message'),
'message'
);
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
modules: { modules: {
module: { module: {
echo: { 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']); await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
assert.equal( assert.equal(
@@ -433,35 +453,35 @@ describe('Client', () => {
), ),
null null
); );
}); }, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'should reconnect after DEBUG RESTART', async client => { async function killClient<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>): Promise<void> {
client.on('error', () => { const onceErrorPromise = once(client, 'error');
// ignore errors await client.sendCommand(['QUIT']);
}); await Promise.all([
onceErrorPromise,
assert.rejects(client.ping(), SocketClosedUnexpectedlyError)
]);
}
await client.sendCommand(['CLIENT', 'SETNAME', 'client']); testUtils.testWithClient('should reconnect when socket disconnects', async client => {
await assert.rejects(client.sendCommand(['DEBUG', 'RESTART'])); await killClient(client);
assert.ok(await client.sendCommand(['CLIENT', 'GETNAME']) === null); await assert.doesNotReject(client.ping());
}); }, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'should SELECT db after reconnection', async client => {
client.on('error', () => {
// ignore errors
});
testUtils.testWithClient('should remember selected db', async client => {
await client.select(1); await client.select(1);
await assert.rejects(client.sendCommand(['DEBUG', 'RESTART'])); await killClient(client);
assert.equal( assert.equal(
(await client.clientInfo()).db, (await client.clientInfo()).db,
1 1
); );
}, { }, {
// because of CLIENT INFO ...GLOBAL.SERVERS.OPEN,
minimumRedisVersion: [6, 2] minimumDockerVersion: [6, 2] // CLIENT INFO
}); });
itWithClient(TestRedisServers.OPEN, 'scanIterator', async client => { testUtils.testWithClient('scanIterator', async client => {
const promises = [], const promises = [],
keys = new Set(); keys = new Set();
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
@@ -478,9 +498,9 @@ describe('Client', () => {
} }
assert.deepEqual(keys, results); assert.deepEqual(keys, results);
}); }, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'hScanIterator', async client => { testUtils.testWithClient('hScanIterator', async client => {
const hash: Record<string, string> = {}; const hash: Record<string, string> = {};
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
hash[i.toString()] = i.toString(); hash[i.toString()] = i.toString();
@@ -494,9 +514,9 @@ describe('Client', () => {
} }
assert.deepEqual(hash, results); assert.deepEqual(hash, results);
}); }, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'sScanIterator', async client => { testUtils.testWithClient('sScanIterator', async client => {
const members = new Set<string>(); const members = new Set<string>();
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
members.add(i.toString()); members.add(i.toString());
@@ -510,9 +530,9 @@ describe('Client', () => {
} }
assert.deepEqual(members, results); assert.deepEqual(members, results);
}); }, GLOBAL.SERVERS.OPEN);
itWithClient(TestRedisServers.OPEN, 'zScanIterator', async client => { testUtils.testWithClient('zScanIterator', async client => {
const members = []; const members = [];
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
members.push({ members.push({
@@ -538,9 +558,9 @@ describe('Client', () => {
[...map.entries()].sort(sort), [...map.entries()].sort(sort),
members.map<MemberTuple>(member => [member.value, member.score]).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(); const subscriber = publisher.duplicate();
await subscriber.connect(); await subscriber.connect();
@@ -603,49 +623,59 @@ describe('Client', () => {
} finally { } finally {
await subscriber.disconnect(); await subscriber.disconnect();
} }
}); }, GLOBAL.SERVERS.OPEN);
it('ConnectionTimeoutError', async () => { testUtils.testWithClient('ConnectionTimeoutError', async client => {
const client = RedisClient.create({ 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: { socket: {
...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
connectTimeout: 1 connectTimeout: 1
} }
}); },
disableClientSetup: true
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;
}
}); });
it('client.quit', async () => { testUtils.testWithClient('client.quit', async client => {
const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
await client.connect(); await client.connect();
try { const pingPromise = client.ping(),
const quitPromise = client.quit(); quitPromise = client.quit();
assert.equal(client.isOpen, false); assert.equal(client.isOpen, false);
await Promise.all([
quitPromise, const [ping] = await Promise.all([
assert.rejects(client.ping(), ClientClosedError) pingPromise,
]); assert.doesNotReject(quitPromise),
} finally { assert.rejects(client.ping(), ClientClosedError)
if (client.isOpen) { ]);
await client.disconnect();
} 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 RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command'; import { RedisMultiQueuedCommand } from '../multi-command';
import EventEmitter from 'events'; import { EventEmitter } from 'events';
import { CommandOptions, commandOptions, isCommandOptions } from '../command-options'; import { CommandOptions, commandOptions, isCommandOptions } from '../command-options';
import { ScanOptions, ZMember } from '../commands/generic-transformers'; import { ScanOptions, ZMember } from '../commands/generic-transformers';
import { ScanCommandOptions } from '../commands/SCAN'; import { ScanCommandOptions } from '../commands/SCAN';
import { HScanTuple } from '../commands/HSCAN'; 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 { Pool, Options as PoolOptions, createPool } from 'generic-pool';
import { ClientClosedError } from '../errors'; import { ClientClosedError, DisconnectsClientError } from '../errors';
import { URL } from 'url'; import { URL } from 'url';
export interface RedisClientOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> { 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> = { 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]>; [C in keyof M[P]]: RedisClientCommandSignature<M[P][C]>;
}; };
}; };
export type WithScripts<S extends RedisScripts> = { 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>; RedisClient<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
export type InstantiableRedisClient<M extends RedisModules, S extends RedisScripts> = export type InstantiableRedisClient<M extends RedisModules, S extends RedisScripts> =
@@ -53,12 +53,15 @@ export interface ClientCommandOptions extends QueueCommandOptions {
isolated?: boolean; 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 { export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> { static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> {
return commandOptions(options); 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({ const Client = <any>extendWithModulesAndScripts({
BaseClass: RedisClient, BaseClass: RedisClient,
modules: plugins?.modules, modules: plugins?.modules,
@@ -74,14 +77,14 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
return Client; 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); 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 // https://www.iana.org/assignments/uri-schemes/prov/redis
const { hostname, port, protocol, username, password, pathname } = new URL(url), const { hostname, port, protocol, username, password, pathname } = new URL(url),
parsed: RedisClientOptions<{}, {}> = { parsed: RedisClientOptions<Record<string, never>, Record<string, never>> = {
socket: { socket: {
host: hostname host: hostname
} }
@@ -98,11 +101,11 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
} }
if (username) { if (username) {
parsed.username = username; parsed.username = decodeURIComponent(username);
} }
if (password) { if (password) {
parsed.password = password; parsed.password = decodeURIComponent(password);
} }
if (pathname.length > 1) { if (pathname.length > 1) {
@@ -177,28 +180,47 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
#initiateSocket(): RedisSocket { #initiateSocket(): RedisSocket {
const socketInitiator = async (): Promise<void> => { const socketInitiator = async (): Promise<void> => {
const v4Commands = this.#options?.legacyMode ? this.#v4 : this, const promises = [];
promises = [];
if (this.#selectedDB !== 0) { 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) { 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) { 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(); const resubscribePromise = this.#queue.resubscribe();
if (resubscribePromise) { if (resubscribePromise) {
promises.push(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) return new RedisSocket(socketInitiator, this.#options?.socket)
@@ -213,6 +235,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
this.#tick(); this.#tick();
}) })
.on('reconnecting', () => this.emit('reconnecting')) .on('reconnecting', () => this.emit('reconnecting'))
.on('drain', () => this.#tick())
.on('end', () => this.emit('end')); .on('end', () => this.emit('end'));
} }
@@ -224,11 +247,14 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
if (!this.#options?.legacyMode) return; if (!this.#options?.legacyMode) return;
(this as any).#v4.sendCommand = this.#sendCommand.bind(this); (this as any).#v4.sendCommand = this.#sendCommand.bind(this);
(this as any).sendCommand = (...args: Array<unknown>): void => { (this as any).sendCommand = (...args: ClientLegacyCommandArguments): void => {
const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] as Function : undefined, let callback: ClientLegacyCallback;
actualArgs = !callback ? args : args.slice(0, -1); if (typeof args[args.length - 1] === 'function') {
this.#sendCommand(actualArgs.flat() as Array<string>) callback = args.pop() as ClientLegacyCallback;
.then((reply: unknown) => { }
this.#sendCommand(transformLegacyCommandArguments(args as LegacyCommandArguments))
.then((reply: RedisCommandRawReply) => {
if (!callback) return; if (!callback) return;
// https://github.com/NodeRedis/node-redis#commands:~:text=minimal%20parsing // 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 // 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) { if (!this.#socket.isOpen) {
throw new ClientClosedError(); return Promise.reject(new ClientClosedError());
} }
if (options?.isolated) { 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); const promise = this.#queue.addCommand<T>(args, options, bufferMode);
this.#tick(); this.#tick();
return await promise; return promise;
} }
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> { 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> { QUIT(): Promise<void> {
return this.#socket.quit(() => { return this.#socket.quit(() => {
const promise = this.#queue.addCommand(['QUIT']); const quitPromise = this.#queue.addCommand(['QUIT']);
this.#tick(); this.#tick();
return promise; return Promise.all([
quitPromise,
this.#destroyIsolationPool()
]);
}); });
} }
quit = this.QUIT; quit = this.QUIT;
#tick(): void { #tick(force = false): void {
if (!this.#socket.isSocketExists) { if (this.#socket.writableNeedDrain || (!force && !this.#socket.isReady)) {
return; return;
} }
this.#socket.cork(); this.#socket.cork();
while (true) { while (!this.#socket.writableNeedDrain) {
const args = this.#queue.getCommandToSend(); const args = this.#queue.getCommandToSend();
if (args === undefined) break; if (args === undefined) break;
let writeResult; this.#socket.writeCommand(args);
for (const toWrite of encodeCommand(args)) {
writeResult = this.#socket.write(toWrite);
}
if (!writeResult) {
break;
}
} }
} }
@@ -463,7 +485,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
for (const key of reply.keys) { for (const key of reply.keys) {
yield key; yield key;
} }
} while (cursor !== 0) } while (cursor !== 0);
} }
async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<HScanTuple> { 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) { for (const tuple of reply.tuples) {
yield tuple; yield tuple;
} }
} while (cursor !== 0) } while (cursor !== 0);
} }
async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable<string> { 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) { for (const member of reply.members) {
yield member; yield member;
} }
} while (cursor !== 0) } while (cursor !== 0);
} }
async* zScanIterator(key: string, options?: ScanOptions): AsyncIterable<ZMember> { 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) { for (const member of reply.members) {
yield member; yield member;
} }
} while (cursor !== 0) } while (cursor !== 0);
} }
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
this.#queue.flushAll(new Error('Disconnecting')); this.#queue.flushAll(new DisconnectsClientError());
await Promise.all([ this.#socket.disconnect();
this.#socket.disconnect(), await this.#destroyIsolationPool();
this.#destroyIsolationPool()
]);
} }
async #destroyIsolationPool(): Promise<void> { async #destroyIsolationPool(): Promise<void> {

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,13 @@
import calculateSlot from 'cluster-key-slot';
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client'; import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES'; import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES';
import { RedisClusterClientOptions, RedisClusterOptions } from '.'; import { RedisClusterClientOptions, RedisClusterOptions } from '.';
import { RedisModules, RedisScripts } from '../commands'; 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> { export interface ClusterNode<M extends RedisModules, S extends RedisScripts> {
id: string; id: string;
client: RedisClientType<M, S>; 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> { 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>> = [], const promises: Array<Promise<void>> = [],
clientsInUse = new Set<string>(); clientsInUse = new Set<string>();
for (const master of masters) { 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 (const { from, to } of master.slots) {
for (let i = from; i < to; i++) { for (let i = from; i <= to; i++) {
this.#slots[i] = slot; 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()) { for (const [url, { client }] of this.#nodeByUrl.entries()) {
if (clientsInUse.has(url)) continue; 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 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 { ClientCommandOptions, RedisClientCommandSignature, RedisClientOptions, RedisClientType, WithModules, WithScripts } from '../client';
import RedisClusterSlots, { ClusterNode } from './cluster-slots'; import RedisClusterSlots, { ClusterNode } from './cluster-slots';
import { extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, extendWithCommands } from '../commander'; import { extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, extendWithCommands } from '../commander';
@@ -7,14 +7,9 @@ import { EventEmitter } from 'events';
import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command'; import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } 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> { export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
modules?: M;
scripts?: S;
}
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisClusterPlugins<M, S> {
rootNodes: Array<RedisClusterClientOptions>; rootNodes: Array<RedisClusterClientOptions>;
defaults?: Partial<RedisClusterClientOptions>; defaults?: Partial<RedisClusterClientOptions>;
useReplicas?: boolean; useReplicas?: boolean;
@@ -25,10 +20,10 @@ type WithCommands = {
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>; [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>; 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 { static extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: RedisCommandArguments): string | Buffer | undefined {
if (command.FIRST_KEY_INDEX === undefined) { if (command.FIRST_KEY_INDEX === undefined) {
return undefined; return undefined;
@@ -39,7 +34,7 @@ export default class RedisCluster<M extends RedisModules = {}, S extends RedisSc
return command.FIRST_KEY_INDEX(...originalArgs); 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({ return new (<any>extendWithModulesAndScripts({
BaseClass: RedisCluster, BaseClass: RedisCluster,
modules: options?.modules, 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> = { 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>; [C in keyof M[P]]: RedisClusterMultiCommandSignature<M[P][C], M, S>;
}; };
}; };
type WithScripts<M extends RedisModules, S extends RedisScripts> = { 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>; RedisClusterMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: string | Buffer, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>; 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); 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 { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils'; import testUtils from '../test-utils';
import { transformArguments } from './ACL_CAT'; import { transformArguments } from './ACL_CAT';
describe('ACL CAT', () => { describe('ACL CAT', () => {
describeHandleMinimumRedisVersion([6]); testUtils.isVersionGreaterThanHook([6]);
describe('transformArguments', () => { describe('transformArguments', () => {
it('simple', () => { it('simple', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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