You've already forked node-redis
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 * fix54124793ad
* 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 * revertd32f1edf8a
* 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:
298
.github/README.md
vendored
Normal file
298
.github/README.md
vendored
Normal file
@@ -0,0 +1,298 @@
|
||||
# Node-Redis
|
||||
|
||||
[](https://codecov.io/gh/redis/node-redis)
|
||||
[](https://codecov.io/gh/redis/node-redis)
|
||||
[](https://codecov.io/gh/redis/node-redis)
|
||||
[](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](../) | [](https://www.npmjs.com/package/redis/v/next) [](https://www.npmjs.com/package/redis/v/next) |
|
||||
| [@node-redis/client](../packages/client) | [](https://www.npmjs.com/package/@node-redis/client/v/next) [](https://www.npmjs.com/package/@node-redis/client/v/next) |
|
||||
| [@node-redis/json](../packages/json) | [](https://www.npmjs.com/package/@node-redis/json/v/next) [](https://www.npmjs.com/package/@node-redis/json/v/next) [Redis JSON](https://oss.redis.com/redisjson/) commands |
|
||||
| [@node-redis/search](../packages/search) | [](https://www.npmjs.com/package/@node-redis/search/v/next) [](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!
|
||||
|
||||
[](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
43
.github/release-drafter-config.yml
vendored
Normal 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
|
46
.github/workflows/benchmark.yml
vendored
46
.github/workflows/benchmark.yml
vendored
@@ -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
|
5
.github/workflows/documentation.yml
vendored
5
.github/workflows/documentation.yml
vendored
@@ -9,21 +9,16 @@ on:
|
||||
jobs:
|
||||
documentation:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2.3.0
|
||||
|
||||
- name: Install Packages
|
||||
run: npm ci
|
||||
|
||||
- name: Generate Documentation
|
||||
run: npm run documentation
|
||||
|
||||
- name: Upload Documentation to Wiki
|
||||
uses: SwiftDocOrg/github-wiki-publish-action@v1
|
||||
with:
|
||||
|
19
.github/workflows/release-drafter.yml
vendored
Normal file
19
.github/workflows/release-drafter.yml
vendored
Normal 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 }}
|
55
.github/workflows/tests.yml
vendored
55
.github/workflows/tests.yml
vendored
@@ -5,6 +5,10 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- v4.0
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- v4.0
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -12,47 +16,32 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
redis-version: [5.x, 6.x]
|
||||
|
||||
node-version: [12, 14, 16]
|
||||
redis-version: [5, 6.0, 6.2]
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Setup Redis
|
||||
uses: shogo82148/actions-setup-redis@v1.12.0
|
||||
with:
|
||||
redis-version: ${{ matrix.redis-version }}
|
||||
auto-start: "false"
|
||||
|
||||
- name: Update npm
|
||||
run: npm i -g npm
|
||||
if: ${{ matrix.node-version <= 14 }}
|
||||
- name: Install Packages
|
||||
run: npm ci
|
||||
|
||||
- name: Build tests tools
|
||||
run: npm run build:tests-tools
|
||||
- name: Run Tests
|
||||
run: npm run test
|
||||
|
||||
- name: Generate lcov
|
||||
run: ./node_modules/.bin/nyc report -r lcov
|
||||
|
||||
- name: Coveralls
|
||||
uses: coverallsapp/github-action@1.1.3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
flag-name: Node ${{ matrix.node-version }} Redis ${{ matrix.redis-version }}
|
||||
parallel: true
|
||||
|
||||
finish:
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@1.1.3
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
parallel-finished: true
|
||||
run: npm run test -- --forbid-only --redis-version=${{ matrix.redis-version }}
|
||||
- name: Upload to Codecov
|
||||
run: |
|
||||
curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import
|
||||
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||
curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM
|
||||
curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig
|
||||
gpgv codecov.SHA256SUM.sig codecov.SHA256SUM
|
||||
shasum -a 256 -c codecov.SHA256SUM
|
||||
chmod +x codecov
|
||||
./codecov
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,8 +1,8 @@
|
||||
.vscode/
|
||||
.idea/
|
||||
node_modules/
|
||||
dist/
|
||||
.nyc_output/
|
||||
.vscode/
|
||||
coverage/
|
||||
dist/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
dump.rdb
|
||||
documentation/
|
||||
|
26
.npmignore
26
.npmignore
@@ -1,18 +1,12 @@
|
||||
.vscode/
|
||||
.idea/
|
||||
node_modules/
|
||||
.nyc_output/
|
||||
coverage/
|
||||
dump.rdb
|
||||
documentation/
|
||||
CONTRIBUTING.md
|
||||
tsconfig.json
|
||||
.deepsource.toml
|
||||
.nycrc.json
|
||||
benchmark/
|
||||
.github/
|
||||
scripts/
|
||||
lib/
|
||||
.vscode/
|
||||
docs/
|
||||
examples/
|
||||
packages/
|
||||
.deepsource.toml
|
||||
.release-it.json
|
||||
CONTRIBUTING.md
|
||||
SECURITY.md
|
||||
index.ts
|
||||
*.spec.*
|
||||
dist/lib/test-utils.*
|
||||
tsconfig.base.json
|
||||
tsconfig.json
|
||||
|
7
.release-it.json
Normal file
7
.release-it.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"git": {
|
||||
"tagName": "redis@${version}",
|
||||
"commitMessage": "Release ${tagName}",
|
||||
"tagAnnotation": "Release ${tagName}"
|
||||
}
|
||||
}
|
@@ -45,12 +45,9 @@ A huge thank you to the original author of Node Redis, [Matthew Ranney](https://
|
||||
|
||||
Node Redis has a full test suite with coverage setup.
|
||||
|
||||
To run the tests, run `npm install` to install dependencies, then run `npm test`.
|
||||
To run the tests, run `npm install` to install dependencies, then run `npm run build:tests-tools && npm test`.
|
||||
|
||||
Note that the test suite assumes that a few tools are installed in your environment, such as:
|
||||
|
||||
- redis (make sure redis-server is not running when starting the tests, it's part of the test-suite to start it and you'll end up with a "port already in use" error)
|
||||
- stunnel (for TLS tests)
|
||||
Note that the test suite assumes that [`docker`](https://www.docker.com/) is installed in your environment.
|
||||
|
||||
### Submitting Code for Review
|
||||
|
||||
|
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
MIT License
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-present Node Redis contributors.
|
||||
|
||||
|
291
README.md
291
README.md
@@ -1,289 +1,2 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/noderedis/node-redis">
|
||||
<img width="128" src="https://static.invertase.io/assets/node_redis_logo.png" />
|
||||
</a>
|
||||
<h2 align="center">Node Redis</h2>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://coveralls.io/github/NodeRedis/node-redis">
|
||||
<img src="https://coveralls.io/repos/github/NodeRedis/node-redis/badge.svg" alt="Coverage Status"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/redis/v/next">
|
||||
<img src="https://img.shields.io/npm/dm/redis.svg" alt="Downloads"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/redis/v/next">
|
||||
<img src="https://img.shields.io/npm/v/redis/next.svg" alt="Version"/>
|
||||
</a>
|
||||
<a href="https://discord.gg/XMMVgxUm">
|
||||
<img src="https://img.shields.io/discord/697882427875393627" alt="Chat"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install redis@next
|
||||
```
|
||||
|
||||
> :warning: The new interface is clean and cool, but if you have an existing code base, you'll want to read the [migration guide](./docs/v3-to-v4.md).
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example
|
||||
|
||||
```typescript
|
||||
import { createClient } from 'redis';
|
||||
|
||||
(async () => {
|
||||
const client = createClient();
|
||||
|
||||
client.on('error', (err) => console.log('Redis Client Error', err));
|
||||
|
||||
await client.connect();
|
||||
|
||||
await client.set('key', 'value');
|
||||
const value = await client.get('key');
|
||||
})();
|
||||
```
|
||||
|
||||
The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in the format `redis[s]://[[username][:password]@][host][:port][/db-number]`:
|
||||
|
||||
```typescript
|
||||
createClient({
|
||||
url: 'redis://alice:foobared@awesome.redis.server:6380'
|
||||
});
|
||||
```
|
||||
|
||||
You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in in the [Wiki](https://github.com/NodeRedis/node-redis/wiki/lib.socket#RedisSocketOptions).
|
||||
|
||||
### Redis Commands
|
||||
|
||||
There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`, etc.):
|
||||
|
||||
```typescript
|
||||
// raw Redis commands
|
||||
await client.HSET('key', 'field', 'value');
|
||||
await client.HGETALL('key');
|
||||
|
||||
// friendly JavaScript commands
|
||||
await client.hSet('key', 'field', 'value');
|
||||
await client.hGetAll('key');
|
||||
```
|
||||
|
||||
Modifiers to commands are specified using a JavaScript object:
|
||||
|
||||
```typescript
|
||||
await client.set('key', 'value', {
|
||||
EX: 10,
|
||||
NX: true
|
||||
});
|
||||
```
|
||||
|
||||
Replies will be transformed into useful data structures:
|
||||
|
||||
```typescript
|
||||
await client.hGetAll('key'); // { field1: 'value1', field2: 'value2' }
|
||||
await client.hVals('key'); // ['value1', 'value2']
|
||||
```
|
||||
|
||||
### Unsupported Redis Commands
|
||||
|
||||
If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`:
|
||||
|
||||
```typescript
|
||||
await client.sendCommand(['SET', 'key', 'value', 'NX']); // 'OK'
|
||||
|
||||
await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2']
|
||||
```
|
||||
|
||||
### Transactions (Multi/Exec)
|
||||
|
||||
Start a [transaction](https://redis.io/topics/transactions) by calling `.multi()`, then chaining your commands. When you're done, call `.exec()` and you'll get an array back with your results:
|
||||
|
||||
```typescript
|
||||
await client.set('another-key', 'another-value');
|
||||
|
||||
const [setKeyReply, otherKeyValue] = await client
|
||||
.multi()
|
||||
.set('key', 'value')
|
||||
.get('another-key')
|
||||
.exec(); // ['OK', 'another-value']
|
||||
```
|
||||
|
||||
You can also [watch](https://redis.io/topics/transactions#optimistic-locking-using-check-and-set) keys by calling `.watch()`. Your transaction will abort if any of the watched keys change.
|
||||
|
||||
To dig deeper into transactions, check out the [Isolated Execution Guide](./docs/isolated-execution.md).
|
||||
|
||||
### Blocking Commands
|
||||
|
||||
Any command can be run on a new connection by specifying the `isolated` option. The newly created connection is closed when the command's `Promise` is fulfilled.
|
||||
|
||||
This pattern works especially well for blocking commands—such as `BLPOP` and `BLMOVE`:
|
||||
|
||||
```typescript
|
||||
import { commandOptions } from 'redis';
|
||||
|
||||
const blPopPromise = client.blPop(commandOptions({ isolated: true }), 'key');
|
||||
|
||||
await client.lPush('key', ['1', '2']);
|
||||
|
||||
await blPopPromise; // '2'
|
||||
```
|
||||
|
||||
To learn more about isolated execution, check out the [guide](./docs/isolated-execution.md).
|
||||
|
||||
### Pub/Sub
|
||||
|
||||
Subscribing to a channel requires a dedicated stand-alone connection. You can easily get one by `.duplicate()`ing an existing Redis connection.
|
||||
|
||||
```typescript
|
||||
const subscriber = client.duplicate();
|
||||
|
||||
await subscriber.connect();
|
||||
```
|
||||
|
||||
Once you have one, simply subscribe and unsubscribe as needed:
|
||||
|
||||
```typescript
|
||||
await subscriber.subscribe('channel', (message) => {
|
||||
console.log(message); // 'message'
|
||||
});
|
||||
|
||||
await subscriber.pSubscribe('channe*', (message, channel) => {
|
||||
console.log(message, channel); // 'message', 'channel'
|
||||
});
|
||||
|
||||
await subscriber.unsubscribe('channel');
|
||||
|
||||
await subscriber.pUnsubscribe('channe*');
|
||||
```
|
||||
|
||||
Publish a message on a channel:
|
||||
|
||||
```typescript
|
||||
await publisher.publish('channel', 'message');
|
||||
```
|
||||
|
||||
### Scan Iterator
|
||||
|
||||
[`SCAN`](https://redis.io/commands/scan) results can be looped over using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator):
|
||||
|
||||
```typescript
|
||||
for await (const key of client.scanIterator()) {
|
||||
// use the key!
|
||||
await client.get(key);
|
||||
}
|
||||
```
|
||||
|
||||
This works with `HSCAN`, `SSCAN`, and `ZSCAN` too:
|
||||
|
||||
```typescript
|
||||
for await (const member of client.hScanIterator('hash')) {}
|
||||
for await (const { field, value } of client.sScanIterator('set')) {}
|
||||
for await (const { member, score } of client.zScanIterator('sorted-set')) {}
|
||||
```
|
||||
|
||||
You can override the default options by providing a configuration object:
|
||||
|
||||
```typescript
|
||||
client.scanIterator({
|
||||
TYPE: 'string', // `SCAN` only
|
||||
MATCH: 'patter*',
|
||||
COUNT: 100,
|
||||
});
|
||||
```
|
||||
|
||||
### Lua Scripts
|
||||
|
||||
Define new functions using [Lua scripts](https://redis.io/commands/eval) which execute on the Redis server:
|
||||
|
||||
```typescript
|
||||
import { createClient } from 'redis';
|
||||
import { defineScript } from 'redis/lua-script';
|
||||
|
||||
(async () => {
|
||||
const client = createClient({
|
||||
scripts: {
|
||||
add: defineScript({
|
||||
NUMBER_OF_KEYS: 1,
|
||||
SCRIPT:
|
||||
"local val = redis.pcall('GET', KEYS[1]);' + 'return val + ARGV[1];",
|
||||
transformArguments(key: string, toAdd: number): Array<string> {
|
||||
return [key, number.toString()];
|
||||
},
|
||||
transformReply(reply: number): number {
|
||||
return reply;
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
await client.set('key', '1');
|
||||
await client.add('key', 2); // 3
|
||||
})();
|
||||
```
|
||||
|
||||
### Cluster
|
||||
|
||||
Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a non-clustered client:
|
||||
|
||||
```typescript
|
||||
import { createCluster } from 'redis';
|
||||
|
||||
(async () => {
|
||||
const cluster = createCluster({
|
||||
rootNodes: [
|
||||
{
|
||||
url: 'redis://10.0.0.1:30001'
|
||||
},
|
||||
{
|
||||
url: 'redis://10.0.0.2:30002'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
cluster.on('error', (err) => console.log('Redis Cluster Error', err));
|
||||
|
||||
await cluster.connect();
|
||||
|
||||
await cluster.set('key', 'value');
|
||||
const value = await cluster.get('key');
|
||||
})();
|
||||
```
|
||||
|
||||
### Auto-Pipelining
|
||||
|
||||
Node Redis will automatically pipeline requests that are made during the same "tick".
|
||||
|
||||
```typescript
|
||||
client.set('Tm9kZSBSZWRpcw==', 'users:1');
|
||||
client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==');
|
||||
```
|
||||
|
||||
Of course, if you don't do something with your Promises you're certain to get [unhandled Promise exceptions](https://nodejs.org/api/process.html#process_event_unhandledrejection). To take advantage of auto-pipelining and handle your Promises, use `Promise.all()`.
|
||||
|
||||
```typescript
|
||||
await Promise.all([
|
||||
client.set('Tm9kZSBSZWRpcw==', 'users:1'),
|
||||
client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==')
|
||||
]);
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md).
|
||||
|
||||
Thank you to all the people who already contributed to Node Redis!
|
||||
|
||||
<a href="https://github.com/NodeRedis/node-redis/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=NodeRedis/node-redis"/>
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
This repository is licensed under the "MIT" license. See [LICENSE](LICENSE).
|
||||
# redis
|
||||
The sources and docs for this package are in the main [node-redis](https://github.com/redis/node-redis) repo.
|
||||
|
@@ -5,9 +5,9 @@
|
||||
Node Redis is generally backwards compatible with very few exceptions, so we recommend users to always use the latest version to experience stability, performance and security.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| 3.1.x | :white_check_mark: |
|
||||
|---------|--------------------|
|
||||
| 4.0.z | :heavy_check_mark: |
|
||||
| 3.1.z | :heavy_check_mark: |
|
||||
| < 3.1 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
1
benchmark/.gitignore
vendored
1
benchmark/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
node_modules
|
@@ -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);
|
||||
}
|
||||
});
|
||||
})
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
849
benchmark/package-lock.json
generated
849
benchmark/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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:../"
|
||||
}
|
||||
}
|
@@ -15,7 +15,7 @@
|
||||
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
|
||||
| password | | ACL password or the old "--requirepass" password |
|
||||
| database | | Database number to connect to (see [`SELECT`](https://redis.io/commands/select) command) |
|
||||
| modules | | Object defining which [Redis Modules](https://redis.io/modules) to include (TODO - document) |
|
||||
| modules | | Object defining which [Redis Modules](../.github/README.md#packages) to include |
|
||||
| scripts | | Object defining Lua Scripts to use with this client (see [Lua Scripts](../README.md#lua-scripts)) |
|
||||
| commandsQueueMaxLength | | Maximum length of the client's internal command queue |
|
||||
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
|
||||
|
56
docs/clustering.md
Normal file
56
docs/clustering.md
Normal 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.
|
@@ -10,7 +10,7 @@ Below are several examples of how to use isolated execution.
|
||||
|
||||
> NOTE: Behind the scences we're using [`generic-pool`](https://www.npmjs.com/package/generic-pool) to provide a pool of connections that can be isolated. Go there to learn more.
|
||||
|
||||
## The Simple Secnario
|
||||
## The Simple Scenario
|
||||
|
||||
This just isolates execution on a single connection. Do what you want with that connection:
|
||||
|
||||
|
1
examples/.gitignore
vendored
Normal file
1
examples/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
package-lock.json
|
76
examples/README.md
Normal file
76
examples/README.md
Normal 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();
|
||||
```
|
32
examples/blocking-list-pop.js
Normal file
32
examples/blocking-list-pop.js
Normal 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();
|
31
examples/command-with-modifiers.js
Normal file
31
examples/command-with-modifiers.js
Normal 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();
|
||||
|
30
examples/connect-as-acl-user.js
Normal file
30
examples/connect-as-acl-user.js
Normal 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();
|
31
examples/lua-multi-incr.js
Normal file
31
examples/lua-multi-incr.js
Normal 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
12
examples/package.json
Normal 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
74
examples/search+json.js
Normal 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
19
examples/set-scan.js
Normal 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();
|
35
index.ts
35
index.ts
@@ -1,8 +1,33 @@
|
||||
import RedisClient from './lib/client';
|
||||
import RedisCluster from './lib/cluster';
|
||||
import { createClient as _createClient, createCluster as _createCluster } from '@node-redis/client';
|
||||
import { RedisScripts } from '@node-redis/client/dist/lib/commands';
|
||||
import { RedisClientOptions, RedisClientType } from '@node-redis/client/dist/lib/client';
|
||||
import { RedisClusterOptions, RedisClusterType } from '@node-redis/client/dist/lib/cluster';
|
||||
import RedisJSON from '@node-redis/json';
|
||||
import RediSearch from '@node-redis/search';
|
||||
|
||||
export const createClient = RedisClient.create;
|
||||
export * from '@node-redis/client';
|
||||
export * from '@node-redis/json';
|
||||
export * from '@node-redis/search';
|
||||
|
||||
export const commandOptions = RedisClient.commandOptions;
|
||||
const modules = {
|
||||
json: RedisJSON,
|
||||
ft: RediSearch
|
||||
};
|
||||
|
||||
export const createCluster = RedisCluster.create;
|
||||
export function createClient<S extends RedisScripts = Record<string, never>>(
|
||||
options?: Omit<RedisClientOptions<never, S>, 'modules'>
|
||||
): RedisClientType<typeof modules, S> {
|
||||
return _createClient({
|
||||
...options,
|
||||
modules
|
||||
});
|
||||
}
|
||||
|
||||
export function createCluster<S extends RedisScripts = Record<string, never>>(
|
||||
options: Omit<RedisClusterOptions<never, S>, 'modules'>
|
||||
): RedisClusterType<typeof modules, S> {
|
||||
return _createCluster({
|
||||
...options,
|
||||
modules
|
||||
});
|
||||
}
|
||||
|
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
@@ -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: ['*']
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
@@ -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]
|
||||
});
|
||||
});
|
@@ -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]
|
||||
});
|
||||
});
|
@@ -1,7 +0,0 @@
|
||||
import { transformReplyTuples } from './generic-transformers';
|
||||
|
||||
export function transformArguments(parameter: string): Array<string> {
|
||||
return ['CONFIG', 'GET', parameter];
|
||||
}
|
||||
|
||||
export const transformReply = transformReplyTuples;
|
@@ -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;
|
@@ -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)
|
||||
}
|
15
lib/ts-declarations/redis-parser.d.ts
vendored
15
lib/ts-declarations/redis-parser.d.ts
vendored
@@ -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;
|
||||
}
|
||||
}
|
3377
package-lock.json
generated
3377
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
65
package.json
65
package.json
@@ -1,66 +1,37 @@
|
||||
{
|
||||
"name": "redis",
|
||||
"version": "4.0.0-rc.3",
|
||||
"description": "A high performance Redis client.",
|
||||
"keywords": [
|
||||
"database",
|
||||
"redis",
|
||||
"pubsub"
|
||||
],
|
||||
"author": "Matt Ranney <mjr@ranney.com>",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Mike Diarmid (Salakar)",
|
||||
"url": "https://github.com/salakar"
|
||||
},
|
||||
{
|
||||
"name": "Ruben Bridgewater (BridgeAR)",
|
||||
"url": "https://github.com/BridgeAR"
|
||||
}
|
||||
],
|
||||
"version": "4.0.0-rc.4",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "nyc -r text-summary -r html mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
|
||||
"test": "npm run test -ws --if-present",
|
||||
"build:client": "npm run build -w ./packages/client",
|
||||
"build:test-utils": "npm run build -w ./packages/test-utils",
|
||||
"build:tests-tools": "npm run build:client && npm run build:test-utils",
|
||||
"build:modules": "find ./packages -mindepth 1 -maxdepth 1 -type d ! -name 'client' ! -name 'test-utils' -exec npm run build -w {} \\;",
|
||||
"build": "tsc",
|
||||
"documentation": "typedoc"
|
||||
"build-all": "npm run build:client && npm run build:test-utils && npm run build:modules && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.0",
|
||||
"generic-pool": "3.8.2",
|
||||
"redis-parser": "3.0.0",
|
||||
"yallist": "4.0.0"
|
||||
"@node-redis/client": "^1.0.0-rc.0",
|
||||
"@node-redis/json": "^1.0.0-rc.0",
|
||||
"@node-redis/search": "^1.0.0-rc.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@tsconfig/node12": "^1.0.9",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/sinon": "^10.0.4",
|
||||
"@types/which": "^2.0.1",
|
||||
"@types/yallist": "^4.0.1",
|
||||
"mocha": "^9.1.2",
|
||||
"nyc": "^15.1.0",
|
||||
"release-it": "^14.11.6",
|
||||
"sinon": "^11.1.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"ts-node": "^10.3.0",
|
||||
"typedoc": "^0.22.5",
|
||||
"typedoc-github-wiki-theme": "^0.6.0",
|
||||
"typedoc-plugin-markdown": "^3.11.3",
|
||||
"typescript": "^4.4.3",
|
||||
"which": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"release-it": "^14.11.7",
|
||||
"typescript": "^4.4.4"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/NodeRedis/node-redis.git"
|
||||
"url": "git://github.com/redis/node-redis.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/NodeRedis/node-redis/issues"
|
||||
"url": "https://github.com/redis/node-redis/issues"
|
||||
},
|
||||
"homepage": "https://github.com/NodeRedis/node-redis"
|
||||
"homepage": "https://github.com/redis/node-redis"
|
||||
}
|
||||
|
15
packages/client/.eslintrc.json
Normal file
15
packages/client/.eslintrc.json
Normal 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
1
packages/client/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
documentation/
|
10
packages/client/.npmignore
Normal file
10
packages/client/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.nyc_output/
|
||||
coverage/
|
||||
documentation/
|
||||
lib/
|
||||
.eslintrc.json
|
||||
.nycrc.json
|
||||
.release-it.json
|
||||
dump.rdb
|
||||
index.ts
|
||||
tsconfig.json
|
4
packages/client/.nycrc.json
Normal file
4
packages/client/.nycrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"exclude": ["**/*.spec.ts", "lib/test-utils.ts", "examples/*"]
|
||||
}
|
10
packages/client/.release-it.json
Normal file
10
packages/client/.release-it.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"git": {
|
||||
"tagName": "client@${version}",
|
||||
"commitMessage": "Release ${tagName}",
|
||||
"tagAnnotation": "Release ${tagName}"
|
||||
},
|
||||
"npm": {
|
||||
"publishArgs": ["--access", "public"]
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@
|
||||
|
||||
## v4.0.0
|
||||
|
||||
This version is a major change and refactor, adding modern JavaScript capabilities and multiple breaking changes. See the [migration guide](./docs/v3-to-v4.md) for tips on how to upgrade.
|
||||
This version is a major change and refactor, adding modern JavaScript capabilities and multiple breaking changes. See the [migration guide](../../docs/v3-to-v4.md) for tips on how to upgrade.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -17,12 +17,35 @@ This version is a major change and refactor, adding modern JavaScript capabiliti
|
||||
|
||||
- Added support for Promises
|
||||
- Added built-in TypeScript declaration files enabling code completion
|
||||
- Added support for [clustering](./README.md#cluster)
|
||||
- Added idiomatic arguments and responses to [Redis commands](./README.md#redis-commands)
|
||||
- Added full support for [Lua Scripts](./README.md#lua-scripts)
|
||||
- Added support for [SCAN iterators](./README.md#scan-iterator)
|
||||
- Added support for [clustering](../../.github/README.md#cluster)
|
||||
- Added idiomatic arguments and responses to [Redis commands](../../.github/README.md#redis-commands)
|
||||
- Added full support for [Lua Scripts](../../.github/README.md#lua-scripts)
|
||||
- Added support for [SCAN iterators](../../.github/README.md#scan-iterator)
|
||||
- Added the ability to extend Node Redis with Redis Module commands
|
||||
|
||||
## v3.1.2
|
||||
|
||||
### Fixes
|
||||
|
||||
- Exclude unnecessary files from tarball
|
||||
|
||||
## v3.1.1
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Upgrade node and dependencies
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix a potential exponential regex in monitor mode
|
||||
|
||||
## v3.1.0 - 31 Mar, 2021
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Upgrade node and dependencies and redis-commands to support Redis 6
|
||||
- Add support for Redis 6 `auth pass [user]`
|
||||
|
||||
## v3.0.0 - 09 Feb, 2020
|
||||
|
||||
This version is mainly a release to distribute all the unreleased changes on master since 2017 and additionally removes
|
2
packages/client/README.md
Normal file
2
packages/client/README.md
Normal 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
10
packages/client/index.ts
Normal 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';
|
@@ -1,25 +1,29 @@
|
||||
import LinkedList from 'yallist';
|
||||
import RedisParser from 'redis-parser';
|
||||
import * as LinkedList from 'yallist';
|
||||
import { AbortError } from '../errors';
|
||||
import { RedisCommandRawReply } from '../commands';
|
||||
import { RedisCommandArguments, RedisCommandRawReply } from '../commands';
|
||||
|
||||
// We need to use 'require', because it's not possible with Typescript to import
|
||||
// classes that are exported as 'module.exports = class`, without esModuleInterop
|
||||
// set to true.
|
||||
const RedisParser = require('redis-parser');
|
||||
|
||||
export interface QueueCommandOptions {
|
||||
asap?: boolean;
|
||||
chainId?: symbol;
|
||||
signal?: any; // TODO: `AbortSignal` type is incorrect
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
interface CommandWaitingToBeSent extends CommandWaitingForReply {
|
||||
args: Array<string | Buffer>;
|
||||
args: RedisCommandArguments;
|
||||
chainId?: symbol;
|
||||
abort?: {
|
||||
signal: any; // TODO: `AbortSignal` type is incorrect
|
||||
signal: AbortSignal;
|
||||
listener(): void;
|
||||
};
|
||||
}
|
||||
|
||||
interface CommandWaitingForReply {
|
||||
resolve(reply?: any): void;
|
||||
resolve(reply?: unknown): void;
|
||||
reject(err: Error): void;
|
||||
channelsCounter?: number;
|
||||
bufferMode?: boolean;
|
||||
@@ -107,7 +111,7 @@ export default class RedisCommandsQueue {
|
||||
this.#maxLength = maxLength;
|
||||
}
|
||||
|
||||
addCommand<T = RedisCommandRawReply>(args: Array<string | Buffer>, options?: QueueCommandOptions, bufferMode?: boolean): Promise<T> {
|
||||
addCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: QueueCommandOptions, bufferMode?: boolean): Promise<T> {
|
||||
if (this.#pubSubState.subscribing || this.#pubSubState.subscribed) {
|
||||
return Promise.reject(new Error('Cannot send commands in PubSub mode'));
|
||||
} else if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
|
||||
@@ -135,7 +139,8 @@ export default class RedisCommandsQueue {
|
||||
signal: options.signal,
|
||||
listener
|
||||
};
|
||||
options.signal.addEventListener('abort', listener, {
|
||||
// AbortSignal type is incorrent
|
||||
(options.signal as any).addEventListener('abort', listener, {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
@@ -246,7 +251,7 @@ export default class RedisCommandsQueue {
|
||||
]);
|
||||
}
|
||||
|
||||
getCommandToSend(): Array<string | Buffer> | undefined {
|
||||
getCommandToSend(): RedisCommandArguments | undefined {
|
||||
const toSend = this.#waitingToBeSent.shift();
|
||||
|
||||
if (toSend) {
|
@@ -229,5 +229,5 @@ export default {
|
||||
UNWATCH,
|
||||
unwatch: UNWATCH,
|
||||
WAIT,
|
||||
wait: WAIT,
|
||||
wait: WAIT
|
||||
};
|
@@ -1,11 +1,12 @@
|
||||
import { strict as assert, AssertionError } from 'assert';
|
||||
import { once } from 'events';
|
||||
import { itWithClient, TEST_REDIS_SERVERS, TestRedisServers, waitTillBeenCalled, isRedisVersionGreaterThan } from '../test-utils';
|
||||
import RedisClient from '.';
|
||||
import { AbortError, ClientClosedError, ConnectionTimeoutError, WatchError } from '../errors';
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
|
||||
import RedisClient, { ClientLegacyCommandArguments, RedisClientType } from '.';
|
||||
import { RedisClientMultiCommandType } from './multi-command';
|
||||
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands';
|
||||
import { AbortError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
|
||||
import { defineScript } from '../lua-script';
|
||||
import { spy } from 'sinon';
|
||||
import { RedisNetSocketOptions } from '../client/socket';
|
||||
import { once } from 'events';
|
||||
|
||||
export const SQUARE_SCRIPT = defineScript({
|
||||
NUMBER_OF_KEYS: 0,
|
||||
@@ -75,204 +76,242 @@ describe('Client', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('createClient with url', async () => {
|
||||
const client = RedisClient.create({
|
||||
url: `redis://localhost:${(TEST_REDIS_SERVERS[TestRedisServers.OPEN].socket as RedisNetSocketOptions)!.port!.toString()}/1`
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
assert.equal(
|
||||
await client.ping(),
|
||||
'PONG'
|
||||
);
|
||||
} finally {
|
||||
await client.disconnect();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
describe('authentication', () => {
|
||||
itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => {
|
||||
testUtils.testWithClient('Client should be authenticated', async client => {
|
||||
assert.equal(
|
||||
await client.ping(),
|
||||
'PONG'
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.PASSWORD);
|
||||
|
||||
it('should not retry connecting if failed due to wrong auth', async () => {
|
||||
const client = RedisClient.create({
|
||||
...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD],
|
||||
password: 'wrongpassword'
|
||||
});
|
||||
testUtils.testWithClient('should not retry connecting if failed due to wrong auth', async client => {
|
||||
let message;
|
||||
if (testUtils.isVersionGreaterThan([6, 2])) {
|
||||
message = 'WRONGPASS invalid username-password pair or user is disabled.';
|
||||
} else if (testUtils.isVersionGreaterThan([6])) {
|
||||
message = 'WRONGPASS invalid username-password pair';
|
||||
} else {
|
||||
message = 'ERR invalid password';
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
client.connect(),
|
||||
{
|
||||
message: isRedisVersionGreaterThan([6]) ?
|
||||
'WRONGPASS invalid username-password pair or user is disabled.' :
|
||||
'ERR invalid password'
|
||||
}
|
||||
{ message }
|
||||
);
|
||||
|
||||
assert.equal(client.isOpen, false);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.PASSWORD,
|
||||
clientOptions: {
|
||||
password: 'wrongpassword'
|
||||
},
|
||||
disableClientSetup: true
|
||||
});
|
||||
|
||||
testUtils.testWithClient('should execute AUTH before SELECT', async client => {
|
||||
assert.equal(
|
||||
(await client.clientInfo()).db,
|
||||
2
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.PASSWORD,
|
||||
clientOptions: {
|
||||
...GLOBAL.SERVERS.PASSWORD.clientOptions,
|
||||
database: 2
|
||||
},
|
||||
minimumDockerVersion: [6, 2]
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacyMode', () => {
|
||||
const client = RedisClient.create({
|
||||
...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
},
|
||||
function sendCommandAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, args: RedisCommandArguments): Promise<RedisCommandRawReply> {
|
||||
return new Promise((resolve, reject) => {
|
||||
(client as any).sendCommand(args, (err: Error | undefined, reply: RedisCommandRawReply) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
resolve(reply);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
testUtils.testWithClient('client.sendCommand should call the callback', async client => {
|
||||
assert.equal(
|
||||
await sendCommandAsync(client, ['PING']),
|
||||
'PONG'
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
});
|
||||
|
||||
before(() => client.connect());
|
||||
afterEach(() => client.v4.flushAll());
|
||||
after(() => client.disconnect());
|
||||
|
||||
it('client.sendCommand should call the callback', done => {
|
||||
(client as any).sendCommand('PING', (err?: Error, reply?: string) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
try {
|
||||
assert.equal(reply, 'PONG');
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('client.sendCommand should work without callback', async () => {
|
||||
(client as any).sendCommand('PING');
|
||||
testUtils.testWithClient('client.sendCommand should work without callback', async client => {
|
||||
client.sendCommand(['PING']);
|
||||
await client.v4.ping(); // make sure the first command was replied
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
it('client.v4.sendCommand should return a promise', async () => {
|
||||
testUtils.testWithClient('client.v4.sendCommand should return a promise', async client => {
|
||||
assert.equal(
|
||||
await client.v4.sendCommand(['PING']),
|
||||
'PONG'
|
||||
);
|
||||
});
|
||||
|
||||
it('client.{command} should accept vardict arguments', 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);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('client.{command} should accept arguments array', done => {
|
||||
(client as any).set(['a', 'b'], (err?: Error, reply?: string) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: ClientLegacyCommandArguments): Promise<RedisCommandRawReply> {
|
||||
return new Promise((resolve, reject) => {
|
||||
(client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
resolve(reply);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
assert.equal(reply, 'OK');
|
||||
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.{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);
|
||||
testUtils.testWithClient('client.{command} should accept arguments array', async client => {
|
||||
assert.equal(
|
||||
await setAsync(client, ['a', 'b']),
|
||||
'OK'
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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 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
|
||||
}
|
||||
});
|
||||
|
||||
it('client.multi.ping.exec should work without callback', async () => {
|
||||
(client as any).multi()
|
||||
testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => {
|
||||
client.multi()
|
||||
.ping()
|
||||
.exec();
|
||||
await client.v4.ping(); // make sure the first command was replied
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
it('client.multi.ping.v4.ping.v4.exec should return a promise', async () => {
|
||||
testUtils.testWithClient('client.multi.ping.v4.ping.v4.exec should return a promise', async client => {
|
||||
assert.deepEqual(
|
||||
await ((client as any).multi()
|
||||
await client.multi()
|
||||
.ping()
|
||||
.v4.ping()
|
||||
.v4.exec()),
|
||||
.v4.exec(),
|
||||
['PONG', 'PONG']
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
it('client.{script} should return a promise', async () => {
|
||||
assert.equal(await client.square(2), 4);
|
||||
testUtils.testWithClient('client.{script} should return a promise', async client => {
|
||||
assert.equal(
|
||||
await client.square(2),
|
||||
4
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true,
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
it('connect, ready, end', async () => {
|
||||
const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
|
||||
|
||||
testUtils.testWithClient('connect, ready, end', async client => {
|
||||
await Promise.all([
|
||||
client.connect(),
|
||||
once(client, 'connect'),
|
||||
once(client, 'ready')
|
||||
once(client, 'ready'),
|
||||
client.connect()
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
client.disconnect(),
|
||||
once(client, 'end')
|
||||
once(client, 'end'),
|
||||
client.disconnect()
|
||||
]);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
disableClientSetup: true
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendCommand', () => {
|
||||
itWithClient(TestRedisServers.OPEN, 'PING', async client => {
|
||||
testUtils.testWithClient('PING', async client => {
|
||||
assert.equal(await client.sendCommand(['PING']), 'PONG');
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'bufferMode', async client => {
|
||||
testUtils.testWithClient('bufferMode', async client => {
|
||||
assert.deepEqual(
|
||||
await client.sendCommand(['PING'], undefined, true),
|
||||
Buffer.from('PONG')
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
describe('AbortController', () => {
|
||||
before(function () {
|
||||
@@ -281,13 +320,13 @@ describe('Client', () => {
|
||||
}
|
||||
});
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'success', async client => {
|
||||
testUtils.testWithClient('success', async client => {
|
||||
await client.sendCommand(['PING'], {
|
||||
signal: new AbortController().signal
|
||||
});
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'AbortError', client => {
|
||||
testUtils.testWithClient('AbortError', client => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
@@ -297,12 +336,12 @@ describe('Client', () => {
|
||||
}),
|
||||
AbortError
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi', () => {
|
||||
itWithClient(TestRedisServers.OPEN, 'simple', async client => {
|
||||
testUtils.testWithClient('simple', async client => {
|
||||
assert.deepEqual(
|
||||
await client.multi()
|
||||
.ping()
|
||||
@@ -311,44 +350,35 @@ describe('Client', () => {
|
||||
.exec(),
|
||||
['PONG', 'OK', 'value']
|
||||
);
|
||||
});
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'should reject the whole chain on error', client => {
|
||||
client.on('error', () => {
|
||||
// ignore errors
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('should reject the whole chain on error', client => {
|
||||
return assert.rejects(
|
||||
client.multi()
|
||||
.ping()
|
||||
.addCommand(['DEBUG', 'RESTART'])
|
||||
.addCommand(['INVALID COMMAND'])
|
||||
.ping()
|
||||
.exec()
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
it('with script', async () => {
|
||||
const client = RedisClient.create({
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
testUtils.testWithClient('with script', async client => {
|
||||
assert.deepEqual(
|
||||
await client.multi()
|
||||
.square(2)
|
||||
.exec(),
|
||||
[4]
|
||||
);
|
||||
} finally {
|
||||
await client.disconnect();
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'WatchError', async client => {
|
||||
testUtils.testWithClient('WatchError', async client => {
|
||||
await client.watch('key');
|
||||
|
||||
await client.set(
|
||||
@@ -365,39 +395,40 @@ describe('Client', () => {
|
||||
.exec(),
|
||||
WatchError
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'execAsPipeline', async client => {
|
||||
testUtils.testWithClient('execAsPipeline', async client => {
|
||||
assert.deepEqual(
|
||||
await client.multi()
|
||||
.ping()
|
||||
.exec(true),
|
||||
['PONG']
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
||||
it('scripts', async () => {
|
||||
const client = RedisClient.create({
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
testUtils.testWithClient('scripts', async client => {
|
||||
assert.equal(
|
||||
await client.square(2),
|
||||
4
|
||||
);
|
||||
} finally {
|
||||
await client.disconnect();
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('modules', async () => {
|
||||
const client = RedisClient.create({
|
||||
testUtils.testWithClient('modules', async client => {
|
||||
assert.equal(
|
||||
await client.module.echo('message'),
|
||||
'message'
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
modules: {
|
||||
module: {
|
||||
echo: {
|
||||
@@ -410,21 +441,10 @@ describe('Client', () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
assert.equal(
|
||||
await client.module.echo('message'),
|
||||
'message'
|
||||
);
|
||||
} finally {
|
||||
await client.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'executeIsolated', async client => {
|
||||
testUtils.testWithClient('executeIsolated', async client => {
|
||||
await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
|
||||
|
||||
assert.equal(
|
||||
@@ -433,35 +453,35 @@ describe('Client', () => {
|
||||
),
|
||||
null
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'should reconnect after DEBUG RESTART', async client => {
|
||||
client.on('error', () => {
|
||||
// ignore errors
|
||||
});
|
||||
async function killClient<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>): Promise<void> {
|
||||
const onceErrorPromise = once(client, 'error');
|
||||
await client.sendCommand(['QUIT']);
|
||||
await Promise.all([
|
||||
onceErrorPromise,
|
||||
assert.rejects(client.ping(), SocketClosedUnexpectedlyError)
|
||||
]);
|
||||
}
|
||||
|
||||
await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
|
||||
await assert.rejects(client.sendCommand(['DEBUG', 'RESTART']));
|
||||
assert.ok(await client.sendCommand(['CLIENT', 'GETNAME']) === null);
|
||||
});
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'should SELECT db after reconnection', async client => {
|
||||
client.on('error', () => {
|
||||
// ignore errors
|
||||
});
|
||||
testUtils.testWithClient('should reconnect when socket disconnects', async client => {
|
||||
await killClient(client);
|
||||
await assert.doesNotReject(client.ping());
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('should remember selected db', async client => {
|
||||
await client.select(1);
|
||||
await assert.rejects(client.sendCommand(['DEBUG', 'RESTART']));
|
||||
await killClient(client);
|
||||
assert.equal(
|
||||
(await client.clientInfo()).db,
|
||||
1
|
||||
);
|
||||
}, {
|
||||
// because of CLIENT INFO
|
||||
minimumRedisVersion: [6, 2]
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
minimumDockerVersion: [6, 2] // CLIENT INFO
|
||||
});
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'scanIterator', async client => {
|
||||
testUtils.testWithClient('scanIterator', async client => {
|
||||
const promises = [],
|
||||
keys = new Set();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
@@ -478,9 +498,9 @@ describe('Client', () => {
|
||||
}
|
||||
|
||||
assert.deepEqual(keys, results);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'hScanIterator', async client => {
|
||||
testUtils.testWithClient('hScanIterator', async client => {
|
||||
const hash: Record<string, string> = {};
|
||||
for (let i = 0; i < 100; i++) {
|
||||
hash[i.toString()] = i.toString();
|
||||
@@ -494,9 +514,9 @@ describe('Client', () => {
|
||||
}
|
||||
|
||||
assert.deepEqual(hash, results);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'sScanIterator', async client => {
|
||||
testUtils.testWithClient('sScanIterator', async client => {
|
||||
const members = new Set<string>();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
members.add(i.toString());
|
||||
@@ -510,9 +530,9 @@ describe('Client', () => {
|
||||
}
|
||||
|
||||
assert.deepEqual(members, results);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'zScanIterator', async client => {
|
||||
testUtils.testWithClient('zScanIterator', async client => {
|
||||
const members = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
members.push({
|
||||
@@ -538,9 +558,9 @@ describe('Client', () => {
|
||||
[...map.entries()].sort(sort),
|
||||
members.map<MemberTuple>(member => [member.value, member.score]).sort(sort)
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'PubSub', async publisher => {
|
||||
testUtils.testWithClient('PubSub', async publisher => {
|
||||
const subscriber = publisher.duplicate();
|
||||
|
||||
await subscriber.connect();
|
||||
@@ -603,49 +623,59 @@ describe('Client', () => {
|
||||
} finally {
|
||||
await subscriber.disconnect();
|
||||
}
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
it('ConnectionTimeoutError', async () => {
|
||||
const client = RedisClient.create({
|
||||
socket: {
|
||||
...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
|
||||
connectTimeout: 1
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
testUtils.testWithClient('ConnectionTimeoutError', async client => {
|
||||
const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
|
||||
start = process.hrtime.bigint();
|
||||
|
||||
while (process.hrtime.bigint() - start < 1_000_000) {
|
||||
// block the event loop for 1ms, to make sure the connection will timeout
|
||||
while (process.hrtime.bigint() - start < 1_000_000) {}
|
||||
}
|
||||
|
||||
await promise;
|
||||
} catch (err) {
|
||||
if (err instanceof AssertionError) {
|
||||
await client.disconnect();
|
||||
}
|
||||
|
||||
throw err;
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
socket: {
|
||||
connectTimeout: 1
|
||||
}
|
||||
},
|
||||
disableClientSetup: true
|
||||
});
|
||||
|
||||
it('client.quit', async () => {
|
||||
const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
|
||||
|
||||
testUtils.testWithClient('client.quit', async client => {
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
const quitPromise = client.quit();
|
||||
const pingPromise = client.ping(),
|
||||
quitPromise = client.quit();
|
||||
assert.equal(client.isOpen, false);
|
||||
await Promise.all([
|
||||
quitPromise,
|
||||
|
||||
const [ping] = await Promise.all([
|
||||
pingPromise,
|
||||
assert.doesNotReject(quitPromise),
|
||||
assert.rejects(client.ping(), ClientClosedError)
|
||||
]);
|
||||
} finally {
|
||||
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
|
||||
});
|
||||
});
|
@@ -4,14 +4,14 @@ import RedisSocket, { RedisSocketOptions, RedisNetSocketOptions, RedisTlsSocketO
|
||||
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
|
||||
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
|
||||
import { RedisMultiQueuedCommand } from '../multi-command';
|
||||
import EventEmitter from 'events';
|
||||
import { EventEmitter } from 'events';
|
||||
import { CommandOptions, commandOptions, isCommandOptions } from '../command-options';
|
||||
import { ScanOptions, ZMember } from '../commands/generic-transformers';
|
||||
import { ScanCommandOptions } from '../commands/SCAN';
|
||||
import { HScanTuple } from '../commands/HSCAN';
|
||||
import { encodeCommand, extendWithCommands, extendWithModulesAndScripts, transformCommandArguments, transformCommandReply } from '../commander';
|
||||
import { extendWithCommands, extendWithModulesAndScripts, LegacyCommandArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
|
||||
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
||||
import { ClientClosedError } from '../errors';
|
||||
import { ClientClosedError, DisconnectsClientError } from '../errors';
|
||||
import { URL } from 'url';
|
||||
|
||||
export interface RedisClientOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
|
||||
@@ -34,16 +34,16 @@ type WithCommands = {
|
||||
};
|
||||
|
||||
export type WithModules<M extends RedisModules> = {
|
||||
[P in keyof M]: {
|
||||
[P in keyof M as M[P] extends never ? never : P]: {
|
||||
[C in keyof M[P]]: RedisClientCommandSignature<M[P][C]>;
|
||||
};
|
||||
};
|
||||
|
||||
export type WithScripts<S extends RedisScripts> = {
|
||||
[P in keyof S]: RedisClientCommandSignature<S[P]>;
|
||||
[P in keyof S as S[P] extends never ? never : P]: RedisClientCommandSignature<S[P]>;
|
||||
};
|
||||
|
||||
export type RedisClientType<M extends RedisModules = {}, S extends RedisScripts = {}> =
|
||||
export type RedisClientType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
|
||||
RedisClient<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
|
||||
|
||||
export type InstantiableRedisClient<M extends RedisModules, S extends RedisScripts> =
|
||||
@@ -53,12 +53,15 @@ export interface ClientCommandOptions extends QueueCommandOptions {
|
||||
isolated?: boolean;
|
||||
}
|
||||
|
||||
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
|
||||
|
||||
export type ClientLegacyCommandArguments = LegacyCommandArguments | [...LegacyCommandArguments, ClientLegacyCallback];
|
||||
export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
|
||||
static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> {
|
||||
return commandOptions(options);
|
||||
}
|
||||
|
||||
static extend<M extends RedisModules = {}, S extends RedisScripts = {}>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
|
||||
static extend<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
|
||||
const Client = <any>extendWithModulesAndScripts({
|
||||
BaseClass: RedisClient,
|
||||
modules: plugins?.modules,
|
||||
@@ -74,14 +77,14 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
return Client;
|
||||
}
|
||||
|
||||
static create<M extends RedisModules = {}, S extends RedisScripts = {}>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
|
||||
static create<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
|
||||
return new (RedisClient.extend(options))(options);
|
||||
}
|
||||
|
||||
static parseURL(url: string): RedisClientOptions<{}, {}> {
|
||||
static parseURL(url: string): RedisClientOptions<Record<string, never>, Record<string, never>> {
|
||||
// https://www.iana.org/assignments/uri-schemes/prov/redis
|
||||
const { hostname, port, protocol, username, password, pathname } = new URL(url),
|
||||
parsed: RedisClientOptions<{}, {}> = {
|
||||
parsed: RedisClientOptions<Record<string, never>, Record<string, never>> = {
|
||||
socket: {
|
||||
host: hostname
|
||||
}
|
||||
@@ -98,11 +101,11 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
}
|
||||
|
||||
if (username) {
|
||||
parsed.username = username;
|
||||
parsed.username = decodeURIComponent(username);
|
||||
}
|
||||
|
||||
if (password) {
|
||||
parsed.password = password;
|
||||
parsed.password = decodeURIComponent(password);
|
||||
}
|
||||
|
||||
if (pathname.length > 1) {
|
||||
@@ -177,28 +180,47 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
|
||||
#initiateSocket(): RedisSocket {
|
||||
const socketInitiator = async (): Promise<void> => {
|
||||
const v4Commands = this.#options?.legacyMode ? this.#v4 : this,
|
||||
promises = [];
|
||||
const promises = [];
|
||||
|
||||
if (this.#selectedDB !== 0) {
|
||||
promises.push(v4Commands.select(RedisClient.commandOptions({ asap: true }), this.#selectedDB));
|
||||
promises.push(
|
||||
this.#queue.addCommand(
|
||||
['SELECT', this.#selectedDB.toString()],
|
||||
{ asap: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.#options?.readonly) {
|
||||
promises.push(v4Commands.readonly(RedisClient.commandOptions({ asap: true })));
|
||||
promises.push(
|
||||
this.#queue.addCommand(
|
||||
COMMANDS.READONLY.transformArguments(),
|
||||
{ asap: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.#options?.username || this.#options?.password) {
|
||||
promises.push(v4Commands.auth(RedisClient.commandOptions({ asap: true }), this.#options));
|
||||
promises.push(
|
||||
this.#queue.addCommand(
|
||||
COMMANDS.AUTH.transformArguments({
|
||||
username: this.#options.username,
|
||||
password: this.#options.password ?? ''
|
||||
}),
|
||||
{ asap: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const resubscribePromise = this.#queue.resubscribe();
|
||||
if (resubscribePromise) {
|
||||
promises.push(resubscribePromise);
|
||||
this.#tick();
|
||||
}
|
||||
|
||||
if (promises.length) {
|
||||
this.#tick(true);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
};
|
||||
|
||||
return new RedisSocket(socketInitiator, this.#options?.socket)
|
||||
@@ -213,6 +235,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
this.#tick();
|
||||
})
|
||||
.on('reconnecting', () => this.emit('reconnecting'))
|
||||
.on('drain', () => this.#tick())
|
||||
.on('end', () => this.emit('end'));
|
||||
}
|
||||
|
||||
@@ -224,11 +247,14 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
if (!this.#options?.legacyMode) return;
|
||||
|
||||
(this as any).#v4.sendCommand = this.#sendCommand.bind(this);
|
||||
(this as any).sendCommand = (...args: Array<unknown>): void => {
|
||||
const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] as Function : undefined,
|
||||
actualArgs = !callback ? args : args.slice(0, -1);
|
||||
this.#sendCommand(actualArgs.flat() as Array<string>)
|
||||
.then((reply: unknown) => {
|
||||
(this as any).sendCommand = (...args: ClientLegacyCommandArguments): void => {
|
||||
let callback: ClientLegacyCallback;
|
||||
if (typeof args[args.length - 1] === 'function') {
|
||||
callback = args.pop() as ClientLegacyCallback;
|
||||
}
|
||||
|
||||
this.#sendCommand(transformLegacyCommandArguments(args as LegacyCommandArguments))
|
||||
.then((reply: RedisCommandRawReply) => {
|
||||
if (!callback) return;
|
||||
|
||||
// https://github.com/NodeRedis/node-redis#commands:~:text=minimal%20parsing
|
||||
@@ -297,9 +323,9 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
}
|
||||
|
||||
// using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
|
||||
async #sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
|
||||
#sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
|
||||
if (!this.#socket.isOpen) {
|
||||
throw new ClientClosedError();
|
||||
return Promise.reject(new ClientClosedError());
|
||||
}
|
||||
|
||||
if (options?.isolated) {
|
||||
@@ -313,7 +339,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
|
||||
const promise = this.#queue.addCommand<T>(args, options, bufferMode);
|
||||
this.#tick();
|
||||
return await promise;
|
||||
return promise;
|
||||
}
|
||||
|
||||
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> {
|
||||
@@ -400,33 +426,29 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
|
||||
QUIT(): Promise<void> {
|
||||
return this.#socket.quit(() => {
|
||||
const promise = this.#queue.addCommand(['QUIT']);
|
||||
const quitPromise = this.#queue.addCommand(['QUIT']);
|
||||
this.#tick();
|
||||
return promise;
|
||||
return Promise.all([
|
||||
quitPromise,
|
||||
this.#destroyIsolationPool()
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
quit = this.QUIT;
|
||||
|
||||
#tick(): void {
|
||||
if (!this.#socket.isSocketExists) {
|
||||
#tick(force = false): void {
|
||||
if (this.#socket.writableNeedDrain || (!force && !this.#socket.isReady)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#socket.cork();
|
||||
|
||||
while (true) {
|
||||
while (!this.#socket.writableNeedDrain) {
|
||||
const args = this.#queue.getCommandToSend();
|
||||
if (args === undefined) break;
|
||||
|
||||
let writeResult;
|
||||
for (const toWrite of encodeCommand(args)) {
|
||||
writeResult = this.#socket.write(toWrite);
|
||||
}
|
||||
|
||||
if (!writeResult) {
|
||||
break;
|
||||
}
|
||||
this.#socket.writeCommand(args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +485,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
for (const key of reply.keys) {
|
||||
yield key;
|
||||
}
|
||||
} while (cursor !== 0)
|
||||
} while (cursor !== 0);
|
||||
}
|
||||
|
||||
async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<HScanTuple> {
|
||||
@@ -474,7 +496,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
for (const tuple of reply.tuples) {
|
||||
yield tuple;
|
||||
}
|
||||
} while (cursor !== 0)
|
||||
} while (cursor !== 0);
|
||||
}
|
||||
|
||||
async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable<string> {
|
||||
@@ -485,7 +507,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
for (const member of reply.members) {
|
||||
yield member;
|
||||
}
|
||||
} while (cursor !== 0)
|
||||
} while (cursor !== 0);
|
||||
}
|
||||
|
||||
async* zScanIterator(key: string, options?: ScanOptions): AsyncIterable<ZMember> {
|
||||
@@ -496,15 +518,13 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
||||
for (const member of reply.members) {
|
||||
yield member;
|
||||
}
|
||||
} while (cursor !== 0)
|
||||
} while (cursor !== 0);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
this.#queue.flushAll(new Error('Disconnecting'));
|
||||
await Promise.all([
|
||||
this.#socket.disconnect(),
|
||||
this.#destroyIsolationPool()
|
||||
]);
|
||||
this.#queue.flushAll(new DisconnectsClientError());
|
||||
this.#socket.disconnect();
|
||||
await this.#destroyIsolationPool();
|
||||
}
|
||||
|
||||
async #destroyIsolationPool(): Promise<void> {
|
@@ -1,7 +1,7 @@
|
||||
import COMMANDS from './commands';
|
||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
||||
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
||||
import { extendWithCommands, extendWithModulesAndScripts } from '../commander';
|
||||
import { extendWithCommands, extendWithModulesAndScripts, LegacyCommandArguments, transformLegacyCommandArguments } from '../commander';
|
||||
|
||||
type RedisClientMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
|
||||
(...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, S>;
|
||||
@@ -11,16 +11,16 @@ type WithCommands<M extends RedisModules, S extends RedisScripts> = {
|
||||
};
|
||||
|
||||
type WithModules<M extends RedisModules, S extends RedisScripts> = {
|
||||
[P in keyof M]: {
|
||||
[P in keyof M as M[P] extends never ? never : P]: {
|
||||
[C in keyof M[P]]: RedisClientMultiCommandSignature<M[P][C], M, S>;
|
||||
};
|
||||
};
|
||||
|
||||
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
|
||||
[P in keyof S]: RedisClientMultiCommandSignature<S[P], M, S>
|
||||
[P in keyof S as S[P] extends never ? never : P]: RedisClientMultiCommandSignature<S[P], M, S>
|
||||
};
|
||||
|
||||
export type RedisClientMultiCommandType<M extends RedisModules = {}, S extends RedisScripts = {}> =
|
||||
export type RedisClientMultiCommandType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
|
||||
RedisClientMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
|
||||
|
||||
export type RedisClientMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
|
||||
@@ -52,8 +52,8 @@ export default class RedisClientMultiCommand {
|
||||
|
||||
#legacyMode(): void {
|
||||
this.v4.addCommand = this.addCommand.bind(this);
|
||||
(this as any).addCommand = (...args: Array<string | Buffer | Array<string | Buffer>>): this => {
|
||||
this.#multi.addCommand(args.flat());
|
||||
(this as any).addCommand = (...args: LegacyCommandArguments): this => {
|
||||
this.#multi.addCommand(transformLegacyCommandArguments(args));
|
||||
return this;
|
||||
};
|
||||
this.v4.exec = this.exec.bind(this);
|
@@ -33,6 +33,6 @@ describe('Socket', () => {
|
||||
return assert.rejects(socket.connect(), {
|
||||
message: '50'
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,7 +1,9 @@
|
||||
import EventEmitter from 'events';
|
||||
import net from 'net';
|
||||
import tls from 'tls';
|
||||
import { ConnectionTimeoutError, ClientClosedError } from '../errors';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as net from 'net';
|
||||
import * as tls from 'tls';
|
||||
import { encodeCommand } from '../commander';
|
||||
import { RedisCommandArguments } from '../commands';
|
||||
import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError } from '../errors';
|
||||
import { promiseTimeout } from '../utils';
|
||||
|
||||
export interface RedisSocketCommonOptions {
|
||||
@@ -20,7 +22,7 @@ export interface RedisUnixSocketOptions extends RedisSocketCommonOptions {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface RedisTlsSocketOptions extends RedisNetSocketOptions, tls.SecureContextOptions {
|
||||
export interface RedisTlsSocketOptions extends RedisNetSocketOptions, tls.SecureContextOptions, tls.CommonConnectionOptions {
|
||||
tls: true;
|
||||
}
|
||||
|
||||
@@ -72,8 +74,18 @@ export default class RedisSocket extends EventEmitter {
|
||||
return this.#isOpen;
|
||||
}
|
||||
|
||||
get isSocketExists(): boolean {
|
||||
return !!this.#socket;
|
||||
#isReady = false;
|
||||
|
||||
get isReady(): boolean {
|
||||
return this.#isReady;
|
||||
}
|
||||
|
||||
// `writable.writableNeedDrain` was added in v15.2.0 and therefore can't be used
|
||||
// https://nodejs.org/api/stream.html#stream_writable_writableneeddrain
|
||||
#writableNeedDrain = false;
|
||||
|
||||
get writableNeedDrain(): boolean {
|
||||
return this.#writableNeedDrain;
|
||||
}
|
||||
|
||||
constructor(initiator?: RedisSocketInitiator, options?: RedisSocketOptions) {
|
||||
@@ -85,33 +97,39 @@ export default class RedisSocket extends EventEmitter {
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (this.#isOpen) {
|
||||
throw new Error('Socket is connection/connecting');
|
||||
throw new Error('Socket already opened');
|
||||
}
|
||||
|
||||
this.#isOpen = true;
|
||||
|
||||
try {
|
||||
await this.#connect();
|
||||
} catch (err) {
|
||||
this.#isOpen = false;
|
||||
throw err;
|
||||
}
|
||||
return this.#connect();
|
||||
}
|
||||
|
||||
async #connect(hadError?: boolean): Promise<void> {
|
||||
this.#isOpen = true;
|
||||
this.#socket = await this.#retryConnection(0, hadError);
|
||||
this.#writableNeedDrain = false;
|
||||
|
||||
if (!this.#isOpen) {
|
||||
this.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('connect');
|
||||
|
||||
if (this.#initiator) {
|
||||
try {
|
||||
await this.#initiator();
|
||||
} catch (err) {
|
||||
this.#socket.end();
|
||||
this.#socket.destroy();
|
||||
this.#socket = undefined;
|
||||
this.#isOpen = false;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!this.#isOpen) return;
|
||||
}
|
||||
|
||||
this.#isReady = true;
|
||||
|
||||
this.emit('ready');
|
||||
}
|
||||
|
||||
@@ -160,10 +178,13 @@ export default class RedisSocket extends EventEmitter {
|
||||
.once('error', (err: Error) => this.#onSocketError(err))
|
||||
.once('close', hadError => {
|
||||
if (!hadError && this.#isOpen) {
|
||||
this.#onSocketError(new Error('Socket closed unexpectedly'));
|
||||
this.#onSocketError(new SocketClosedUnexpectedlyError());
|
||||
}
|
||||
})
|
||||
.on('drain', () => this.emit('drain'))
|
||||
.on('drain', () => {
|
||||
this.#writableNeedDrain = false;
|
||||
this.emit('drain');
|
||||
})
|
||||
.on('data', (data: Buffer) => this.emit('data', data));
|
||||
|
||||
resolve(socket);
|
||||
@@ -186,30 +207,32 @@ export default class RedisSocket extends EventEmitter {
|
||||
}
|
||||
|
||||
#onSocketError(err: Error): void {
|
||||
this.#socket = undefined;
|
||||
this.#isReady = false;
|
||||
this.emit('error', err);
|
||||
|
||||
this.#connect(true)
|
||||
.catch(err => this.emit('error', err));
|
||||
this.#connect(true).catch(() => {
|
||||
// the error was already emitted, silently ignore it
|
||||
});
|
||||
}
|
||||
|
||||
write(toWrite: string | Buffer): boolean {
|
||||
writeCommand(args: RedisCommandArguments): void {
|
||||
if (!this.#socket) {
|
||||
throw new ClientClosedError();
|
||||
}
|
||||
|
||||
return this.#socket.write(toWrite);
|
||||
for (const toWrite of encodeCommand(args)) {
|
||||
this.#writableNeedDrain = !this.#socket.write(toWrite);
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(ignoreIsOpen = false): Promise<void> {
|
||||
if ((!ignoreIsOpen && !this.#isOpen) || !this.#socket) {
|
||||
disconnect(): void {
|
||||
if (!this.#socket) {
|
||||
throw new ClientClosedError();
|
||||
} else {
|
||||
this.#isOpen = false;
|
||||
this.#isOpen = this.#isReady = false;
|
||||
}
|
||||
|
||||
this.#socket.end();
|
||||
await EventEmitter.once(this.#socket, 'end');
|
||||
this.#socket.destroy();
|
||||
this.#socket = undefined;
|
||||
this.emit('end');
|
||||
}
|
||||
@@ -220,15 +243,8 @@ export default class RedisSocket extends EventEmitter {
|
||||
}
|
||||
|
||||
this.#isOpen = false;
|
||||
|
||||
|
||||
try {
|
||||
await fn();
|
||||
await this.disconnect(true);
|
||||
} catch (err) {
|
||||
this.#isOpen = true;
|
||||
throw err;
|
||||
}
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
#isCorked = false;
|
@@ -1,9 +1,13 @@
|
||||
import calculateSlot from 'cluster-key-slot';
|
||||
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
|
||||
import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES';
|
||||
import { RedisClusterClientOptions, RedisClusterOptions } from '.';
|
||||
import { RedisModules, RedisScripts } from '../commands';
|
||||
|
||||
// We need to use 'require', because it's not possible with Typescript to import
|
||||
// function that are exported as 'module.exports = function`, without esModuleInterop
|
||||
// set to true.
|
||||
const calculateSlot = require('cluster-key-slot');
|
||||
|
||||
export interface ClusterNode<M extends RedisModules, S extends RedisScripts> {
|
||||
id: string;
|
||||
client: RedisClientType<M, S>;
|
||||
@@ -69,7 +73,7 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
||||
}
|
||||
|
||||
async #reset(masters: Array<RedisClusterMasterNode>): Promise<void> {
|
||||
// Override this.#slots and add not existing clients to this.#clientByKey
|
||||
// Override this.#slots and add not existing clients to this.#nodeByUrl
|
||||
const promises: Array<Promise<void>> = [],
|
||||
clientsInUse = new Set<string>();
|
||||
for (const master of masters) {
|
||||
@@ -82,13 +86,13 @@ export default class RedisClusterSlots<M extends RedisModules, S extends RedisSc
|
||||
};
|
||||
|
||||
for (const { from, to } of master.slots) {
|
||||
for (let i = from; i < to; i++) {
|
||||
for (let i = from; i <= to; i++) {
|
||||
this.#slots[i] = slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unused clients from this.#clientBykey using clientsInUse
|
||||
// Remove unused clients from this.#nodeByUrl using clientsInUse
|
||||
for (const [url, { client }] of this.#nodeByUrl.entries()) {
|
||||
if (clientsInUse.has(url)) continue;
|
||||
|
105
packages/client/lib/cluster/index.spec.ts
Normal file
105
packages/client/lib/cluster/index.spec.ts
Normal 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
|
||||
});
|
||||
});
|
@@ -1,5 +1,5 @@
|
||||
import COMMANDS from './commands';
|
||||
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisModules, RedisScript, RedisScripts } from '../commands';
|
||||
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
||||
import { ClientCommandOptions, RedisClientCommandSignature, RedisClientOptions, RedisClientType, WithModules, WithScripts } from '../client';
|
||||
import RedisClusterSlots, { ClusterNode } from './cluster-slots';
|
||||
import { extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, extendWithCommands } from '../commander';
|
||||
@@ -7,14 +7,9 @@ import { EventEmitter } from 'events';
|
||||
import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
|
||||
import { RedisMultiQueuedCommand } from '../multi-command';
|
||||
|
||||
export type RedisClusterClientOptions = Omit<RedisClientOptions<{}, {}>, 'modules' | 'scripts'>;
|
||||
export type RedisClusterClientOptions = Omit<RedisClientOptions<Record<string, never>, Record<string, never>>, 'modules' | 'scripts'>;
|
||||
|
||||
export interface RedisClusterPlugins<M extends RedisModules, S extends RedisScripts> {
|
||||
modules?: M;
|
||||
scripts?: S;
|
||||
}
|
||||
|
||||
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisClusterPlugins<M, S> {
|
||||
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
|
||||
rootNodes: Array<RedisClusterClientOptions>;
|
||||
defaults?: Partial<RedisClusterClientOptions>;
|
||||
useReplicas?: boolean;
|
||||
@@ -25,10 +20,10 @@ type WithCommands = {
|
||||
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
|
||||
};
|
||||
|
||||
export type RedisClusterType<M extends RedisModules = {}, S extends RedisScripts = {}> =
|
||||
export type RedisClusterType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
|
||||
RedisCluster<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
|
||||
|
||||
export default class RedisCluster<M extends RedisModules = {}, S extends RedisScripts = {}> extends EventEmitter {
|
||||
export default class RedisCluster<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> extends EventEmitter {
|
||||
static extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: RedisCommandArguments): string | Buffer | undefined {
|
||||
if (command.FIRST_KEY_INDEX === undefined) {
|
||||
return undefined;
|
||||
@@ -39,7 +34,7 @@ export default class RedisCluster<M extends RedisModules = {}, S extends RedisSc
|
||||
return command.FIRST_KEY_INDEX(...originalArgs);
|
||||
}
|
||||
|
||||
static create<M extends RedisModules = {}, S extends RedisScripts = {}>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
|
||||
static create<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
|
||||
return new (<any>extendWithModulesAndScripts({
|
||||
BaseClass: RedisCluster,
|
||||
modules: options?.modules,
|
@@ -12,16 +12,16 @@ type WithCommands<M extends RedisModules, S extends RedisScripts> = {
|
||||
};
|
||||
|
||||
type WithModules<M extends RedisModules, S extends RedisScripts> = {
|
||||
[P in keyof M]: {
|
||||
[P in keyof M as M[P] extends never ? never : P]: {
|
||||
[C in keyof M[P]]: RedisClusterMultiCommandSignature<M[P][C], M, S>;
|
||||
};
|
||||
};
|
||||
|
||||
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
|
||||
[P in keyof S]: RedisClusterMultiCommandSignature<S[P], M, S>
|
||||
[P in keyof S as S[P] extends never ? never : P]: RedisClusterMultiCommandSignature<S[P], M, S>
|
||||
};
|
||||
|
||||
export type RedisClusterMultiCommandType<M extends RedisModules = {}, S extends RedisScripts = {}> =
|
||||
export type RedisClusterMultiCommandType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
|
||||
RedisClusterMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
|
||||
|
||||
export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: string | Buffer, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
|
@@ -113,3 +113,18 @@ export function transformCommandReply(
|
||||
|
||||
return command.transformReply(rawReply, preserved);
|
||||
}
|
||||
|
||||
export type LegacyCommandArguments = Array<string | number | Buffer | LegacyCommandArguments>;
|
||||
|
||||
export function transformLegacyCommandArguments(args: LegacyCommandArguments, flat: RedisCommandArguments = []): RedisCommandArguments {
|
||||
for (const arg of args) {
|
||||
if (Array.isArray(arg)) {
|
||||
transformLegacyCommandArguments(arg, flat);
|
||||
continue;
|
||||
}
|
||||
|
||||
flat.push(typeof arg === 'number' ? arg.toString() : arg);
|
||||
}
|
||||
|
||||
return flat;
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion } from '../test-utils';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_CAT';
|
||||
|
||||
describe('ACL CAT', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion, itWithClient, TestRedisServers } from '../test-utils';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './ACL_DELUSER';
|
||||
|
||||
describe('ACL DELUSER', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('string', () => {
|
||||
@@ -21,10 +21,10 @@ describe('ACL DELUSER', () => {
|
||||
});
|
||||
});
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'client.aclDelUser', async client => {
|
||||
testUtils.testWithClient('client.aclDelUser', async client => {
|
||||
assert.equal(
|
||||
await client.aclDelUser('dosenotexists'),
|
||||
0
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion } from '../test-utils';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_GENPASS';
|
||||
|
||||
describe('ACL GENPASS', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
32
packages/client/lib/commands/ACL_GETUSER.spec.ts
Normal file
32
packages/client/lib/commands/ACL_GETUSER.spec.ts
Normal 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);
|
||||
});
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion } from '../test-utils';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_LIST';
|
||||
|
||||
describe('ACL LIST', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion } from '../test-utils';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_SAVE';
|
||||
|
||||
describe('ACL SAVE', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion } from '../test-utils';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments, transformReply } from './ACL_LOG';
|
||||
|
||||
describe('ACL LOG', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion } from '../test-utils';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_LOG_RESET';
|
||||
|
||||
describe('ACL LOG RESET', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion } from '../test-utils';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_LOAD';
|
||||
|
||||
describe('ACL LOAD', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion } from '../test-utils';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_SETUSER';
|
||||
|
||||
describe('ACL SETUSER', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('string', () => {
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion } from '../test-utils';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_USERS';
|
||||
|
||||
describe('ACL USERS', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
@@ -1,9 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describeHandleMinimumRedisVersion } from '../test-utils';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_WHOAMI';
|
||||
|
||||
describe('ACL WHOAMI', () => {
|
||||
describeHandleMinimumRedisVersion([6]);
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
@@ -1,7 +1,7 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './APPEND';
|
||||
|
||||
describe('AUTH', () => {
|
||||
describe('APPEND', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 'value'),
|
@@ -1,5 +1,5 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { TestRedisServers, itWithClient } from '../test-utils';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITCOUNT';
|
||||
|
||||
describe('BITCOUNT', () => {
|
||||
@@ -22,10 +22,10 @@ describe('BITCOUNT', () => {
|
||||
});
|
||||
});
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'client.bitCount', async client => {
|
||||
testUtils.testWithClient('client.bitCount', async client => {
|
||||
assert.equal(
|
||||
await client.bitCount('key'),
|
||||
0
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
@@ -1,5 +1,5 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { TestRedisServers, itWithClient } from '../test-utils';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITFIELD';
|
||||
|
||||
describe('BITFIELD', () => {
|
||||
@@ -33,10 +33,10 @@ describe('BITFIELD', () => {
|
||||
);
|
||||
});
|
||||
|
||||
itWithClient(TestRedisServers.OPEN, 'client.bitField', async client => {
|
||||
testUtils.testWithClient('client.bitField', async client => {
|
||||
assert.deepEqual(
|
||||
await client.bitField('key', []),
|
||||
[]
|
||||
);
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user