1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-07 13:22:56 +03:00
* wip

* Worked on phrasing etc for v5 doc changes.

* Removed quite repetition of 'Rather'

* Update v4-to-v5.md

* Update v4-to-v5.md

* Update v4-to-v5.md

* WIP

* WIP

* clean SET command

* some more commands, multi.exec<'typed'>

* "typed" multi

* WIP

* upgrade deps

* wip

* wip

* fix #2469

* wip

* npm update

* wip

* wip

* wip

* wip

* some tests

* tests.yml

* wip

* wip

* merge master into v5

* some more commands

* some more commands

* WIP

* Release client@2.0.0-next.1

---------

Co-authored-by: Simon Prickett <simon@redislabs.com>
This commit is contained in:
Leibale Eidelman
2023-04-30 11:18:46 -04:00
committed by GitHub
parent 3273c8540d
commit b4bb68d8ab
540 changed files with 20282 additions and 17587 deletions

View File

@@ -10,15 +10,13 @@ jobs:
documentation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Use Node.js
uses: actions/setup-node@v2.3.0
uses: actions/setup-node@v3
- name: Install Packages
run: npm ci
- name: Build tests tools
run: npm run build:tests-tools
- name: Generate Documentation
run: npm run documentation
- name: Upload

View File

@@ -16,14 +16,14 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: ['14', '16', '18', '19']
redis-version: ['5', '6.0', '6.2', '7.0']
node-version: ['16', '18', '19', '20']
redis-version: ['5', '6.0', '6.2', '7.0', '7.2-rc']
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.3.0
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Update npm
@@ -32,7 +32,7 @@ jobs:
- name: Install Packages
run: npm ci
- name: Build tests tools
run: npm run build:tests-tools
run: npm run build:client && npm run build:test-utils
- name: Run Tests
run: npm run test -- -- --forbid-only --redis-version=${{ matrix.redis-version }}
- name: Upload to Codecov

338
README.md
View File

@@ -16,7 +16,7 @@ node-redis is a modern, high performance [Redis](https://redis.io) client for No
| Name | Description |
|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [redis](./) | [![Downloads](https://img.shields.io/npm/dm/redis.svg)](https://www.npmjs.com/package/redis) [![Version](https://img.shields.io/npm/v/redis.svg)](https://www.npmjs.com/package/redis) |
| [redis](./packages/redis) | [![Downloads](https://img.shields.io/npm/dm/redis.svg)](https://www.npmjs.com/package/redis) [![Version](https://img.shields.io/npm/v/redis.svg)](https://www.npmjs.com/package/redis) |
| [@redis/client](./packages/client) | [![Downloads](https://img.shields.io/npm/dm/@redis/client.svg)](https://www.npmjs.com/package/@redis/client) [![Version](https://img.shields.io/npm/v/@redis/client.svg)](https://www.npmjs.com/package/@redis/client) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/client/) |
| [@redis/bloom](./packages/bloom) | [![Downloads](https://img.shields.io/npm/dm/@redis/bloom.svg)](https://www.npmjs.com/package/@redis/bloom) [![Version](https://img.shields.io/npm/v/@redis/bloom.svg)](https://www.npmjs.com/package/@redis/bloom) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/bloom/) [Redis Bloom](https://oss.redis.com/redisbloom/) commands |
| [@redis/graph](./packages/graph) | [![Downloads](https://img.shields.io/npm/dm/@redis/graph.svg)](https://www.npmjs.com/package/@redis/graph) [![Version](https://img.shields.io/npm/v/@redis/graph.svg)](https://www.npmjs.com/package/@redis/graph) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/graph/) [Redis Graph](https://oss.redis.com/redisgraph/) commands |
@@ -24,342 +24,6 @@ node-redis is a modern, high performance [Redis](https://redis.io) client for No
| [@redis/search](./packages/search) | [![Downloads](https://img.shields.io/npm/dm/@redis/search.svg)](https://www.npmjs.com/package/@redis/search) [![Version](https://img.shields.io/npm/v/@redis/search.svg)](https://www.npmjs.com/package/@redis/search) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/search/) [RediSearch](https://oss.redis.com/redisearch/) commands |
| [@redis/time-series](./packages/time-series) | [![Downloads](https://img.shields.io/npm/dm/@redis/time-series.svg)](https://www.npmjs.com/package/@redis/time-series) [![Version](https://img.shields.io/npm/v/@redis/time-series.svg)](https://www.npmjs.com/package/@redis/time-series) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/time-series/) [Redis Time-Series](https://oss.redis.com/redistimeseries/) commands |
> :warning: In version 4.1.0 we moved our subpackages from `@node-redis` to `@redis`. If you're just using `npm install redis`, you don't need to do anything—it'll upgrade automatically. If you're using the subpackages directly, you'll need to point to the new scope (e.g. `@redis/client` instead of `@node-redis/client`).
## Installation
Start a redis via docker:
``` bash
docker run -p 6379:6379 -it redis/redis-stack-server:latest
```
To install node-redis, simply:
```bash
npm install redis
```
> :warning: The new interface is clean and cool, but if you have an existing codebase, you'll want to read the [migration guide](./docs/v3-to-v4.md).
Looking for a high-level library to handle object mapping? See [redis-om-node](https://github.com/redis/redis-om-node)!
## Usage
### Basic Example
```typescript
import { createClient } from 'redis';
const client = 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');
await client.disconnect();
```
The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in the format `redis[s]://[[username][:password]@][host][:port][/db-number]`:
```typescript
createClient({
url: 'redis://alice:foobared@awesome.redis.server:6380'
});
```
You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in the [client configuration guide](./docs/client-configuration.md).
To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean. `client.isOpen` is also available. This returns `true` when the client's underlying socket is open, and `false` when it isn't (for example when the client is still connecting or reconnecting after a network error).
### Redis Commands
There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`, etc.):
```typescript
// raw Redis commands
await client.HSET('key', 'field', 'value');
await client.HGETALL('key');
// friendly JavaScript commands
await client.hSet('key', 'field', 'value');
await client.hGetAll('key');
```
Modifiers to commands are specified using a JavaScript object:
```typescript
await client.set('key', 'value', {
EX: 10,
NX: true
});
```
Replies will be transformed into useful data structures:
```typescript
await client.hGetAll('key'); // { field1: 'value1', field2: 'value2' }
await client.hVals('key'); // ['value1', 'value2']
```
`Buffer`s are supported as well:
```typescript
await client.hSet('key', 'field', Buffer.from('value')); // 'OK'
await client.hGetAll(
commandOptions({ returnBuffers: true }),
'key'
); // { field: <Buffer 76 61 6c 75 65> }
```
### 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
See the [Pub/Sub overview](./docs/pub-sub.md).
### Scan Iterator
[`SCAN`](https://redis.io/commands/scan) results can be looped over using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator):
```typescript
for await (const key of client.scanIterator()) {
// use the key!
await client.get(key);
}
```
This works with `HSCAN`, `SSCAN`, and `ZSCAN` too:
```typescript
for await (const { field, value } of client.hScanIterator('hash')) {}
for await (const member of client.sScanIterator('set')) {}
for await (const { score, value } of client.zScanIterator('sorted-set')) {}
```
You can override the default options by providing a configuration object:
```typescript
client.scanIterator({
TYPE: 'string', // `SCAN` only
MATCH: 'patter*',
COUNT: 100
});
```
### [Programmability](https://redis.io/docs/manual/programmability/)
Redis provides a programming interface allowing code execution on the redis server.
#### [Functions](https://redis.io/docs/manual/programmability/functions-intro/)
The following example retrieves a key in redis, returning the value of the key, incremented by an integer. For example, if your key _foo_ has the value _17_ and we run `add('foo', 25)`, it returns the answer to Life, the Universe and Everything.
```lua
#!lua name=library
redis.register_function {
function_name = 'add',
callback = function(keys, args) return redis.call('GET', keys[1]) + args[1] end,
flags = { 'no-writes' }
}
```
Here is the same example, but in a format that can be pasted into the `redis-cli`.
```
FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name=\"add\", callback=function(keys, args) return redis.call('GET', keys[1])+args[1] end, flags={\"no-writes\"}}"
```
Load the prior redis function on the _redis server_ before running the example below.
```typescript
import { createClient } from 'redis';
const client = createClient({
functions: {
library: {
add: {
NUMBER_OF_KEYS: 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.library.add('key', 2); // 3
```
#### [Lua Scripts](https://redis.io/docs/manual/programmability/eval-intro/)
The following is an end-to-end example of the prior concept.
```typescript
import { createClient, defineScript } from 'redis';
const client = createClient({
scripts: {
add: defineScript({
NUMBER_OF_KEYS: 1,
SCRIPT:
'return redis.call("GET", KEYS[1]) + 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.
### Events
The Node Redis client class is an Nodejs EventEmitter and it emits an event each time the network status changes:
| Name | When | Listener arguments |
|-------------------------|------------------------------------------------------------------------------------|------------------------------------------------------------|
| `connect` | Initiating a connection to the server | *No arguments* |
| `ready` | Client is ready to use | *No arguments* |
| `end` | Connection has been closed (via `.quit()` or `.disconnect()`) | *No arguments* |
| `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` |
| `reconnecting` | Client is trying to reconnect to the server | *No arguments* |
| `sharded-channel-moved` | See [here](./docs/pub-sub.md#sharded-channel-moved-event) | See [here](./docs/pub-sub.md#sharded-channel-moved-event) |
> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and an `error` occurs, that error will be thrown and the Node.js process will exit. See the [`EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details.
> The client will not emit [any other events](./docs/v3-to-v4.md#all-the-removed-events) beyond those listed above.
## Supported Redis versions
Node Redis is supported with the following versions of Redis:
| Version | Supported |
|---------|--------------------|
| 7.0.z | :heavy_check_mark: |
| 6.2.z | :heavy_check_mark: |
| 6.0.z | :heavy_check_mark: |
| 5.0.z | :heavy_check_mark: |
| < 5.0 | :x: |
> Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support.
## Contributing
If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md).

View File

@@ -0,0 +1,20 @@
import Redis from 'ioredis';
export default async (host) => {
const client = new Redis({
host,
lazyConnect: true,
enableAutoPipelining: true
});
await client.connect();
return {
benchmark() {
return client.ping();
},
teardown() {
return client.disconnect();
}
}
};

View File

@@ -0,0 +1,21 @@
import { createClient } from 'redis-local';
export default async (host) => {
const client = createClient({
socket: {
host
},
RESP: 2
});
await client.connect();
return {
benchmark() {
return client.ping();
},
teardown() {
return client.disconnect();
}
};
};

View File

@@ -0,0 +1,27 @@
import { createClient } from 'redis-local';
import PING from 'redis-local/dist/lib/commands/PING.js';
export default async (host) => {
const client = createClient({
socket: {
host
},
RESP: 3,
modules: {
module: {
ping: PING.default
}
}
});
await client.connect();
return {
benchmark() {
return client.withFlags({}).module.ping();
},
teardown() {
return client.disconnect();
}
};
};

View File

@@ -0,0 +1,27 @@
import { createClient } from 'redis-local';
import PING from 'redis-local/dist/lib/commands/PING.js';
export default async (host) => {
const client = createClient({
socket: {
host
},
RESP: 3,
modules: {
module: {
ping: PING.default
}
}
});
await client.connect();
return {
benchmark() {
return client.module.ping();
},
teardown() {
return client.disconnect();
}
};
};

View File

@@ -0,0 +1,21 @@
import { createClient } from 'redis-local';
export default async (host) => {
const client = createClient({
socket: {
host
},
RESP: 3
});
await client.connect();
return {
benchmark() {
return client.ping();
},
teardown() {
return client.disconnect();
}
};
};

View File

@@ -1,4 +1,4 @@
import { createClient } from '@redis/client';
import { createClient } from 'redis-v4';
export default async (host) => {
const client = createClient({

View File

@@ -71,13 +71,13 @@ const benchmarkStart = process.hrtime.bigint(),
histogram = await run(times),
benchmarkNanoseconds = process.hrtime.bigint() - benchmarkStart,
json = {
timestamp,
// timestamp,
operationsPerSecond: times / Number(benchmarkNanoseconds) * 1_000_000_000,
p0: histogram.getValueAtPercentile(0),
p50: histogram.getValueAtPercentile(50),
p95: histogram.getValueAtPercentile(95),
p99: histogram.getValueAtPercentile(99),
p100: histogram.getValueAtPercentile(100)
// p0: histogram.getValueAtPercentile(0),
// p50: histogram.getValueAtPercentile(50),
// p95: histogram.getValueAtPercentile(95),
// p99: histogram.getValueAtPercentile(99),
// p100: histogram.getValueAtPercentile(100)
};
console.log(`[${basename(path)}]:`);
console.table(json);

View File

@@ -1,16 +1,17 @@
{
"name": "@redis/client-benchmark",
"lockfileVersion": 2,
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@redis/client-benchmark",
"dependencies": {
"@redis/client": "../packages/client",
"hdr-histogram-js": "3.0.0",
"ioredis": "5.3.0",
"redis-v3": "npm:redis@3.1.2",
"yargs": "17.6.2"
"ioredis": "5",
"redis-local": "file:../packages/client",
"redis-v3": "npm:redis@3",
"redis-v4": "npm:redis@4",
"yargs": "17.7.1"
}
},
"node_modules/@assemblyscript/loader": {
@@ -19,14 +20,22 @@
"integrity": "sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw=="
},
"node_modules/@ioredis/commands": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz",
"integrity": "sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg=="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
},
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.5.0",
"resolved": "file:../packages/client",
"license": "MIT",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
"integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
@@ -36,6 +45,38 @@
"node": ">=14"
}
},
"node_modules/@redis/graph": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -181,9 +222,9 @@
}
},
"node_modules/ioredis": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.0.tgz",
"integrity": "sha512-Id9jKHhsILuIZpHc61QkagfVdUj2Rag5GzG1TGEvRNeM7dtTOjICgjC+tvqYxi//PuX2wjQ+Xjva2ONBuf92Pw==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
@@ -214,12 +255,12 @@
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo="
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
},
"node_modules/ms": {
"version": "2.1.2",
@@ -239,15 +280,29 @@
"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=",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-local": {
"name": "@redis/client",
"version": "1.5.6",
"resolved": "file:../packages/client",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"dependencies": {
"redis-errors": "^1.0.0"
},
@@ -282,10 +337,24 @@
"node": ">=0.10"
}
},
"node_modules/redis-v4": {
"name": "redis",
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz",
"integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==",
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.5.7",
"@redis/graph": "1.1.0",
"@redis/json": "1.0.4",
"@redis/search": "1.1.2",
"@redis/time-series": "1.0.4"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"engines": {
"node": ">=0.10.0"
}
@@ -349,9 +418,9 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yargs": {
"version": "17.6.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz",
"integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==",
"version": "17.7.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
"integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
@@ -373,258 +442,5 @@
"node": ">=12"
}
}
},
"dependencies": {
"@assemblyscript/loader": {
"version": "0.19.23",
"resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.19.23.tgz",
"integrity": "sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw=="
},
"@ioredis/commands": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz",
"integrity": "sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg=="
},
"@redis/client": {
"version": "1.5.0",
"requires": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
}
},
"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"
}
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
}
},
"cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="
},
"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=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="
},
"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=="
},
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
},
"generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g=="
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"hdr-histogram-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-3.0.0.tgz",
"integrity": "sha512-/EpvQI2/Z98mNFYEnlqJ8Ogful8OpArLG/6Tf2bPnkutBVLIeMVNHjk1ZDfshF2BUweipzbk+dB1hgSB7SIakw==",
"requires": {
"@assemblyscript/loader": "^0.19.21",
"base64-js": "^1.2.0",
"pako": "^1.0.3"
}
},
"ioredis": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.0.tgz",
"integrity": "sha512-Id9jKHhsILuIZpHc61QkagfVdUj2Rag5GzG1TGEvRNeM7dtTOjICgjC+tvqYxi//PuX2wjQ+Xjva2ONBuf92Pw==",
"requires": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
}
},
"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=="
},
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
},
"lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"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"
}
},
"redis-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"
},
"dependencies": {
"denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
}
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
},
"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"
}
},
"wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yargs": {
"version": "17.6.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz",
"integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==",
"requires": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
}
},
"yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
}
}
}

View File

@@ -7,10 +7,11 @@
"start": "node ."
},
"dependencies": {
"@redis/client": "../packages/client",
"hdr-histogram-js": "3.0.0",
"ioredis": "5.3.0",
"redis-v3": "npm:redis@3.1.2",
"yargs": "17.6.2"
"ioredis": "5",
"redis-local": "file:../packages/client",
"redis-v3": "npm:redis@3",
"redis-v4": "npm:redis@4",
"yargs": "17.7.1"
}
}

30
docs/RESP3.md Normal file
View File

@@ -0,0 +1,30 @@
# RESP3 => JS Type Mappings:
- Null (`_`) => `null`
- Boolean (`#`) => `boolean`
- Number (`:`) => `number | string`
- Big Number (`(`) => `BigInt | string`
- Double (`,`) => `number | string`
- Simple String (`+`) => `string | Buffer`
- Blob String (`$`) => `string | Buffer`
- Verbatim String (`=`) => `string | Buffer | VerbatimString` (TODO: `VerbatimString` typedoc link)
- Simple Error (`-`) => `ErrorReply`
- Blob Error (`!`) => `ErrorReply`
- Array (`*`) => `Array`
- Set (`~`) => `Array | Set`
- Map (`%`) => `object | Map | Array`
- Push (`>`) => `Array` => PubSub push/`'push'` event
> NOTE: the first type is the default type
## Map keys and Set members
When decoding Map to `Map | object` or Set to `Set`, keys/members of type "Simple String" or "Blob String" will be decoded as `string`s (ignoring flags) to allow lookup by type. If you need them as `Buffer`s, make sure to decode `Map`s/`Set`s as `Array`s.
## Not Implemented
These parts of RESP3 are not yet implemented in Redis itself (at the time of writing), so are not yet implemented in the Node-Redis client either:
- [Attribute type](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#attribute-type)
- [Streamed strings](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#streamed-strings)
- [Streamed aggregated data types](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#streamed-aggregated-data-types)

View File

@@ -32,7 +32,7 @@ const value = await cluster.get('key');
| 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 |
| 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 |
| minimizeConnections | `false` | When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes. Useful for short-term or Pub/Sub-only connections. |
@@ -105,9 +105,11 @@ createCluster({
## Command Routing
TODO request response policy
### Commands that operate on Redis Keys
Commands such as `GET`, `SET`, etc. are routed by the first key, for instance `MGET 1 2 3` will be routed by the key `1`.
Commands such as `GET`, `SET`, etc. are routed by the first key specified. For example `MGET 1 2 3` will be routed by the key `1`.
### [Server Commands](https://redis.io/commands#server)
@@ -115,4 +117,4 @@ Admin commands such as `MEMORY STATS`, `FLUSHALL`, etc. are not attached to the
### "Forwarded Commands"
Certain commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. This client sends these commands to a random node in order to spread the load across the cluster.
Certain commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. The client sends these commands to a random node in order to spread the load across the cluster.

14
docs/todo.md Normal file
View File

@@ -0,0 +1,14 @@
# Missing functionality
- `HEXISTS`: accepts one field only, should be the same as `EXISTS`
# Replies
`String` -> `Double`:
- `INCRBYFLOAT`
- `HINCRBYFLOAT`
- `GEODIST`
`Number` -> `Boolean`:
- `HSETNX` (deprecated)
- `SCRIPT EXISTS`

91
docs/v4-to-v5.md Normal file
View File

@@ -0,0 +1,91 @@
# v4 to v5 migration guide
## Command Options
In v4, command options are passed as a first optional argument:
```javascript
await client.get('key'); // `string | null`
await client.get(client.commandOptions({ returnBuffers: true }), 'key'); // `Buffer | null`
```
This has a couple of flaws:
1. The argument types are checked in runtime, which is a performance hit.
2. Code suggestions are less readable/usable, due to "function overloading".
3. Overall, "user code" is not as readable as it could be.
### The new API for v5
With the new API, instead of passing the options directly to the commands we use a "proxy client" to store them:
```javascript
await client.get('key'); // `string | null`
const proxyClient = client.withCommandOptions({
flags: {
[TYPES.BLOB_STRING]: Buffer
}
});
await proxyClient.get('key'); // `Buffer | null`
```
`withCommandOptions` can be used to override all of the command options, without reusing any existing ones.
To override just a specific option, use the following functions:
- `withFlags` - override `flags` only.
- `asap` - override `asap` to `true`.
- `isolated` - override `isolated` to `true`.
## Quit VS Disconnect
The `QUIT` command has been deprecated in Redis 7.2 and should now also be considered deprecated in Node-Redis. Instead of sending a `QUIT` command to the server, the client can simply close the network connection.
Rather than using `client.quit()`, your code should use `client.close()` or `client.disconnect()`.
TODO difference between `close` and `disconnect`...
## Scan Iterators
TODO
Yields chunks instead of individual items:
```javascript
for await (const chunk of client.scanIterator()) {
// `chunk` type is `Array<string>`
// will allow multi keys operations
await client.del(chunk);
}
```
## Commands
Some command arguments/replies have changed to align more closely to data types returned by Redis:
- `ACL GETUSER`: `selectors`
- `CLIENT KILL`: `enum ClientKillFilters` -> `const CLIENT_KILL_FILTERS` [^enum-to-constants]
- `CLUSTER FAILOVER`: `enum FailoverModes` -> `const FAILOVER_MODES` [^enum-to-constants]
- `LCS IDX`: `length` has been changed to `len`, `matches` has been changed from `Array<{ key1: RangeReply; key2: RangeReply; }>` to `Array<[key1: RangeReply, key2: RangeReply]>`
- `HEXISTS`: `boolean` -> `number` [^boolean-to-number]
- `HRANDFIELD_COUNT_WITHVALUES`: `Record<BlobString, BlobString>` -> `Array<{ field: BlobString; value: BlobString; }>` (it can return duplicates).
- `SCAN`, `HSCAN`, `SSCAN`, and `ZSCAN`: cursor type is `string` instead of `number`?
- `HSETNX`: `boolean` -> `number` [^boolean-to-number]
- `ZINTER`: instead of `client.ZINTER('11, { WEIGHTS: [1] })` use `client.ZINTER({ key: '1', weight: 1 }])`
- `SETNX`: `boolean` -> `number` [^boolean-to-number]
- `COPY`: `destinationDb` -> `DB`, `replace` -> `REPLACE`, `boolean` -> `number` [^boolean-to-number]
- `EXPIRE`: `boolean` -> `number` [^boolean-to-number]
- `EXPIREAT`: `boolean` -> `number` [^boolean-to-number]
- `MOVE`: `boolean` -> `number` [^boolean-to-number]
- `PEXPIRE`: `boolean` -> `number` [^boolean-to-number]
- `PEXPIREAT`: `boolean` -> `number` [^boolean-to-number]
- `RENAMENX`: `boolean` -> `number` [^boolean-to-number]
- `HSCAN`: `tuples` has been renamed to `entries`
- `PFADD`: `boolean` -> `number` [^boolean-to-number]
- `SCRIPT EXISTS`: `Array<boolean>` -> `Array<number>` [^boolean-to-number]
- `SISMEMBER`: `boolean` -> `number` [^boolean-to-number]
- `SMISMEMBER`: `Array<boolean>` -> `Array<number>` [^boolean-to-number]
[^enum-to-constants]: TODO
[^boolean-to-number]: TODO

39
docs/v5.md Normal file
View File

@@ -0,0 +1,39 @@
# RESP3 Support
[RESP3](./RESP3.md)
```javascript
const client = createClient({
RESP: 3
});
client.on('error', err => console.error(err));
await client.connect();
client.hGetAll('key'); // Record<string, string>
client.withFlags({
[TYPES.MAP]: Map
}).hGetAll('key'); // Map<string, string>
client.withFlags({
[TYPES.MAP]: Map,
[TYPES.BLOB_STRING]: Buffer
}).hGetAll('key'); // Map<string, Buffer>
```
# `multi.exec<'typed'>` / `multi.execTyped`
We have introduced the ability to perform a "typed" `MULTI`/`EXEC` transaction. Rather than returning `Array<ReplyUnion>`, a transaction invoked with `.exec<'typed'>` will return types appropriate to the commands in the transaction where possible:
```javascript
const multi = client.multi().ping();
await multi.exec(); // Array<ReplyUnion>
await multi.exec<'typed'>(); // [string]
await multi.execTyped(); // [string]
```
# Request & Reply Policies
see [here](../docs/clustering.md#command-routing).

View File

@@ -6,7 +6,7 @@
"private": true,
"type": "module",
"dependencies": {
"redis": "../"
"redis": "../packages/client"
}
}

View File

@@ -1,77 +0,0 @@
import {
RedisModules,
RedisFunctions,
RedisScripts,
createClient as _createClient,
RedisClientOptions,
RedisClientType as _RedisClientType,
createCluster as _createCluster,
RedisClusterOptions,
RedisClusterType as _RedisClusterType
} from '@redis/client';
import RedisBloomModules from '@redis/bloom';
import RedisGraph from '@redis/graph';
import RedisJSON from '@redis/json';
import RediSearch from '@redis/search';
import RedisTimeSeries from '@redis/time-series';
export * from '@redis/client';
export * from '@redis/bloom';
export * from '@redis/graph';
export * from '@redis/json';
export * from '@redis/search';
export * from '@redis/time-series';
const modules = {
...RedisBloomModules,
graph: RedisGraph,
json: RedisJSON,
ft: RediSearch,
ts: RedisTimeSeries
};
export type RedisDefaultModules = typeof modules;
export type RedisClientType<
M extends RedisModules = RedisDefaultModules,
F extends RedisFunctions = Record<string, never>,
S extends RedisScripts = Record<string, never>
> = _RedisClientType<M, F, S>;
export function createClient<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(
options?: RedisClientOptions<M, F, S>
): _RedisClientType<RedisDefaultModules & M, F, S> {
return _createClient({
...options,
modules: {
...modules,
...(options?.modules as M)
}
});
}
export type RedisClusterType<
M extends RedisModules = RedisDefaultModules,
F extends RedisFunctions = Record<string, never>,
S extends RedisScripts = Record<string, never>
> = _RedisClusterType<M, F, S>;
export function createCluster<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(
options: RedisClusterOptions<M, F, S>
): RedisClusterType<RedisDefaultModules & M, F, S> {
return _createCluster({
...options,
modules: {
...modules,
...(options?.modules as M)
}
});
}

2371
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,6 @@
{
"name": "redis",
"description": "A modern, high performance Redis client",
"version": "4.6.6",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist/"
],
"name": "redis-monorepo",
"private": true,
"workspaces": [
"./packages/*"
],
@@ -15,36 +8,14 @@
"test": "npm run test -ws --if-present",
"build:client": "npm run build -w ./packages/client",
"build:test-utils": "npm run build -w ./packages/test-utils",
"build:tests-tools": "npm run build:client && npm run build:test-utils",
"build:modules": "find ./packages -mindepth 1 -maxdepth 1 -type d ! -name 'client' ! -name 'test-utils' -exec npm run build -w {} \\;",
"build": "tsc",
"build-all": "npm run build:client && npm run build:test-utils && npm run build:modules && npm run build",
"build:modules": "npm run build -w ./packages/bloom -w ./packages/graph -w ./packages/json -w ./packages/search -w ./packages/time-series",
"build:redis": "npm run build -w ./packages/redis",
"build": "npm run build:client && npm run build:test-utils && npm run build:modules && npm run build:redis",
"documentation": "npm run documentation -ws --if-present",
"gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot <documentation@bot>'"
},
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.5.7",
"@redis/graph": "1.1.0",
"@redis/json": "1.0.4",
"@redis/search": "1.1.2",
"@redis/time-series": "1.0.4"
},
"devDependencies": {
"@tsconfig/node14": "^1.0.3",
"gh-pages": "^5.0.0",
"release-it": "^15.6.0",
"typescript": "^4.9.5"
},
"repository": {
"type": "git",
"url": "git://github.com/redis/node-redis.git"
},
"bugs": {
"url": "https://github.com/redis/node-redis/issues"
},
"homepage": "https://github.com/redis/node-redis",
"keywords": [
"redis"
]
"@tsconfig/node16": "^1.0.3",
"gh-pages": "^5.0.0"
}
}

View File

@@ -1,4 +1,4 @@
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
export const FIRST_KEY_INDEX = 1;
@@ -39,7 +39,7 @@ export function transformArguments(
}
args.push('ITEMS');
return pushVerdictArguments(args, items);
return pushVariadicArguments(args, items);
}
export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';

View File

@@ -1,5 +1,5 @@
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
export const FIRST_KEY_INDEX = 1;
@@ -9,7 +9,7 @@ export function transformArguments(
key: string,
items: string | Array<string>
): RedisCommandArguments {
return pushVerdictArguments(['CMS.QUERY', key], items);
return pushVariadicArguments(['CMS.QUERY', key], items);
}
export declare function transformReply(): Array<number>;

View File

@@ -10,7 +10,7 @@ import * as INSERTNX from './INSERTNX';
import * as LOADCHUNK from './LOADCHUNK';
import * as RESERVE from './RESERVE';
import * as SCANDUMP from './SCANDUMP';
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
export default {
@@ -58,5 +58,5 @@ export function pushInsertOptions(
}
args.push('ITEMS');
return pushVerdictArguments(args, items);
return pushVariadicArguments(args, items);
}

View File

@@ -1,5 +1,5 @@
import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { pushVerdictArgument } from '@redis/client/dist/lib/commands/generic-transformers';
import { pushVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
import { CompressionOption, pushCompressionArgument } from '.';
export const FIRST_KEY_INDEX = 1;
@@ -13,7 +13,7 @@ export function transformArguments(
srcKeys: RedisCommandArgument | Array<RedisCommandArgument>,
options?: MergeOptions
): RedisCommandArguments {
const args = pushVerdictArgument(
const args = pushVariadicArgument(
['TDIGEST.MERGE', destKey],
srcKeys
);

View File

@@ -1,5 +1,5 @@
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
export const FIRST_KEY_INDEX = 1;
@@ -7,7 +7,7 @@ export function transformArguments(
key: string,
items: string | Array<string>
): RedisCommandArguments {
return pushVerdictArguments(['TOPK.ADD', key], items);
return pushVariadicArguments(['TOPK.ADD', key], items);
}
export declare function transformReply(): Array<null | string>;

View File

@@ -1,5 +1,5 @@
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
export const FIRST_KEY_INDEX = 1;
@@ -9,7 +9,7 @@ export function transformArguments(
key: string,
items: string | Array<string>
): RedisCommandArguments {
return pushVerdictArguments(['TOPK.COUNT', key], items);
return pushVariadicArguments(['TOPK.COUNT', key], items);
}
export declare function transformReply(): Array<number>;

View File

@@ -1,5 +1,5 @@
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
export const FIRST_KEY_INDEX = 1;
@@ -9,7 +9,7 @@ export function transformArguments(
key: string,
items: string | Array<string>
): RedisCommandArguments {
return pushVerdictArguments(['TOPK.QUERY', key], items);
return pushVariadicArguments(['TOPK.QUERY', key], items);
}
export declare function transformReply(): Array<number>;

View File

@@ -18,13 +18,13 @@
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@redis/test-utils": "*",
"@types/node": "^18.14.1",
"@types/node": "^18.16.1",
"nyc": "^15.1.0",
"release-it": "^15.6.0",
"release-it": "^15.10.1",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.1",
"typedoc": "^0.23.25",
"typescript": "^4.9.5"
"typedoc": "^0.24.6",
"typescript": "^5.0.4"
},
"repository": {
"type": "git",

View File

@@ -12,4 +12,4 @@
"rules": {
"semi": [2, "always"]
}
}
}

View File

@@ -1,24 +1,19 @@
import RedisClient from './lib/client';
import RedisCluster from './lib/cluster';
export { RedisClientType, RedisClientOptions } from './lib/client';
export { RedisModules, RedisFunctions, RedisScripts } from './lib/commands';
export { RedisModules, RedisFunctions, RedisScripts, RespVersions } from './lib/RESP/types';
export { RESP_TYPES } from './lib/RESP/decoder';
export { VerbatimString } from './lib/RESP/verbatim-string';
export { defineScript } from './lib/lua-script';
// export * from './lib/errors';
import RedisClient, { RedisClientType, RedisClientOptions } from './lib/client';
export { RedisClientType, RedisClientOptions };
export const createClient = RedisClient.create;
export const commandOptions = RedisClient.commandOptions;
export { RedisClusterType, RedisClusterOptions } from './lib/cluster';
import RedisCluster, { RedisClusterType, RedisClusterOptions } from './lib/cluster';
export { RedisClusterType, RedisClusterOptions };
export const createCluster = RedisCluster.create;
export { defineScript } from './lib/lua-script';
// export { GeoReplyWith } from './lib/commands/generic-transformers';
export * from './lib/errors';
// export { SetOptions } from './lib/commands/SET';
export { GeoReplyWith } from './lib/commands/generic-transformers';
export { SetOptions } from './lib/commands/SET';
export { RedisFlushModes } from './lib/commands/FLUSHALL';
// export { RedisFlushModes } from './lib/commands/FLUSHALL';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,195 @@
// import { strict as assert } from 'assert';
// import { SinonSpy, spy } from 'sinon';
// import RESP2Decoder from './decoder';
// import { ErrorReply } from '../../errors';
// interface DecoderAndSpies {
// decoder: RESP2Decoder;
// returnStringsAsBuffersSpy: SinonSpy;
// onReplySpy: SinonSpy;
// }
// function createDecoderAndSpies(returnStringsAsBuffers: boolean): DecoderAndSpies {
// const returnStringsAsBuffersSpy = spy(() => returnStringsAsBuffers),
// onReplySpy = spy();
// return {
// decoder: new RESP2Decoder({
// returnStringsAsBuffers: returnStringsAsBuffersSpy,
// onReply: onReplySpy
// }),
// returnStringsAsBuffersSpy,
// onReplySpy
// };
// }
// function writeChunks(stream: RESP2Decoder, buffer: Buffer) {
// let i = 0;
// while (i < buffer.length) {
// stream.write(buffer.slice(i, ++i));
// }
// }
// type Replies = Array<Array<unknown>>;
// interface TestsOptions {
// toWrite: Buffer;
// returnStringsAsBuffers: boolean;
// replies: Replies;
// }
// function generateTests({
// toWrite,
// returnStringsAsBuffers,
// replies
// }: TestsOptions): void {
// it('single chunk', () => {
// const { decoder, returnStringsAsBuffersSpy, onReplySpy } =
// createDecoderAndSpies(returnStringsAsBuffers);
// decoder.write(toWrite);
// assert.equal(returnStringsAsBuffersSpy.callCount, replies.length);
// testReplies(onReplySpy, replies);
// });
// it('multiple chunks', () => {
// const { decoder, returnStringsAsBuffersSpy, onReplySpy } =
// createDecoderAndSpies(returnStringsAsBuffers);
// writeChunks(decoder, toWrite);
// assert.equal(returnStringsAsBuffersSpy.callCount, replies.length);
// testReplies(onReplySpy, replies);
// });
// }
// function testReplies(spy: SinonSpy, replies: Replies): void {
// if (!replies) {
// assert.equal(spy.callCount, 0);
// return;
// }
// assert.equal(spy.callCount, replies.length);
// for (const [i, reply] of replies.entries()) {
// assert.deepEqual(
// spy.getCall(i).args,
// reply
// );
// }
// }
// describe('RESP2Parser', () => {
// describe('Simple String', () => {
// describe('as strings', () => {
// generateTests({
// toWrite: Buffer.from('+OK\r\n'),
// returnStringsAsBuffers: false,
// replies: [['OK']]
// });
// });
// describe('as buffers', () => {
// generateTests({
// toWrite: Buffer.from('+OK\r\n'),
// returnStringsAsBuffers: true,
// replies: [[Buffer.from('OK')]]
// });
// });
// });
// describe('Error', () => {
// generateTests({
// toWrite: Buffer.from('-ERR\r\n'),
// returnStringsAsBuffers: false,
// replies: [[new ErrorReply('ERR')]]
// });
// });
// describe('Integer', () => {
// describe('-1', () => {
// generateTests({
// toWrite: Buffer.from(':-1\r\n'),
// returnStringsAsBuffers: false,
// replies: [[-1]]
// });
// });
// describe('0', () => {
// generateTests({
// toWrite: Buffer.from(':0\r\n'),
// returnStringsAsBuffers: false,
// replies: [[0]]
// });
// });
// });
// describe('Bulk String', () => {
// describe('null', () => {
// generateTests({
// toWrite: Buffer.from('$-1\r\n'),
// returnStringsAsBuffers: false,
// replies: [[null]]
// });
// });
// describe('as strings', () => {
// generateTests({
// toWrite: Buffer.from('$2\r\naa\r\n'),
// returnStringsAsBuffers: false,
// replies: [['aa']]
// });
// });
// describe('as buffers', () => {
// generateTests({
// toWrite: Buffer.from('$2\r\naa\r\n'),
// returnStringsAsBuffers: true,
// replies: [[Buffer.from('aa')]]
// });
// });
// });
// describe('Array', () => {
// describe('null', () => {
// generateTests({
// toWrite: Buffer.from('*-1\r\n'),
// returnStringsAsBuffers: false,
// replies: [[null]]
// });
// });
// const arrayBuffer = Buffer.from(
// '*5\r\n' +
// '+OK\r\n' +
// '-ERR\r\n' +
// ':0\r\n' +
// '$1\r\na\r\n' +
// '*0\r\n'
// );
// describe('as strings', () => {
// generateTests({
// toWrite: arrayBuffer,
// returnStringsAsBuffers: false,
// replies: [[[
// 'OK',
// new ErrorReply('ERR'),
// 0,
// 'a',
// []
// ]]]
// });
// });
// describe('as buffers', () => {
// generateTests({
// toWrite: arrayBuffer,
// returnStringsAsBuffers: true,
// replies: [[[
// Buffer.from('OK'),
// new ErrorReply('ERR'),
// 0,
// Buffer.from('a'),
// []
// ]]]
// });
// });
// });
// });

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
import { strict as assert } from 'assert';
import { describe } from 'mocha';
import encodeCommand from './encoder';
describe('RESP Encoder', () => {
it('1 byte', () => {
assert.deepEqual(
encodeCommand(['a', 'z']),
['*2\r\n$1\r\na\r\n$1\r\nz\r\n']
);
});
it('2 bytes', () => {
assert.deepEqual(
encodeCommand(['א', 'ת']),
['*2\r\n$2\r\nא\r\n$2\r\nת\r\n']
);
});
it('4 bytes', () => {
assert.deepEqual(
[...encodeCommand(['🐣', '🐤'])],
['*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n']
);
});
it('buffer', () => {
assert.deepEqual(
encodeCommand([Buffer.from('string')]),
['*1\r\n$6\r\n', Buffer.from('string'), '\r\n']
);
});
});

View File

@@ -0,0 +1,28 @@
import { RedisArgument } from "./types";
const CRLF = '\r\n';
export default function encodeCommand(args: Array<RedisArgument>): Array<RedisArgument> {
const toWrite: Array<RedisArgument> = [];
let strings = '*' + args.length + CRLF;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (typeof arg === 'string') {
strings += '$' + Buffer.byteLength(arg) + CRLF + arg + CRLF;
} else if (arg instanceof Buffer) {
toWrite.push(
strings + '$' + arg.length.toString() + CRLF,
arg
);
strings = CRLF;
} else {
throw new TypeError(`"arguments[${i}]" must be of type "string | Buffer", got ${typeof arg} instead.`);
}
}
toWrite.push(strings);
return toWrite;
}

View File

@@ -0,0 +1,417 @@
import { RedisScriptConfig, SHA1 } from '../lua-script';
import { RESP_TYPES } from './decoder';
import { VerbatimString } from './verbatim-string';
export type RESP_TYPES = typeof RESP_TYPES;
export type RespTypes = RESP_TYPES[keyof RESP_TYPES];
type RespType<
RESP_TYPE extends RespTypes,
DEFAULT,
TYPES = never,
FLAG_TYPES = DEFAULT | TYPES
> = (DEFAULT | TYPES) & {
RESP_TYPE: RESP_TYPE;
DEFAULT: DEFAULT;
TYPES: TYPES;
FLAG: Flag<FLAG_TYPES>;
};
export type NullReply = RespType<
RESP_TYPES['NULL'],
null
>;
export type BooleanReply<
T extends boolean = boolean
> = RespType<
RESP_TYPES['BOOLEAN'],
T
>;
export type NumberReply<
T extends number = number
> = RespType<
RESP_TYPES['NUMBER'],
T,
`${T}`,
number | string
>;
export type BigNumberReply<
T extends bigint = bigint
> = RespType<
RESP_TYPES['BIG_NUMBER'],
T,
number | `${T}`,
bigint | number | string
>;
export type DoubleReply<
T extends number = number
> = RespType<
RESP_TYPES['DOUBLE'],
T,
`${T}`,
number | string
>;
export type SimpleStringReply<
T extends string = string
> = RespType<
RESP_TYPES['SIMPLE_STRING'],
T,
Buffer,
string | Buffer
>;
export type BlobStringReply<
T extends string = string
> = RespType<
RESP_TYPES['BLOB_STRING'],
T,
Buffer,
string | Buffer
>;
export type VerbatimStringReply<
T extends string = string
> = RespType<
RESP_TYPES['VERBATIM_STRING'],
T,
Buffer | VerbatimString,
string | Buffer | VerbatimString
>;
export type SimpleErrorReply = RespType<
RESP_TYPES['SIMPLE_ERROR'],
Buffer
>;
export type BlobErrorReply = RespType<
RESP_TYPES['BLOB_ERROR'],
Buffer
>;
export type ArrayReply<T> = RespType<
RESP_TYPES['ARRAY'],
Array<T>,
never,
Array<any>
>;
export type TuplesReply<T extends [...Array<unknown>]> = RespType<
RESP_TYPES['ARRAY'],
T,
never,
Array<any>
>;
export type SetReply<T> = RespType<
RESP_TYPES['SET'],
Array<T>,
Set<T>,
Array<any> | Set<any>
>;
export type MapReply<K, V> = RespType<
RESP_TYPES['MAP'],
{ [key: string]: V },
Map<K, V> | Array<K | V>,
Map<any, any> | Array<any>
>;
type MapKeyValue = [key: BlobStringReply, value: unknown];
type MapTuples = Array<MapKeyValue>;
export type TuplesToMapReply<T extends MapTuples> = RespType<
RESP_TYPES['MAP'],
{
[P in T[number] as P[0] extends BlobStringReply<infer S> ? S : never]: P[1];
},
Map<T[number][0], T[number][1]> | FlattenTuples<T>
>;
type FlattenTuples<T> = (
T extends [] ? [] :
T extends [MapKeyValue] ? T[0] :
T extends [MapKeyValue, ...infer R] ? [
...T[0],
...FlattenTuples<R>
] :
never
);
export type ReplyUnion = NullReply | BooleanReply | NumberReply | BigNumberReply | DoubleReply | SimpleStringReply | BlobStringReply | VerbatimStringReply | SimpleErrorReply | BlobErrorReply |
// cannot reuse ArrayReply, SetReply and MapReply because of circular reference
RespType<
RESP_TYPES['ARRAY'],
Array<ReplyUnion>
> |
RespType<
RESP_TYPES['SET'],
Array<ReplyUnion>,
Set<ReplyUnion>
> |
RespType<
RESP_TYPES['MAP'],
{ [key: string]: ReplyUnion },
Map<ReplyUnion, ReplyUnion> | Array<ReplyUnion | ReplyUnion>
>;
export type Reply = ReplyWithFlags<ReplyUnion, {}>;
export type Flag<T> = ((...args: any) => T) | (new (...args: any) => T);
type RespTypeUnion<T> = T extends RespType<RespTypes, unknown, unknown, infer FLAG_TYPES> ? FLAG_TYPES : never;
export type Flags = {
[P in RespTypes]?: Flag<RespTypeUnion<Extract<ReplyUnion, RespType<P, any, any, any>>>>;
};
type MapKey<
T,
FLAGS extends Flags
> = ReplyWithFlags<T, FLAGS & {
// simple and blob strings as map keys decoded as strings
[RESP_TYPES.SIMPLE_STRING]: StringConstructor;
[RESP_TYPES.BLOB_STRING]: StringConstructor;
}>;
export type ReplyWithFlags<
REPLY,
FLAGS extends Flags
> = (
// if REPLY is a type, extract the coresponding type from FLAGS or use the default type
REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
FLAGS[RESP_TYPE] extends Flag<infer T> ?
ReplyWithFlags<Extract<DEFAULT | TYPES, T>, FLAGS> :
ReplyWithFlags<DEFAULT, FLAGS>
: (
// if REPLY is a known generic type, convert its generic arguments
// TODO: tuples?
REPLY extends Array<infer T> ? Array<ReplyWithFlags<T, FLAGS>> :
REPLY extends Set<infer T> ? Set<ReplyWithFlags<T, FLAGS>> :
REPLY extends Map<infer K, infer V> ? Map<MapKey<K, FLAGS>, ReplyWithFlags<V, FLAGS>> :
// `Date` & `Buffer` are supersets of `Record`, so they need to be checked first
REPLY extends Date ? REPLY :
REPLY extends Buffer ? REPLY :
REPLY extends Record<PropertyKey, any> ? {
[P in keyof REPLY]: ReplyWithFlags<REPLY[P], FLAGS>;
} :
// otherwise, just return the REPLY as is
REPLY
)
);
export type TransformReply = (this: void, reply: any, preserve?: any) => any; // TODO;
export type RedisArgument = string | Buffer;
export type CommandArguments = Array<RedisArgument> & { preserve?: unknown };
export const REQUEST_POLICIES = {
/**
* TODO
*/
ALL_NODES: 'all_nodes',
/**
* TODO
*/
ALL_SHARDS: 'all_shards',
/**
* TODO
*/
SPECIAL: 'special'
} as const;
export type REQUEST_POLICIES = typeof REQUEST_POLICIES;
export type RequestPolicies = REQUEST_POLICIES[keyof REQUEST_POLICIES];
export const RESPONSE_POLICIES = {
/**
* TODO
*/
ONE_SUCCEEDED: 'one_succeeded',
/**
* TODO
*/
ALL_SUCCEEDED: 'all_succeeded',
/**
* TODO
*/
LOGICAL_AND: 'agg_logical_and',
/**
* TODO
*/
SPECIAL: 'special'
} as const;
export type RESPONSE_POLICIES = typeof RESPONSE_POLICIES;
export type ResponsePolicies = RESPONSE_POLICIES[keyof RESPONSE_POLICIES];
export type CommandPolicies = {
request?: RequestPolicies | null;
response?: ResponsePolicies | null;
};
export type Command = {
FIRST_KEY_INDEX?: number | ((this: void, ...args: Array<any>) => RedisArgument | undefined);
IS_READ_ONLY?: boolean;
POLICIES?: CommandPolicies;
transformArguments(this: void, ...args: Array<any>): CommandArguments;
TRANSFORM_LEGACY_REPLY?: boolean;
transformReply: TransformReply | Record<RespVersions, TransformReply>;
};
export type RedisCommands = Record<string, Command>;
export type RedisModules = Record<string, RedisCommands>;
export interface RedisFunction extends Command {
NUMBER_OF_KEYS?: number;
}
export type RedisFunctions = Record<string, Record<string, RedisFunction>>;
export type RedisScript = RedisScriptConfig & SHA1;
export type RedisScripts = Record<string, RedisScript>;
// TODO: move to Commander?
export interface CommanderConfig<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions
> {
modules?: M;
functions?: F;
scripts?: S;
/**
* TODO
*/
RESP?: RESP;
}
type Resp2Array<T> = (
T extends [] ? [] :
T extends [infer ITEM] ? [Resp2Reply<ITEM>] :
T extends [infer ITEM, ...infer REST] ? [
Resp2Reply<ITEM>,
...Resp2Array<REST>
] :
T extends Array<infer ITEM> ? Array<Resp2Reply<ITEM>> :
never
);
export type Resp2Reply<RESP3REPLY> = (
RESP3REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
// TODO: RESP3 only scalar types
RESP_TYPE extends RESP_TYPES['DOUBLE'] ? BlobStringReply :
RESP_TYPE extends RESP_TYPES['ARRAY'] | RESP_TYPES['SET'] ? RespType<
RESP_TYPE,
Resp2Array<DEFAULT>
> :
RESP_TYPE extends RESP_TYPES['MAP'] ? RespType<
RESP_TYPES['ARRAY'],
Resp2Array<Extract<TYPES, Array<any>>>
> :
RespType<
RESP_TYPE,
DEFAULT,
TYPES
> :
RESP3REPLY
);
export type RespVersions = 2 | 3;
export type CommandReply<
COMMAND extends Command,
RESP extends RespVersions
> = (
// if transformReply is a function, use its return type
COMMAND['transformReply'] extends (...args: any) => infer T ? T :
// if transformReply[RESP] is a function, use its return type
COMMAND['transformReply'] extends Record<RESP, (...args: any) => infer T> ? T :
// otherwise use the generic reply type
Reply
);
export type CommandSignature<
COMMAND extends Command,
RESP extends RespVersions,
FLAGS extends Flags
> = (...args: Parameters<COMMAND['transformArguments']>) => Promise<ReplyWithFlags<CommandReply<COMMAND, RESP>, FLAGS>>;
export type CommandWithPoliciesSignature<
COMMAND extends Command,
RESP extends RespVersions,
FLAGS extends Flags,
POLICIES extends CommandPolicies
> = (...args: Parameters<COMMAND['transformArguments']>) => Promise<
ReplyWithPolicy<
ReplyWithFlags<CommandReply<COMMAND, RESP>, FLAGS>,
MergePolicies<COMMAND, POLICIES>
>
>;
export type MergePolicies<
COMMAND extends Command,
POLICIES extends CommandPolicies
> = Omit<COMMAND['POLICIES'], keyof POLICIES> & POLICIES;
type ReplyWithPolicy<
REPLY,
POLICIES extends CommandPolicies,
> = (
POLICIES['request'] extends REQUEST_POLICIES['SPECIAL'] ? never :
POLICIES['request'] extends null | undefined ? REPLY :
unknown extends POLICIES['request'] ? REPLY :
POLICIES['response'] extends RESPONSE_POLICIES['SPECIAL'] ? never :
POLICIES['response'] extends RESPONSE_POLICIES['ALL_SUCCEEDED' | 'ONE_SUCCEEDED' | 'LOGICAL_AND'] ? REPLY :
// otherwise, return array of replies
Array<REPLY>
);
const SAME = {
transformArguments(key: string): Array<string> {
return ['GET', key];
},
transformReply: () => 'default' as const
} satisfies Command;
type SAME_DEFAULT = CommandWithPoliciesSignature<
typeof SAME,
2,
{},
{
request: REQUEST_POLICIES['ALL_NODES'];
response: RESPONSE_POLICIES['SPECIAL'];
}
>;
// type SAME_RESP2 = CommandReply<typeof SAME, 2>;
// type SAME_COMMAND_RESP2 = CommandSignuture<typeof SAME, 2>;
// type SAME_RESP3 = CommandReply<typeof SAME, 3>;
// type SAME_COMMAND_RESP3 = CommandSignuture<typeof SAME, 3>;
// interface Test {
// /**
// * This is a test
// */
// a: 'a';
// }
// const DIFFERENT = {
// transformArguments(key: string): Array<string> {
// return ['GET', key];
// },
// transformReply: {
// 2: () => null as any as Test,
// 3: () => '3' as const
// }
// } satisfies Command;
// type DIFFERENT_RESP2 = CommandReply<typeof DIFFERENT, 2>;
// type DIFFERENT_COMMAND_RESP2 = CommandSignuture<typeof DIFFERENT, 2>;
// type DIFFERENT_RESP3 = CommandReply<typeof DIFFERENT, 3>;
// type DIFFERENT_COMMAND_RESP3 = CommandSignuture<typeof DIFFERENT, 3>;
// const a = null as any as DIFFERENT_COMMAND_RESP2;
// const b = await a('a');
// b.a

View File

@@ -0,0 +1,8 @@
export class VerbatimString extends String {
constructor(
public format: string,
value: string
) {
super(value);
}
}

View File

@@ -1,14 +0,0 @@
import { strict as assert } from 'assert';
import BufferComposer from './buffer';
describe('Buffer Composer', () => {
const composer = new BufferComposer();
it('should compose two buffers', () => {
composer.write(Buffer.from([0]));
assert.deepEqual(
composer.end(Buffer.from([1])),
Buffer.from([0, 1])
);
});
});

View File

@@ -1,18 +0,0 @@
import { Composer } from './interface';
export default class BufferComposer implements Composer<Buffer> {
private chunks: Array<Buffer> = [];
write(buffer: Buffer): void {
this.chunks.push(buffer);
}
end(buffer: Buffer): Buffer {
this.write(buffer);
return Buffer.concat(this.chunks.splice(0));
}
reset() {
this.chunks = [];
}
}

View File

@@ -1,7 +0,0 @@
export interface Composer<T> {
write(buffer: Buffer): void;
end(buffer: Buffer): T;
reset(): void;
}

View File

@@ -1,14 +0,0 @@
import { strict as assert } from 'assert';
import StringComposer from './string';
describe('String Composer', () => {
const composer = new StringComposer();
it('should compose two strings', () => {
composer.write(Buffer.from([0]));
assert.deepEqual(
composer.end(Buffer.from([1])),
Buffer.from([0, 1]).toString()
);
});
});

View File

@@ -1,22 +0,0 @@
import { StringDecoder } from 'string_decoder';
import { Composer } from './interface';
export default class StringComposer implements Composer<string> {
private decoder = new StringDecoder();
private string = '';
write(buffer: Buffer): void {
this.string += this.decoder.write(buffer);
}
end(buffer: Buffer): string {
const string = this.string + this.decoder.end(buffer);
this.string = '';
return string;
}
reset() {
this.string = '';
}
}

View File

@@ -1,195 +0,0 @@
import { strict as assert } from 'assert';
import { SinonSpy, spy } from 'sinon';
import RESP2Decoder from './decoder';
import { ErrorReply } from '../../errors';
interface DecoderAndSpies {
decoder: RESP2Decoder;
returnStringsAsBuffersSpy: SinonSpy;
onReplySpy: SinonSpy;
}
function createDecoderAndSpies(returnStringsAsBuffers: boolean): DecoderAndSpies {
const returnStringsAsBuffersSpy = spy(() => returnStringsAsBuffers),
onReplySpy = spy();
return {
decoder: new RESP2Decoder({
returnStringsAsBuffers: returnStringsAsBuffersSpy,
onReply: onReplySpy
}),
returnStringsAsBuffersSpy,
onReplySpy
};
}
function writeChunks(stream: RESP2Decoder, buffer: Buffer) {
let i = 0;
while (i < buffer.length) {
stream.write(buffer.slice(i, ++i));
}
}
type Replies = Array<Array<unknown>>;
interface TestsOptions {
toWrite: Buffer;
returnStringsAsBuffers: boolean;
replies: Replies;
}
function generateTests({
toWrite,
returnStringsAsBuffers,
replies
}: TestsOptions): void {
it('single chunk', () => {
const { decoder, returnStringsAsBuffersSpy, onReplySpy } =
createDecoderAndSpies(returnStringsAsBuffers);
decoder.write(toWrite);
assert.equal(returnStringsAsBuffersSpy.callCount, replies.length);
testReplies(onReplySpy, replies);
});
it('multiple chunks', () => {
const { decoder, returnStringsAsBuffersSpy, onReplySpy } =
createDecoderAndSpies(returnStringsAsBuffers);
writeChunks(decoder, toWrite);
assert.equal(returnStringsAsBuffersSpy.callCount, replies.length);
testReplies(onReplySpy, replies);
});
}
function testReplies(spy: SinonSpy, replies: Replies): void {
if (!replies) {
assert.equal(spy.callCount, 0);
return;
}
assert.equal(spy.callCount, replies.length);
for (const [i, reply] of replies.entries()) {
assert.deepEqual(
spy.getCall(i).args,
reply
);
}
}
describe('RESP2Parser', () => {
describe('Simple String', () => {
describe('as strings', () => {
generateTests({
toWrite: Buffer.from('+OK\r\n'),
returnStringsAsBuffers: false,
replies: [['OK']]
});
});
describe('as buffers', () => {
generateTests({
toWrite: Buffer.from('+OK\r\n'),
returnStringsAsBuffers: true,
replies: [[Buffer.from('OK')]]
});
});
});
describe('Error', () => {
generateTests({
toWrite: Buffer.from('-ERR\r\n'),
returnStringsAsBuffers: false,
replies: [[new ErrorReply('ERR')]]
});
});
describe('Integer', () => {
describe('-1', () => {
generateTests({
toWrite: Buffer.from(':-1\r\n'),
returnStringsAsBuffers: false,
replies: [[-1]]
});
});
describe('0', () => {
generateTests({
toWrite: Buffer.from(':0\r\n'),
returnStringsAsBuffers: false,
replies: [[0]]
});
});
});
describe('Bulk String', () => {
describe('null', () => {
generateTests({
toWrite: Buffer.from('$-1\r\n'),
returnStringsAsBuffers: false,
replies: [[null]]
});
});
describe('as strings', () => {
generateTests({
toWrite: Buffer.from('$2\r\naa\r\n'),
returnStringsAsBuffers: false,
replies: [['aa']]
});
});
describe('as buffers', () => {
generateTests({
toWrite: Buffer.from('$2\r\naa\r\n'),
returnStringsAsBuffers: true,
replies: [[Buffer.from('aa')]]
});
});
});
describe('Array', () => {
describe('null', () => {
generateTests({
toWrite: Buffer.from('*-1\r\n'),
returnStringsAsBuffers: false,
replies: [[null]]
});
});
const arrayBuffer = Buffer.from(
'*5\r\n' +
'+OK\r\n' +
'-ERR\r\n' +
':0\r\n' +
'$1\r\na\r\n' +
'*0\r\n'
);
describe('as strings', () => {
generateTests({
toWrite: arrayBuffer,
returnStringsAsBuffers: false,
replies: [[[
'OK',
new ErrorReply('ERR'),
0,
'a',
[]
]]]
});
});
describe('as buffers', () => {
generateTests({
toWrite: arrayBuffer,
returnStringsAsBuffers: true,
replies: [[[
Buffer.from('OK'),
new ErrorReply('ERR'),
0,
Buffer.from('a'),
[]
]]]
});
});
});
});

View File

@@ -1,257 +0,0 @@
import { ErrorReply } from '../../errors';
import { Composer } from './composers/interface';
import BufferComposer from './composers/buffer';
import StringComposer from './composers/string';
// RESP2 specification
// https://redis.io/topics/protocol
enum Types {
SIMPLE_STRING = 43, // +
ERROR = 45, // -
INTEGER = 58, // :
BULK_STRING = 36, // $
ARRAY = 42 // *
}
enum ASCII {
CR = 13, // \r
ZERO = 48,
MINUS = 45
}
export type Reply = string | Buffer | ErrorReply | number | null | Array<Reply>;
type ArrayReply = Array<Reply> | null;
export type ReturnStringsAsBuffers = () => boolean;
interface RESP2Options {
returnStringsAsBuffers: ReturnStringsAsBuffers;
onReply(reply: Reply): unknown;
}
interface ArrayInProcess {
array: Array<Reply>;
pushCounter: number;
}
// Using TypeScript `private` and not the build-in `#` to avoid __classPrivateFieldGet and __classPrivateFieldSet
export default class RESP2Decoder {
constructor(private options: RESP2Options) {}
private cursor = 0;
private type?: Types;
private bufferComposer = new BufferComposer();
private stringComposer = new StringComposer();
private currentStringComposer: BufferComposer | StringComposer = this.stringComposer;
reset() {
this.cursor = 0;
this.type = undefined;
this.bufferComposer.reset();
this.stringComposer.reset();
this.currentStringComposer = this.stringComposer;
}
write(chunk: Buffer): void {
while (this.cursor < chunk.length) {
if (!this.type) {
this.currentStringComposer = this.options.returnStringsAsBuffers() ?
this.bufferComposer :
this.stringComposer;
this.type = chunk[this.cursor];
if (++this.cursor >= chunk.length) break;
}
const reply = this.parseType(chunk, this.type);
if (reply === undefined) break;
this.type = undefined;
this.options.onReply(reply);
}
this.cursor -= chunk.length;
}
private parseType(chunk: Buffer, type: Types, arraysToKeep?: number): Reply | undefined {
switch (type) {
case Types.SIMPLE_STRING:
return this.parseSimpleString(chunk);
case Types.ERROR:
return this.parseError(chunk);
case Types.INTEGER:
return this.parseInteger(chunk);
case Types.BULK_STRING:
return this.parseBulkString(chunk);
case Types.ARRAY:
return this.parseArray(chunk, arraysToKeep);
}
}
private compose<
C extends Composer<T>,
T = C extends Composer<infer TT> ? TT : never
>(
chunk: Buffer,
composer: C
): T | undefined {
for (let i = this.cursor; i < chunk.length; i++) {
if (chunk[i] === ASCII.CR) {
const reply = composer.end(
chunk.subarray(this.cursor, i)
);
this.cursor = i + 2;
return reply;
}
}
const toWrite = chunk.subarray(this.cursor);
composer.write(toWrite);
this.cursor = chunk.length;
}
private parseSimpleString(chunk: Buffer): string | Buffer | undefined {
return this.compose(chunk, this.currentStringComposer);
}
private parseError(chunk: Buffer): ErrorReply | undefined {
const message = this.compose(chunk, this.stringComposer);
if (message !== undefined) {
return new ErrorReply(message);
}
}
private integer = 0;
private isNegativeInteger?: boolean;
private parseInteger(chunk: Buffer): number | undefined {
if (this.isNegativeInteger === undefined) {
this.isNegativeInteger = chunk[this.cursor] === ASCII.MINUS;
if (this.isNegativeInteger && ++this.cursor === chunk.length) return;
}
do {
const byte = chunk[this.cursor];
if (byte === ASCII.CR) {
const integer = this.isNegativeInteger ? -this.integer : this.integer;
this.integer = 0;
this.isNegativeInteger = undefined;
this.cursor += 2;
return integer;
}
this.integer = this.integer * 10 + byte - ASCII.ZERO;
} while (++this.cursor < chunk.length);
}
private bulkStringRemainingLength?: number;
private parseBulkString(chunk: Buffer): string | Buffer | null | undefined {
if (this.bulkStringRemainingLength === undefined) {
const length = this.parseInteger(chunk);
if (length === undefined) return;
if (length === -1) return null;
this.bulkStringRemainingLength = length;
if (this.cursor >= chunk.length) return;
}
const end = this.cursor + this.bulkStringRemainingLength;
if (chunk.length >= end) {
const reply = this.currentStringComposer.end(
chunk.subarray(this.cursor, end)
);
this.bulkStringRemainingLength = undefined;
this.cursor = end + 2;
return reply;
}
const toWrite = chunk.subarray(this.cursor);
this.currentStringComposer.write(toWrite);
this.bulkStringRemainingLength -= toWrite.length;
this.cursor = chunk.length;
}
private arraysInProcess: Array<ArrayInProcess> = [];
private initializeArray = false;
private arrayItemType?: Types;
private parseArray(chunk: Buffer, arraysToKeep = 0): ArrayReply | undefined {
if (this.initializeArray || this.arraysInProcess.length === arraysToKeep) {
const length = this.parseInteger(chunk);
if (length === undefined) {
this.initializeArray = true;
return undefined;
}
this.initializeArray = false;
this.arrayItemType = undefined;
if (length === -1) {
return this.returnArrayReply(null, arraysToKeep, chunk);
} else if (length === 0) {
return this.returnArrayReply([], arraysToKeep, chunk);
}
this.arraysInProcess.push({
array: new Array(length),
pushCounter: 0
});
}
while (this.cursor < chunk.length) {
if (!this.arrayItemType) {
this.arrayItemType = chunk[this.cursor];
if (++this.cursor >= chunk.length) break;
}
const item = this.parseType(
chunk,
this.arrayItemType,
arraysToKeep + 1
);
if (item === undefined) break;
this.arrayItemType = undefined;
const reply = this.pushArrayItem(item, arraysToKeep);
if (reply !== undefined) return reply;
}
}
private returnArrayReply(reply: ArrayReply, arraysToKeep: number, chunk?: Buffer): ArrayReply | undefined {
if (this.arraysInProcess.length <= arraysToKeep) return reply;
return this.pushArrayItem(reply, arraysToKeep, chunk);
}
private pushArrayItem(item: Reply, arraysToKeep: number, chunk?: Buffer): ArrayReply | undefined {
const to = this.arraysInProcess[this.arraysInProcess.length - 1]!;
to.array[to.pushCounter] = item;
if (++to.pushCounter === to.array.length) {
return this.returnArrayReply(
this.arraysInProcess.pop()!.array,
arraysToKeep,
chunk
);
} else if (chunk && chunk.length > this.cursor) {
return this.parseArray(chunk, arraysToKeep);
}
}
}

View File

@@ -1,33 +0,0 @@
import { strict as assert } from 'assert';
import { describe } from 'mocha';
import encodeCommand from './encoder';
describe('RESP2 Encoder', () => {
it('1 byte', () => {
assert.deepEqual(
encodeCommand(['a', 'z']),
['*2\r\n$1\r\na\r\n$1\r\nz\r\n']
);
});
it('2 bytes', () => {
assert.deepEqual(
encodeCommand(['א', 'ת']),
['*2\r\n$2\r\nא\r\n$2\r\nת\r\n']
);
});
it('4 bytes', () => {
assert.deepEqual(
[...encodeCommand(['🐣', '🐤'])],
['*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n']
);
});
it('buffer', () => {
assert.deepEqual(
encodeCommand([Buffer.from('string')]),
['*1\r\n$6\r\n', Buffer.from('string'), '\r\n']
);
});
});

View File

@@ -1,28 +0,0 @@
import { RedisCommandArgument, RedisCommandArguments } from '../../commands';
const CRLF = '\r\n';
export default function encodeCommand(args: RedisCommandArguments): Array<RedisCommandArgument> {
const toWrite: Array<RedisCommandArgument> = [];
let strings = '*' + args.length + CRLF;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (typeof arg === 'string') {
strings += '$' + Buffer.byteLength(arg) + CRLF + arg + CRLF;
} else if (arg instanceof Buffer) {
toWrite.push(
strings + '$' + arg.length.toString() + CRLF,
arg
);
strings = CRLF;
} else {
throw new TypeError('Invalid argument type');
}
}
toWrite.push(strings);
return toWrite;
}

View File

@@ -1,111 +1,153 @@
import * as LinkedList from 'yallist';
import { AbortError, ErrorReply } from '../errors';
import { RedisCommandArguments, RedisCommandRawReply } from '../commands';
import RESP2Decoder from './RESP2/decoder';
import encodeCommand from './RESP2/encoder';
import encodeCommand from '../RESP/encoder';
import { Decoder, PUSH_FLAGS, RESP_TYPES } from '../RESP/decoder';
import { CommandArguments, Flags, ReplyUnion, RespVersions } from '../RESP/types';
import { ChannelListeners, PubSub, PubSubCommand, PubSubListener, PubSubType, PubSubTypeListeners } from './pub-sub';
import { AbortError, ErrorReply } from '../errors';
import { EventEmitter } from 'stream';
export interface QueueCommandOptions {
asap?: boolean;
chainId?: symbol;
signal?: AbortSignal;
returnBuffers?: boolean;
flags?: Flags;
}
export interface CommandWaitingToBeSent extends CommandWaitingForReply {
args: RedisCommandArguments;
args: CommandArguments;
chainId?: symbol;
abort?: {
signal: AbortSignal;
listener(): void;
};
removeAbortListener?(): void;
}
interface CommandWaitingForReply {
resolve(reply?: unknown): void;
reject(err: unknown): void;
channelsCounter?: number;
returnBuffers?: boolean;
flags?: Flags;
}
const PONG = Buffer.from('pong');
export type OnShardedChannelMoved = (channel: string, listeners: ChannelListeners) => void;
const PONG = Buffer.from('pong');
const RESP2_PUSH_FLAGS = {
...PUSH_FLAGS,
[RESP_TYPES.SIMPLE_STRING]: Buffer
};
export default class RedisCommandsQueue {
static #flushQueue<T extends CommandWaitingForReply>(queue: LinkedList<T>, err: Error): void {
while (queue.length) {
queue.shift()!.reject(err);
}
}
private readonly _maxLength: number | null | undefined;
private readonly _waitingToBeSent = new LinkedList<CommandWaitingToBeSent>();
private readonly _waitingForReply = new LinkedList<CommandWaitingForReply>();
private readonly _onShardedChannelMoved: OnShardedChannelMoved;
readonly #maxLength: number | null | undefined;
readonly #waitingToBeSent = new LinkedList<CommandWaitingToBeSent>();
readonly #waitingForReply = new LinkedList<CommandWaitingForReply>();
readonly #onShardedChannelMoved: OnShardedChannelMoved;
readonly #pubSub = new PubSub();
private readonly _pubSub = new PubSub();
get isPubSubActive() {
return this.#pubSub.isActive;
return this._pubSub.isActive;
}
#chainInExecution: symbol | undefined;
private _chainInExecution: symbol | undefined;
#decoder = new RESP2Decoder({
returnStringsAsBuffers: () => {
return !!this.#waitingForReply.head?.value.returnBuffers ||
this.#pubSub.isActive;
},
onReply: reply => {
if (this.#pubSub.isActive && Array.isArray(reply)) {
if (this.#pubSub.handleMessageReply(reply as Array<Buffer>)) return;
const isShardedUnsubscribe = PubSub.isShardedUnsubscribe(reply as Array<Buffer>);
if (isShardedUnsubscribe && !this.#waitingForReply.length) {
const channel = (reply[1] as Buffer).toString();
this.#onShardedChannelMoved(
channel,
this.#pubSub.removeShardedListeners(channel)
);
return;
} else if (isShardedUnsubscribe || PubSub.isStatusReply(reply as Array<Buffer>)) {
const head = this.#waitingForReply.head!.value;
if (
(Number.isNaN(head.channelsCounter!) && reply[2] === 0) ||
--head.channelsCounter! === 0
) {
this.#waitingForReply.shift()!.resolve();
}
return;
}
if (PONG.equals(reply[0] as Buffer)) {
const { resolve, returnBuffers } = this.#waitingForReply.shift()!,
buffer = ((reply[1] as Buffer).length === 0 ? reply[0] : reply[1]) as Buffer;
resolve(returnBuffers ? buffer : buffer.toString());
return;
}
}
const { resolve, reject } = this.#waitingForReply.shift()!;
if (reply instanceof ErrorReply) {
reject(reply);
} else {
resolve(reply);
}
}
});
decoder: Decoder;
constructor(
respVersion: RespVersions | null | undefined,
maxLength: number | null | undefined,
onShardedChannelMoved: OnShardedChannelMoved
onShardedChannelMoved: EventEmitter['emit']
) {
this.#maxLength = maxLength;
this.#onShardedChannelMoved = onShardedChannelMoved;
this.decoder = this._initiateDecoder(respVersion);
this._maxLength = maxLength;
this._onShardedChannelMoved = onShardedChannelMoved;
}
addCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: QueueCommandOptions): Promise<T> {
if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
private _initiateDecoder(respVersion: RespVersions | null | undefined) {
return respVersion === 3 ?
this._initiateResp3Decoder() :
this._initiateResp2Decoder();
}
private _onReply(reply: ReplyUnion) {
this._waitingForReply.shift()!.resolve(reply);
}
private _onErrorReply(err: ErrorReply) {
this._waitingForReply.shift()!.reject(err);
}
private _onPush(push: Array<any>) {
// TODO: type
if (this._pubSub.handleMessageReply(push)) return true;
const isShardedUnsubscribe = PubSub.isShardedUnsubscribe(push);
if (isShardedUnsubscribe && !this._waitingForReply.length) {
const channel = push[1].toString();
this._onShardedChannelMoved(
channel,
this._pubSub.removeShardedListeners(channel)
);
return true;
} else if (isShardedUnsubscribe || PubSub.isStatusReply(push)) {
const head = this._waitingForReply.head!.value;
if (
(Number.isNaN(head.channelsCounter!) && push[2] === 0) ||
--head.channelsCounter! === 0
) {
this._waitingForReply.shift()!.resolve();
}
return true;
}
}
private _getFlags() {
return this._waitingForReply.head!.value.flags ?? {};
}
private _initiateResp3Decoder() {
return new Decoder({
onReply: reply => this._onReply(reply),
onErrorReply: err => this._onErrorReply(err),
onPush: push => {
if (!this._onPush(push)) {
}
},
getFlags: () => this._getFlags()
});
}
private _initiateResp2Decoder() {
return new Decoder({
onReply: reply => {
if (this._pubSub.isActive && Array.isArray(reply)) {
if (this._onPush(reply)) return;
if (PONG.equals(reply[0] as Buffer)) {
const { resolve, flags } = this._waitingForReply.shift()!,
buffer = ((reply[1] as Buffer).length === 0 ? reply[0] : reply[1]) as Buffer;
resolve(flags?.[RESP_TYPES.SIMPLE_STRING] === Buffer ? buffer : buffer.toString());
return;
}
}
this._onReply(reply);
},
onErrorReply: err => this._onErrorReply(err),
// PUSH type does not exist in RESP2
// PubSub is handled in onReply
// @ts-expect-error
onPush: undefined,
getFlags: () => {
// PubSub push is an Array in RESP2
return this._pubSub.isActive ?
RESP2_PUSH_FLAGS :
this._getFlags();
}
});
}
addCommand<T>(args: CommandArguments, options?: QueueCommandOptions): Promise<T> {
if (this._maxLength && this._waitingToBeSent.length + this._waitingForReply.length >= this._maxLength) {
return Promise.reject(new Error('The queue is full'));
} else if (options?.signal?.aborted) {
return Promise.reject(new AbortError());
@@ -115,30 +157,26 @@ export default class RedisCommandsQueue {
const node = new LinkedList.Node<CommandWaitingToBeSent>({
args,
chainId: options?.chainId,
returnBuffers: options?.returnBuffers,
flags: options?.flags,
resolve,
reject
});
if (options?.signal) {
const listener = () => {
this.#waitingToBeSent.removeNode(node);
this._waitingToBeSent.removeNode(node);
node.value.reject(new AbortError());
};
node.value.abort = {
signal: options.signal,
listener
};
// AbortSignal type is incorrent
(options.signal as any).addEventListener('abort', listener, {
once: true
});
node.value.removeAbortListener = () => options.signal?.removeEventListener('abort', listener);
options.signal.addEventListener('abort', listener, { once: true });
}
if (options?.asap) {
this.#waitingToBeSent.unshiftNode(node);
this._waitingToBeSent.unshiftNode(node);
} else {
this.#waitingToBeSent.pushNode(node);
this._waitingToBeSent.pushNode(node);
}
});
}
@@ -149,8 +187,8 @@ export default class RedisCommandsQueue {
listener: PubSubListener<T>,
returnBuffers?: T
) {
return this.#pushPubSubCommand(
this.#pubSub.subscribe(type, channels, listener, returnBuffers)
return this._pushPubSubCommand(
this._pubSub.subscribe(type, channels, listener, returnBuffers)
);
}
@@ -160,17 +198,17 @@ export default class RedisCommandsQueue {
listener?: PubSubListener<T>,
returnBuffers?: T
) {
return this.#pushPubSubCommand(
this.#pubSub.unsubscribe(type, channels, listener, returnBuffers)
return this._pushPubSubCommand(
this._pubSub.unsubscribe(type, channels, listener, returnBuffers)
);
}
resubscribe(): Promise<any> | undefined {
const commands = this.#pubSub.resubscribe();
const commands = this._pubSub.resubscribe();
if (!commands.length) return;
return Promise.all(
commands.map(command => this.#pushPubSubCommand(command))
commands.map(command => this._pushPubSubCommand(command))
);
}
@@ -179,29 +217,29 @@ export default class RedisCommandsQueue {
channel: string,
listeners: ChannelListeners
) {
return this.#pushPubSubCommand(
this.#pubSub.extendChannelListeners(type, channel, listeners)
return this._pushPubSubCommand(
this._pubSub.extendChannelListeners(type, channel, listeners)
);
}
extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) {
return this.#pushPubSubCommand(
this.#pubSub.extendTypeListeners(type, listeners)
return this._pushPubSubCommand(
this._pubSub.extendTypeListeners(type, listeners)
);
}
getPubSubListeners(type: PubSubType) {
return this.#pubSub.getTypeListeners(type);
return this._pubSub.getTypeListeners(type);
}
#pushPubSubCommand(command: PubSubCommand) {
private _pushPubSubCommand(command: PubSubCommand) {
if (command === undefined) return;
return new Promise<void>((resolve, reject) => {
this.#waitingToBeSent.push({
this._waitingToBeSent.push({
args: command.args,
channelsCounter: command.channelsCounter,
returnBuffers: true,
flags: PUSH_FLAGS,
resolve: () => {
command.resolve();
resolve();
@@ -214,11 +252,11 @@ export default class RedisCommandsQueue {
});
}
getCommandToSend(): RedisCommandArguments | undefined {
const toSend = this.#waitingToBeSent.shift();
getCommandToSend(): CommandArguments | undefined {
const toSend = this._waitingToBeSent.shift();
if (!toSend) return;
let encoded: RedisCommandArguments;
let encoded: CommandArguments;
try {
encoded = encodeCommand(toSend.args);
} catch (err) {
@@ -226,38 +264,56 @@ export default class RedisCommandsQueue {
return;
}
this.#waitingForReply.push({
resolve: toSend.resolve,
reject: toSend.reject,
channelsCounter: toSend.channelsCounter,
returnBuffers: toSend.returnBuffers
});
this.#chainInExecution = toSend.chainId;
// TODO
// reuse `toSend`
(toSend.args as any) = undefined;
if (toSend.removeAbortListener) {
toSend.removeAbortListener();
(toSend.removeAbortListener as any) = undefined;
}
this._waitingForReply.push(toSend);
this._chainInExecution = toSend.chainId;
return encoded;
}
onReplyChunk(chunk: Buffer): void {
this.#decoder.write(chunk);
#flushWaitingForReply(err: Error): void {
while (this._waitingForReply.head) {
this._waitingForReply.shift()!.reject(err);
}
}
static #flushWaitingToBeSent(command: CommandWaitingToBeSent, err: Error) {
command.removeAbortListener?.();
command.reject(err);
}
flushWaitingForReply(err: Error): void {
this.#decoder.reset();
this.#pubSub.reset();
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
this.decoder.reset();
this._pubSub.reset();
if (!this.#chainInExecution) return;
this.#flushWaitingForReply(err);
while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) {
this.#waitingToBeSent.shift();
if (!this._chainInExecution) return;
while (this._waitingToBeSent.head?.value.chainId === this._chainInExecution) {
RedisCommandsQueue.#flushWaitingToBeSent(
this._waitingToBeSent.shift()!,
err
);
}
this.#chainInExecution = undefined;
this._chainInExecution = undefined;
}
flushAll(err: Error): void {
this.#decoder.reset();
this.#pubSub.reset();
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err);
this.decoder.reset();
this._pubSub.reset();
this.#flushWaitingForReply(err);
while (this._waitingToBeSent.head) {
RedisCommandsQueue.#flushWaitingToBeSent(
this._waitingToBeSent.shift()!,
err
);
}
}
}

View File

@@ -1,359 +0,0 @@
import CLUSTER_COMMANDS from '../cluster/commands';
import * as ACL_CAT from '../commands/ACL_CAT';
import * as ACL_DELUSER from '../commands/ACL_DELUSER';
import * as ACL_DRYRUN from '../commands/ACL_DRYRUN';
import * as ACL_GENPASS from '../commands/ACL_GENPASS';
import * as ACL_GETUSER from '../commands/ACL_GETUSER';
import * as ACL_LIST from '../commands/ACL_LIST';
import * as ACL_LOAD from '../commands/ACL_LOAD';
import * as ACL_LOG_RESET from '../commands/ACL_LOG_RESET';
import * as ACL_LOG from '../commands/ACL_LOG';
import * as ACL_SAVE from '../commands/ACL_SAVE';
import * as ACL_SETUSER from '../commands/ACL_SETUSER';
import * as ACL_USERS from '../commands/ACL_USERS';
import * as ACL_WHOAMI from '../commands/ACL_WHOAMI';
import * as ASKING from '../commands/ASKING';
import * as AUTH from '../commands/AUTH';
import * as BGREWRITEAOF from '../commands/BGREWRITEAOF';
import * as BGSAVE from '../commands/BGSAVE';
import * as CLIENT_CACHING from '../commands/CLIENT_CACHING';
import * as CLIENT_GETNAME from '../commands/CLIENT_GETNAME';
import * as CLIENT_GETREDIR from '../commands/CLIENT_GETREDIR';
import * as CLIENT_ID from '../commands/CLIENT_ID';
import * as CLIENT_KILL from '../commands/CLIENT_KILL';
import * as CLIENT_LIST from '../commands/CLIENT_LIST';
import * as CLIENT_NO_EVICT from '../commands/CLIENT_NO-EVICT';
import * as CLIENT_PAUSE from '../commands/CLIENT_PAUSE';
import * as CLIENT_SETNAME from '../commands/CLIENT_SETNAME';
import * as CLIENT_TRACKING from '../commands/CLIENT_TRACKING';
import * as CLIENT_TRACKINGINFO from '../commands/CLIENT_TRACKINGINFO';
import * as CLIENT_UNPAUSE from '../commands/CLIENT_UNPAUSE';
import * as CLIENT_INFO from '../commands/CLIENT_INFO';
import * as CLUSTER_ADDSLOTS from '../commands/CLUSTER_ADDSLOTS';
import * as CLUSTER_ADDSLOTSRANGE from '../commands/CLUSTER_ADDSLOTSRANGE';
import * as CLUSTER_BUMPEPOCH from '../commands/CLUSTER_BUMPEPOCH';
import * as CLUSTER_COUNT_FAILURE_REPORTS from '../commands/CLUSTER_COUNT-FAILURE-REPORTS';
import * as CLUSTER_COUNTKEYSINSLOT from '../commands/CLUSTER_COUNTKEYSINSLOT';
import * as CLUSTER_DELSLOTS from '../commands/CLUSTER_DELSLOTS';
import * as CLUSTER_DELSLOTSRANGE from '../commands/CLUSTER_DELSLOTSRANGE';
import * as CLUSTER_FAILOVER from '../commands/CLUSTER_FAILOVER';
import * as CLUSTER_FLUSHSLOTS from '../commands/CLUSTER_FLUSHSLOTS';
import * as CLUSTER_FORGET from '../commands/CLUSTER_FORGET';
import * as CLUSTER_GETKEYSINSLOT from '../commands/CLUSTER_GETKEYSINSLOT';
import * as CLUSTER_INFO from '../commands/CLUSTER_INFO';
import * as CLUSTER_KEYSLOT from '../commands/CLUSTER_KEYSLOT';
import * as CLUSTER_LINKS from '../commands/CLUSTER_LINKS';
import * as CLUSTER_MEET from '../commands/CLUSTER_MEET';
import * as CLUSTER_MYID from '../commands/CLUSTER_MYID';
import * as CLUSTER_NODES from '../commands/CLUSTER_NODES';
import * as CLUSTER_REPLICAS from '../commands/CLUSTER_REPLICAS';
import * as CLUSTER_REPLICATE from '../commands/CLUSTER_REPLICATE';
import * as CLUSTER_RESET from '../commands/CLUSTER_RESET';
import * as CLUSTER_SAVECONFIG from '../commands/CLUSTER_SAVECONFIG';
import * as CLUSTER_SET_CONFIG_EPOCH from '../commands/CLUSTER_SET-CONFIG-EPOCH';
import * as CLUSTER_SETSLOT from '../commands/CLUSTER_SETSLOT';
import * as CLUSTER_SLOTS from '../commands/CLUSTER_SLOTS';
import * as COMMAND_COUNT from '../commands/COMMAND_COUNT';
import * as COMMAND_GETKEYS from '../commands/COMMAND_GETKEYS';
import * as COMMAND_GETKEYSANDFLAGS from '../commands/COMMAND_GETKEYSANDFLAGS';
import * as COMMAND_INFO from '../commands/COMMAND_INFO';
import * as COMMAND_LIST from '../commands/COMMAND_LIST';
import * as COMMAND from '../commands/COMMAND';
import * as CONFIG_GET from '../commands/CONFIG_GET';
import * as CONFIG_RESETASTAT from '../commands/CONFIG_RESETSTAT';
import * as CONFIG_REWRITE from '../commands/CONFIG_REWRITE';
import * as CONFIG_SET from '../commands/CONFIG_SET';
import * as DBSIZE from '../commands/DBSIZE';
import * as DISCARD from '../commands/DISCARD';
import * as ECHO from '../commands/ECHO';
import * as FAILOVER from '../commands/FAILOVER';
import * as FLUSHALL from '../commands/FLUSHALL';
import * as FLUSHDB from '../commands/FLUSHDB';
import * as FUNCTION_DELETE from '../commands/FUNCTION_DELETE';
import * as FUNCTION_DUMP from '../commands/FUNCTION_DUMP';
import * as FUNCTION_FLUSH from '../commands/FUNCTION_FLUSH';
import * as FUNCTION_KILL from '../commands/FUNCTION_KILL';
import * as FUNCTION_LIST_WITHCODE from '../commands/FUNCTION_LIST_WITHCODE';
import * as FUNCTION_LIST from '../commands/FUNCTION_LIST';
import * as FUNCTION_LOAD from '../commands/FUNCTION_LOAD';
import * as FUNCTION_RESTORE from '../commands/FUNCTION_RESTORE';
import * as FUNCTION_STATS from '../commands/FUNCTION_STATS';
import * as HELLO from '../commands/HELLO';
import * as INFO from '../commands/INFO';
import * as KEYS from '../commands/KEYS';
import * as LASTSAVE from '../commands/LASTSAVE';
import * as LATENCY_DOCTOR from '../commands/LATENCY_DOCTOR';
import * as LATENCY_GRAPH from '../commands/LATENCY_GRAPH';
import * as LOLWUT from '../commands/LOLWUT';
import * as MEMORY_DOCTOR from '../commands/MEMORY_DOCTOR';
import * as MEMORY_MALLOC_STATS from '../commands/MEMORY_MALLOC-STATS';
import * as MEMORY_PURGE from '../commands/MEMORY_PURGE';
import * as MEMORY_STATS from '../commands/MEMORY_STATS';
import * as MEMORY_USAGE from '../commands/MEMORY_USAGE';
import * as MODULE_LIST from '../commands/MODULE_LIST';
import * as MODULE_LOAD from '../commands/MODULE_LOAD';
import * as MODULE_UNLOAD from '../commands/MODULE_UNLOAD';
import * as MOVE from '../commands/MOVE';
import * as PING from '../commands/PING';
import * as PUBSUB_CHANNELS from '../commands/PUBSUB_CHANNELS';
import * as PUBSUB_NUMPAT from '../commands/PUBSUB_NUMPAT';
import * as PUBSUB_NUMSUB from '../commands/PUBSUB_NUMSUB';
import * as PUBSUB_SHARDCHANNELS from '../commands/PUBSUB_SHARDCHANNELS';
import * as RANDOMKEY from '../commands/RANDOMKEY';
import * as READONLY from '../commands/READONLY';
import * as READWRITE from '../commands/READWRITE';
import * as REPLICAOF from '../commands/REPLICAOF';
import * as RESTORE_ASKING from '../commands/RESTORE-ASKING';
import * as ROLE from '../commands/ROLE';
import * as SAVE from '../commands/SAVE';
import * as SCAN from '../commands/SCAN';
import * as SCRIPT_DEBUG from '../commands/SCRIPT_DEBUG';
import * as SCRIPT_EXISTS from '../commands/SCRIPT_EXISTS';
import * as SCRIPT_FLUSH from '../commands/SCRIPT_FLUSH';
import * as SCRIPT_KILL from '../commands/SCRIPT_KILL';
import * as SCRIPT_LOAD from '../commands/SCRIPT_LOAD';
import * as SHUTDOWN from '../commands/SHUTDOWN';
import * as SWAPDB from '../commands/SWAPDB';
import * as TIME from '../commands/TIME';
import * as UNWATCH from '../commands/UNWATCH';
import * as WAIT from '../commands/WAIT';
export default {
...CLUSTER_COMMANDS,
ACL_CAT,
aclCat: ACL_CAT,
ACL_DELUSER,
aclDelUser: ACL_DELUSER,
ACL_DRYRUN,
aclDryRun: ACL_DRYRUN,
ACL_GENPASS,
aclGenPass: ACL_GENPASS,
ACL_GETUSER,
aclGetUser: ACL_GETUSER,
ACL_LIST,
aclList: ACL_LIST,
ACL_LOAD,
aclLoad: ACL_LOAD,
ACL_LOG_RESET,
aclLogReset: ACL_LOG_RESET,
ACL_LOG,
aclLog: ACL_LOG,
ACL_SAVE,
aclSave: ACL_SAVE,
ACL_SETUSER,
aclSetUser: ACL_SETUSER,
ACL_USERS,
aclUsers: ACL_USERS,
ACL_WHOAMI,
aclWhoAmI: ACL_WHOAMI,
ASKING,
asking: ASKING,
AUTH,
auth: AUTH,
BGREWRITEAOF,
bgRewriteAof: BGREWRITEAOF,
BGSAVE,
bgSave: BGSAVE,
CLIENT_CACHING,
clientCaching: CLIENT_CACHING,
CLIENT_GETNAME,
clientGetName: CLIENT_GETNAME,
CLIENT_GETREDIR,
clientGetRedir: CLIENT_GETREDIR,
CLIENT_ID,
clientId: CLIENT_ID,
CLIENT_KILL,
clientKill: CLIENT_KILL,
'CLIENT_NO-EVICT': CLIENT_NO_EVICT,
clientNoEvict: CLIENT_NO_EVICT,
CLIENT_LIST,
clientList: CLIENT_LIST,
CLIENT_PAUSE,
clientPause: CLIENT_PAUSE,
CLIENT_SETNAME,
clientSetName: CLIENT_SETNAME,
CLIENT_TRACKING,
clientTracking: CLIENT_TRACKING,
CLIENT_TRACKINGINFO,
clientTrackingInfo: CLIENT_TRACKINGINFO,
CLIENT_UNPAUSE,
clientUnpause: CLIENT_UNPAUSE,
CLIENT_INFO,
clientInfo: CLIENT_INFO,
CLUSTER_ADDSLOTS,
clusterAddSlots: CLUSTER_ADDSLOTS,
CLUSTER_ADDSLOTSRANGE,
clusterAddSlotsRange: CLUSTER_ADDSLOTSRANGE,
CLUSTER_BUMPEPOCH,
clusterBumpEpoch: CLUSTER_BUMPEPOCH,
CLUSTER_COUNT_FAILURE_REPORTS,
clusterCountFailureReports: CLUSTER_COUNT_FAILURE_REPORTS,
CLUSTER_COUNTKEYSINSLOT,
clusterCountKeysInSlot: CLUSTER_COUNTKEYSINSLOT,
CLUSTER_DELSLOTS,
clusterDelSlots: CLUSTER_DELSLOTS,
CLUSTER_DELSLOTSRANGE,
clusterDelSlotsRange: CLUSTER_DELSLOTSRANGE,
CLUSTER_FAILOVER,
clusterFailover: CLUSTER_FAILOVER,
CLUSTER_FLUSHSLOTS,
clusterFlushSlots: CLUSTER_FLUSHSLOTS,
CLUSTER_FORGET,
clusterForget: CLUSTER_FORGET,
CLUSTER_GETKEYSINSLOT,
clusterGetKeysInSlot: CLUSTER_GETKEYSINSLOT,
CLUSTER_INFO,
clusterInfo: CLUSTER_INFO,
CLUSTER_KEYSLOT,
clusterKeySlot: CLUSTER_KEYSLOT,
CLUSTER_LINKS,
clusterLinks: CLUSTER_LINKS,
CLUSTER_MEET,
clusterMeet: CLUSTER_MEET,
CLUSTER_MYID,
clusterMyId: CLUSTER_MYID,
CLUSTER_NODES,
clusterNodes: CLUSTER_NODES,
CLUSTER_REPLICAS,
clusterReplicas: CLUSTER_REPLICAS,
CLUSTER_REPLICATE,
clusterReplicate: CLUSTER_REPLICATE,
CLUSTER_RESET,
clusterReset: CLUSTER_RESET,
CLUSTER_SAVECONFIG,
clusterSaveConfig: CLUSTER_SAVECONFIG,
CLUSTER_SET_CONFIG_EPOCH,
clusterSetConfigEpoch: CLUSTER_SET_CONFIG_EPOCH,
CLUSTER_SETSLOT,
clusterSetSlot: CLUSTER_SETSLOT,
CLUSTER_SLOTS,
clusterSlots: CLUSTER_SLOTS,
COMMAND_COUNT,
commandCount: COMMAND_COUNT,
COMMAND_GETKEYS,
commandGetKeys: COMMAND_GETKEYS,
COMMAND_GETKEYSANDFLAGS,
commandGetKeysAndFlags: COMMAND_GETKEYSANDFLAGS,
COMMAND_INFO,
commandInfo: COMMAND_INFO,
COMMAND_LIST,
commandList: COMMAND_LIST,
COMMAND,
command: COMMAND,
CONFIG_GET,
configGet: CONFIG_GET,
CONFIG_RESETASTAT,
configResetStat: CONFIG_RESETASTAT,
CONFIG_REWRITE,
configRewrite: CONFIG_REWRITE,
CONFIG_SET,
configSet: CONFIG_SET,
DBSIZE,
dbSize: DBSIZE,
DISCARD,
discard: DISCARD,
ECHO,
echo: ECHO,
FAILOVER,
failover: FAILOVER,
FLUSHALL,
flushAll: FLUSHALL,
FLUSHDB,
flushDb: FLUSHDB,
FUNCTION_DELETE,
functionDelete: FUNCTION_DELETE,
FUNCTION_DUMP,
functionDump: FUNCTION_DUMP,
FUNCTION_FLUSH,
functionFlush: FUNCTION_FLUSH,
FUNCTION_KILL,
functionKill: FUNCTION_KILL,
FUNCTION_LIST_WITHCODE,
functionListWithCode: FUNCTION_LIST_WITHCODE,
FUNCTION_LIST,
functionList: FUNCTION_LIST,
FUNCTION_LOAD,
functionLoad: FUNCTION_LOAD,
FUNCTION_RESTORE,
functionRestore: FUNCTION_RESTORE,
FUNCTION_STATS,
functionStats: FUNCTION_STATS,
HELLO,
hello: HELLO,
INFO,
info: INFO,
KEYS,
keys: KEYS,
LASTSAVE,
lastSave: LASTSAVE,
LATENCY_DOCTOR,
latencyDoctor: LATENCY_DOCTOR,
LATENCY_GRAPH,
latencyGraph: LATENCY_GRAPH,
LOLWUT,
lolwut: LOLWUT,
MEMORY_DOCTOR,
memoryDoctor: MEMORY_DOCTOR,
'MEMORY_MALLOC-STATS': MEMORY_MALLOC_STATS,
memoryMallocStats: MEMORY_MALLOC_STATS,
MEMORY_PURGE,
memoryPurge: MEMORY_PURGE,
MEMORY_STATS,
memoryStats: MEMORY_STATS,
MEMORY_USAGE,
memoryUsage: MEMORY_USAGE,
MODULE_LIST,
moduleList: MODULE_LIST,
MODULE_LOAD,
moduleLoad: MODULE_LOAD,
MODULE_UNLOAD,
moduleUnload: MODULE_UNLOAD,
MOVE,
move: MOVE,
PING,
ping: PING,
PUBSUB_CHANNELS,
pubSubChannels: PUBSUB_CHANNELS,
PUBSUB_NUMPAT,
pubSubNumPat: PUBSUB_NUMPAT,
PUBSUB_NUMSUB,
pubSubNumSub: PUBSUB_NUMSUB,
PUBSUB_SHARDCHANNELS,
pubSubShardChannels: PUBSUB_SHARDCHANNELS,
RANDOMKEY,
randomKey: RANDOMKEY,
READONLY,
readonly: READONLY,
READWRITE,
readwrite: READWRITE,
REPLICAOF,
replicaOf: REPLICAOF,
'RESTORE-ASKING': RESTORE_ASKING,
restoreAsking: RESTORE_ASKING,
ROLE,
role: ROLE,
SAVE,
save: SAVE,
SCAN,
scan: SCAN,
SCRIPT_DEBUG,
scriptDebug: SCRIPT_DEBUG,
SCRIPT_EXISTS,
scriptExists: SCRIPT_EXISTS,
SCRIPT_FLUSH,
scriptFlush: SCRIPT_FLUSH,
SCRIPT_KILL,
scriptKill: SCRIPT_KILL,
SCRIPT_LOAD,
scriptLoad: SCRIPT_LOAD,
SHUTDOWN,
shutdown: SHUTDOWN,
SWAPDB,
swapDb: SWAPDB,
TIME,
time: TIME,
UNWATCH,
unwatch: UNWATCH,
WAIT,
wait: WAIT
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,200 +1,259 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction, RedisCommands } from '../commands';
import COMMANDS from '../commands';
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
import { attachCommands, attachExtensions, transformLegacyCommandArguments } from '../commander';
import { ReplyWithFlags, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, Flags, ReplyUnion } from '../RESP/types';
import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander';
type CommandSignature<
C extends RedisCommand,
REPLIES extends Array<unknown>,
C extends Command,
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = (...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, F, S>;
S extends RedisScripts,
RESP extends RespVersions,
FLAGS extends Flags
> = (...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<
[...REPLIES, ReplyWithFlags<CommandReply<C, RESP>, FLAGS>],
M,
F,
S,
RESP,
FLAGS
>;
type WithCommands<
REPLIES extends Array<unknown>,
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
S extends RedisScripts,
RESP extends RespVersions,
FLAGS extends Flags
> = {
[P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], M, F, S>;
[P in keyof typeof COMMANDS]: CommandSignature<REPLIES, (typeof COMMANDS)[P], M, F, S, RESP, FLAGS>;
};
type WithModules<
REPLIES extends Array<unknown>,
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
S extends RedisScripts,
RESP extends RespVersions,
FLAGS extends Flags
> = {
[P in keyof M as ExcludeMappedString<P>]: {
[C in keyof M[P] as ExcludeMappedString<C>]: CommandSignature<M[P][C], M, F, S>;
[P in keyof M]: {
[C in keyof M[P]]: CommandSignature<REPLIES, M[P][C], M, F, S, RESP, FLAGS>;
};
};
type WithFunctions<
REPLIES extends Array<unknown>,
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
S extends RedisScripts,
RESP extends RespVersions,
FLAGS extends Flags
> = {
[P in keyof F as ExcludeMappedString<P>]: {
[FF in keyof F[P] as ExcludeMappedString<FF>]: CommandSignature<F[P][FF], M, F, S>;
[L in keyof F]: {
[C in keyof F[L]]: CommandSignature<REPLIES, F[L][C], M, F, S, RESP, FLAGS>;
};
};
type WithScripts<
REPLIES extends Array<unknown>,
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
S extends RedisScripts,
RESP extends RespVersions,
FLAGS extends Flags
> = {
[P in keyof S as ExcludeMappedString<P>]: CommandSignature<S[P], M, F, S>;
[P in keyof S]: CommandSignature<REPLIES, S[P], M, F, S, RESP, FLAGS>;
};
export type RedisClientMultiCommandType<
REPLIES extends Array<any>,
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = RedisClientMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
S extends RedisScripts,
RESP extends RespVersions,
FLAGS extends Flags
> = (
RedisClientMultiCommand<REPLIES> &
WithCommands<REPLIES, M, F, S, RESP, FLAGS> &
WithModules<REPLIES, M, F, S, RESP, FLAGS> &
WithFunctions<REPLIES, M, F, S, RESP, FLAGS> &
WithScripts<REPLIES, M, F, S, RESP, FLAGS>
);
type InstantiableRedisMultiCommand<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = new (...args: ConstructorParameters<typeof RedisClientMultiCommand>) => RedisClientMultiCommandType<M, F, S>;
type MULTI_REPLY = {
GENERIC: 'generic';
TYPED: 'typed';
};
type MultiReply = MULTI_REPLY[keyof MULTI_REPLY];
type ReplyType<T extends MultiReply, REPLIES> = T extends MULTI_REPLY['TYPED'] ? REPLIES : Array<ReplyUnion>;
export type RedisClientMultiExecutor = (
queue: Array<RedisMultiQueuedCommand>,
selectedDB?: number,
chainId?: symbol
) => Promise<Array<RedisCommandRawReply>>;
) => Promise<Array<unknown>>;
export default class RedisClientMultiCommand<REPLIES = []> extends RedisMultiCommand {
static #createCommand(command: Command, resp: RespVersions) {
const transformReply = getTransformReply(command, resp);
return function (this: RedisClientMultiCommand) {
return this.addCommand(
command.transformArguments.apply(undefined, arguments as any),
transformReply
);
};
}
static #createModuleCommand(command: Command, resp: RespVersions) {
const transformReply = getTransformReply(command, resp);
return function (this: { self: RedisClientMultiCommand }) {
return this.self.addCommand(
command.transformArguments.apply(undefined, arguments as any),
transformReply
);
};
}
static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
const prefix = functionArgumentsPrefix(name, fn),
transformReply = getTransformReply(fn, resp);
return function (this: { self: RedisClientMultiCommand }) {
const fnArgs = fn.transformArguments.apply(undefined, arguments as any),
args: CommandArguments = prefix.concat(fnArgs);
args.preserve = fnArgs.preserve;
return this.self.addCommand(
args,
transformReply
);
};
}
static #createScriptCommand(script: RedisScript, resp: RespVersions) {
const transformReply = getTransformReply(script, resp);
return function (this: RedisClientMultiCommand) {
return this.addScript(
script,
script.transformArguments.apply(undefined, arguments as any),
transformReply
);
};
}
export default class RedisClientMultiCommand {
static extend<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(extensions?: RedisExtensions<M, F, S>): InstantiableRedisMultiCommand<M, F, S> {
return attachExtensions({
M extends RedisModules = Record<string, never>,
F extends RedisFunctions = Record<string, never>,
S extends RedisScripts = Record<string, never>,
RESP extends RespVersions = 2
>(config?: CommanderConfig<M, F, S, RESP>) {
return attachConfig({
BaseClass: RedisClientMultiCommand,
modulesExecutor: RedisClientMultiCommand.prototype.commandsExecutor,
modules: extensions?.modules,
functionsExecutor: RedisClientMultiCommand.prototype.functionsExecutor,
functions: extensions?.functions,
scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor,
scripts: extensions?.scripts
commands: COMMANDS,
createCommand: RedisClientMultiCommand.#createCommand,
createModuleCommand: RedisClientMultiCommand.#createModuleCommand,
createFunctionCommand: RedisClientMultiCommand.#createFunctionCommand,
createScriptCommand: RedisClientMultiCommand.#createScriptCommand,
config
});
}
readonly #multi = new RedisMultiCommand();
// readonly #multi = new RedisMultiCommand();
readonly #executor: RedisClientMultiExecutor;
readonly v4: Record<string, any> = {};
// readonly v4: Record<string, any> = {};
#selectedDB?: number;
constructor(executor: RedisClientMultiExecutor, legacyMode = false) {
super();
this.#executor = executor;
if (legacyMode) {
this.#legacyMode();
}
// if (legacyMode) {
// this.#legacyMode();
// }
}
#legacyMode(): void {
this.v4.addCommand = this.addCommand.bind(this);
(this as any).addCommand = (...args: Array<any>): this => {
this.#multi.addCommand(transformLegacyCommandArguments(args));
return this;
};
this.v4.exec = this.exec.bind(this);
(this as any).exec = (callback?: (err: Error | null, replies?: Array<unknown>) => unknown): void => {
this.v4.exec()
.then((reply: Array<unknown>) => {
if (!callback) return;
// #legacyMode(): void {
// this.v4.addCommand = this.addCommand.bind(this);
// (this as any).addCommand = (...args: Array<any>): this => {
// this.#multi.addCommand(transformLegacyCommandArguments(args));
// return this;
// };
// this.v4.exec = this.exec.bind(this);
// (this as any).exec = (callback?: (err: Error | null, replies?: Array<unknown>) => unknown): void => {
// this.v4.exec()
// .then((reply: Array<unknown>) => {
// if (!callback) return;
callback(null, reply);
})
.catch((err: Error) => {
if (!callback) {
// this.emit('error', err);
return;
}
// callback(null, reply);
// })
// .catch((err: Error) => {
// if (!callback) {
// // this.emit('error', err);
// return;
// }
callback(err);
});
};
// callback(err);
// });
// };
for (const [ name, command ] of Object.entries(COMMANDS as RedisCommands)) {
this.#defineLegacyCommand(name, command);
(this as any)[name.toLowerCase()] ??= (this as any)[name];
}
}
// for (const [name, command] of Object.entries(COMMANDS as RedisCommands)) {
// this.#defineLegacyCommand(name, command);
// (this as any)[name.toLowerCase()] ??= (this as any)[name];
// }
// }
#defineLegacyCommand(this: any, name: string, command?: RedisCommand): void {
this.v4[name] = this[name].bind(this.v4);
this[name] = command && command.TRANSFORM_LEGACY_REPLY && command.transformReply ?
(...args: Array<unknown>) => {
this.#multi.addCommand(
[name, ...transformLegacyCommandArguments(args)],
command.transformReply
);
return this;
} :
(...args: Array<unknown>) => this.addCommand(name, ...args);
}
// #defineLegacyCommand(this: any, name: string, command?: RedisCommand): void {
// this.v4[name] = this[name].bind(this.v4);
// this[name] = command && command.TRANSFORM_LEGACY_REPLY && command.transformReply ?
// (...args: Array<unknown>) => {
// this.#multi.addCommand(
// [name, ...transformLegacyCommandArguments(args)],
// command.transformReply
// );
// return this;
// } :
// (...args: Array<unknown>) => this.addCommand(name, ...args);
// }
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
return this.addCommand(
command.transformArguments(...args),
command.transformReply
);
}
SELECT(db: number, transformReply?: RedisCommand['transformReply']): this {
SELECT(db: number, transformReply?: TransformReply): this {
this.#selectedDB = db;
return this.addCommand(['SELECT', db.toString()], transformReply);
}
select = this.SELECT;
addCommand(args: RedisCommandArguments, transformReply?: RedisCommand['transformReply']): this {
this.#multi.addCommand(args, transformReply);
return this;
}
async exec<T extends MultiReply = MULTI_REPLY['GENERIC']>(execAsPipeline = false) {
if (execAsPipeline) return this.execAsPipeline<T>();
functionsExecutor(fn: RedisFunction, args: Array<unknown>, name: string): this {
this.#multi.addFunction(name, fn, args);
return this;
}
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
this.#multi.addScript(script, args);
return this;
}
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
if (execAsPipeline) {
return this.execAsPipeline();
}
return this.#multi.handleExecReplies(
return this.handleExecReplies(
await this.#executor(
this.#multi.queue,
this.queue,
this.#selectedDB,
RedisMultiCommand.generateChainId()
)
);
) as ReplyType<T, REPLIES>;
}
EXEC = this.exec;
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
if (this.#multi.queue.length === 0) return [];
execTyped(execAsPipeline = false) {
return this.exec<MULTI_REPLY['TYPED']>(execAsPipeline);
}
return this.#multi.transformReplies(
async execAsPipeline<T extends MultiReply = MULTI_REPLY['GENERIC']>() {
if (this.queue.length === 0) return [] as ReplyType<T, REPLIES>;
return this.transformReplies(
await this.#executor(
this.#multi.queue,
this.queue,
this.#selectedDB
)
);
) as ReplyType<T, REPLIES>;
}
execAsPipelineTyped() {
return this.execAsPipeline<MULTI_REPLY['TYPED']>();
}
}
attachCommands({
BaseClass: RedisClientMultiCommand,
commands: COMMANDS,
executor: RedisClientMultiCommand.prototype.commandsExecutor
});

View File

@@ -1,4 +1,4 @@
import { RedisCommandArgument } from "../commands";
import { RedisArgument } from "../RESP/types";
export enum PubSubType {
CHANNELS = 'CHANNELS',
@@ -90,7 +90,7 @@ export class PubSub {
listener: PubSubListener<T>,
returnBuffers?: T
) {
const args: Array<RedisCommandArgument> = [COMMANDS[type].subscribe],
const args: Array<RedisArgument> = [COMMANDS[type].subscribe],
channelsArray = PubSub.#channelsArray(channels);
for (const channel of channelsArray) {
let channelListeners = this.#listeners[type].get(channel);
@@ -184,7 +184,7 @@ export class PubSub {
}
extendTypeListeners(type: PubSubType, listeners: PubSubTypeListeners) {
const args: Array<RedisCommandArgument> = [COMMANDS[type].subscribe];
const args: Array<RedisArgument> = [COMMANDS[type].subscribe];
for (const [channel, channelListeners] of listeners) {
if (this.#extendChannelListeners(type, channel, channelListeners)) {
args.push(channel);
@@ -236,7 +236,7 @@ export class PubSub {
);
}
const args: Array<RedisCommandArgument> = [COMMANDS[type].unsubscribe];
const args: Array<RedisArgument> = [COMMANDS[type].unsubscribe];
for (const channel of channelsArray) {
const sets = listeners.get(channel);
if (sets) {
@@ -288,7 +288,7 @@ export class PubSub {
}
#unsubscribeCommand(
args: Array<RedisCommandArgument>,
args: Array<RedisArgument>,
channelsCounter: number,
removeListeners: () => void
) {

View File

@@ -1,9 +1,9 @@
import { EventEmitter } from 'events';
import * as net from 'net';
import * as tls from 'tls';
import { RedisCommandArguments } from '../commands';
import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError, ReconnectStrategyError } from '../errors';
import { promiseTimeout } from '../utils';
import { RedisArgument } from '../RESP/types';
export interface RedisSocketCommonOptions {
/**
@@ -199,7 +199,7 @@ export default class RedisSocket extends EventEmitter {
.off('error', reject)
.once('error', (err: Error) => this.#onSocketError(err))
.once('close', hadError => {
if (!hadError && this.#isReady && this.#socket === socket) {
if (!hadError && this.#isOpen && this.#socket === socket) {
this.#onSocketError(new SocketClosedUnexpectedlyError());
}
})
@@ -240,7 +240,7 @@ export default class RedisSocket extends EventEmitter {
});
}
writeCommand(args: RedisCommandArguments): void {
writeCommand(args: Array<RedisArgument>): void {
if (!this.#socket) {
throw new ClientClosedError();
}

View File

@@ -1,12 +1,12 @@
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
import { RedisClusterClientOptions, RedisClusterOptions } from '.';
import { RedisCommandArgument, RedisFunctions, RedisModules, RedisScripts } from '../commands';
import { RootNodesUnavailableError } from '../errors';
import { ClusterSlotsNode } from '../commands/CLUSTER_SLOTS';
import RedisClient, { RedisClientOptions, RedisClientType } from '../client';
import { types } from 'util';
import { ChannelListeners, PubSubType, PubSubTypeListeners } from '../client/pub-sub';
import { EventEmitter } from 'stream';
import { ChannelListeners, PubSubType, PubSubTypeListeners } from '../client/pub-sub';
import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions } from '../RESP/types';
// TODO: ?!
// 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.
@@ -26,24 +26,26 @@ type ValueOrPromise<T> = T | Promise<T>;
type ClientOrPromise<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = ValueOrPromise<RedisClientType<M, F, S>>;
S extends RedisScripts,
RESP extends RespVersions = 2
> = ValueOrPromise<RedisClientType<M, F, S, RESP>>;
export interface Node<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
S extends RedisScripts,
RESP extends RespVersions
> {
address: string;
client?: ClientOrPromise<M, F, S>;
client?: ClientOrPromise<M, F, S, RESP>;
}
export interface ShardNode<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> extends Node<M, F, S> {
id: string;
S extends RedisScripts,
RESP extends RespVersions
> extends Node<M, F, S, RESP> {
host: string;
port: number;
readonly: boolean;
@@ -52,32 +54,36 @@ export interface ShardNode<
export interface MasterNode<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> extends ShardNode<M, F, S> {
pubSubClient?: ClientOrPromise<M, F, S>;
S extends RedisScripts,
RESP extends RespVersions
> extends ShardNode<M, F, S, RESP> {
pubSubClient?: ClientOrPromise<M, F, S, RESP>;
}
export interface Shard<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
S extends RedisScripts,
RESP extends RespVersions
> {
master: MasterNode<M, F, S>;
replicas?: Array<ShardNode<M, F, S>>;
nodesIterator?: IterableIterator<ShardNode<M, F, S>>;
master: MasterNode<M, F, S, RESP>;
replicas?: Array<ShardNode<M, F, S, RESP>>;
nodesIterator?: IterableIterator<ShardNode<M, F, S, RESP>>;
}
type ShardWithReplicas<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = Shard<M, F, S> & Required<Pick<Shard<M, F, S>, 'replicas'>>;
S extends RedisScripts,
RESP extends RespVersions
> = Shard<M, F, S, RESP> & Required<Pick<Shard<M, F, S, RESP>, 'replicas'>>;
export type PubSubNode<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = Required<Node<M, F, S>>;
S extends RedisScripts,
RESP extends RespVersions
> = Required<Node<M, F, S, RESP>>;
type PubSubToResubscribe = Record<
PubSubType.CHANNELS | PubSubType.PATTERNS,
@@ -93,86 +99,89 @@ export type OnShardedChannelMovedError = (
export default class RedisClusterSlots<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
S extends RedisScripts,
RESP extends RespVersions
> {
static #SLOTS = 16384;
private static _SLOTS = 16384;
readonly #options: RedisClusterOptions<M, F, S>;
readonly #Client: InstantiableRedisClient<M, F, S>;
readonly #emit: EventEmitter['emit'];
slots = new Array<Shard<M, F, S>>(RedisClusterSlots.#SLOTS);
shards = new Array<Shard<M, F, S>>();
masters = new Array<ShardNode<M, F, S>>();
replicas = new Array<ShardNode<M, F, S>>();
readonly nodeByAddress = new Map<string, MasterNode<M, F, S> | ShardNode<M, F, S>>();
pubSubNode?: PubSubNode<M, F, S>;
private readonly _options: RedisClusterOptions<M, F, S, RESP>;
private readonly _clientFactory: ReturnType<typeof RedisClient.factory<M, F, S, RESP>>;
private readonly _emit: EventEmitter['emit'];
slots = new Array<Shard<M, F, S, RESP>>(RedisClusterSlots._SLOTS);
shards = new Array<Shard<M, F, S,RESP>>();
masters = new Array<ShardNode<M, F, S, RESP>>();
replicas = new Array<ShardNode<M, F, S, RESP>>();
readonly nodeByAddress = new Map<string, MasterNode<M, F, S, RESP> | ShardNode<M, F, S, RESP>>();
pubSubNode?: PubSubNode<M, F, S, RESP>;
#isOpen = false;
private _isOpen = false;
get isOpen() {
return this.#isOpen;
return this._isOpen;
}
constructor(
options: RedisClusterOptions<M, F, S>,
options: RedisClusterOptions<M, F, S, RESP>,
emit: EventEmitter['emit']
) {
this.#options = options;
this.#Client = RedisClient.extend(options);
this.#emit = emit;
this._options = options;
this._clientFactory = RedisClient.factory(options);
this._emit = emit;
}
async connect() {
if (this.#isOpen) {
if (this._isOpen) {
throw new Error('Cluster already open');
}
this.#isOpen = true;
this._isOpen = true;
try {
await this.#discoverWithRootNodes();
await this._discoverWithRootNodes();
} catch (err) {
this.#isOpen = false;
this._isOpen = false;
throw err;
}
}
async #discoverWithRootNodes() {
let start = Math.floor(Math.random() * this.#options.rootNodes.length);
for (let i = start; i < this.#options.rootNodes.length; i++) {
if (await this.#discover(this.#options.rootNodes[i])) return;
private async _discoverWithRootNodes() {
let start = Math.floor(Math.random() * this._options.rootNodes.length);
for (let i = start; i < this._options.rootNodes.length; i++) {
if (await this._discover(this._options.rootNodes[i])) return;
}
for (let i = 0; i < start; i++) {
if (await this.#discover(this.#options.rootNodes[i])) return;
if (await this._discover(this._options.rootNodes[i])) return;
}
throw new RootNodesUnavailableError();
}
#resetSlots() {
this.slots = new Array(RedisClusterSlots.#SLOTS);
private _resetSlots() {
this.slots = new Array(RedisClusterSlots._SLOTS);
this.shards = [];
this.masters = [];
this.replicas = [];
this.#randomNodeIterator = undefined;
this._randomNodeIterator = undefined;
}
async #discover(rootNode?: RedisClusterClientOptions) {
this.#resetSlots();
private async _discover(rootNode: RedisClusterClientOptions) {
this._resetSlots();
const addressesInUse = new Set<string>();
try {
const shards = await this.#getShards(rootNode),
const shards = await this._getShards(rootNode),
promises: Array<Promise<unknown>> = [],
eagerConnect = this.#options.minimizeConnections !== true;
eagerConnect = this._options.minimizeConnections !== true;
type a = typeof shards;
for (const { from, to, master, replicas } of shards) {
const shard: Shard<M, F, S> = {
master: this.#initiateSlotNode(master, false, eagerConnect, addressesInUse, promises)
const shard: Shard<M, F, S, RESP> = {
master: this._initiateSlotNode(master, false, eagerConnect, addressesInUse, promises)
};
if (this.#options.useReplicas) {
if (this._options.useReplicas) {
shard.replicas = replicas.map(replica =>
this.#initiateSlotNode(replica, true, eagerConnect, addressesInUse, promises)
this._initiateSlotNode(replica, true, eagerConnect, addressesInUse, promises)
);
}
@@ -197,7 +206,7 @@ export default class RedisClusterSlots<
if (channelsListeners.size || patternsListeners.size) {
promises.push(
this.#initiatePubSubClient({
this._initiatePubSubClient({
[PubSubType.CHANNELS]: channelsListeners,
[PubSubType.PATTERNS]: patternsListeners
})
@@ -211,14 +220,14 @@ export default class RedisClusterSlots<
if (node.client) {
promises.push(
this.#execOnNodeClient(node.client, client => client.disconnect())
this._execOnNodeClient(node.client, client => client.disconnect())
);
}
const { pubSubClient } = node as MasterNode<M, F, S>;
const { pubSubClient } = node as MasterNode<M, F, S, RESP>;
if (pubSubClient) {
promises.push(
this.#execOnNodeClient(pubSubClient, client => client.disconnect())
this._execOnNodeClient(pubSubClient, client => client.disconnect())
);
}
@@ -229,95 +238,82 @@ export default class RedisClusterSlots<
return true;
} catch (err) {
this.#emit('error', err);
this._emit('error', err);
return false;
}
}
async #getShards(rootNode?: RedisClusterClientOptions) {
const client = new this.#Client(
this.#clientOptionsDefaults(rootNode, true)
);
private async _getShards(rootNode: RedisClusterClientOptions) {
const options = this._clientOptionsDefaults(rootNode)!;
options.socket ??= {};
options.socket.reconnectStrategy = false;
options.RESP = this._options.RESP;
client.on('error', err => this.#emit('error', err));
const client = RedisClient.factory(this._options)(options);
client.on('error', err => this._emit('error', err));
await client.connect();
try {
// using `CLUSTER SLOTS` and not `CLUSTER SHARDS` to support older versions
// switch to `CLUSTER SHARDS` when Redis 7.0 will be the minimum supported version
return await client.clusterSlots();
} finally {
await client.disconnect();
}
}
#getNodeAddress(address: string): NodeAddress | undefined {
switch (typeof this.#options.nodeAddressMap) {
private _getNodeAddress(address: string): NodeAddress | undefined {
switch (typeof this._options.nodeAddressMap) {
case 'object':
return this.#options.nodeAddressMap[address];
return this._options.nodeAddressMap[address];
case 'function':
return this.#options.nodeAddressMap(address);
return this._options.nodeAddressMap(address);
}
}
#clientOptionsDefaults(
options?: RedisClusterClientOptions,
disableReconnect?: boolean
): RedisClusterClientOptions | undefined {
let result: RedisClusterClientOptions | undefined;
if (this.#options.defaults) {
private _clientOptionsDefaults(options?: RedisClientOptions): RedisClientOptions | undefined {
if (!this._options.defaults) return options;
let socket;
if (this.#options.defaults.socket) {
if (this._options.defaults.socket) {
socket = options?.socket ? {
...this.#options.defaults.socket,
...this._options.defaults.socket,
...options.socket
} : this.#options.defaults.socket;
} : this._options.defaults.socket;
} else {
socket = options?.socket;
}
result = {
...this.#options.defaults,
return {
...this._options.defaults,
...options,
socket
};
} else {
result = options;
}
if (disableReconnect) {
result ??= {};
result.socket ??= {};
result.socket.reconnectStrategy = false;
}
return result;
}
#initiateSlotNode(
{ id, ip, port }: ClusterSlotsNode,
private _initiateSlotNode(
slotAddress: NodeAddress,
readonly: boolean,
eagerConnent: boolean,
addressesInUse: Set<string>,
promises: Array<Promise<unknown>>
) {
const address = `${ip}:${port}`;
const address = `${slotAddress.host}:${slotAddress.port}`;
addressesInUse.add(address);
let node = this.nodeByAddress.get(address);
if (!node) {
node = {
id,
host: ip,
port,
...slotAddress,
address,
readonly,
client: undefined
};
if (eagerConnent) {
promises.push(this.#createNodeClient(node));
promises.push(this._createNodeClient(node));
}
this.nodeByAddress.set(address, node);
@@ -328,28 +324,29 @@ export default class RedisClusterSlots<
return node;
}
async #createClient(
node: ShardNode<M, F, S>,
private async _createClient(
node: ShardNode<M, F, S, RESP>,
readonly = node.readonly
) {
const client = new this.#Client(
this.#clientOptionsDefaults({
socket: this.#getNodeAddress(node.address) ?? {
const client = this._clientFactory(
this._clientOptionsDefaults({
socket: this._getNodeAddress(node.address) ?? {
host: node.host,
port: node.port
},
readonly
readonly,
RESP: this._options.RESP
})
);
client.on('error', err => this.#emit('error', err));
client.on('error', err => this._emit('error', err));
await client.connect();
return client;
}
#createNodeClient(node: ShardNode<M, F, S>) {
const promise = this.#createClient(node)
private _createNodeClient(node: ShardNode<M, F, S, RESP>) {
const promise = this._createClient(node)
.then(client => {
node.client = client;
return client;
@@ -362,46 +359,46 @@ export default class RedisClusterSlots<
return promise;
}
nodeClient(node: ShardNode<M, F, S>) {
return node.client ?? this.#createNodeClient(node);
nodeClient(node: ShardNode<M, F, S, RESP>) {
return node.client ?? this._createNodeClient(node);
}
#runningRediscoverPromise?: Promise<void>;
async rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
this.#runningRediscoverPromise ??= this.#rediscover(startWith)
async rediscover(startWith: RedisClientType<M, F, S, RESP>): Promise<void> {
this.#runningRediscoverPromise ??= this._rediscover(startWith)
.finally(() => this.#runningRediscoverPromise = undefined);
return this.#runningRediscoverPromise;
}
async #rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
if (await this.#discover(startWith.options)) return;
private async _rediscover(startWith: RedisClientType<M, F, S, RESP>): Promise<void> {
if (await this._discover(startWith.options!)) return;
return this.#discoverWithRootNodes();
return this._discoverWithRootNodes();
}
quit(): Promise<void> {
return this.#destroy(client => client.quit());
return this._destroy(client => client.quit());
}
disconnect(): Promise<void> {
return this.#destroy(client => client.disconnect());
return this._destroy(client => client.disconnect());
}
async #destroy(fn: (client: RedisClientType<M, F, S>) => Promise<unknown>): Promise<void> {
this.#isOpen = false;
private async _destroy(fn: (client: RedisClientType<M, F, S, RESP>) => Promise<unknown>): Promise<void> {
this._isOpen = false;
const promises = [];
for (const { master, replicas } of this.shards) {
if (master.client) {
promises.push(
this.#execOnNodeClient(master.client, fn)
this._execOnNodeClient(master.client, fn)
);
}
if (master.pubSubClient) {
promises.push(
this.#execOnNodeClient(master.pubSubClient, fn)
this._execOnNodeClient(master.pubSubClient, fn)
);
}
@@ -409,7 +406,7 @@ export default class RedisClusterSlots<
for (const { client } of replicas) {
if (client) {
promises.push(
this.#execOnNodeClient(client, fn)
this._execOnNodeClient(client, fn)
);
}
}
@@ -417,19 +414,19 @@ export default class RedisClusterSlots<
}
if (this.pubSubNode) {
promises.push(this.#execOnNodeClient(this.pubSubNode.client, fn));
promises.push(this._execOnNodeClient(this.pubSubNode.client, fn));
this.pubSubNode = undefined;
}
this.#resetSlots();
this._resetSlots();
this.nodeByAddress.clear();
await Promise.allSettled(promises);
}
#execOnNodeClient(
client: ClientOrPromise<M, F, S>,
fn: (client: RedisClientType<M, F, S>) => Promise<unknown>
private _execOnNodeClient(
client: ClientOrPromise<M, F, S, RESP>,
fn: (client: RedisClientType<M, F, S, RESP>) => Promise<unknown>
) {
return types.isPromise(client) ?
client.then(fn) :
@@ -437,9 +434,9 @@ export default class RedisClusterSlots<
}
getClient(
firstKey: RedisCommandArgument | undefined,
firstKey: RedisArgument | undefined,
isReadonly: boolean | undefined
): ClientOrPromise<M, F, S> {
): ClientOrPromise<M, F, S, RESP> {
if (!firstKey) {
return this.nodeClient(this.getRandomNode());
}
@@ -452,7 +449,7 @@ export default class RedisClusterSlots<
return this.nodeClient(this.getSlotRandomNode(slotNumber));
}
*#iterateAllNodes() {
private *_iterateAllNodes() {
let i = Math.floor(Math.random() * (this.masters.length + this.replicas.length));
if (i < this.masters.length) {
do {
@@ -480,14 +477,14 @@ export default class RedisClusterSlots<
}
}
#randomNodeIterator?: IterableIterator<ShardNode<M, F, S>>;
_randomNodeIterator?: IterableIterator<ShardNode<M, F, S, RESP>>;
getRandomNode() {
this.#randomNodeIterator ??= this.#iterateAllNodes();
return this.#randomNodeIterator.next().value as ShardNode<M, F, S>;
this._randomNodeIterator ??= this._iterateAllNodes();
return this._randomNodeIterator.next().value as ShardNode<M, F, S, RESP>;
}
*#slotNodesIterator(slot: ShardWithReplicas<M, F, S>) {
private *_slotNodesIterator(slot: ShardWithReplicas<M, F, S, RESP>) {
let i = Math.floor(Math.random() * (1 + slot.replicas.length));
if (i < slot.replicas.length) {
do {
@@ -510,8 +507,8 @@ export default class RedisClusterSlots<
return slot.master;
}
slot.nodesIterator ??= this.#slotNodesIterator(slot as ShardWithReplicas<M, F, S>);
return slot.nodesIterator.next().value as ShardNode<M, F, S>;
slot.nodesIterator ??= this._slotNodesIterator(slot as ShardWithReplicas<M, F, S, RESP>);
return slot.nodesIterator.next().value as ShardNode<M, F, S, RESP>;
}
getMasterByAddress(address: string) {
@@ -524,10 +521,10 @@ export default class RedisClusterSlots<
getPubSubClient() {
return this.pubSubNode ?
this.pubSubNode.client :
this.#initiatePubSubClient();
this._initiatePubSubClient();
}
async #initiatePubSubClient(toResubscribe?: PubSubToResubscribe) {
private async _initiatePubSubClient(toResubscribe?: PubSubToResubscribe) {
const index = Math.floor(Math.random() * (this.masters.length + this.replicas.length)),
node = index < this.masters.length ?
this.masters[index] :
@@ -535,7 +532,7 @@ export default class RedisClusterSlots<
this.pubSubNode = {
address: node.address,
client: this.#createClient(node, true)
client: this._createClient(node, true)
.then(async client => {
if (toResubscribe) {
await Promise.all([
@@ -553,11 +550,11 @@ export default class RedisClusterSlots<
})
};
return this.pubSubNode.client as Promise<RedisClientType<M, F, S>>;
return this.pubSubNode.client as Promise<RedisClientType<M, F, S, RESP>>;
}
async executeUnsubscribeCommand(
unsubscribe: (client: RedisClientType<M, F, S>) => Promise<void>
unsubscribe: (client: RedisClientType<M, F, S, RESP>) => Promise<void>
): Promise<void> {
const client = await this.getPubSubClient();
await unsubscribe(client);
@@ -573,8 +570,8 @@ export default class RedisClusterSlots<
return master.pubSubClient ?? this.#initiateShardedPubSubClient(master);
}
#initiateShardedPubSubClient(master: MasterNode<M, F, S>) {
const promise = this.#createClient(master, true)
#initiateShardedPubSubClient(master: MasterNode<M, F, S, RESP>) {
const promise = this._createClient(master, true)
.then(client => {
client.on('server-sunsubscribe', async (channel, listeners) => {
try {
@@ -586,7 +583,7 @@ export default class RedisClusterSlots<
listeners
);
} catch (err) {
this.#emit('sharded-shannel-moved-error', err, channel, listeners);
this._emit('sharded-shannel-moved-error', err, channel, listeners);
}
});
@@ -605,7 +602,7 @@ export default class RedisClusterSlots<
async executeShardedUnsubscribeCommand(
channel: string,
unsubscribe: (client: RedisClientType<M, F, S>) => Promise<void>
unsubscribe: (client: RedisClientType<M, F, S, RESP>) => Promise<void>
): Promise<void> {
const { master } = this.slots[calculateSlot(channel)];
if (!master.pubSubClient) return Promise.resolve();

View File

@@ -1,637 +0,0 @@
import * as APPEND from '../commands/APPEND';
import * as BITCOUNT from '../commands/BITCOUNT';
import * as BITFIELD_RO from '../commands/BITFIELD_RO';
import * as BITFIELD from '../commands/BITFIELD';
import * as BITOP from '../commands/BITOP';
import * as BITPOS from '../commands/BITPOS';
import * as BLMOVE from '../commands/BLMOVE';
import * as BLMPOP from '../commands/BLMPOP';
import * as BLPOP from '../commands/BLPOP';
import * as BRPOP from '../commands/BRPOP';
import * as BRPOPLPUSH from '../commands/BRPOPLPUSH';
import * as BZMPOP from '../commands/BZMPOP';
import * as BZPOPMAX from '../commands/BZPOPMAX';
import * as BZPOPMIN from '../commands/BZPOPMIN';
import * as COPY from '../commands/COPY';
import * as DECR from '../commands/DECR';
import * as DECRBY from '../commands/DECRBY';
import * as DEL from '../commands/DEL';
import * as DUMP from '../commands/DUMP';
import * as EVAL_RO from '../commands/EVAL_RO';
import * as EVAL from '../commands/EVAL';
import * as EVALSHA_RO from '../commands/EVALSHA_RO';
import * as EVALSHA from '../commands/EVALSHA';
import * as EXISTS from '../commands/EXISTS';
import * as EXPIRE from '../commands/EXPIRE';
import * as EXPIREAT from '../commands/EXPIREAT';
import * as EXPIRETIME from '../commands/EXPIRETIME';
import * as FCALL_RO from '../commands/FCALL_RO';
import * as FCALL from '../commands/FCALL';
import * as GEOADD from '../commands/GEOADD';
import * as GEODIST from '../commands/GEODIST';
import * as GEOHASH from '../commands/GEOHASH';
import * as GEOPOS from '../commands/GEOPOS';
import * as GEORADIUS_RO_WITH from '../commands/GEORADIUS_RO_WITH';
import * as GEORADIUS_RO from '../commands/GEORADIUS_RO';
import * as GEORADIUS_WITH from '../commands/GEORADIUS_WITH';
import * as GEORADIUS from '../commands/GEORADIUS';
import * as GEORADIUSBYMEMBER_RO_WITH from '../commands/GEORADIUSBYMEMBER_RO_WITH';
import * as GEORADIUSBYMEMBER_RO from '../commands/GEORADIUSBYMEMBER_RO';
import * as GEORADIUSBYMEMBER_WITH from '../commands/GEORADIUSBYMEMBER_WITH';
import * as GEORADIUSBYMEMBER from '../commands/GEORADIUSBYMEMBER';
import * as GEORADIUSBYMEMBERSTORE from '../commands/GEORADIUSBYMEMBERSTORE';
import * as GEORADIUSSTORE from '../commands/GEORADIUSSTORE';
import * as GEOSEARCH_WITH from '../commands/GEOSEARCH_WITH';
import * as GEOSEARCH from '../commands/GEOSEARCH';
import * as GEOSEARCHSTORE from '../commands/GEOSEARCHSTORE';
import * as GET from '../commands/GET';
import * as GETBIT from '../commands/GETBIT';
import * as GETDEL from '../commands/GETDEL';
import * as GETEX from '../commands/GETEX';
import * as GETRANGE from '../commands/GETRANGE';
import * as GETSET from '../commands/GETSET';
import * as HDEL from '../commands/HDEL';
import * as HEXISTS from '../commands/HEXISTS';
import * as HGET from '../commands/HGET';
import * as HGETALL from '../commands/HGETALL';
import * as HINCRBY from '../commands/HINCRBY';
import * as HINCRBYFLOAT from '../commands/HINCRBYFLOAT';
import * as HKEYS from '../commands/HKEYS';
import * as HLEN from '../commands/HLEN';
import * as HMGET from '../commands/HMGET';
import * as HRANDFIELD_COUNT_WITHVALUES from '../commands/HRANDFIELD_COUNT_WITHVALUES';
import * as HRANDFIELD_COUNT from '../commands/HRANDFIELD_COUNT';
import * as HRANDFIELD from '../commands/HRANDFIELD';
import * as HSCAN from '../commands/HSCAN';
import * as HSET from '../commands/HSET';
import * as HSETNX from '../commands/HSETNX';
import * as HSTRLEN from '../commands/HSTRLEN';
import * as HVALS from '../commands/HVALS';
import * as INCR from '../commands/INCR';
import * as INCRBY from '../commands/INCRBY';
import * as INCRBYFLOAT from '../commands/INCRBYFLOAT';
import * as LCS_IDX_WITHMATCHLEN from '../commands/LCS_IDX_WITHMATCHLEN';
import * as LCS_IDX from '../commands/LCS_IDX';
import * as LCS_LEN from '../commands/LCS_LEN';
import * as LCS from '../commands/LCS';
import * as LINDEX from '../commands/LINDEX';
import * as LINSERT from '../commands/LINSERT';
import * as LLEN from '../commands/LLEN';
import * as LMOVE from '../commands/LMOVE';
import * as LMPOP from '../commands/LMPOP';
import * as LPOP_COUNT from '../commands/LPOP_COUNT';
import * as LPOP from '../commands/LPOP';
import * as LPOS_COUNT from '../commands/LPOS_COUNT';
import * as LPOS from '../commands/LPOS';
import * as LPUSH from '../commands/LPUSH';
import * as LPUSHX from '../commands/LPUSHX';
import * as LRANGE from '../commands/LRANGE';
import * as LREM from '../commands/LREM';
import * as LSET from '../commands/LSET';
import * as LTRIM from '../commands/LTRIM';
import * as MGET from '../commands/MGET';
import * as MIGRATE from '../commands/MIGRATE';
import * as MSET from '../commands/MSET';
import * as MSETNX from '../commands/MSETNX';
import * as OBJECT_ENCODING from '../commands/OBJECT_ENCODING';
import * as OBJECT_FREQ from '../commands/OBJECT_FREQ';
import * as OBJECT_IDLETIME from '../commands/OBJECT_IDLETIME';
import * as OBJECT_REFCOUNT from '../commands/OBJECT_REFCOUNT';
import * as PERSIST from '../commands/PERSIST';
import * as PEXPIRE from '../commands/PEXPIRE';
import * as PEXPIREAT from '../commands/PEXPIREAT';
import * as PEXPIRETIME from '../commands/PEXPIRETIME';
import * as PFADD from '../commands/PFADD';
import * as PFCOUNT from '../commands/PFCOUNT';
import * as PFMERGE from '../commands/PFMERGE';
import * as PSETEX from '../commands/PSETEX';
import * as PTTL from '../commands/PTTL';
import * as PUBLISH from '../commands/PUBLISH';
import * as RENAME from '../commands/RENAME';
import * as RENAMENX from '../commands/RENAMENX';
import * as RPOP_COUNT from '../commands/RPOP_COUNT';
import * as RPOP from '../commands/RPOP';
import * as RPOPLPUSH from '../commands/RPOPLPUSH';
import * as RPUSH from '../commands/RPUSH';
import * as RPUSHX from '../commands/RPUSHX';
import * as SADD from '../commands/SADD';
import * as SCARD from '../commands/SCARD';
import * as SDIFF from '../commands/SDIFF';
import * as SDIFFSTORE from '../commands/SDIFFSTORE';
import * as SET from '../commands/SET';
import * as SETBIT from '../commands/SETBIT';
import * as SETEX from '../commands/SETEX';
import * as SETNX from '../commands/SETNX';
import * as SETRANGE from '../commands/SETRANGE';
import * as SINTER from '../commands/SINTER';
import * as SINTERCARD from '../commands/SINTERCARD';
import * as SINTERSTORE from '../commands/SINTERSTORE';
import * as SISMEMBER from '../commands/SISMEMBER';
import * as SMEMBERS from '../commands/SMEMBERS';
import * as SMISMEMBER from '../commands/SMISMEMBER';
import * as SMOVE from '../commands/SMOVE';
import * as SORT_RO from '../commands/SORT_RO';
import * as SORT_STORE from '../commands/SORT_STORE';
import * as SORT from '../commands/SORT';
import * as SPOP from '../commands/SPOP';
import * as SPUBLISH from '../commands/SPUBLISH';
import * as SRANDMEMBER_COUNT from '../commands/SRANDMEMBER_COUNT';
import * as SRANDMEMBER from '../commands/SRANDMEMBER';
import * as SREM from '../commands/SREM';
import * as SSCAN from '../commands/SSCAN';
import * as STRLEN from '../commands/STRLEN';
import * as SUNION from '../commands/SUNION';
import * as SUNIONSTORE from '../commands/SUNIONSTORE';
import * as TOUCH from '../commands/TOUCH';
import * as TTL from '../commands/TTL';
import * as TYPE from '../commands/TYPE';
import * as UNLINK from '../commands/UNLINK';
import * as WATCH from '../commands/WATCH';
import * as XACK from '../commands/XACK';
import * as XADD from '../commands/XADD';
import * as XAUTOCLAIM_JUSTID from '../commands/XAUTOCLAIM_JUSTID';
import * as XAUTOCLAIM from '../commands/XAUTOCLAIM';
import * as XCLAIM_JUSTID from '../commands/XCLAIM_JUSTID';
import * as XCLAIM from '../commands/XCLAIM';
import * as XDEL from '../commands/XDEL';
import * as XGROUP_CREATE from '../commands/XGROUP_CREATE';
import * as XGROUP_CREATECONSUMER from '../commands/XGROUP_CREATECONSUMER';
import * as XGROUP_DELCONSUMER from '../commands/XGROUP_DELCONSUMER';
import * as XGROUP_DESTROY from '../commands/XGROUP_DESTROY';
import * as XGROUP_SETID from '../commands/XGROUP_SETID';
import * as XINFO_CONSUMERS from '../commands/XINFO_CONSUMERS';
import * as XINFO_GROUPS from '../commands/XINFO_GROUPS';
import * as XINFO_STREAM from '../commands/XINFO_STREAM';
import * as XLEN from '../commands/XLEN';
import * as XPENDING_RANGE from '../commands/XPENDING_RANGE';
import * as XPENDING from '../commands/XPENDING';
import * as XRANGE from '../commands/XRANGE';
import * as XREAD from '../commands/XREAD';
import * as XREADGROUP from '../commands/XREADGROUP';
import * as XREVRANGE from '../commands/XREVRANGE';
import * as XSETID from '../commands/XSETID';
import * as XTRIM from '../commands/XTRIM';
import * as ZADD from '../commands/ZADD';
import * as ZCARD from '../commands/ZCARD';
import * as ZCOUNT from '../commands/ZCOUNT';
import * as ZDIFF_WITHSCORES from '../commands/ZDIFF_WITHSCORES';
import * as ZDIFF from '../commands/ZDIFF';
import * as ZDIFFSTORE from '../commands/ZDIFFSTORE';
import * as ZINCRBY from '../commands/ZINCRBY';
import * as ZINTER_WITHSCORES from '../commands/ZINTER_WITHSCORES';
import * as ZINTER from '../commands/ZINTER';
import * as ZINTERCARD from '../commands/ZINTERCARD';
import * as ZINTERSTORE from '../commands/ZINTERSTORE';
import * as ZLEXCOUNT from '../commands/ZLEXCOUNT';
import * as ZMPOP from '../commands/ZMPOP';
import * as ZMSCORE from '../commands/ZMSCORE';
import * as ZPOPMAX_COUNT from '../commands/ZPOPMAX_COUNT';
import * as ZPOPMAX from '../commands/ZPOPMAX';
import * as ZPOPMIN_COUNT from '../commands/ZPOPMIN_COUNT';
import * as ZPOPMIN from '../commands/ZPOPMIN';
import * as ZRANDMEMBER_COUNT_WITHSCORES from '../commands/ZRANDMEMBER_COUNT_WITHSCORES';
import * as ZRANDMEMBER_COUNT from '../commands/ZRANDMEMBER_COUNT';
import * as ZRANDMEMBER from '../commands/ZRANDMEMBER';
import * as ZRANGE_WITHSCORES from '../commands/ZRANGE_WITHSCORES';
import * as ZRANGE from '../commands/ZRANGE';
import * as ZRANGEBYLEX from '../commands/ZRANGEBYLEX';
import * as ZRANGEBYSCORE_WITHSCORES from '../commands/ZRANGEBYSCORE_WITHSCORES';
import * as ZRANGEBYSCORE from '../commands/ZRANGEBYSCORE';
import * as ZRANGESTORE from '../commands/ZRANGESTORE';
import * as ZRANK from '../commands/ZRANK';
import * as ZREM from '../commands/ZREM';
import * as ZREMRANGEBYLEX from '../commands/ZREMRANGEBYLEX';
import * as ZREMRANGEBYRANK from '../commands/ZREMRANGEBYRANK';
import * as ZREMRANGEBYSCORE from '../commands/ZREMRANGEBYSCORE';
import * as ZREVRANK from '../commands/ZREVRANK';
import * as ZSCAN from '../commands/ZSCAN';
import * as ZSCORE from '../commands/ZSCORE';
import * as ZUNION_WITHSCORES from '../commands/ZUNION_WITHSCORES';
import * as ZUNION from '../commands/ZUNION';
import * as ZUNIONSTORE from '../commands/ZUNIONSTORE';
export default {
APPEND,
append: APPEND,
BITCOUNT,
bitCount: BITCOUNT,
BITFIELD_RO,
bitFieldRo: BITFIELD_RO,
BITFIELD,
bitField: BITFIELD,
BITOP,
bitOp: BITOP,
BITPOS,
bitPos: BITPOS,
BLMOVE,
blMove: BLMOVE,
BLMPOP,
blmPop: BLMPOP,
BLPOP,
blPop: BLPOP,
BRPOP,
brPop: BRPOP,
BRPOPLPUSH,
brPopLPush: BRPOPLPUSH,
BZMPOP,
bzmPop: BZMPOP,
BZPOPMAX,
bzPopMax: BZPOPMAX,
BZPOPMIN,
bzPopMin: BZPOPMIN,
COPY,
copy: COPY,
DECR,
decr: DECR,
DECRBY,
decrBy: DECRBY,
DEL,
del: DEL,
DUMP,
dump: DUMP,
EVAL_RO,
evalRo: EVAL_RO,
EVAL,
eval: EVAL,
EVALSHA,
evalSha: EVALSHA,
EVALSHA_RO,
evalShaRo: EVALSHA_RO,
EXISTS,
exists: EXISTS,
EXPIRE,
expire: EXPIRE,
EXPIREAT,
expireAt: EXPIREAT,
EXPIRETIME,
expireTime: EXPIRETIME,
FCALL_RO,
fCallRo: FCALL_RO,
FCALL,
fCall: FCALL,
GEOADD,
geoAdd: GEOADD,
GEODIST,
geoDist: GEODIST,
GEOHASH,
geoHash: GEOHASH,
GEOPOS,
geoPos: GEOPOS,
GEORADIUS_RO_WITH,
geoRadiusRoWith: GEORADIUS_RO_WITH,
GEORADIUS_RO,
geoRadiusRo: GEORADIUS_RO,
GEORADIUS_WITH,
geoRadiusWith: GEORADIUS_WITH,
GEORADIUS,
geoRadius: GEORADIUS,
GEORADIUSBYMEMBER_RO_WITH,
geoRadiusByMemberRoWith: GEORADIUSBYMEMBER_RO_WITH,
GEORADIUSBYMEMBER_RO,
geoRadiusByMemberRo: GEORADIUSBYMEMBER_RO,
GEORADIUSBYMEMBER_WITH,
geoRadiusByMemberWith: GEORADIUSBYMEMBER_WITH,
GEORADIUSBYMEMBER,
geoRadiusByMember: GEORADIUSBYMEMBER,
GEORADIUSBYMEMBERSTORE,
geoRadiusByMemberStore: GEORADIUSBYMEMBERSTORE,
GEORADIUSSTORE,
geoRadiusStore: GEORADIUSSTORE,
GEOSEARCH_WITH,
geoSearchWith: GEOSEARCH_WITH,
GEOSEARCH,
geoSearch: GEOSEARCH,
GEOSEARCHSTORE,
geoSearchStore: GEOSEARCHSTORE,
GET,
get: GET,
GETBIT,
getBit: GETBIT,
GETDEL,
getDel: GETDEL,
GETEX,
getEx: GETEX,
GETRANGE,
getRange: GETRANGE,
GETSET,
getSet: GETSET,
HDEL,
hDel: HDEL,
HEXISTS,
hExists: HEXISTS,
HGET,
hGet: HGET,
HGETALL,
hGetAll: HGETALL,
HINCRBY,
hIncrBy: HINCRBY,
HINCRBYFLOAT,
hIncrByFloat: HINCRBYFLOAT,
HKEYS,
hKeys: HKEYS,
HLEN,
hLen: HLEN,
HMGET,
hmGet: HMGET,
HRANDFIELD_COUNT_WITHVALUES,
hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES,
HRANDFIELD_COUNT,
hRandFieldCount: HRANDFIELD_COUNT,
HRANDFIELD,
hRandField: HRANDFIELD,
HSCAN,
hScan: HSCAN,
HSET,
hSet: HSET,
HSETNX,
hSetNX: HSETNX,
HSTRLEN,
hStrLen: HSTRLEN,
HVALS,
hVals: HVALS,
INCR,
incr: INCR,
INCRBY,
incrBy: INCRBY,
INCRBYFLOAT,
incrByFloat: INCRBYFLOAT,
LCS_IDX_WITHMATCHLEN,
lcsIdxWithMatchLen: LCS_IDX_WITHMATCHLEN,
LCS_IDX,
lcsIdx: LCS_IDX,
LCS_LEN,
lcsLen: LCS_LEN,
LCS,
lcs: LCS,
LINDEX,
lIndex: LINDEX,
LINSERT,
lInsert: LINSERT,
LLEN,
lLen: LLEN,
LMOVE,
lMove: LMOVE,
LMPOP,
lmPop: LMPOP,
LPOP_COUNT,
lPopCount: LPOP_COUNT,
LPOP,
lPop: LPOP,
LPOS_COUNT,
lPosCount: LPOS_COUNT,
LPOS,
lPos: LPOS,
LPUSH,
lPush: LPUSH,
LPUSHX,
lPushX: LPUSHX,
LRANGE,
lRange: LRANGE,
LREM,
lRem: LREM,
LSET,
lSet: LSET,
LTRIM,
lTrim: LTRIM,
MGET,
mGet: MGET,
MIGRATE,
migrate: MIGRATE,
MSET,
mSet: MSET,
MSETNX,
mSetNX: MSETNX,
OBJECT_ENCODING,
objectEncoding: OBJECT_ENCODING,
OBJECT_FREQ,
objectFreq: OBJECT_FREQ,
OBJECT_IDLETIME,
objectIdleTime: OBJECT_IDLETIME,
OBJECT_REFCOUNT,
objectRefCount: OBJECT_REFCOUNT,
PERSIST,
persist: PERSIST,
PEXPIRE,
pExpire: PEXPIRE,
PEXPIREAT,
pExpireAt: PEXPIREAT,
PEXPIRETIME,
pExpireTime: PEXPIRETIME,
PFADD,
pfAdd: PFADD,
PFCOUNT,
pfCount: PFCOUNT,
PFMERGE,
pfMerge: PFMERGE,
PSETEX,
pSetEx: PSETEX,
PTTL,
pTTL: PTTL,
PUBLISH,
publish: PUBLISH,
RENAME,
rename: RENAME,
RENAMENX,
renameNX: RENAMENX,
RPOP_COUNT,
rPopCount: RPOP_COUNT,
RPOP,
rPop: RPOP,
RPOPLPUSH,
rPopLPush: RPOPLPUSH,
RPUSH,
rPush: RPUSH,
RPUSHX,
rPushX: RPUSHX,
SADD,
sAdd: SADD,
SCARD,
sCard: SCARD,
SDIFF,
sDiff: SDIFF,
SDIFFSTORE,
sDiffStore: SDIFFSTORE,
SINTER,
sInter: SINTER,
SINTERCARD,
sInterCard: SINTERCARD,
SINTERSTORE,
sInterStore: SINTERSTORE,
SET,
set: SET,
SETBIT,
setBit: SETBIT,
SETEX,
setEx: SETEX,
SETNX,
setNX: SETNX,
SETRANGE,
setRange: SETRANGE,
SISMEMBER,
sIsMember: SISMEMBER,
SMEMBERS,
sMembers: SMEMBERS,
SMISMEMBER,
smIsMember: SMISMEMBER,
SMOVE,
sMove: SMOVE,
SORT_RO,
sortRo: SORT_RO,
SORT_STORE,
sortStore: SORT_STORE,
SORT,
sort: SORT,
SPOP,
sPop: SPOP,
SPUBLISH,
sPublish: SPUBLISH,
SRANDMEMBER_COUNT,
sRandMemberCount: SRANDMEMBER_COUNT,
SRANDMEMBER,
sRandMember: SRANDMEMBER,
SREM,
sRem: SREM,
SSCAN,
sScan: SSCAN,
STRLEN,
strLen: STRLEN,
SUNION,
sUnion: SUNION,
SUNIONSTORE,
sUnionStore: SUNIONSTORE,
TOUCH,
touch: TOUCH,
TTL,
ttl: TTL,
TYPE,
type: TYPE,
UNLINK,
unlink: UNLINK,
WATCH,
watch: WATCH,
XACK,
xAck: XACK,
XADD,
xAdd: XADD,
XAUTOCLAIM_JUSTID,
xAutoClaimJustId: XAUTOCLAIM_JUSTID,
XAUTOCLAIM,
xAutoClaim: XAUTOCLAIM,
XCLAIM,
xClaim: XCLAIM,
XCLAIM_JUSTID,
xClaimJustId: XCLAIM_JUSTID,
XDEL,
xDel: XDEL,
XGROUP_CREATE,
xGroupCreate: XGROUP_CREATE,
XGROUP_CREATECONSUMER,
xGroupCreateConsumer: XGROUP_CREATECONSUMER,
XGROUP_DELCONSUMER,
xGroupDelConsumer: XGROUP_DELCONSUMER,
XGROUP_DESTROY,
xGroupDestroy: XGROUP_DESTROY,
XGROUP_SETID,
xGroupSetId: XGROUP_SETID,
XINFO_CONSUMERS,
xInfoConsumers: XINFO_CONSUMERS,
XINFO_GROUPS,
xInfoGroups: XINFO_GROUPS,
XINFO_STREAM,
xInfoStream: XINFO_STREAM,
XLEN,
xLen: XLEN,
XPENDING_RANGE,
xPendingRange: XPENDING_RANGE,
XPENDING,
xPending: XPENDING,
XRANGE,
xRange: XRANGE,
XREAD,
xRead: XREAD,
XREADGROUP,
xReadGroup: XREADGROUP,
XREVRANGE,
xRevRange: XREVRANGE,
XSETID,
xSetId: XSETID,
XTRIM,
xTrim: XTRIM,
ZADD,
zAdd: ZADD,
ZCARD,
zCard: ZCARD,
ZCOUNT,
zCount: ZCOUNT,
ZDIFF_WITHSCORES,
zDiffWithScores: ZDIFF_WITHSCORES,
ZDIFF,
zDiff: ZDIFF,
ZDIFFSTORE,
zDiffStore: ZDIFFSTORE,
ZINCRBY,
zIncrBy: ZINCRBY,
ZINTER_WITHSCORES,
zInterWithScores: ZINTER_WITHSCORES,
ZINTER,
zInter: ZINTER,
ZINTERCARD,
zInterCard: ZINTERCARD,
ZINTERSTORE,
zInterStore: ZINTERSTORE,
ZLEXCOUNT,
zLexCount: ZLEXCOUNT,
ZMPOP,
zmPop: ZMPOP,
ZMSCORE,
zmScore: ZMSCORE,
ZPOPMAX_COUNT,
zPopMaxCount: ZPOPMAX_COUNT,
ZPOPMAX,
zPopMax: ZPOPMAX,
ZPOPMIN_COUNT,
zPopMinCount: ZPOPMIN_COUNT,
ZPOPMIN,
zPopMin: ZPOPMIN,
ZRANDMEMBER_COUNT_WITHSCORES,
zRandMemberCountWithScores: ZRANDMEMBER_COUNT_WITHSCORES,
ZRANDMEMBER_COUNT,
zRandMemberCount: ZRANDMEMBER_COUNT,
ZRANDMEMBER,
zRandMember: ZRANDMEMBER,
ZRANGE_WITHSCORES,
zRangeWithScores: ZRANGE_WITHSCORES,
ZRANGE,
zRange: ZRANGE,
ZRANGEBYLEX,
zRangeByLex: ZRANGEBYLEX,
ZRANGEBYSCORE_WITHSCORES,
zRangeByScoreWithScores: ZRANGEBYSCORE_WITHSCORES,
ZRANGEBYSCORE,
zRangeByScore: ZRANGEBYSCORE,
ZRANGESTORE,
zRangeStore: ZRANGESTORE,
ZRANK,
zRank: ZRANK,
ZREM,
zRem: ZREM,
ZREMRANGEBYLEX,
zRemRangeByLex: ZREMRANGEBYLEX,
ZREMRANGEBYRANK,
zRemRangeByRank: ZREMRANGEBYRANK,
ZREMRANGEBYSCORE,
zRemRangeByScore: ZREMRANGEBYSCORE,
ZREVRANK,
zRevRank: ZREVRANK,
ZSCAN,
zScan: ZSCAN,
ZSCORE,
zScore: ZSCORE,
ZUNION_WITHSCORES,
zUnionWithScores: ZUNION_WITHSCORES,
ZUNION,
zUnion: ZUNION,
ZUNIONSTORE,
zUnionStore: ZUNIONSTORE
};

View File

@@ -1,362 +1,362 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
import RedisCluster from '.';
import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
import { commandOptions } from '../command-options';
import { SQUARE_SCRIPT } from '../client/index.spec';
import { RootNodesUnavailableError } from '../errors';
import { spy } from 'sinon';
import { promiseTimeout } from '../utils';
import RedisClient from '../client';
// import { strict as assert } from 'assert';
// import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
// import RedisCluster from '.';
// import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
// import { commandOptions } from '../command-options';
// import { SQUARE_SCRIPT } from '../client/index.spec';
// import { RootNodesUnavailableError } from '../errors';
// import { spy } from 'sinon';
// import { promiseTimeout } from '../utils';
// import RedisClient from '../client';
describe('Cluster', () => {
testUtils.testWithCluster('sendCommand', async cluster => {
assert.equal(
await cluster.sendCommand(undefined, true, ['PING']),
'PONG'
);
}, GLOBAL.CLUSTERS.OPEN);
// describe('Cluster', () => {
// testUtils.testWithCluster('sendCommand', async cluster => {
// assert.equal(
// await cluster.sendCommand(undefined, true, ['PING']),
// 'PONG'
// );
// }, GLOBAL.CLUSTERS.OPEN);
testUtils.testWithCluster('isOpen', async cluster => {
assert.equal(cluster.isOpen, true);
await cluster.disconnect();
assert.equal(cluster.isOpen, false);
}, GLOBAL.CLUSTERS.OPEN);
// testUtils.testWithCluster('isOpen', async cluster => {
// assert.equal(cluster.isOpen, true);
// await cluster.disconnect();
// assert.equal(cluster.isOpen, false);
// }, GLOBAL.CLUSTERS.OPEN);
testUtils.testWithCluster('connect should throw if already connected', async cluster => {
await assert.rejects(cluster.connect());
}, GLOBAL.CLUSTERS.OPEN);
// testUtils.testWithCluster('connect should throw if already connected', async cluster => {
// await assert.rejects(cluster.connect());
// }, 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('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('scripts', async cluster => {
// assert.equal(
// await cluster.square(2),
// 4
// );
// }, {
// ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: {
// scripts: {
// square: SQUARE_SCRIPT
// }
// }
// });
it('should throw RootNodesUnavailableError', async () => {
const cluster = RedisCluster.create({
rootNodes: []
});
// it('should throw RootNodesUnavailableError', async () => {
// const cluster = RedisCluster.create({
// rootNodes: []
// });
try {
await assert.rejects(
cluster.connect(),
RootNodesUnavailableError
);
} catch (err) {
await cluster.disconnect();
throw err;
}
});
// try {
// await assert.rejects(
// cluster.connect(),
// RootNodesUnavailableError
// );
// } catch (err) {
// await cluster.disconnect();
// throw err;
// }
// });
testUtils.testWithCluster('should handle live resharding', async cluster => {
const slot = 12539,
key = 'key',
value = 'value';
await cluster.set(key, value);
// testUtils.testWithCluster('should handle live resharding', async cluster => {
// const slot = 12539,
// key = 'key',
// value = 'value';
// await cluster.set(key, value);
const importing = cluster.slots[0].master,
migrating = cluster.slots[slot].master,
[ importingClient, migratingClient ] = await Promise.all([
cluster.nodeClient(importing),
cluster.nodeClient(migrating)
]);
// const importing = cluster.slots[0].master,
// migrating = cluster.slots[slot].master,
// [ importingClient, migratingClient ] = await Promise.all([
// cluster.nodeClient(importing),
// cluster.nodeClient(migrating)
// ]);
await Promise.all([
importingClient.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, migrating.id),
migratingClient.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, importing.id)
]);
// await Promise.all([
// importingClient.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, migrating.id),
// migratingClient.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, importing.id)
// ]);
// should be able to get the key from the migrating node
assert.equal(
await cluster.get(key),
value
);
// // should be able to get the key from the migrating node
// assert.equal(
// await cluster.get(key),
// value
// );
await migratingClient.migrate(
importing.host,
importing.port,
key,
0,
10
);
// await migratingClient.migrate(
// importing.host,
// importing.port,
// key,
// 0,
// 10
// );
// should be able to get the key from the importing node using `ASKING`
assert.equal(
await cluster.get(key),
value
);
// // should be able to get the key from the importing node using `ASKING`
// assert.equal(
// await cluster.get(key),
// value
// );
await Promise.all([
importingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id),
migratingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id),
]);
// await Promise.all([
// importingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id),
// migratingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id),
// ]);
// should handle `MOVED` errors
assert.equal(
await cluster.get(key),
value
);
}, {
serverArguments: [],
numberOfMasters: 2
});
// // should handle `MOVED` errors
// assert.equal(
// await cluster.get(key),
// value
// );
// }, {
// serverArguments: [],
// numberOfMasters: 2
// });
testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => {
const totalNodes = cluster.masters.length + cluster.replicas.length,
ids = new Set<string>();
for (let i = 0; i < totalNodes; i++) {
ids.add(cluster.getRandomNode().id);
}
// testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => {
// const totalNodes = cluster.masters.length + cluster.replicas.length,
// ids = new Set<string>();
// for (let i = 0; i < totalNodes; i++) {
// ids.add(cluster.getRandomNode().id);
// }
assert.equal(ids.size, totalNodes);
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
// assert.equal(ids.size, totalNodes);
// }, GLOBAL.CLUSTERS.WITH_REPLICAS);
testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => {
const totalNodes = 1 + cluster.slots[0].replicas!.length,
ids = new Set<string>();
for (let i = 0; i < totalNodes; i++) {
ids.add(cluster.getSlotRandomNode(0).id);
}
// testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => {
// const totalNodes = 1 + cluster.slots[0].replicas!.length,
// ids = new Set<string>();
// for (let i = 0; i < totalNodes; i++) {
// ids.add(cluster.getSlotRandomNode(0).id);
// }
assert.equal(ids.size, totalNodes);
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
// assert.equal(ids.size, totalNodes);
// }, GLOBAL.CLUSTERS.WITH_REPLICAS);
testUtils.testWithCluster('cluster topology', async cluster => {
assert.equal(cluster.slots.length, 16384);
const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS;
assert.equal(cluster.shards.length, numberOfMasters);
assert.equal(cluster.masters.length, numberOfMasters);
assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters);
assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas);
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
// testUtils.testWithCluster('cluster topology', async cluster => {
// assert.equal(cluster.slots.length, 16384);
// const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS;
// assert.equal(cluster.shards.length, numberOfMasters);
// assert.equal(cluster.masters.length, numberOfMasters);
// assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters);
// assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas);
// }, GLOBAL.CLUSTERS.WITH_REPLICAS);
testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => {
const masters = cluster.getMasters();
assert.ok(Array.isArray(masters));
for (const master of masters) {
assert.equal(typeof master.id, 'string');
assert.ok(master.client instanceof RedisClient);
}
}, {
...GLOBAL.CLUSTERS.OPEN,
clusterConfiguration: {
minimizeConnections: undefined // reset to default
}
});
// testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => {
// const masters = cluster.getMasters();
// assert.ok(Array.isArray(masters));
// for (const master of masters) {
// assert.equal(typeof master.id, 'string');
// assert.ok(master.client instanceof RedisClient);
// }
// }, {
// ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: {
// minimizeConnections: undefined // reset to default
// }
// });
testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => {
const master = cluster.getSlotMaster(0);
assert.equal(typeof master.id, 'string');
assert.ok(master.client instanceof RedisClient);
}, {
...GLOBAL.CLUSTERS.OPEN,
clusterConfiguration: {
minimizeConnections: undefined // reset to default
}
});
// testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => {
// const master = cluster.getSlotMaster(0);
// assert.equal(typeof master.id, 'string');
// assert.ok(master.client instanceof RedisClient);
// }, {
// ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: {
// minimizeConnections: undefined // reset to default
// }
// });
testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => {
await assert.rejects(cluster.mGet(['a', 'b']));
}, GLOBAL.CLUSTERS.OPEN);
// testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => {
// await assert.rejects(cluster.mGet(['a', 'b']));
// }, GLOBAL.CLUSTERS.OPEN);
testUtils.testWithCluster('should send commands with commandOptions to correct cluster slot (without redirections)', async cluster => {
// 'a' and 'b' hash to different cluster slots (see previous unit test)
// -> maxCommandRedirections 0: rejects on MOVED/ASK reply
await cluster.set(commandOptions({ isolated: true }), 'a', '1'),
await cluster.set(commandOptions({ isolated: true }), 'b', '2'),
// testUtils.testWithCluster('should send commands with commandOptions to correct cluster slot (without redirections)', async cluster => {
// // 'a' and 'b' hash to different cluster slots (see previous unit test)
// // -> maxCommandRedirections 0: rejects on MOVED/ASK reply
// await cluster.set(commandOptions({ isolated: true }), 'a', '1'),
// await cluster.set(commandOptions({ isolated: true }), 'b', '2'),
assert.equal(await cluster.get('a'), '1');
assert.equal(await cluster.get('b'), '2');
}, {
...GLOBAL.CLUSTERS.OPEN,
clusterConfiguration: {
maxCommandRedirections: 0
}
});
// assert.equal(await cluster.get('a'), '1');
// assert.equal(await cluster.get('b'), '2');
// }, {
// ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: {
// maxCommandRedirections: 0
// }
// });
describe('minimizeConnections', () => {
testUtils.testWithCluster('false', async cluster => {
for (const master of cluster.masters) {
assert.ok(master.client instanceof RedisClient);
}
}, {
...GLOBAL.CLUSTERS.OPEN,
clusterConfiguration: {
minimizeConnections: false
}
});
// describe('minimizeConnections', () => {
// testUtils.testWithCluster('false', async cluster => {
// for (const master of cluster.masters) {
// assert.ok(master.client instanceof RedisClient);
// }
// }, {
// ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: {
// minimizeConnections: false
// }
// });
testUtils.testWithCluster('true', async cluster => {
for (const master of cluster.masters) {
assert.equal(master.client, undefined);
}
}, {
...GLOBAL.CLUSTERS.OPEN,
clusterConfiguration: {
minimizeConnections: true
}
});
});
// testUtils.testWithCluster('true', async cluster => {
// for (const master of cluster.masters) {
// assert.equal(master.client, undefined);
// }
// }, {
// ...GLOBAL.CLUSTERS.OPEN,
// clusterConfiguration: {
// minimizeConnections: true
// }
// });
// });
describe('PubSub', () => {
testUtils.testWithCluster('subscribe & unsubscribe', async cluster => {
const listener = spy();
// describe('PubSub', () => {
// testUtils.testWithCluster('subscribe & unsubscribe', async cluster => {
// const listener = spy();
await cluster.subscribe('channel', listener);
// await cluster.subscribe('channel', listener);
await Promise.all([
waitTillBeenCalled(listener),
cluster.publish('channel', 'message')
]);
// await Promise.all([
// waitTillBeenCalled(listener),
// cluster.publish('channel', 'message')
// ]);
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
// assert.ok(listener.calledOnceWithExactly('message', 'channel'));
await cluster.unsubscribe('channel', listener);
// await cluster.unsubscribe('channel', listener);
assert.equal(cluster.pubSubNode, undefined);
}, GLOBAL.CLUSTERS.OPEN);
// assert.equal(cluster.pubSubNode, undefined);
// }, GLOBAL.CLUSTERS.OPEN);
testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => {
const listener = spy();
// testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => {
// const listener = spy();
await cluster.pSubscribe('channe*', listener);
// await cluster.pSubscribe('channe*', listener);
await Promise.all([
waitTillBeenCalled(listener),
cluster.publish('channel', 'message')
]);
// await Promise.all([
// waitTillBeenCalled(listener),
// cluster.publish('channel', 'message')
// ]);
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
// assert.ok(listener.calledOnceWithExactly('message', 'channel'));
await cluster.pUnsubscribe('channe*', listener);
// await cluster.pUnsubscribe('channe*', listener);
assert.equal(cluster.pubSubNode, undefined);
}, GLOBAL.CLUSTERS.OPEN);
// assert.equal(cluster.pubSubNode, undefined);
// }, GLOBAL.CLUSTERS.OPEN);
testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => {
const listener = spy();
await cluster.subscribe('channel', listener);
// testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => {
// const listener = spy();
// await cluster.subscribe('channel', listener);
assert.ok(cluster.pubSubNode);
const [ migrating, importing ] = cluster.masters[0].address === cluster.pubSubNode.address ?
cluster.masters :
[cluster.masters[1], cluster.masters[0]],
[ migratingClient, importingClient ] = await Promise.all([
cluster.nodeClient(migrating),
cluster.nodeClient(importing)
]);
// assert.ok(cluster.pubSubNode);
// const [ migrating, importing ] = cluster.masters[0].address === cluster.pubSubNode.address ?
// cluster.masters :
// [cluster.masters[1], cluster.masters[0]],
// [ migratingClient, importingClient ] = await Promise.all([
// cluster.nodeClient(migrating),
// cluster.nodeClient(importing)
// ]);
const range = cluster.slots[0].master === migrating ? {
key: 'bar', // 5061
start: 0,
end: 8191
} : {
key: 'foo', // 12182
start: 8192,
end: 16383
};
// const range = cluster.slots[0].master === migrating ? {
// key: 'bar', // 5061
// start: 0,
// end: 8191
// } : {
// key: 'foo', // 12182
// start: 8192,
// end: 16383
// };
await Promise.all([
migratingClient.clusterDelSlotsRange(range),
importingClient.clusterDelSlotsRange(range),
importingClient.clusterAddSlotsRange(range)
]);
// await Promise.all([
// migratingClient.clusterDelSlotsRange(range),
// importingClient.clusterDelSlotsRange(range),
// importingClient.clusterAddSlotsRange(range)
// ]);
// wait for migrating node to be notified about the new topology
while ((await migratingClient.clusterInfo()).state !== 'ok') {
await promiseTimeout(50);
}
// // wait for migrating node to be notified about the new topology
// while ((await migratingClient.clusterInfo()).state !== 'ok') {
// await promiseTimeout(50);
// }
// make sure to cause `MOVED` error
await cluster.get(range.key);
// // make sure to cause `MOVED` error
// await cluster.get(range.key);
await Promise.all([
cluster.publish('channel', 'message'),
waitTillBeenCalled(listener)
]);
// await Promise.all([
// cluster.publish('channel', 'message'),
// waitTillBeenCalled(listener)
// ]);
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
}, {
serverArguments: [],
numberOfMasters: 2,
minimumDockerVersion: [7]
});
// assert.ok(listener.calledOnceWithExactly('message', 'channel'));
// }, {
// serverArguments: [],
// numberOfMasters: 2,
// minimumDockerVersion: [7]
// });
testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => {
const listener = spy();
// testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => {
// const listener = spy();
await cluster.sSubscribe('channel', listener);
// await cluster.sSubscribe('channel', listener);
await Promise.all([
waitTillBeenCalled(listener),
cluster.sPublish('channel', 'message')
]);
// await Promise.all([
// waitTillBeenCalled(listener),
// cluster.sPublish('channel', 'message')
// ]);
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
// assert.ok(listener.calledOnceWithExactly('message', 'channel'));
await cluster.sUnsubscribe('channel', listener);
// await cluster.sUnsubscribe('channel', listener);
// 10328 is the slot of `channel`
assert.equal(cluster.slots[10328].master.pubSubClient, undefined);
}, {
...GLOBAL.CLUSTERS.OPEN,
minimumDockerVersion: [7]
});
// // 10328 is the slot of `channel`
// assert.equal(cluster.slots[10328].master.pubSubClient, undefined);
// }, {
// ...GLOBAL.CLUSTERS.OPEN,
// minimumDockerVersion: [7]
// });
testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => {
const SLOT = 10328,
migrating = cluster.slots[SLOT].master,
importing = cluster.masters.find(master => master !== migrating)!,
[ migratingClient, importingClient ] = await Promise.all([
cluster.nodeClient(migrating),
cluster.nodeClient(importing)
]);
// testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => {
// const SLOT = 10328,
// migrating = cluster.slots[SLOT].master,
// importing = cluster.masters.find(master => master !== migrating)!,
// [ migratingClient, importingClient ] = await Promise.all([
// cluster.nodeClient(migrating),
// cluster.nodeClient(importing)
// ]);
await Promise.all([
migratingClient.clusterDelSlots(SLOT),
importingClient.clusterDelSlots(SLOT),
importingClient.clusterAddSlots(SLOT)
]);
// await Promise.all([
// migratingClient.clusterDelSlots(SLOT),
// importingClient.clusterDelSlots(SLOT),
// importingClient.clusterAddSlots(SLOT)
// ]);
// wait for migrating node to be notified about the new topology
while ((await migratingClient.clusterInfo()).state !== 'ok') {
await promiseTimeout(50);
}
// // wait for migrating node to be notified about the new topology
// while ((await migratingClient.clusterInfo()).state !== 'ok') {
// await promiseTimeout(50);
// }
const listener = spy();
// const listener = spy();
// will trigger `MOVED` error
await cluster.sSubscribe('channel', listener);
// // will trigger `MOVED` error
// await cluster.sSubscribe('channel', listener);
await Promise.all([
waitTillBeenCalled(listener),
cluster.sPublish('channel', 'message')
]);
// await Promise.all([
// waitTillBeenCalled(listener),
// cluster.sPublish('channel', 'message')
// ]);
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
}, {
serverArguments: [],
minimumDockerVersion: [7]
});
});
});
// assert.ok(listener.calledOnceWithExactly('message', 'channel'));
// }, {
// serverArguments: [],
// minimumDockerVersion: [7]
// });
// });
// });

View File

@@ -1,24 +1,25 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, RedisFunction } from '../commands';
import { ClientCommandOptions, RedisClientOptions, RedisClientType, WithFunctions, WithModules, WithScripts } from '../client';
import RedisClusterSlots, { NodeAddressMap, ShardNode } from './cluster-slots';
import { attachExtensions, transformCommandReply, attachCommands, transformCommandArguments } from '../commander';
import { ClientCommandOptions, RedisClientOptions, RedisClientType } from '../client';
import { Command, CommandArguments, CommanderConfig, CommandPolicies, CommandSignature, CommandWithPoliciesSignature, Flags, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, TransformReply } from '../RESP/types';
import COMMANDS from '../commands';
import { EventEmitter } from 'events';
import RedisClusterMultiCommand, { InstantiableRedisClusterMultiCommandType, RedisClusterMultiCommandType } from './multi-command';
import { RedisMultiQueuedCommand } from '../multi-command';
import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
import RedisClusterSlots, { NodeAddressMap, ShardNode } from './cluster-slots';
// import RedisClusterMultiCommand, { InstantiableRedisClusterMultiCommandType, RedisClusterMultiCommandType } from './multi-command';
// import { RedisMultiQueuedCommand } from '../multi-command';
import { PubSubListener } from '../client/pub-sub';
import { ErrorReply } from '../errors';
export type RedisClusterClientOptions = Omit<
RedisClientOptions,
'modules' | 'functions' | 'scripts' | 'database'
'modules' | 'functions' | 'scripts' | 'database' | 'RESP'
>;
export interface RedisClusterOptions<
M extends RedisModules = Record<string, never>,
F extends RedisFunctions = Record<string, never>,
S extends RedisScripts = Record<string, never>
> extends RedisExtensions<M, F, S> {
M extends RedisModules = RedisModules,
F extends RedisFunctions = RedisFunctions,
S extends RedisScripts = RedisScripts,
RESP extends RespVersions = RespVersions
> extends CommanderConfig<M, F, S, RESP> {
/**
* Should contain details for some of the cluster nodes that the client will use to discover
* the "cluster topology". We recommend including details for at least 3 nodes here.
@@ -37,6 +38,7 @@ export interface RedisClusterOptions<
/**
* When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes.
*/
// TODO: replicas only mode?
useReplicas?: boolean;
/**
* The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors.
@@ -45,220 +47,339 @@ export interface RedisClusterOptions<
/**
* Mapping between the addresses in the cluster (see `CLUSTER SHARDS`) and the addresses the client should connect to
* Useful when the cluster is running on another network
*
*/
nodeAddressMap?: NodeAddressMap;
}
type WithCommands = {
[P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>;
type WithCommands<
RESP extends RespVersions,
FLAGS extends Flags,
POLICIES extends CommandPolicies
> = {
[P in keyof typeof COMMANDS]: CommandWithPoliciesSignature<(typeof COMMANDS)[P], RESP, FLAGS, POLICIES>;
};
export type RedisClusterType<
M extends RedisModules = Record<string, never>,
F extends RedisFunctions = Record<string, never>,
S extends RedisScripts = Record<string, never>
> = RedisCluster<M, F, S> & WithCommands & WithModules<M> & WithFunctions<F> & WithScripts<S>;
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2,
FLAGS extends Flags = {},
POLICIES extends CommandPolicies = {}
> = RedisCluster<M, F, S, RESP, FLAGS, POLICIES> & WithCommands<RESP, FLAGS, POLICIES>;
// & WithModules<M> & WithFunctions<F> & WithScripts<S>
export interface ClusterCommandOptions extends ClientCommandOptions {
policies?: CommandPolicies;
}
type ProxyCluster = RedisCluster<RedisModules, RedisFunctions, RedisScripts, RespVersions, Flags, CommandPolicies> & { commandOptions?: ClusterCommandOptions };
type NamespaceProxyCluster = { self: ProxyCluster };
export default class RedisCluster<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
S extends RedisScripts,
RESP extends RespVersions,
FLAGS extends Flags,
POLICIES extends CommandPolicies
> extends EventEmitter {
static extractFirstKey(
command: RedisCommand,
originalArgs: Array<unknown>,
redisArgs: RedisCommandArguments
): RedisCommandArgument | undefined {
private static _extractFirstKey<C extends Command>(
command: C,
args: Parameters<C['transformArguments']>,
redisArgs: Array<RedisArgument>
): RedisArgument | undefined {
if (command.FIRST_KEY_INDEX === undefined) {
return undefined;
} else if (typeof command.FIRST_KEY_INDEX === 'number') {
return redisArgs[command.FIRST_KEY_INDEX];
}
return command.FIRST_KEY_INDEX(...originalArgs);
return command.FIRST_KEY_INDEX(...args);
}
private static _createCommand(command: Command, resp: RespVersions) {
const transformReply = getTransformReply(command, resp);
return async function (this: ProxyCluster) {
const args = command.transformArguments.apply(undefined, arguments as any),
firstKey = RedisCluster._extractFirstKey(
command,
arguments as any,
args
),
reply = await this.sendCommand(
firstKey,
command.IS_READ_ONLY,
args,
this.commandOptions,
command.POLICIES
);
return transformReply ?
transformReply(reply, args.preserve) :
reply;
};
}
private static _createModuleCommand(command: Command, resp: RespVersions) {
const transformReply = getTransformReply(command, resp);
return async function (this: NamespaceProxyCluster) {
const args = command.transformArguments.apply(undefined, arguments as any),
firstKey = RedisCluster._extractFirstKey(
command,
arguments as any,
args
),
reply = await this.self.sendCommand(
firstKey,
command.IS_READ_ONLY,
args,
this.self.commandOptions,
command.POLICIES
);
return transformReply ?
transformReply(reply, args.preserve) :
reply;
};
}
private static _createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) {
const prefix = functionArgumentsPrefix(name, fn),
transformReply = getTransformReply(fn, resp);
return async function (this: NamespaceProxyCluster) {
const fnArgs = fn.transformArguments.apply(undefined, arguments as any),
args = prefix.concat(fnArgs),
firstKey = RedisCluster._extractFirstKey(
fn,
arguments as any,
args
),
reply = await this.self.sendCommand(
firstKey,
fn.IS_READ_ONLY,
args,
this.self.commandOptions,
fn.POLICIES
);
return transformReply ?
transformReply(reply, fnArgs.preserve) :
reply;
};
}
private static _createScriptCommand(script: RedisScript, resp: RespVersions) {
const prefix = scriptArgumentsPrefix(script),
transformReply = getTransformReply(script, resp);
return async function (this: ProxyCluster) {
const scriptArgs = script.transformArguments.apply(undefined, arguments as any),
args = prefix.concat(scriptArgs),
firstKey = RedisCluster._extractFirstKey(
script,
arguments as any,
args
),
reply = await this.sendCommand(
firstKey,
script.IS_READ_ONLY,
args,
this.commandOptions,
script.POLICIES
);
return transformReply ?
transformReply(reply, scriptArgs.preserve) :
reply;
};
}
static factory<
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2
>(config?: CommanderConfig<M, F, S, RESP>) {
const Cluster = attachConfig({
BaseClass: RedisCluster,
commands: COMMANDS,
createCommand: RedisCluster._createCommand,
createFunctionCommand: RedisCluster._createFunctionCommand,
createModuleCommand: RedisCluster._createModuleCommand,
createScriptCommand: RedisCluster._createScriptCommand,
config
});
// Client.prototype.Multi = RedisClientMultiCommand.extend(config);
return (options?: Omit<RedisClusterOptions, keyof Exclude<typeof config, undefined>>) => {
// returning a proxy of the client to prevent the namespaces.self to leak between proxies
// namespaces will be bootstraped on first access per proxy
return Object.create(new Cluster(options)) as RedisClusterType<M, F, S, RESP>;
};
}
static create<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(options?: RedisClusterOptions<M, F, S>): RedisClusterType<M, F, S> {
return new (attachExtensions({
BaseClass: RedisCluster,
modulesExecutor: RedisCluster.prototype.commandsExecutor,
modules: options?.modules,
functionsExecutor: RedisCluster.prototype.functionsExecutor,
functions: options?.functions,
scriptsExecutor: RedisCluster.prototype.scriptsExecutor,
scripts: options?.scripts
}))(options);
M extends RedisModules = {},
F extends RedisFunctions = {},
S extends RedisScripts = {},
RESP extends RespVersions = 2
>(options?: RedisClusterOptions<M, F, S, RESP>) {
return RedisCluster.factory(options)(options);
}
readonly #options: RedisClusterOptions<M, F, S>;
private readonly _options: RedisClusterOptions<M, F, S, RESP>;
readonly #slots: RedisClusterSlots<M, F, S>;
private readonly _slots: RedisClusterSlots<M, F, S, RESP>;
/**
* An array of the cluster slots, each slot contain its `master` and `replicas`.
* Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica).
*/
get slots() {
return this.#slots.slots;
return this._slots.slots;
}
/**
* An array of cluster shards, each shard contain its `master` and `replicas`.
* Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica).
*/
get shards() {
return this.#slots.shards;
return this._slots.shards;
}
/**
* An array of the cluster masters.
* Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific master node.
*/
get masters() {
return this.#slots.masters;
return this._slots.masters;
}
/**
* An array of the cluster replicas.
* Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific replica node.
*/
get replicas() {
return this.#slots.replicas;
return this._slots.replicas;
}
/**
* A map form a node address (`<host>:<port>`) to its shard, each shard contain its `master` and `replicas`.
* Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica).
*/
get nodeByAddress() {
return this.#slots.nodeByAddress;
return this._slots.nodeByAddress;
}
/**
* The current pub/sub node.
*/
get pubSubNode() {
return this.#slots.pubSubNode;
return this._slots.pubSubNode;
}
readonly #Multi: InstantiableRedisClusterMultiCommandType<M, F, S>;
// readonly #Multi: InstantiableRedisClusterMultiCommandType<M, F, S>;
get isOpen() {
return this.#slots.isOpen;
return this._slots.isOpen;
}
constructor(options: RedisClusterOptions<M, F, S>) {
constructor(options: RedisClusterOptions<M, F, S, RESP>) {
super();
this.#options = options;
this.#slots = new RedisClusterSlots(options, this.emit.bind(this));
this.#Multi = RedisClusterMultiCommand.extend(options);
this._options = options;
this._slots = new RedisClusterSlots(options, this.emit.bind(this));
// this.#Multi = RedisClusterMultiCommand.extend(options);
}
duplicate(overrides?: Partial<RedisClusterOptions<M, F, S>>): RedisClusterType<M, F, S> {
return new (Object.getPrototypeOf(this).constructor)({
...this.#options,
...this._options,
...overrides
});
}
connect() {
return this.#slots.connect();
return this._slots.connect();
}
async commandsExecutor<C extends RedisCommand>(
command: C,
args: Array<unknown>
): Promise<RedisCommandReply<C>> {
const { jsArgs, args: redisArgs, options } = transformCommandArguments(command, args);
return transformCommandReply(
command,
await this.sendCommand(
RedisCluster.extractFirstKey(command, jsArgs, redisArgs),
command.IS_READ_ONLY,
redisArgs,
options
),
redisArgs.preserve
);
withCommandOptions<T extends ClusterCommandOptions>(options: T) {
const proxy = Object.create(this);
proxy.commandOptions = options;
return proxy as RedisClusterType<
M,
F,
S,
RESP,
T['flags'] extends Flags ? T['flags'] : {},
T['policies'] extends CommandPolicies ? T['policies'] : {}
>;
}
async sendCommand<T = RedisCommandRawReply>(
firstKey: RedisCommandArgument | undefined,
private _commandOptionsProxy<
K extends keyof ClusterCommandOptions,
V extends ClusterCommandOptions[K]
>(
key: K,
value: V
) {
const proxy = Object.create(this);
proxy.commandOptions = Object.create((this as unknown as ProxyCluster).commandOptions ?? null);
proxy.commandOptions[key] = value;
return proxy as RedisClusterType<
M,
F,
S,
RESP,
K extends 'flags' ? V extends Flags ? V : {} : FLAGS,
K extends 'policies' ? V extends CommandPolicies ? V : {} : POLICIES
>;
}
/**
* Override the `flags` command option
*/
withFlags<FLAGS extends Flags>(flags: FLAGS) {
return this._commandOptionsProxy('flags', flags);
}
/**
* Override the `policies` command option
* TODO
*/
withPolicies<POLICIES extends CommandPolicies> (policies: POLICIES) {
return this._commandOptionsProxy('policies', policies);
}
async sendCommand<T = ReplyUnion>(
firstKey: RedisArgument | undefined,
isReadonly: boolean | undefined,
args: RedisCommandArguments,
options?: ClientCommandOptions
args: CommandArguments,
options?: ClusterCommandOptions,
deafultPolicies?: CommandPolicies
): Promise<T> {
return this.#execute(
firstKey,
isReadonly,
client => client.sendCommand<T>(args, options)
);
}
// const requestPolicy = options?.policies?.request ?? deafultPolicies?.request,
// responsePolicy = options?.policies?.response ?? deafultPolicies?.response;
async functionsExecutor<F extends RedisFunction>(
fn: F,
args: Array<unknown>,
name: string,
): Promise<RedisCommandReply<F>> {
const { args: redisArgs, options } = transformCommandArguments(fn, args);
return transformCommandReply(
fn,
await this.executeFunction(
name,
fn,
args,
redisArgs,
options
),
redisArgs.preserve
);
}
async executeFunction(
name: string,
fn: RedisFunction,
originalArgs: Array<unknown>,
redisArgs: RedisCommandArguments,
options?: ClientCommandOptions
): Promise<RedisCommandRawReply> {
return this.#execute(
RedisCluster.extractFirstKey(fn, originalArgs, redisArgs),
fn.IS_READ_ONLY,
client => client.executeFunction(name, fn, redisArgs, options)
);
}
async scriptsExecutor<S extends RedisScript>(script: S, args: Array<unknown>): Promise<RedisCommandReply<S>> {
const { args: redisArgs, options } = transformCommandArguments(script, args);
return transformCommandReply(
script,
await this.executeScript(
script,
args,
redisArgs,
options
),
redisArgs.preserve
);
}
async executeScript(
script: RedisScript,
originalArgs: Array<unknown>,
redisArgs: RedisCommandArguments,
options?: ClientCommandOptions
): Promise<RedisCommandRawReply> {
return this.#execute(
RedisCluster.extractFirstKey(script, originalArgs, redisArgs),
script.IS_READ_ONLY,
client => client.executeScript(script, redisArgs, options)
);
}
async #execute<Reply>(
firstKey: RedisCommandArgument | undefined,
isReadonly: boolean | undefined,
executor: (client: RedisClientType<M, F, S>) => Promise<Reply>
): Promise<Reply> {
const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16;
let client = await this.#slots.getClient(firstKey, isReadonly);
for (let i = 0;; i++) {
const maxCommandRedirections = this._options.maxCommandRedirections ?? 16;
let client = await this._slots.getClient(firstKey, isReadonly);
for (let i = 0; ; i++) {
try {
return await executor(client);
return await client.sendCommand<T>(args, options);
} catch (err) {
if (++i > maxCommandRedirections || !(err instanceof ErrorReply)) {
// TODO: error class
if (++i > maxCommandRedirections || !(err instanceof Error)) {
throw err;
}
if (err.message.startsWith('ASK')) {
const address = err.message.substring(err.message.lastIndexOf(' ') + 1);
let redirectTo = await this.#slots.getMasterByAddress(address);
let redirectTo = await this._slots.getMasterByAddress(address);
if (!redirectTo) {
await this.#slots.rediscover(client);
redirectTo = await this.#slots.getMasterByAddress(address);
await this._slots.rediscover(client);
redirectTo = await this._slots.getMasterByAddress(address);
}
if (!redirectTo) {
@@ -269,8 +390,8 @@ export default class RedisCluster<
client = redirectTo;
continue;
} else if (err.message.startsWith('MOVED')) {
await this.#slots.rediscover(client);
client = await this.#slots.getClient(firstKey, isReadonly);
await this._slots.rediscover(client);
client = await this._slots.getClient(firstKey, isReadonly);
continue;
}
@@ -279,27 +400,27 @@ export default class RedisCluster<
}
}
MULTI(routing?: RedisCommandArgument): RedisClusterMultiCommandType<M, F, S> {
return new this.#Multi(
(commands: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => {
return this.#execute(
firstKey,
false,
client => client.multiExecutor(commands, undefined, chainId)
);
},
routing
);
}
// MULTI(routing?: RedisCommandArgument): RedisClusterMultiCommandType<M, F, S> {
// return new this.#Multi(
// (commands: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => {
// return this.#execute(
// firstKey,
// false,
// client => client.multiExecutor(commands, undefined, chainId)
// );
// },
// routing
// );
// }
multi = this.MULTI;
// multi = this.MULTI;
async SUBSCRIBE<T extends boolean = false>(
channels: string | Array<string>,
listener: PubSubListener<T>,
bufferMode?: T
) {
return (await this.#slots.getPubSubClient())
return (await this._slots.getPubSubClient())
.SUBSCRIBE(channels, listener, bufferMode);
}
@@ -310,7 +431,7 @@ export default class RedisCluster<
listener?: PubSubListener<boolean>,
bufferMode?: T
) {
return this.#slots.executeUnsubscribeCommand(client =>
return this._slots.executeUnsubscribeCommand(client =>
client.UNSUBSCRIBE(channels, listener, bufferMode)
);
}
@@ -322,7 +443,7 @@ export default class RedisCluster<
listener: PubSubListener<T>,
bufferMode?: T
) {
return (await this.#slots.getPubSubClient())
return (await this._slots.getPubSubClient())
.PSUBSCRIBE(patterns, listener, bufferMode);
}
@@ -333,7 +454,7 @@ export default class RedisCluster<
listener?: PubSubListener<T>,
bufferMode?: T
) {
return this.#slots.executeUnsubscribeCommand(client =>
return this._slots.executeUnsubscribeCommand(client =>
client.PUNSUBSCRIBE(patterns, listener, bufferMode)
);
}
@@ -345,10 +466,10 @@ export default class RedisCluster<
listener: PubSubListener<T>,
bufferMode?: T
) {
const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16,
const maxCommandRedirections = this._options.maxCommandRedirections ?? 16,
firstChannel = Array.isArray(channels) ? channels[0] : channels;
let client = await this.#slots.getShardedPubSubClient(firstChannel);
for (let i = 0;; i++) {
let client = await this._slots.getShardedPubSubClient(firstChannel);
for (let i = 0; ; i++) {
try {
return await client.SSUBSCRIBE(channels, listener, bufferMode);
} catch (err) {
@@ -357,8 +478,8 @@ export default class RedisCluster<
}
if (err.message.startsWith('MOVED')) {
await this.#slots.rediscover(client);
client = await this.#slots.getShardedPubSubClient(firstChannel);
await this._slots.rediscover(client);
client = await this._slots.getShardedPubSubClient(firstChannel);
continue;
}
@@ -374,7 +495,7 @@ export default class RedisCluster<
listener: PubSubListener<T>,
bufferMode?: T
) {
return this.#slots.executeShardedUnsubscribeCommand(
return this._slots.executeShardedUnsubscribeCommand(
Array.isArray(channels) ? channels[0] : channels,
client => client.SUNSUBSCRIBE(channels, listener, bufferMode)
);
@@ -382,28 +503,37 @@ export default class RedisCluster<
sUnsubscribe = this.SUNSUBSCRIBE;
quit(): Promise<void> {
return this.#slots.quit();
}
// quit(): Promise<void> {
// return this.#slots.quit();
// }
disconnect(): Promise<void> {
return this.#slots.disconnect();
return this._slots.disconnect();
}
nodeClient(node: ShardNode<M, F, S>) {
return this.#slots.nodeClient(node);
nodeClient(node: ShardNode<M, F, S, RESP>) {
return this._slots.nodeClient(node);
}
/**
* Returns a random node from the cluster.
* Userful for running "forward" commands (like PUBLISH) on a random node.
*/
getRandomNode() {
return this.#slots.getRandomNode();
return this._slots.getRandomNode();
}
/**
* Get a random node from a slot.
* Useful for running readonly commands on a slot.
*/
getSlotRandomNode(slot: number) {
return this.#slots.getSlotRandomNode(slot);
return this._slots.getSlotRandomNode(slot);
}
/**
* @deprecated use `.masters` instead
* TODO
*/
getMasters() {
return this.masters;
@@ -411,14 +541,9 @@ export default class RedisCluster<
/**
* @deprecated use `.slots[<SLOT>]` instead
* TODO
*/
getSlotMaster(slot: number) {
return this.slots[slot].master;
}
}
attachCommands({
BaseClass: RedisCluster,
commands: COMMANDS,
executor: RedisCluster.prototype.commandsExecutor
});

View File

@@ -1,141 +1,141 @@
import COMMANDS from './commands';
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction } from '../commands';
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
import { attachCommands, attachExtensions } from '../commander';
import RedisCluster from '.';
// import COMMANDS from './commands';
// import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction } from '../commands';
// import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
// import { attachCommands, attachExtensions } from '../commander';
// import RedisCluster from '.';
type RedisClusterMultiCommandSignature<
C extends RedisCommand,
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = (...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<M, F, S>;
// type RedisClusterMultiCommandSignature<
// C extends RedisCommand,
// M extends RedisModules,
// F extends RedisFunctions,
// S extends RedisScripts
// > = (...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<M, F, S>;
type WithCommands<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, F, S>;
};
// type WithCommands<
// M extends RedisModules,
// F extends RedisFunctions,
// S extends RedisScripts
// > = {
// [P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, F, S>;
// };
type WithModules<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof M as ExcludeMappedString<P>]: {
[C in keyof M[P] as ExcludeMappedString<C>]: RedisClusterMultiCommandSignature<M[P][C], M, F, S>;
};
};
// type WithModules<
// M extends RedisModules,
// F extends RedisFunctions,
// S extends RedisScripts
// > = {
// [P in keyof M as ExcludeMappedString<P>]: {
// [C in keyof M[P] as ExcludeMappedString<C>]: RedisClusterMultiCommandSignature<M[P][C], M, F, S>;
// };
// };
type WithFunctions<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof F as ExcludeMappedString<P>]: {
[FF in keyof F[P] as ExcludeMappedString<FF>]: RedisClusterMultiCommandSignature<F[P][FF], M, F, S>;
};
};
// type WithFunctions<
// M extends RedisModules,
// F extends RedisFunctions,
// S extends RedisScripts
// > = {
// [P in keyof F as ExcludeMappedString<P>]: {
// [FF in keyof F[P] as ExcludeMappedString<FF>]: RedisClusterMultiCommandSignature<F[P][FF], M, F, S>;
// };
// };
type WithScripts<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = {
[P in keyof S as ExcludeMappedString<P>]: RedisClusterMultiCommandSignature<S[P], M, F, S>;
};
// type WithScripts<
// M extends RedisModules,
// F extends RedisFunctions,
// S extends RedisScripts
// > = {
// [P in keyof S as ExcludeMappedString<P>]: RedisClusterMultiCommandSignature<S[P], M, F, S>;
// };
export type RedisClusterMultiCommandType<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = RedisClusterMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
// export type RedisClusterMultiCommandType<
// M extends RedisModules,
// F extends RedisFunctions,
// S extends RedisScripts
// > = RedisClusterMultiCommand & WithCommands<M, F, S> & WithModules<M, F, S> & WithFunctions<M, F, S> & WithScripts<M, F, S>;
export type InstantiableRedisClusterMultiCommandType<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
> = new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<M, F, S>;
// export type InstantiableRedisClusterMultiCommandType<
// M extends RedisModules,
// F extends RedisFunctions,
// S extends RedisScripts
// > = new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<M, F, S>;
export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
// export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: RedisCommandArgument, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
export default class RedisClusterMultiCommand {
readonly #multi = new RedisMultiCommand();
readonly #executor: RedisClusterMultiExecutor;
#firstKey: RedisCommandArgument | undefined;
// export default class RedisClusterMultiCommand {
// readonly #multi = new RedisMultiCommand();
// readonly #executor: RedisClusterMultiExecutor;
// #firstKey: RedisCommandArgument | undefined;
static extend<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts
>(extensions?: RedisExtensions<M, F, S>): InstantiableRedisClusterMultiCommandType<M, F, S> {
return attachExtensions({
BaseClass: RedisClusterMultiCommand,
modulesExecutor: RedisClusterMultiCommand.prototype.commandsExecutor,
modules: extensions?.modules,
functionsExecutor: RedisClusterMultiCommand.prototype.functionsExecutor,
functions: extensions?.functions,
scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor,
scripts: extensions?.scripts
});
}
// static extend<
// M extends RedisModules,
// F extends RedisFunctions,
// S extends RedisScripts
// >(extensions?: RedisExtensions<M, F, S>): InstantiableRedisClusterMultiCommandType<M, F, S> {
// return attachExtensions({
// BaseClass: RedisClusterMultiCommand,
// modulesExecutor: RedisClusterMultiCommand.prototype.commandsExecutor,
// modules: extensions?.modules,
// functionsExecutor: RedisClusterMultiCommand.prototype.functionsExecutor,
// functions: extensions?.functions,
// scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor,
// scripts: extensions?.scripts
// });
// }
constructor(executor: RedisClusterMultiExecutor, firstKey?: RedisCommandArgument) {
this.#executor = executor;
this.#firstKey = firstKey;
}
// constructor(executor: RedisClusterMultiExecutor, firstKey?: RedisCommandArgument) {
// this.#executor = executor;
// this.#firstKey = firstKey;
// }
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
const transformedArguments = command.transformArguments(...args);
this.#firstKey ??= RedisCluster.extractFirstKey(command, args, transformedArguments);
return this.addCommand(undefined, transformedArguments, command.transformReply);
}
// commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
// const transformedArguments = command.transformArguments(...args);
// this.#firstKey ??= RedisCluster.extractFirstKey(command, args, transformedArguments);
// return this.addCommand(undefined, transformedArguments, command.transformReply);
// }
addCommand(
firstKey: RedisCommandArgument | undefined,
args: RedisCommandArguments,
transformReply?: RedisCommand['transformReply']
): this {
this.#firstKey ??= firstKey;
this.#multi.addCommand(args, transformReply);
return this;
}
// addCommand(
// firstKey: RedisCommandArgument | undefined,
// args: RedisCommandArguments,
// transformReply?: RedisCommand['transformReply']
// ): this {
// this.#firstKey ??= firstKey;
// this.#multi.addCommand(args, transformReply);
// return this;
// }
functionsExecutor(fn: RedisFunction, args: Array<unknown>, name: string): this {
const transformedArguments = this.#multi.addFunction(name, fn, args);
this.#firstKey ??= RedisCluster.extractFirstKey(fn, args, transformedArguments);
return this;
}
// functionsExecutor(fn: RedisFunction, args: Array<unknown>, name: string): this {
// const transformedArguments = this.#multi.addFunction(name, fn, args);
// this.#firstKey ??= RedisCluster.extractFirstKey(fn, args, transformedArguments);
// return this;
// }
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
const transformedArguments = this.#multi.addScript(script, args);
this.#firstKey ??= RedisCluster.extractFirstKey(script, args, transformedArguments);
return this;
}
// scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
// const transformedArguments = this.#multi.addScript(script, args);
// this.#firstKey ??= RedisCluster.extractFirstKey(script, args, transformedArguments);
// return this;
// }
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
if (execAsPipeline) {
return this.execAsPipeline();
}
// async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
// if (execAsPipeline) {
// return this.execAsPipeline();
// }
return this.#multi.handleExecReplies(
await this.#executor(this.#multi.queue, this.#firstKey, RedisMultiCommand.generateChainId())
);
}
// return this.#multi.handleExecReplies(
// await this.#executor(this.#multi.queue, this.#firstKey, RedisMultiCommand.generateChainId())
// );
// }
EXEC = this.exec;
// EXEC = this.exec;
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
return this.#multi.transformReplies(
await this.#executor(this.#multi.queue, this.#firstKey)
);
}
}
// async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
// return this.#multi.transformReplies(
// await this.#executor(this.#multi.queue, this.#firstKey)
// );
// }
// }
attachCommands({
BaseClass: RedisClusterMultiCommand,
commands: COMMANDS,
executor: RedisClusterMultiCommand.prototype.commandsExecutor
});
// attachCommands({
// BaseClass: RedisClusterMultiCommand,
// commands: COMMANDS,
// executor: RedisClusterMultiCommand.prototype.commandsExecutor
// });

View File

@@ -1,14 +0,0 @@
const symbol = Symbol('Command Options');
export type CommandOptions<T> = T & {
readonly [symbol]: true;
};
export function commandOptions<T>(options: T): CommandOptions<T> {
(options as any)[symbol] = true;
return options as CommandOptions<T>;
}
export function isCommandOptions<T>(options: any): options is CommandOptions<T> {
return options?.[symbol] === true;
}

View File

@@ -1,165 +1,115 @@
import { Command, CommanderConfig, RedisCommands, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions } from './RESP/types';
import { ClientCommandOptions } from './client';
import { CommandOptions, isCommandOptions } from './command-options';
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandReply, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts } from './commands';
type Instantiable<T = any> = new (...args: Array<any>) => T;
type CommandsExecutor<C extends RedisCommand = RedisCommand> =
(command: C, args: Array<unknown>, name: string) => unknown;
interface AttachCommandsConfig<C extends RedisCommand> {
BaseClass: Instantiable;
commands: Record<string, C>;
executor: CommandsExecutor<C>;
interface AttachConfigOptions<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions
> {
BaseClass: new (...args: any) => any;
commands: RedisCommands;
createCommand(command: Command, resp: RespVersions): (...args: any) => any;
createModuleCommand(command: Command, resp: RespVersions): (...args: any) => any;
createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions): (...args: any) => any;
createScriptCommand(script: RedisScript, resp: RespVersions): (...args: any) => any;
config?: CommanderConfig<M, F, S, RESP>;
}
export function attachCommands<C extends RedisCommand>({
export function attachConfig<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions
>({
BaseClass,
commands,
executor
}: AttachCommandsConfig<C>): void {
createCommand,
createModuleCommand,
createFunctionCommand,
createScriptCommand,
config
}: AttachConfigOptions<M, F, S, RESP>) {
const RESP = config?.RESP ?? 2,
Class: any = class extends BaseClass {};
for (const [name, command] of Object.entries(commands)) {
BaseClass.prototype[name] = function (...args: Array<unknown>): unknown {
return executor.call(this, command, args, name);
};
}
}
interface AttachExtensionsConfig<T extends Instantiable = Instantiable> {
BaseClass: T;
modulesExecutor: CommandsExecutor;
modules?: RedisModules;
functionsExecutor: CommandsExecutor<RedisFunction>;
functions?: RedisFunctions;
scriptsExecutor: CommandsExecutor<RedisScript>;
scripts?: RedisScripts;
}
export function attachExtensions(config: AttachExtensionsConfig): any {
let Commander;
if (config.modules) {
Commander = attachWithNamespaces({
BaseClass: config.BaseClass,
namespaces: config.modules,
executor: config.modulesExecutor
});
Class.prototype[name] = createCommand(command, RESP);
}
if (config.functions) {
Commander = attachWithNamespaces({
BaseClass: Commander ?? config.BaseClass,
namespaces: config.functions,
executor: config.functionsExecutor
});
if (config?.modules) {
for (const [moduleName, module] of Object.entries(config.modules)) {
const fns = Object.create(null);
for (const [name, command] of Object.entries(module)) {
fns[name] = createModuleCommand(command, RESP);
}
if (config.scripts) {
Commander ??= class extends config.BaseClass {};
attachCommands({
BaseClass: Commander,
commands: config.scripts,
executor: config.scriptsExecutor
});
}
return Commander ?? config.BaseClass;
}
interface AttachWithNamespacesConfig<C extends RedisCommand> {
BaseClass: Instantiable;
namespaces: Record<string, Record<string, C>>;
executor: CommandsExecutor<C>;
}
function attachWithNamespaces<C extends RedisCommand>({
BaseClass,
namespaces,
executor
}: AttachWithNamespacesConfig<C>): any {
const Commander = class extends BaseClass {
constructor(...args: Array<any>) {
super(...args);
for (const namespace of Object.keys(namespaces)) {
this[namespace] = Object.create(this[namespace], {
self: {
value: this
}
});
attachNamespace(Class.prototype, moduleName, fns);
}
}
};
for (const [namespace, commands] of Object.entries(namespaces)) {
Commander.prototype[namespace] = {};
if (config?.functions) {
for (const [library, commands] of Object.entries(config.functions)) {
const fns = Object.create(null);
for (const [name, command] of Object.entries(commands)) {
Commander.prototype[namespace][name] = function (...args: Array<unknown>): unknown {
return executor.call(this.self, command, args, name);
};
fns[name] = createFunctionCommand(name, command, RESP);
}
attachNamespace(Class.prototype, library, fns);
}
}
return Commander;
if (config?.scripts) {
for (const [name, script] of Object.entries(config.scripts)) {
Class.prototype[name] = createScriptCommand(script, RESP);
}
}
return Class;
}
export function transformCommandArguments<T = ClientCommandOptions>(
command: RedisCommand,
args: Array<unknown>
): {
jsArgs: Array<unknown>;
args: RedisCommandArguments;
options: CommandOptions<T> | undefined;
} {
let options;
if (isCommandOptions<T>(args[0])) {
options = args[0];
args = args.slice(1);
function attachNamespace(prototype: any, name: PropertyKey, fns: any) {
Object.defineProperty(prototype, name, {
get() {
const value = Object.create(fns);
value.self = this;
Object.defineProperty(this, name, { value });
return value;
}
return {
jsArgs: args,
args: command.transformArguments(...args),
options
};
}
export function transformLegacyCommandArguments(args: Array<any>): Array<any> {
return args.flat().map(arg => {
return typeof arg === 'number' || arg instanceof Date ?
arg.toString() :
arg;
});
}
export function transformCommandReply<C extends RedisCommand>(
command: C,
rawReply: unknown,
preserved: unknown
): RedisCommandReply<C> {
if (!command.transformReply) {
return rawReply as RedisCommandReply<C>;
}
export function getTransformReply(command: Command, resp: RespVersions) {
switch (typeof command.transformReply) {
case 'function':
return command.transformReply;
return command.transformReply(rawReply, preserved);
case 'object':
return command.transformReply[resp];
}
}
export function fCallArguments(
name: RedisCommandArgument,
fn: RedisFunction,
args: RedisCommandArguments
): RedisCommandArguments {
const actualArgs: RedisCommandArguments = [
export function functionArgumentsPrefix(name: string, fn: RedisFunction) {
const prefix: Array<string | Buffer> = [
fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL',
name
];
if (fn.NUMBER_OF_KEYS !== undefined) {
actualArgs.push(fn.NUMBER_OF_KEYS.toString());
prefix.push(fn.NUMBER_OF_KEYS.toString());
}
actualArgs.push(...args);
return actualArgs;
return prefix;
}
export function scriptArgumentsPrefix(script: RedisScript) {
const prefix: Array<string | Buffer> = [
script.IS_READ_ONLY ? 'EVALSHA_RO' : 'EVALSHA',
script.SHA1
];
if (script.NUMBER_OF_KEYS !== undefined) {
prefix.push(script.NUMBER_OF_KEYS.toString());
}
return prefix;
}

View File

@@ -1,6 +1,6 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_CAT';
import testUtils, { GLOBAL } from '../test-utils';
import ACL_CAT from './ACL_CAT';
describe('ACL CAT', () => {
testUtils.isVersionGreaterThanHook([6]);
@@ -8,16 +8,24 @@ describe('ACL CAT', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
ACL_CAT.transformArguments(),
['ACL', 'CAT']
);
});
it('with categoryName', () => {
assert.deepEqual(
transformArguments('dangerous'),
ACL_CAT.transformArguments('dangerous'),
['ACL', 'CAT', 'dangerous']
);
});
});
testUtils.testWithClient('client.aclCat', async client => {
const categories = await client.aclCat();
assert.ok(Array.isArray(categories));
for (const category of categories) {
assert.equal(typeof category, 'string');
}
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,13 +1,16 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
export function transformArguments(categoryName?: RedisCommandArgument): RedisCommandArguments {
const args: RedisCommandArguments = ['ACL', 'CAT'];
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(categoryName?: RedisArgument) {
const args: Array<RedisArgument> = ['ACL', 'CAT'];
if (categoryName) {
args.push(categoryName);
}
return args;
}
export declare function transformReply(): Array<RedisCommandArgument>;
},
transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
} as const satisfies Command;

View File

@@ -1,6 +1,6 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './ACL_DELUSER';
import ACL_DELUSER from './ACL_DELUSER';
describe('ACL DELUSER', () => {
testUtils.isVersionGreaterThanHook([6]);
@@ -8,14 +8,14 @@ describe('ACL DELUSER', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('username'),
ACL_DELUSER.transformArguments('username'),
['ACL', 'DELUSER', 'username']
);
});
it('array', () => {
assert.deepEqual(
transformArguments(['1', '2']),
ACL_DELUSER.transformArguments(['1', '2']),
['ACL', 'DELUSER', '1', '2']
);
});
@@ -23,8 +23,8 @@ describe('ACL DELUSER', () => {
testUtils.testWithClient('client.aclDelUser', async client => {
assert.equal(
await client.aclDelUser('dosenotexists'),
0
typeof await client.aclDelUser('user'),
'number'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,10 +1,11 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { pushVerdictArguments } from './generic-transformers';
import { NumberReply, Command } from '../RESP/types';
import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers';
export function transformArguments(
username: RedisCommandArgument | Array<RedisCommandArgument>
): RedisCommandArguments {
return pushVerdictArguments(['ACL', 'DELUSER'], username);
}
export declare function transformReply(): number;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(username: RedisVariadicArgument) {
return pushVariadicArguments(['ACL', 'DELUSER'], username);
},
transformReply: undefined as unknown as () => NumberReply
} as const satisfies Command;

View File

@@ -1,13 +1,13 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './ACL_DRYRUN';
import ACL_DRYRUN from './ACL_DRYRUN';
describe('ACL DRYRUN', () => {
testUtils.isVersionGreaterThanHook([7]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('default', ['GET', 'key']),
ACL_DRYRUN.transformArguments('default', ['GET', 'key']),
['ACL', 'DRYRUN', 'default', 'GET', 'key']
);
});

View File

@@ -1,18 +1,16 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types';
export const IS_READ_ONLY = true;
export function transformArguments(
username: RedisCommandArgument,
command: Array<RedisCommandArgument>
): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(username: RedisArgument, command: Array<RedisArgument>) {
return [
'ACL',
'DRYRUN',
username,
...command
];
}
export declare function transformReply(): RedisCommandArgument;
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | BlobStringReply
} as const satisfies Command;

View File

@@ -1,6 +1,6 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_GENPASS';
import testUtils, { GLOBAL } from '../test-utils';
import ACL_GENPASS from './ACL_GENPASS';
describe('ACL GENPASS', () => {
testUtils.isVersionGreaterThanHook([6]);
@@ -8,16 +8,23 @@ describe('ACL GENPASS', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
ACL_GENPASS.transformArguments(),
['ACL', 'GENPASS']
);
});
it('with bits', () => {
assert.deepEqual(
transformArguments(128),
ACL_GENPASS.transformArguments(128),
['ACL', 'GENPASS', '128']
);
});
});
testUtils.testWithClient('client.aclGenPass', async client => {
assert.equal(
typeof await client.aclGenPass(),
'string'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,6 +1,9 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { BlobStringReply, Command } from '../RESP/types';
export function transformArguments(bits?: number): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(bits?: number) {
const args = ['ACL', 'GENPASS'];
if (bits) {
@@ -8,6 +11,7 @@ export function transformArguments(bits?: number): RedisCommandArguments {
}
return args;
}
},
transformReply: undefined as unknown as () => BlobStringReply
} as const satisfies Command;
export declare function transformReply(): RedisCommandArgument;

View File

@@ -1,44 +1,34 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './ACL_GETUSER';
import ACL_GETUSER from './ACL_GETUSER';
describe('ACL GETUSER', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('username'),
ACL_GETUSER.transformArguments('username'),
['ACL', 'GETUSER', 'username']
);
});
testUtils.testWithClient('client.aclGetUser', async client => {
const expectedReply: any = {
passwords: [],
commands: '+@all',
};
const reply = await client.aclGetUser('default');
assert.ok(Array.isArray(reply.passwords));
assert.equal(typeof reply.commands, 'string');
assert.ok(Array.isArray(reply.flags));
if (testUtils.isVersionGreaterThan([7])) {
expectedReply.flags = ['on', 'nopass'];
expectedReply.keys = '~*';
expectedReply.channels = '&*';
expectedReply.selectors = [];
assert.equal(typeof reply.keys, 'string');
assert.equal(typeof reply.channels, 'string');
assert.ok(Array.isArray(reply.selectors));
} else {
expectedReply.keys = ['*'];
expectedReply.selectors = undefined;
assert.ok(Array.isArray(reply.keys));
if (testUtils.isVersionGreaterThan([6, 2])) {
expectedReply.flags = ['on', 'allkeys', 'allchannels', 'allcommands', 'nopass'];
expectedReply.channels = ['*'];
} else {
expectedReply.flags = ['on', 'allkeys', 'allcommands', 'nopass'];
expectedReply.channels = undefined;
assert.ok(Array.isArray(reply.channels));
}
}
assert.deepEqual(
await client.aclGetUser('default'),
expectedReply
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,40 +1,40 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, Resp2Reply, Command } from '../RESP/types';
export function transformArguments(username: RedisCommandArgument): RedisCommandArguments {
type AclUser = TuplesToMapReply<[
[BlobStringReply<'flags'>, ArrayReply<BlobStringReply>],
[BlobStringReply<'passwords'>, ArrayReply<BlobStringReply>],
[BlobStringReply<'commands'>, BlobStringReply],
/** changed to BlobStringReply in 7.0 */
[BlobStringReply<'keys'>, ArrayReply<BlobStringReply> | BlobStringReply],
/** added in 6.2, changed to BlobStringReply in 7.0 */
[BlobStringReply<'channels'>, ArrayReply<BlobStringReply> | BlobStringReply],
/** added in 7.0 */
[BlobStringReply<'selectors'>, ArrayReply<TuplesToMapReply<[
[BlobStringReply<'commands'>, BlobStringReply],
[BlobStringReply<'keys'>, BlobStringReply],
[BlobStringReply<'channels'>, BlobStringReply]
]>>],
]>;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(username: RedisArgument) {
return ['ACL', 'GETUSER', username];
}
type AclGetUserRawReply = [
'flags',
Array<RedisCommandArgument>,
'passwords',
Array<RedisCommandArgument>,
'commands',
RedisCommandArgument,
'keys',
Array<RedisCommandArgument> | RedisCommandArgument,
'channels',
Array<RedisCommandArgument> | RedisCommandArgument,
'selectors' | undefined,
Array<Array<string>> | undefined
];
interface AclUser {
flags: Array<RedisCommandArgument>;
passwords: Array<RedisCommandArgument>;
commands: RedisCommandArgument;
keys: Array<RedisCommandArgument> | RedisCommandArgument;
channels: Array<RedisCommandArgument> | RedisCommandArgument;
selectors?: Array<Array<string>>;
}
export function transformReply(reply: AclGetUserRawReply): AclUser {
return {
},
transformReply: {
2: (reply: Resp2Reply<AclUser>) => ({
flags: reply[1],
passwords: reply[3],
commands: reply[5],
keys: reply[7],
channels: reply[9],
selectors: reply[11]
};
}
selectors: reply[11]?.map(selector => ({
commands: selector[1],
keys: selector[3],
channels: selector[5]
}))
}),
3: undefined as unknown as () => AclUser
}
} as const satisfies Command;

View File

@@ -1,14 +1,22 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_LIST';
import testUtils, { GLOBAL } from '../test-utils';
import ACL_LIST from './ACL_LIST';
describe('ACL LIST', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
ACL_LIST.transformArguments(),
['ACL', 'LIST']
);
});
testUtils.testWithClient('client.aclList', async client => {
const users = await client.aclList();
assert.ok(Array.isArray(users));
for (const user of users) {
assert.equal(typeof user, 'string');
}
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,7 +1,10 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
export function transformArguments(): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['ACL', 'LIST'];
}
export declare function transformReply(): Array<RedisCommandArgument>;
},
transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
} as const satisfies Command;

View File

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

View File

@@ -1,7 +1,10 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { SimpleStringReply, Command } from '../RESP/types';
export function transformArguments(): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['ACL', 'LOAD'];
}
export declare function transformReply(): RedisCommandArgument;
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,6 +1,6 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments, transformReply } from './ACL_LOG';
import testUtils, { GLOBAL } from '../test-utils';
import ACL_LOG from './ACL_LOG';
describe('ACL LOG', () => {
testUtils.isVersionGreaterThanHook([6]);
@@ -8,46 +8,46 @@ describe('ACL LOG', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
ACL_LOG.transformArguments(),
['ACL', 'LOG']
);
});
it('with count', () => {
assert.deepEqual(
transformArguments(10),
ACL_LOG.transformArguments(10),
['ACL', 'LOG', '10']
);
});
});
it('transformReply', () => {
assert.deepEqual(
transformReply([[
'count',
1,
'reason',
'auth',
'context',
'toplevel',
'object',
'AUTH',
'username',
'someuser',
'age-seconds',
'4.096',
'client-info',
'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
]]),
[{
count: 1,
reason: 'auth',
context: 'toplevel',
object: 'AUTH',
username: 'someuser',
ageSeconds: 4.096,
clientInfo: 'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
}]
);
});
testUtils.testWithClient('client.aclLog', async client => {
// make sure to create at least one log
await Promise.all([
client.aclSetUser('test', 'on >test'),
client.auth({
username: 'test',
password: 'test'
}),
client.auth({
username: 'default',
password: ''
})
]);
const logs = await client.aclLog();
assert.ok(Array.isArray(logs));
for (const log of logs) {
assert.equal(typeof log.count, 'number');
assert.equal(typeof log.timestamp, 'number');
assert.equal(typeof log.username, 'string');
assert.equal(typeof log.clientId, 'string');
assert.equal(typeof log.command, 'string');
assert.equal(typeof log.args, 'string');
assert.equal(typeof log.key, 'string');
assert.equal(typeof log.result, 'number');
assert.equal(typeof log.duration, 'number');
}
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,50 +1,38 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { Resp2Reply } from '../RESP/types';
import { ArrayReply, BlobStringReply, Command, NumberReply, TuplesToMapReply } from '../RESP/types';
export function transformArguments(count?: number): RedisCommandArguments {
export type AclLogReply = ArrayReply<TuplesToMapReply<[
[BlobStringReply<'count'>, NumberReply],
[BlobStringReply<'reason'>, BlobStringReply],
[BlobStringReply<'context'>, BlobStringReply],
[BlobStringReply<'object'>, BlobStringReply],
[BlobStringReply<'username'>, BlobStringReply],
[BlobStringReply<'age-seconds'>, BlobStringReply],
[BlobStringReply<'client-info'>, BlobStringReply]
]>>;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(count?: number) {
const args = ['ACL', 'LOG'];
if (count) {
if (count !== undefined) {
args.push(count.toString());
}
return args;
}
type AclLogRawReply = [
_: RedisCommandArgument,
count: number,
_: RedisCommandArgument,
reason: RedisCommandArgument,
_: RedisCommandArgument,
context: RedisCommandArgument,
_: RedisCommandArgument,
object: RedisCommandArgument,
_: RedisCommandArgument,
username: RedisCommandArgument,
_: RedisCommandArgument,
ageSeconds: RedisCommandArgument,
_: RedisCommandArgument,
clientInfo: RedisCommandArgument
];
interface AclLog {
count: number;
reason: RedisCommandArgument;
context: RedisCommandArgument;
object: RedisCommandArgument;
username: RedisCommandArgument;
ageSeconds: number;
clientInfo: RedisCommandArgument;
}
export function transformReply(reply: Array<AclLogRawReply>): Array<AclLog> {
return reply.map(log => ({
count: log[1],
reason: log[3],
context: log[5],
object: log[7],
username: log[9],
ageSeconds: Number(log[11]),
clientInfo: log[13]
}));
}
},
transformReply: {
2: (reply: Resp2Reply<AclLogReply>) => ({
count: Number(reply[1]),
reason: reply[3],
context: reply[5],
object: reply[7],
username: reply[9],
'age-seconds': Number(reply[11]),
'client-info': reply[13]
}),
3: undefined as unknown as () => AclLogReply
}
} as const satisfies Command;

View File

@@ -1,14 +1,21 @@
import { strict as assert } from 'assert';
import testUtils from '../test-utils';
import { transformArguments } from './ACL_LOG_RESET';
import testUtils, { GLOBAL } from '../test-utils';
import ACL_LOG_RESET from './ACL_LOG_RESET';
describe('ACL LOG RESET', () => {
testUtils.isVersionGreaterThanHook([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
ACL_LOG_RESET.transformArguments(),
['ACL', 'LOG', 'RESET']
);
});
testUtils.testWithClient('client.aclLogReset', async client => {
assert.equal(
await client.aclLogReset(),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,7 +1,11 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { SimpleStringReply, Command } from '../RESP/types';
import ACL_LOG from './ACL_LOG';
export function transformArguments(): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: ACL_LOG.IS_READ_ONLY,
transformArguments() {
return ['ACL', 'LOG', 'RESET'];
}
export declare function transformReply(): RedisCommandArgument;
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

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

View File

@@ -1,7 +1,10 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { SimpleStringReply, Command } from '../RESP/types';
export function transformArguments(): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['ACL', 'SAVE'];
}
export declare function transformReply(): RedisCommandArgument;
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,11 +1,11 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { pushVerdictArguments } from './generic-transformers';
import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers';
export function transformArguments(
username: RedisCommandArgument,
rule: RedisCommandArgument | Array<RedisCommandArgument>
): RedisCommandArguments {
return pushVerdictArguments(['ACL', 'SETUSER', username], rule);
}
export declare function transformReply(): RedisCommandArgument;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(username: RedisArgument, rule: RedisVariadicArgument) {
return pushVariadicArguments(['ACL', 'SETUSER', username], rule);
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,7 +1,10 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { ArrayReply, BlobStringReply, Command } from '../RESP/types';
export function transformArguments(): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['ACL', 'USERS'];
}
export declare function transformReply(): Array<RedisCommandArgument>;
},
transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
} as const satisfies Command;

View File

@@ -1,7 +1,10 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { BlobStringReply, Command } from '../RESP/types';
export function transformArguments(): RedisCommandArguments {
return ['ACL', 'WHOAMI'];
}
export declare function transformReply(): RedisCommandArgument;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['ACL', 'USERS'];
},
transformReply: undefined as unknown as () => BlobStringReply
} as const satisfies Command;

View File

@@ -1,11 +1,22 @@
import { strict as assert } from 'assert';
import { transformArguments } from './APPEND';
import testUtils, { GLOBAL } from '../test-utils';
import APPEND from './APPEND';
describe('APPEND', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'value'),
APPEND.transformArguments('key', 'value'),
['APPEND', 'key', 'value']
);
});
testUtils.testAll('append', async client => {
assert.equal(
await client.append('key', 'value'),
5
);
}, {
client: GLOBAL.SERVERS.OPEN,
cluster: GLOBAL.CLUSTERS.OPEN
});
});

View File

@@ -1,12 +1,10 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { RedisArgument, NumberReply, Command } from '../RESP/types';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(
key: RedisCommandArgument,
value: RedisCommandArgument
): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: 1,
IS_READ_ONLY: false,
transformArguments(key: RedisArgument, value: RedisArgument) {
return ['APPEND', key, value];
}
export declare function transformReply(): number;
},
transformReply: undefined as unknown as () => NumberReply
} as const satisfies Command;

View File

@@ -1,7 +1,10 @@
import { RedisCommandArguments, RedisCommandArgument } from '.';
import { SimpleStringReply, Command } from '../RESP/types';
export function transformArguments(): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['ASKING'];
}
export declare function transformReply(): RedisCommandArgument;
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,11 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './AUTH';
import AUTH from './AUTH';
describe('AUTH', () => {
describe('transformArguments', () => {
it('password only', () => {
assert.deepEqual(
transformArguments({
AUTH.transformArguments({
password: 'password'
}),
['AUTH', 'password']
@@ -14,7 +14,7 @@ describe('AUTH', () => {
it('username & password', () => {
assert.deepEqual(
transformArguments({
AUTH.transformArguments({
username: 'username',
password: 'password'
}),

View File

@@ -1,16 +1,23 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { RedisArgument, SimpleStringReply, Command } from '../RESP/types';
export interface AuthOptions {
username?: RedisCommandArgument;
password: RedisCommandArgument;
username?: RedisArgument;
password: RedisArgument;
}
export function transformArguments({ username, password }: AuthOptions): RedisCommandArguments {
if (!username) {
return ['AUTH', password];
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments({ username, password }: AuthOptions) {
const args: Array<RedisArgument> = ['AUTH'];
if (username !== undefined) {
args.push(username);
}
return ['AUTH', username, password];
}
args.push(password);
export declare function transformReply(): RedisCommandArgument;
return args;
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,7 +1,10 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { SimpleStringReply, Command } from '../RESP/types';
export function transformArguments(): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['BGREWRITEAOF'];
}
export declare function transformReply(): RedisCommandArgument;
},
transformReply: undefined as unknown as () => SimpleStringReply
} as const satisfies Command;

View File

@@ -1,17 +1,10 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { SimpleStringReply, Command } from '../RESP/types';
interface BgSaveOptions {
SCHEDULE?: true;
}
export function transformArguments(options?: BgSaveOptions): RedisCommandArguments {
const args = ['BGSAVE'];
if (options?.SCHEDULE) {
args.push('SCHEDULE');
}
return args;
}
export declare function transformReply(): RedisCommandArgument;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['BGSAVE'];
},
transformReply: undefined as unknown as () => SimpleStringReply
} as const satisfies Command;

View File

@@ -1,19 +1,15 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { RedisArgument, NumberReply, Command } from '../RESP/types';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
interface BitCountRange {
export interface BitCountRange {
start: number;
end: number;
mode?: 'BYTE' | 'BIT';
}
export function transformArguments(
key: RedisCommandArgument,
range?: BitCountRange
): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: 1,
IS_READ_ONLY: true,
transformArguments(key: RedisArgument, range?: BitCountRange) {
const args = ['BITCOUNT', key];
if (range) {
@@ -28,6 +24,6 @@ export function transformArguments(
}
return args;
}
export declare function transformReply(): number;
},
transformReply: undefined as unknown as () => NumberReply
} as const satisfies Command;

View File

@@ -1,11 +1,11 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BITFIELD';
import BITFIELD from './BITFIELD';
describe('BITFIELD', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', [{
BITFIELD.transformArguments('key', [{
operation: 'OVERFLOW',
behavior: 'WRAP'
}, {
@@ -33,7 +33,7 @@ describe('BITFIELD', () => {
);
});
testUtils.testWithClient('client.bitField', async client => {
testUtils.testAll('bitField', async client => {
assert.deepEqual(
await client.bitField('key', [{
operation: 'GET',
@@ -42,5 +42,8 @@ describe('BITFIELD', () => {
}]),
[0]
);
}, GLOBAL.SERVERS.OPEN);
}, {
client: GLOBAL.SERVERS.OPEN,
cluster: GLOBAL.CLUSTERS.OPEN
});
});

View File

@@ -1,4 +1,4 @@
export const FIRST_KEY_INDEX = 1;
import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '../RESP/types';
export type BitFieldEncoding = `${'i' | 'u'}${number}`;
@@ -11,30 +11,37 @@ export interface BitFieldGetOperation extends BitFieldOperation<'GET'> {
offset: number | string;
}
interface BitFieldSetOperation extends BitFieldOperation<'SET'> {
export interface BitFieldSetOperation extends BitFieldOperation<'SET'> {
encoding: BitFieldEncoding;
offset: number | string;
value: number;
}
interface BitFieldIncrByOperation extends BitFieldOperation<'INCRBY'> {
export interface BitFieldIncrByOperation extends BitFieldOperation<'INCRBY'> {
encoding: BitFieldEncoding;
offset: number | string;
increment: number;
}
interface BitFieldOverflowOperation extends BitFieldOperation<'OVERFLOW'> {
export interface BitFieldOverflowOperation extends BitFieldOperation<'OVERFLOW'> {
behavior: string;
}
type BitFieldOperations = Array<
export type BitFieldOperations = Array<
BitFieldGetOperation |
BitFieldSetOperation |
BitFieldIncrByOperation |
BitFieldOverflowOperation
>;
export function transformArguments(key: string, operations: BitFieldOperations): Array<string> {
export type BitFieldRoOperations = Array<
Omit<BitFieldGetOperation, 'operation'>
>;
export default {
FIRST_KEY_INDEX: 1,
IS_READ_ONLY: false,
transformArguments(key: RedisArgument, operations: BitFieldOperations) {
const args = ['BITFIELD', key];
for (const options of operations) {
@@ -75,6 +82,6 @@ export function transformArguments(key: string, operations: BitFieldOperations):
}
return args;
}
export declare function transformReply(): Array<number | null>;
},
transformReply: undefined as unknown as () => ArrayReply<NumberReply | NullReply>
} as const satisfies Command;

View File

@@ -1,13 +1,13 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BITFIELD_RO';
import BITFIELD_RO from './BITFIELD_RO';
describe('BITFIELD RO', () => {
describe('BITFIELD_RO', () => {
testUtils.isVersionGreaterThanHook([6, 2]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', [{
BITFIELD_RO.transformArguments('key', [{
encoding: 'i8',
offset: 0
}]),
@@ -15,7 +15,7 @@ describe('BITFIELD RO', () => {
);
});
testUtils.testWithClient('client.bitFieldRo', async client => {
testUtils.testAll('bitFieldRo', async client => {
assert.deepEqual(
await client.bitFieldRo('key', [{
encoding: 'i8',
@@ -23,5 +23,8 @@ describe('BITFIELD RO', () => {
}]),
[0]
);
}, GLOBAL.SERVERS.OPEN);
}, {
client: GLOBAL.SERVERS.OPEN,
cluster: GLOBAL.CLUSTERS.OPEN
});
});

View File

@@ -1,15 +1,14 @@
import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
import { BitFieldGetOperation } from './BITFIELD';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
type BitFieldRoOperations = Array<
Omit<BitFieldGetOperation, 'operation'> &
Partial<Pick<BitFieldGetOperation, 'operation'>>
export type BitFieldRoOperations = Array<
Omit<BitFieldGetOperation, 'operation'>
>;
export function transformArguments(key: string, operations: BitFieldRoOperations): Array<string> {
export default {
FIRST_KEY_INDEX: 1,
IS_READ_ONLY: true,
transformArguments(key: RedisArgument, operations: BitFieldRoOperations) {
const args = ['BITFIELD_RO', key];
for (const operation of operations) {
@@ -21,6 +20,6 @@ export function transformArguments(key: string, operations: BitFieldRoOperations
}
return args;
}
export declare function transformReply(): Array<number | null>;
},
transformReply: undefined as unknown as () => ArrayReply<NumberReply>
} as const satisfies Command;

View File

@@ -1,35 +1,31 @@
import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './BITOP';
import BITOP from './BITOP';
describe('BITOP', () => {
describe('transformArguments', () => {
it('single key', () => {
assert.deepEqual(
transformArguments('AND', 'destKey', 'key'),
BITOP.transformArguments('AND', 'destKey', 'key'),
['BITOP', 'AND', 'destKey', 'key']
);
});
it('multiple keys', () => {
assert.deepEqual(
transformArguments('AND', 'destKey', ['1', '2']),
BITOP.transformArguments('AND', 'destKey', ['1', '2']),
['BITOP', 'AND', 'destKey', '1', '2']
);
});
});
testUtils.testWithClient('client.bitOp', async client => {
testUtils.testAll('client.bitOp', async client => {
assert.equal(
await client.bitOp('AND', 'destKey', 'key'),
await client.bitOp('AND', '{tag}destKey', '{tag}key'),
0
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithCluster('cluster.bitOp', async cluster => {
assert.equal(
await cluster.bitOp('AND', '{tag}destKey', '{tag}key'),
0
);
}, GLOBAL.CLUSTERS.OPEN);
}, {
client: GLOBAL.SERVERS.OPEN,
cluster: GLOBAL.CLUSTERS.OPEN
});
});

View File

@@ -1,16 +1,17 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { pushVerdictArguments } from './generic-transformers';
import { NumberReply, Command, RedisArgument } from '../RESP/types';
import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = 2;
export type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT';
type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT';
export function transformArguments(
export default {
FIRST_KEY_INDEX: 2,
IS_READ_ONLY: false,
transformArguments(
operation: BitOperations,
destKey: RedisCommandArgument,
key: RedisCommandArgument | Array<RedisCommandArgument>
): RedisCommandArguments {
return pushVerdictArguments(['BITOP', operation, destKey], key);
}
export declare function transformReply(): number;
destKey: RedisArgument,
key: RedisVariadicArgument
) {
return pushVariadicArguments(['BITOP', operation, destKey], key);
},
transformReply: undefined as unknown as () => NumberReply
} as const satisfies Command;

View File

@@ -1,17 +1,16 @@
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { RedisArgument, NumberReply, Command } from '../RESP/types';
import { BitValue } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(
key: RedisCommandArgument,
export default {
FIRST_KEY_INDEX: 1,
IS_READ_ONLY: true,
transformArguments(
key: RedisArgument,
bit: BitValue,
start?: number,
end?: number,
mode?: 'BYTE' | 'BIT'
): RedisCommandArguments {
) {
const args = ['BITPOS', key, bit.toString()];
if (typeof start === 'number') {
@@ -27,6 +26,6 @@ export function transformArguments(
}
return args;
}
export declare function transformReply(): number;
},
transformReply: undefined as unknown as () => NumberReply
} as const satisfies Command;

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