You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-06 02:15:48 +03:00
V5 bringing RESP3, Sentinel and TypeMapping to node-redis
RESP3 Support - Some commands responses in RESP3 aren't stable yet and therefore return an "untyped" ReplyUnion. Sentinel TypeMapping Correctly types Multi commands Note: some API changes to be further documented in v4-to-v5.md
This commit is contained in:
50
.github/release-drafter-base.yml
vendored
Normal file
50
.github/release-drafter-base.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name-template: 'json@$NEXT_PATCH_VERSION'
|
||||||
|
tag-template: 'json@$NEXT_PATCH_VERSION'
|
||||||
|
autolabeler:
|
||||||
|
- label: 'chore'
|
||||||
|
files:
|
||||||
|
- '*.md'
|
||||||
|
- '.github/*'
|
||||||
|
- label: 'bug'
|
||||||
|
branch:
|
||||||
|
- '/bug-.+'
|
||||||
|
- label: 'chore'
|
||||||
|
branch:
|
||||||
|
- '/chore-.+'
|
||||||
|
- label: 'feature'
|
||||||
|
branch:
|
||||||
|
- '/feature-.+'
|
||||||
|
categories:
|
||||||
|
- title: 'Breaking Changes'
|
||||||
|
labels:
|
||||||
|
- 'breakingchange'
|
||||||
|
- title: '🚀 New Features'
|
||||||
|
labels:
|
||||||
|
- 'feature'
|
||||||
|
- 'enhancement'
|
||||||
|
- title: '🐛 Bug Fixes'
|
||||||
|
labels:
|
||||||
|
- 'fix'
|
||||||
|
- 'bugfix'
|
||||||
|
- 'bug'
|
||||||
|
- title: '🧰 Maintenance'
|
||||||
|
label:
|
||||||
|
- 'chore'
|
||||||
|
- 'maintenance'
|
||||||
|
- 'documentation'
|
||||||
|
- 'docs'
|
||||||
|
|
||||||
|
change-template: '- $TITLE (#$NUMBER)'
|
||||||
|
include-paths:
|
||||||
|
- 'packages/json'
|
||||||
|
exclude-labels:
|
||||||
|
- 'skip-changelog'
|
||||||
|
template: |
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
We'd like to thank all the contributors who worked on this release!
|
||||||
|
|
||||||
|
$CONTRIBUTORS
|
2
.github/workflows/documentation.yml
vendored
2
.github/workflows/documentation.yml
vendored
@@ -17,8 +17,6 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
- name: Install Packages
|
- name: Install Packages
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build tests tools
|
|
||||||
run: npm run build:tests-tools
|
|
||||||
- name: Generate Documentation
|
- name: Generate Documentation
|
||||||
run: npm run documentation
|
run: npm run documentation
|
||||||
- name: Upload
|
- name: Upload
|
||||||
|
11
.github/workflows/tests.yml
vendored
11
.github/workflows/tests.yml
vendored
@@ -5,11 +5,12 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- v4.0
|
- v4.0
|
||||||
|
- v5
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- v4.0
|
- v4.0
|
||||||
|
- v5
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -17,7 +18,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version: ['18', '20']
|
node-version: ['18', '20']
|
||||||
redis-version: ['5', '6.0', '6.2', '7.0', '7.2', '7.4-rc2']
|
redis-version: ['6.2.6-v17', '7.2.0-v13', '7.4.0-v1']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -31,10 +32,10 @@ jobs:
|
|||||||
if: ${{ matrix.node-version <= 14 }}
|
if: ${{ matrix.node-version <= 14 }}
|
||||||
- name: Install Packages
|
- name: Install Packages
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build tests tools
|
- name: Build
|
||||||
run: npm run build:tests-tools
|
run: npm run build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: npm run test -- -- --forbid-only --redis-version=${{ matrix.redis-version }}
|
run: npm run test -ws --if-present -- --forbid-only --redis-version=${{ matrix.redis-version }}
|
||||||
- name: Upload to Codecov
|
- name: Upload to Codecov
|
||||||
run: |
|
run: |
|
||||||
curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import
|
curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ node_modules/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dump.rdb
|
dump.rdb
|
||||||
documentation/
|
documentation/
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
12
.npmignore
12
.npmignore
@@ -1,12 +0,0 @@
|
|||||||
.github/
|
|
||||||
.vscode/
|
|
||||||
docs/
|
|
||||||
examples/
|
|
||||||
packages/
|
|
||||||
.deepsource.toml
|
|
||||||
.release-it.json
|
|
||||||
CONTRIBUTING.md
|
|
||||||
SECURITY.md
|
|
||||||
index.ts
|
|
||||||
tsconfig.base.json
|
|
||||||
tsconfig.json
|
|
344
README.md
344
README.md
@@ -25,25 +25,11 @@ node-redis is a modern, high performance [Redis](https://redis.io) client for No
|
|||||||
|
|
||||||
[Work at Redis](https://redis.com/company/careers/jobs/)
|
[Work at Redis](https://redis.com/company/careers/jobs/)
|
||||||
|
|
||||||
## Packages
|
|
||||||
|
|
||||||
| Name | Description |
|
|
||||||
|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| [redis](./) | [](https://www.npmjs.com/package/redis) [](https://www.npmjs.com/package/redis) |
|
|
||||||
| [@redis/client](./packages/client) | [](https://www.npmjs.com/package/@redis/client) [](https://www.npmjs.com/package/@redis/client) [](https://redis.js.org/documentation/client/) |
|
|
||||||
| [@redis/bloom](./packages/bloom) | [](https://www.npmjs.com/package/@redis/bloom) [](https://www.npmjs.com/package/@redis/bloom) [](https://redis.js.org/documentation/bloom/) [Redis Bloom](https://oss.redis.com/redisbloom/) commands |
|
|
||||||
| [@redis/graph](./packages/graph) | [](https://www.npmjs.com/package/@redis/graph) [](https://www.npmjs.com/package/@redis/graph) [](https://redis.js.org/documentation/graph/) [Redis Graph](https://oss.redis.com/redisgraph/) commands |
|
|
||||||
| [@redis/json](./packages/json) | [](https://www.npmjs.com/package/@redis/json) [](https://www.npmjs.com/package/@redis/json) [](https://redis.js.org/documentation/json/) [Redis JSON](https://oss.redis.com/redisjson/) commands |
|
|
||||||
| [@redis/search](./packages/search) | [](https://www.npmjs.com/package/@redis/search) [](https://www.npmjs.com/package/@redis/search) [](https://redis.js.org/documentation/search/) [RediSearch](https://oss.redis.com/redisearch/) commands |
|
|
||||||
| [@redis/time-series](./packages/time-series) | [](https://www.npmjs.com/package/@redis/time-series) [](https://www.npmjs.com/package/@redis/time-series) [](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
|
## Installation
|
||||||
|
|
||||||
Start a redis via docker:
|
Start a redis-server via docker (or any other method you prefer):
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
docker run -p 6379:6379 -it redis/redis-stack-server:latest
|
docker run -p 6379:6379 -it redis/redis-stack-server:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -53,323 +39,21 @@ To install node-redis, simply:
|
|||||||
npm install redis
|
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).
|
> "redis" is the "whole in one" package that includes all the other packages. If you only need a subset of the commands, you can install the individual packages. See the list below.
|
||||||
|
|
||||||
Looking for a high-level library to handle object mapping? See [redis-om-node](https://github.com/redis/redis-om-node)!
|
## Packages
|
||||||
|
|
||||||
## Usage
|
| Name | Description |
|
||||||
|
|------------------------------------------------|---------------------------------------------------------------------------------------------|
|
||||||
|
| [`redis`](./packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules |
|
||||||
|
| [`@redis/client`](./packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) |
|
||||||
|
| [`@redis/bloom`](./packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands |
|
||||||
|
| [`@redis/graph`](./packages/graph) | [Redis Graph](https://redis.io/docs/data-types/probabilistic/) commands |
|
||||||
|
| [`@redis/json`](./packages/json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands |
|
||||||
|
| [`@redis/search`](./packages/search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands |
|
||||||
|
| [`@redis/time-series`](./packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands |
|
||||||
|
|
||||||
### Basic Example
|
> Looking for a high-level library to handle object mapping? See [redis-om-node](https://github.com/redis/redis-om-node)!
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createClient } from 'redis';
|
|
||||||
|
|
||||||
const client = await createClient()
|
|
||||||
.on('error', err => console.log('Redis Client Error', err))
|
|
||||||
.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
|
## Contributing
|
||||||
|
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
import { hideBin } from 'yargs/helpers';
|
import { hideBin } from 'yargs/helpers';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'node:fs';
|
||||||
import { fork } from 'child_process';
|
import { fork } from 'node:child_process';
|
||||||
import { URL, fileURLToPath } from 'url';
|
import { URL, fileURLToPath } from 'node:url';
|
||||||
import { once } from 'events';
|
import { once } from 'node:events';
|
||||||
import { extname } from 'path';
|
import { extname } from 'node:path';
|
||||||
|
|
||||||
async function getPathChoices() {
|
async function getPathChoices() {
|
||||||
const dirents = await fs.readdir(new URL('.', import.meta.url), {
|
const dirents = await fs.readdir(new URL('.', import.meta.url), {
|
||||||
|
20
benchmark/lib/ping/ioredis-auto-pipeline.js
Normal file
20
benchmark/lib/ping/ioredis-auto-pipeline.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
21
benchmark/lib/ping/local-resp2.js
Normal file
21
benchmark/lib/ping/local-resp2.js
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
23
benchmark/lib/ping/local-resp3-buffer-proxy.js
Normal file
23
benchmark/lib/ping/local-resp3-buffer-proxy.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { createClient, RESP_TYPES } from 'redis-local';
|
||||||
|
|
||||||
|
export default async (host) => {
|
||||||
|
const client = createClient({
|
||||||
|
socket: {
|
||||||
|
host
|
||||||
|
},
|
||||||
|
RESP: 3
|
||||||
|
}).withTypeMapping({
|
||||||
|
[RESP_TYPES.SIMPLE_STRING]: Buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
benchmark() {
|
||||||
|
return client.ping();
|
||||||
|
},
|
||||||
|
teardown() {
|
||||||
|
return client.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
24
benchmark/lib/ping/local-resp3-buffer.js
Normal file
24
benchmark/lib/ping/local-resp3-buffer.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { createClient, RESP_TYPES } from 'redis-local';
|
||||||
|
|
||||||
|
export default async (host) => {
|
||||||
|
const client = createClient({
|
||||||
|
socket: {
|
||||||
|
host
|
||||||
|
},
|
||||||
|
commandOptions: {
|
||||||
|
[RESP_TYPES.SIMPLE_STRING]: Buffer
|
||||||
|
},
|
||||||
|
RESP: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
benchmark() {
|
||||||
|
return client.ping();
|
||||||
|
},
|
||||||
|
teardown() {
|
||||||
|
return client.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
27
benchmark/lib/ping/local-resp3-module-with-flags.js
Normal file
27
benchmark/lib/ping/local-resp3-module-with-flags.js
Normal 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.withTypeMapping({}).module.ping();
|
||||||
|
},
|
||||||
|
teardown() {
|
||||||
|
return client.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
27
benchmark/lib/ping/local-resp3-module.js
Normal file
27
benchmark/lib/ping/local-resp3-module.js
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
21
benchmark/lib/ping/local-resp3.js
Normal file
21
benchmark/lib/ping/local-resp3.js
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@@ -1,6 +1,6 @@
|
|||||||
import { createClient } from 'redis-v3';
|
import { createClient } from 'redis-v3';
|
||||||
import { once } from 'events';
|
import { once } from 'node:events';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
export default async (host) => {
|
export default async (host) => {
|
||||||
const client = createClient({ host }),
|
const client = createClient({ host }),
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { createClient } from '@redis/client';
|
import { createClient } from 'redis-v4';
|
||||||
|
|
||||||
export default async (host) => {
|
export default async (host) => {
|
||||||
const client = createClient({
|
const client = createClient({
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
import { hideBin } from 'yargs/helpers';
|
import { hideBin } from 'yargs/helpers';
|
||||||
import { basename } from 'path';
|
import { basename } from 'node:path';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'node:fs';
|
||||||
import * as hdr from 'hdr-histogram-js';
|
import * as hdr from 'hdr-histogram-js';
|
||||||
hdr.initWebAssemblySync();
|
hdr.initWebAssemblySync();
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ const benchmarkStart = process.hrtime.bigint(),
|
|||||||
histogram = await run(times),
|
histogram = await run(times),
|
||||||
benchmarkNanoseconds = process.hrtime.bigint() - benchmarkStart,
|
benchmarkNanoseconds = process.hrtime.bigint() - benchmarkStart,
|
||||||
json = {
|
json = {
|
||||||
timestamp,
|
// timestamp,
|
||||||
operationsPerSecond: times / Number(benchmarkNanoseconds) * 1_000_000_000,
|
operationsPerSecond: times / Number(benchmarkNanoseconds) * 1_000_000_000,
|
||||||
p0: histogram.getValueAtPercentile(0),
|
p0: histogram.getValueAtPercentile(0),
|
||||||
p50: histogram.getValueAtPercentile(50),
|
p50: histogram.getValueAtPercentile(50),
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
import { hideBin } from 'yargs/helpers';
|
import { hideBin } from 'yargs/helpers';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
const { size } = yargs(hideBin(process.argv))
|
const { size } = yargs(hideBin(process.argv))
|
||||||
.option('size', {
|
.option('size', {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { createClient } from 'redis-v3';
|
import { createClient } from 'redis-v3';
|
||||||
import { once } from 'events';
|
import { once } from 'node:events';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
export default async (host, { randomString }) => {
|
export default async (host, { randomString }) => {
|
||||||
const client = createClient({ host }),
|
const client = createClient({ host }),
|
||||||
|
87
benchmark/package-lock.json
generated
87
benchmark/package-lock.json
generated
@@ -6,11 +6,12 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "@redis/client-benchmark",
|
"name": "@redis/client-benchmark",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@redis/client": "../packages/client",
|
|
||||||
"hdr-histogram-js": "3.0.0",
|
"hdr-histogram-js": "3.0.0",
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5",
|
||||||
"redis-v3": "npm:redis@3.1.2",
|
"redis-local": "file:../packages/client",
|
||||||
"yargs": "17.7.2"
|
"redis-v3": "npm:redis@3",
|
||||||
|
"redis-v4": "npm:redis@4",
|
||||||
|
"yargs": "17.7.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@assemblyscript/loader": {
|
"node_modules/@assemblyscript/loader": {
|
||||||
@@ -23,10 +24,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
|
||||||
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
|
"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": {
|
"node_modules/@redis/client": {
|
||||||
"version": "1.5.7",
|
"version": "1.5.7",
|
||||||
"resolved": "file:../packages/client",
|
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
|
||||||
"license": "MIT",
|
"integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cluster-key-slot": "1.1.2",
|
"cluster-key-slot": "1.1.2",
|
||||||
"generic-pool": "3.9.0",
|
"generic-pool": "3.9.0",
|
||||||
@@ -36,6 +45,38 @@
|
|||||||
"node": ">=14"
|
"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": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
@@ -244,6 +285,20 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/redis-parser": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||||
@@ -282,6 +337,20 @@
|
|||||||
"node": ">=0.10"
|
"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": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@@ -349,9 +418,9 @@
|
|||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
},
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
|
||||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
"integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cliui": "^8.0.1",
|
"cliui": "^8.0.1",
|
||||||
"escalade": "^3.1.1",
|
"escalade": "^3.1.1",
|
||||||
|
@@ -7,10 +7,11 @@
|
|||||||
"start": "node ."
|
"start": "node ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@redis/client": "../packages/client",
|
|
||||||
"hdr-histogram-js": "3.0.0",
|
"hdr-histogram-js": "3.0.0",
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5",
|
||||||
"redis-v3": "npm:redis@3.1.2",
|
"redis-local": "file:../packages/client",
|
||||||
"yargs": "17.7.2"
|
"redis-v3": "npm:redis@3",
|
||||||
|
"redis-v4": "npm:redis@4",
|
||||||
|
"yargs": "17.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
docs/RESP.md
Normal file
46
docs/RESP.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Mapping RESP types
|
||||||
|
|
||||||
|
RESP, which stands for **R**edis **SE**rialization **P**rotocol, is the protocol used by Redis to communicate with clients. This document shows how RESP types can be mapped to JavaScript types. You can learn more about RESP itself in the [offical documentation](https://redis.io/docs/reference/protocol-spec/).
|
||||||
|
|
||||||
|
By default, each type is mapped to the first option in the lists below. To change this, configure a [`typeMapping`](.).
|
||||||
|
|
||||||
|
## RESP2
|
||||||
|
|
||||||
|
- Integer (`:`) => `number`
|
||||||
|
- Simple String (`+`) => `string | Buffer`
|
||||||
|
- Blob String (`$`) => `string | Buffer`
|
||||||
|
- Simple Error (`-`) => `ErrorReply`
|
||||||
|
- Array (`*`) => `Array`
|
||||||
|
|
||||||
|
> NOTE: the first type is the default type
|
||||||
|
|
||||||
|
## RESP3
|
||||||
|
|
||||||
|
- 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 a Map to `Map | object` or a Set to `Set`, keys and members of type "Simple String" or "Blob String" will be decoded as `string`s which enables lookups by value, ignoring type mapping. If you want them as `Buffer`s, decode them as `Array`s instead.
|
||||||
|
|
||||||
|
### 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)
|
@@ -1,18 +1,19 @@
|
|||||||
# `createClient` configuration
|
# `createClient` configuration
|
||||||
|
|
||||||
| Property | Default | Description |
|
| Property | Default | Description |
|
||||||
|--------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|------------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| url | | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) |
|
| url | | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) |
|
||||||
| socket | | Socket connection properties. Unlisted [`net.connect`](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) properties (and [`tls.connect`](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)) are also supported |
|
| socket | | Socket connection properties. Unlisted [`net.connect`](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) properties (and [`tls.connect`](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)) are also supported |
|
||||||
| socket.port | `6379` | Redis server port |
|
| socket.port | `6379` | Redis server port |
|
||||||
| socket.host | `'localhost'` | Redis server hostname |
|
| socket.host | `'localhost'` | Redis server hostname |
|
||||||
| socket.family | `0` | IP Stack version (one of `4 \| 6 \| 0`) |
|
| socket.family | `0` | IP Stack version (one of `4 \| 6 \| 0`) |
|
||||||
| socket.path | | Path to the UNIX Socket |
|
| socket.path | | Path to the UNIX Socket |
|
||||||
| socket.connectTimeout | `5000` | Connection Timeout (in milliseconds) |
|
| socket.connectTimeout | `5000` | Connection timeout (in milliseconds) |
|
||||||
| socket.noDelay | `true` | Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
|
| socket.noDelay | `true` | Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
|
||||||
| socket.keepAlive | `5000` | Toggle [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality |
|
| socket.keepAlive | `true` | Toggle [`keep-alive`](https://nodejs.org/api/net.html#socketsetkeepaliveenable-initialdelay) functionality |
|
||||||
|
| socket.keepAliveInitialDelay | `5000` | If set to a positive number, it sets the initial delay before the first keepalive probe is sent on an idle socket |
|
||||||
| socket.tls | | See explanation and examples [below](#TLS) |
|
| socket.tls | | See explanation and examples [below](#TLS) |
|
||||||
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
|
| socket.reconnectStrategy | Exponential backoff with a maximum of 2000 ms; plus 0-200 ms random jitter. | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
|
||||||
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
|
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
|
||||||
| password | | ACL password or the old "--requirepass" password |
|
| password | | ACL password or the old "--requirepass" password |
|
||||||
| name | | Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) |
|
| name | | Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) |
|
||||||
@@ -34,12 +35,19 @@ When the socket closes unexpectedly (without calling `.quit()`/`.disconnect()`),
|
|||||||
2. `number` -> wait for `X` milliseconds before reconnecting.
|
2. `number` -> wait for `X` milliseconds before reconnecting.
|
||||||
3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
|
3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
|
||||||
|
|
||||||
By default the strategy is `Math.min(retries * 50, 500)`, but it can be overwritten like so:
|
By default the strategy uses exponential backoff, but it can be overwritten like so:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
createClient({
|
createClient({
|
||||||
socket: {
|
socket: {
|
||||||
reconnectStrategy: retries => Math.min(retries * 50, 1000)
|
reconnectStrategy: retries => {
|
||||||
|
// Generate a random jitter between 0 – 200 ms:
|
||||||
|
const jitter = Math.floor(Math.random() * 200);
|
||||||
|
// Delay is an exponential back off, (times^2) * 50 ms, with a maximum value of 2000 ms:
|
||||||
|
const delay = Math.min(Math.pow(2, retries) * 50, 2000);
|
||||||
|
|
||||||
|
return delay + jitter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
@@ -4,26 +4,22 @@
|
|||||||
|
|
||||||
Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a regular client instance:
|
Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a regular client instance:
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
import { createCluster } from 'redis';
|
import { createCluster } from 'redis';
|
||||||
|
|
||||||
const cluster = createCluster({
|
const cluster = await createCluster({
|
||||||
rootNodes: [
|
rootNodes: [{
|
||||||
{
|
|
||||||
url: 'redis://10.0.0.1:30001'
|
url: 'redis://10.0.0.1:30001'
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
url: 'redis://10.0.0.2:30002'
|
url: 'redis://10.0.0.2:30002'
|
||||||
}
|
}]
|
||||||
]
|
})
|
||||||
});
|
.on('error', err => console.log('Redis Cluster Error', err))
|
||||||
|
.connect();
|
||||||
cluster.on('error', (err) => console.log('Redis Cluster Error', err));
|
|
||||||
|
|
||||||
await cluster.connect();
|
|
||||||
|
|
||||||
await cluster.set('key', 'value');
|
await cluster.set('key', 'value');
|
||||||
const value = await cluster.get('key');
|
const value = await cluster.get('key');
|
||||||
|
await cluster.close();
|
||||||
```
|
```
|
||||||
|
|
||||||
## `createCluster` configuration
|
## `createCluster` configuration
|
||||||
@@ -32,7 +28,7 @@ const value = await cluster.get('key');
|
|||||||
|
|
||||||
| Property | Default | Description |
|
| 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 |
|
| 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 |
|
| 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. |
|
| 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. |
|
||||||
@@ -41,9 +37,11 @@ const value = await cluster.get('key');
|
|||||||
| modules | | Included [Redis Modules](../README.md#packages) |
|
| modules | | Included [Redis Modules](../README.md#packages) |
|
||||||
| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) |
|
| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) |
|
||||||
| functions | | Function definitions (see [Functions](../README.md#functions)) |
|
| functions | | Function definitions (see [Functions](../README.md#functions)) |
|
||||||
|
|
||||||
## Auth with password and username
|
## Auth with password and username
|
||||||
|
|
||||||
Specifying the password in the URL or a root node will only affect the connection to that specific node. In case you want to set the password for all the connections being created from a cluster instance, use the `defaults` option.
|
Specifying the password in the URL or a root node will only affect the connection to that specific node. In case you want to set the password for all the connections being created from a cluster instance, use the `defaults` option.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
createCluster({
|
createCluster({
|
||||||
rootNodes: [{
|
rootNodes: [{
|
||||||
@@ -107,7 +105,7 @@ createCluster({
|
|||||||
|
|
||||||
### Commands that operate on Redis Keys
|
### 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)
|
### [Server Commands](https://redis.io/commands#server)
|
||||||
|
|
||||||
@@ -115,4 +113,4 @@ Admin commands such as `MEMORY STATS`, `FLUSHALL`, etc. are not attached to the
|
|||||||
|
|
||||||
### "Forwarded Commands"
|
### "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.
|
||||||
|
68
docs/command-options.md
Normal file
68
docs/command-options.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Command Options
|
||||||
|
|
||||||
|
> :warning: The command options API in v5 has breaking changes from the previous version. For more details, refer to the [v4-to-v5 guide](./v4-to-v5.md#command-options).
|
||||||
|
|
||||||
|
Command Options are used to create "proxy clients" that change the behavior of executed commands. See the sections below for details.
|
||||||
|
|
||||||
|
## Type Mapping
|
||||||
|
|
||||||
|
Some [RESP types](./RESP.md) can be mapped to more than one JavaScript type. For example, "Blob String" can be mapped to `string` or `Buffer`. You can override the default type mapping using the `withTypeMapping` function:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await client.get('key'); // `string | null`
|
||||||
|
|
||||||
|
const proxyClient = client.withTypeMapping({
|
||||||
|
[TYPES.BLOB_STRING]: Buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
await proxyClient.get('key'); // `Buffer | null`
|
||||||
|
```
|
||||||
|
|
||||||
|
See [RESP](./RESP.md) for a full list of types.
|
||||||
|
|
||||||
|
## Abort Signal
|
||||||
|
|
||||||
|
The client [batches commands](./FAQ.md#how-are-commands-batched) before sending them to Redis. Commands that haven't been written to the socket yet can be aborted using the [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) API:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const controller = new AbortController(),
|
||||||
|
client = client.withAbortSignal(controller.signal);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const promise = client.get('key');
|
||||||
|
controller.abort();
|
||||||
|
await promise;
|
||||||
|
} catch (err) {
|
||||||
|
// AbortError
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ASAP
|
||||||
|
|
||||||
|
Commands that are executed in the "asap" mode are added to the beginning of the "to sent" queue.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const asapClient = client.asap();
|
||||||
|
await asapClient.ping();
|
||||||
|
```
|
||||||
|
|
||||||
|
## `withCommandOptions`
|
||||||
|
|
||||||
|
You can set all of the above command options in a single call with the `withCommandOptions` function:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.withCommandOptions({
|
||||||
|
typeMapping: ...,
|
||||||
|
abortSignal: ...,
|
||||||
|
asap: ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If any of the above options are omitted, the default value will be used. For example, the following client would **not** be in ASAP mode:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.asap().withCommandOptions({
|
||||||
|
typeMapping: ...,
|
||||||
|
abortSignal: ...
|
||||||
|
});
|
||||||
|
```
|
@@ -1,67 +0,0 @@
|
|||||||
# Isolated Execution
|
|
||||||
|
|
||||||
Sometimes you want to run your commands on an exclusive connection. There are a few reasons to do this:
|
|
||||||
|
|
||||||
- You're using [transactions]() and need to `WATCH` a key or keys for changes.
|
|
||||||
- You want to run a blocking command that will take over the connection, such as `BLPOP` or `BLMOVE`.
|
|
||||||
- You're using the `MONITOR` command which also takes over a connection.
|
|
||||||
|
|
||||||
Below are several examples of how to use isolated execution.
|
|
||||||
|
|
||||||
> NOTE: Behind the scenes we're using [`generic-pool`](https://www.npmjs.com/package/generic-pool) to provide a pool of connections that can be isolated. Go there to learn more.
|
|
||||||
|
|
||||||
## The Simple Scenario
|
|
||||||
|
|
||||||
This just isolates execution on a single connection. Do what you want with that connection:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await client.executeIsolated(async isolatedClient => {
|
|
||||||
await isolatedClient.set('key', 'value');
|
|
||||||
await isolatedClient.get('key');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Transactions
|
|
||||||
|
|
||||||
Things get a little more complex with transactions. Here we are `.watch()`ing some keys. If the keys change during the transaction, a `WatchError` is thrown when `.exec()` is called:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
await client.executeIsolated(async isolatedClient => {
|
|
||||||
await isolatedClient.watch('key');
|
|
||||||
|
|
||||||
const multi = isolatedClient.multi()
|
|
||||||
.ping()
|
|
||||||
.get('key');
|
|
||||||
|
|
||||||
if (Math.random() > 0.5) {
|
|
||||||
await isolatedClient.watch('another-key');
|
|
||||||
multi.set('another-key', await isolatedClient.get('another-key') / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return multi.exec();
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof WatchError) {
|
|
||||||
// the transaction aborted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Blocking Commands
|
|
||||||
|
|
||||||
For blocking commands, you can execute a tidy little one-liner:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await client.executeIsolated(isolatedClient => isolatedClient.blPop('key'));
|
|
||||||
```
|
|
||||||
|
|
||||||
Or, you can just run the command directly, and provide the `isolated` option:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await client.blPop(
|
|
||||||
commandOptions({ isolated: true }),
|
|
||||||
'key'
|
|
||||||
);
|
|
||||||
```
|
|
74
docs/pool.md
Normal file
74
docs/pool.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# `RedisClientPool`
|
||||||
|
|
||||||
|
Sometimes you want to run your commands on an exclusive connection. There are a few reasons to do this:
|
||||||
|
|
||||||
|
- You want to run a blocking command that will take over the connection, such as `BLPOP` or `BLMOVE`.
|
||||||
|
- You're using [transactions](https://redis.io/docs/interact/transactions/) and need to `WATCH` a key or keys for changes.
|
||||||
|
- Some more...
|
||||||
|
|
||||||
|
For those use cases you'll need to create a connection pool.
|
||||||
|
|
||||||
|
## Creating a pool
|
||||||
|
|
||||||
|
You can create a pool using the `createClientPool` function:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createClientPool } from 'redis';
|
||||||
|
|
||||||
|
const pool = await createClientPool()
|
||||||
|
.on('error', err => console.error('Redis Client Pool Error', err));
|
||||||
|
```
|
||||||
|
|
||||||
|
the function accepts two arguments, the client configuration (see [here](./client-configuration.md) for more details), and the pool configuration:
|
||||||
|
|
||||||
|
| Property | Default | Description |
|
||||||
|
|----------------|---------|--------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| minimum | 1 | The minimum clients the pool should hold to. The pool won't close clients if the pool size is less than the minimum. |
|
||||||
|
| maximum | 100 | The maximum clients the pool will have at once. The pool won't create any more resources and queue requests in memory. |
|
||||||
|
| acquireTimeout | 3000 | The maximum time (in ms) a task can wait in the queue. The pool will reject the task with `TimeoutError` in case of a timeout. |
|
||||||
|
| cleanupDelay | 3000 | The time to wait before cleaning up unused clients. |
|
||||||
|
|
||||||
|
You can also create a pool from a client (reusing it's configuration):
|
||||||
|
```javascript
|
||||||
|
const pool = await client.createPool()
|
||||||
|
.on('error', err => console.error('Redis Client Pool Error', err));
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Simple Scenario
|
||||||
|
|
||||||
|
All the client APIs are exposed on the pool instance directly, and will execute the commands using one of the available clients.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await pool.sendCommand(['PING']); // 'PONG'
|
||||||
|
await client.ping(); // 'PONG'
|
||||||
|
await client.withTypeMapping({
|
||||||
|
[RESP_TYPES.SIMPLE_STRING]: Buffer
|
||||||
|
}).ping(); // Buffer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transactions
|
||||||
|
|
||||||
|
Things get a little more complex with transactions. Here we are `.watch()`ing some keys. If the keys change during the transaction, a `WatchError` is thrown when `.exec()` is called:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
await pool.execute(async client => {
|
||||||
|
await client.watch('key');
|
||||||
|
|
||||||
|
const multi = client.multi()
|
||||||
|
.ping()
|
||||||
|
.get('key');
|
||||||
|
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
await client.watch('another-key');
|
||||||
|
multi.set('another-key', await client.get('another-key') / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return multi.exec();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof WatchError) {
|
||||||
|
// the transaction aborted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
76
docs/programmability.md
Normal file
76
docs/programmability.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# [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,
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
|
transformArguments(key: string, toAdd: number): Array<string> {
|
||||||
|
return [key, toAdd.toString()];
|
||||||
|
},
|
||||||
|
transformReply: undefined as unknown as () => NumberReply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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, NumberReply } from 'redis';
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
scripts: {
|
||||||
|
add: defineScript({
|
||||||
|
SCRIPT: 'return redis.call("GET", KEYS[1]) + ARGV[1];',
|
||||||
|
NUMBER_OF_KEYS: 1,
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
|
transformArguments(key: string, toAdd: number): Array<string> {
|
||||||
|
return [key, toAdd.toString()];
|
||||||
|
},
|
||||||
|
transformReply: undefined as unknown as () => NumberReply
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
await client.set('key', '1');
|
||||||
|
await client.add('key', 2); // 3
|
||||||
|
```
|
@@ -1,18 +1,20 @@
|
|||||||
# Pub/Sub
|
# Pub/Sub
|
||||||
|
|
||||||
The Pub/Sub API is implemented by `RedisClient` and `RedisCluster`.
|
The Pub/Sub API is implemented by `RedisClient`, `RedisCluster`, and `RedisSentinel`.
|
||||||
|
|
||||||
## Pub/Sub with `RedisClient`
|
## Pub/Sub with `RedisClient`
|
||||||
|
|
||||||
Pub/Sub requires a dedicated stand-alone client. You can easily get one by `.duplicate()`ing an existing `RedisClient`:
|
### RESP2
|
||||||
|
|
||||||
```typescript
|
Using RESP2, Pub/Sub "takes over" the connection (a client with subscriptions will not execute commands), therefore it requires a dedicated connection. You can easily get one by `.duplicate()`ing an existing `RedisClient`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
const subscriber = client.duplicate();
|
const subscriber = client.duplicate();
|
||||||
subscriber.on('error', err => console.error(err));
|
subscriber.on('error', err => console.error(err));
|
||||||
await subscriber.connect();
|
await subscriber.connect();
|
||||||
```
|
```
|
||||||
|
|
||||||
When working with a `RedisCluster`, this is handled automatically for you.
|
> When working with either `RedisCluster` or `RedisSentinel`, this is handled automatically for you.
|
||||||
|
|
||||||
### `sharded-channel-moved` event
|
### `sharded-channel-moved` event
|
||||||
|
|
||||||
@@ -29,6 +31,8 @@ The event listener signature is as follows:
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> When working with `RedisCluster`, this is handled automatically for you.
|
||||||
|
|
||||||
## Subscribing
|
## Subscribing
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@@ -39,7 +43,7 @@ await client.pSubscribe('channe*', listener);
|
|||||||
await client.sSubscribe('channel', listener);
|
await client.sSubscribe('channel', listener);
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ Subscribing to the same channel more than once will create multiple listeners which will each be called when a message is recieved.
|
> ⚠️ Subscribing to the same channel more than once will create multiple listeners, each of which will be called when a message is received.
|
||||||
|
|
||||||
## Publishing
|
## Publishing
|
||||||
|
|
||||||
|
30
docs/scan-iterators.md
Normal file
30
docs/scan-iterators.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Scan Iterators
|
||||||
|
|
||||||
|
> :warning: The scan iterators API in v5 has breaking changes from the previous version. For more details, refer to the [v4-to-v5 guide](./v4-to-v5.md#scan-iterators).
|
||||||
|
|
||||||
|
[`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):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
for await (const keys of client.scanIterator()) {
|
||||||
|
const values = await client.mGet(keys);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This works with `HSCAN`, `SSCAN`, and `ZSCAN` too:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
for await (const entries of client.hScanIterator('hash')) {}
|
||||||
|
for await (const members of client.sScanIterator('set')) {}
|
||||||
|
for await (const membersWithScores of client.zScanIterator('sorted-set')) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can override the default options by providing a configuration object:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.scanIterator({
|
||||||
|
cursor: '0', // optional, defaults to '0'
|
||||||
|
TYPE: 'string', // `SCAN` only
|
||||||
|
MATCH: 'patter*',
|
||||||
|
COUNT: 100
|
||||||
|
});
|
||||||
|
```
|
100
docs/sentinel.md
Normal file
100
docs/sentinel.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Redis Sentinel
|
||||||
|
|
||||||
|
The [Redis Sentinel](https://redis.io/docs/management/sentinel/) object of node-redis provides a high level object that provides access to a high availability redis installation managed by Redis Sentinel to provide enumeration of master and replica nodes belonging to an installation as well as reconfigure itself on demand for failover and topology changes.
|
||||||
|
|
||||||
|
## Basic Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createSentinel } from 'redis';
|
||||||
|
|
||||||
|
const sentinel = await createSentinel({
|
||||||
|
name: 'sentinel-db',
|
||||||
|
sentinelRootNodes: [{
|
||||||
|
host: 'example',
|
||||||
|
port: 1234
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.on('error', err => console.error('Redis Sentinel Error', err));
|
||||||
|
.connect();
|
||||||
|
|
||||||
|
await sentinel.set('key', 'value');
|
||||||
|
const value = await sentinel.get('key');
|
||||||
|
await sentinel.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, we configure the sentinel object to fetch the configuration for the database Redis Sentinel is monitoring as "sentinel-db" with one of the sentinels being located at `example:1234`, then using it like a regular Redis client.
|
||||||
|
|
||||||
|
## `createSentinel` configuration
|
||||||
|
|
||||||
|
| Property | Default | Description |
|
||||||
|
|-----------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| name | | The sentinel identifier for a particular database cluster |
|
||||||
|
| sentinelRootNodes | | An array of root nodes that are part of the sentinel cluster, which will be used to get the 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 sentinel configuration from the server |
|
||||||
|
| maxCommandRediscovers | `16` | The maximum number of times a command will retry due to topology changes. |
|
||||||
|
| nodeClientOptions | | The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with |
|
||||||
|
| sentinelClientOptions | | The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with |
|
||||||
|
| masterPoolSize | `1` | The number of clients connected to the master node |
|
||||||
|
| replicaPoolSize | `0` | The number of clients connected to each replica node. When greater than 0, the client will distribute the load by executing read-only commands (such as `GET`, `GEOSEARCH`, etc.) across all the cluster nodes. |
|
||||||
|
| reserveClient | `false` | When `true`, one client will be reserved for the sentinel object. When `false`, the sentinel object will wait for the first available client from the pool. |
|
||||||
|
## PubSub
|
||||||
|
|
||||||
|
It supports PubSub via the normal mechanisms, including migrating the listeners if the node they are connected to goes down.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await sentinel.subscribe('channel', message => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
await sentinel.unsubscribe('channel');
|
||||||
|
```
|
||||||
|
|
||||||
|
see [the PubSub guide](./pub-sub.md) for more details.
|
||||||
|
|
||||||
|
## Sentinel as a pool
|
||||||
|
|
||||||
|
The sentinel object provides the ability to manage a pool of clients for the master node:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
createSentinel({
|
||||||
|
// ...
|
||||||
|
masterPoolSize: 10
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition, it also provides the ability have a pool of clients connected to the replica nodes, and to direct all read-only commands to them:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
createSentinel({
|
||||||
|
// ...
|
||||||
|
replicaPoolSize: 10
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Master client lease
|
||||||
|
|
||||||
|
Sometimes multiple commands needs to run on an exclusive client (for example, using `WATCH/MULTI/EXEC`).
|
||||||
|
|
||||||
|
There are 2 ways to get a client lease:
|
||||||
|
|
||||||
|
`.use()`
|
||||||
|
```javascript
|
||||||
|
const result = await sentinel.use(async client => {
|
||||||
|
await client.watch('key');
|
||||||
|
return client.multi()
|
||||||
|
.get('key')
|
||||||
|
.exec();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`.getMasterClientLease()`
|
||||||
|
```javascript
|
||||||
|
const clientLease = await sentinel.getMasterClientLease();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await clientLease.watch('key');
|
||||||
|
const resp = await clientLease.multi()
|
||||||
|
.get('key')
|
||||||
|
.exec();
|
||||||
|
} finally {
|
||||||
|
clientLease.release();
|
||||||
|
}
|
||||||
|
```
|
6
docs/todo.md
Normal file
6
docs/todo.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
- "Isolation Pool" -> pool
|
||||||
|
- Cluster request response policies (either implement, or block "server" commands in cluster)
|
||||||
|
|
||||||
|
Docs:
|
||||||
|
- [Command Options](./command-options.md)
|
||||||
|
- [RESP](./RESP.md)
|
53
docs/transactions.md
Normal file
53
docs/transactions.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# [Transactions](https://redis.io/docs/interact/transactions/) ([`MULTI`](https://redis.io/commands/multi/)/[`EXEC`](https://redis.io/commands/exec/))
|
||||||
|
|
||||||
|
Start a [transaction](https://redis.io/docs/interact/transactions/) by calling `.multi()`, then chaining your commands. When you're done, call `.exec()` and you'll get an array back with your results:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const [setReply, getReply] = await client.multi()
|
||||||
|
.set('key', 'value')
|
||||||
|
.get('another-key')
|
||||||
|
.exec();
|
||||||
|
```
|
||||||
|
|
||||||
|
## `exec<'typed'>()`/`execTyped()`
|
||||||
|
|
||||||
|
A transaction invoked with `.exec<'typed'>`/`execTyped()` will return types appropriate to the commands in the transaction:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const multi = client.multi().ping();
|
||||||
|
await multi.exec(); // Array<ReplyUnion>
|
||||||
|
await multi.exec<'typed'>(); // [string]
|
||||||
|
await multi.execTyped(); // [string]
|
||||||
|
```
|
||||||
|
|
||||||
|
> :warning: this only works when all the commands are invoked in a single "call chain"
|
||||||
|
|
||||||
|
## [`WATCH`](https://redis.io/commands/watch/)
|
||||||
|
|
||||||
|
You can also [watch](https://redis.io/docs/interact/transactions/#optimistic-locking-using-check-and-set) keys by calling `.watch()`. Your transaction will abort if any of the watched keys change or if the client reconnected between the `watch` and `exec` calls.
|
||||||
|
|
||||||
|
The `WATCH` state is stored on the connection (by the server). In case you need to run multiple `WATCH` & `MULTI` in parallel you'll need to use a [pool](./pool.md).
|
||||||
|
|
||||||
|
## `execAsPipeline`
|
||||||
|
|
||||||
|
`execAsPipeline` will execute the commands without "wrapping" it with `MULTI` & `EXEC` (and lose the transactional semantics).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await client.multi()
|
||||||
|
.get('a')
|
||||||
|
.get('b')
|
||||||
|
.execAsPipeline();
|
||||||
|
```
|
||||||
|
|
||||||
|
the diffrence between the above pipeline and `Promise.all`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await Promise.all([
|
||||||
|
client.get('a'),
|
||||||
|
client.get('b')
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
is that if the socket disconnects during the pipeline, any unwritten commands will be discarded. i.e. if the socket disconnects after `GET a` is written to the socket, but before `GET b` is:
|
||||||
|
- using `Promise.all` - the client will try to execute `GET b` when the socket reconnects
|
||||||
|
- using `execAsPipeline` - `GET b` promise will be rejected as well
|
240
docs/v4-to-v5.md
Normal file
240
docs/v4-to-v5.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# v4 to v5 migration guide
|
||||||
|
|
||||||
|
## Client Configuration
|
||||||
|
|
||||||
|
### Keep Alive
|
||||||
|
|
||||||
|
To better align with Node.js build-in [`net`](https://nodejs.org/api/net.html) and [`tls`](https://nodejs.org/api/tls.html) modules, the `keepAlive` option has been split into 2 options: `keepAlive` (`boolean`) and `keepAliveInitialDelay` (`number`). The defaults remain `true` and `5000`.
|
||||||
|
|
||||||
|
### Legacy Mode
|
||||||
|
|
||||||
|
In the previous version, you could access "legacy" mode by creating a client and passing in `{ legacyMode: true }`. Now, you can create one off of an existing client by calling the `.legacy()` function. This allows easier access to both APIs and enables better TypeScript support.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// use `client` for the current API
|
||||||
|
const client = createClient();
|
||||||
|
await client.set('key', 'value');
|
||||||
|
|
||||||
|
// use `legacyClient` for the "legacy" API
|
||||||
|
const legacyClient = client.legacy();
|
||||||
|
legacyClient.set('key', 'value', (err, reply) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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({
|
||||||
|
typeMapping: {
|
||||||
|
[TYPES.BLOB_STRING]: Buffer
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await proxyClient.get('key'); // `Buffer | null`
|
||||||
|
```
|
||||||
|
|
||||||
|
for more information, see the [Command Options guide](./command-options.md).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
`client.QUIT/quit()` is replaced by `client.close()`. and, to avoid confusion, `client.disconnect()` has been renamed to `client.destroy()`.
|
||||||
|
|
||||||
|
## Scan Iterators
|
||||||
|
|
||||||
|
Iterator commands like `SCAN`, `HSCAN`, `SSCAN`, and `ZSCAN` return collections of elements (depending on the data type). However, v4 iterators loop over these collections and yield individual items:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
for await (const key of client.scanIterator()) {
|
||||||
|
console.log(key, await client.get(key));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This mismatch can be awkward and makes "multi-key" commands like `MGET`, `UNLINK`, etc. pointless. So, in v5 the iterators now yield a collection instead of an element:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
for await (const keys of client.scanIterator()) {
|
||||||
|
// we can now meaningfully utilize "multi-key" commands
|
||||||
|
console.log(keys, await client.mGet(keys));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
for more information, see the [Scan Iterators guide](./scan-iterators.md).
|
||||||
|
|
||||||
|
## Isolation Pool
|
||||||
|
|
||||||
|
In v4, `RedisClient` had the ability to create a pool of connections using an "Isolation Pool" on top of the "main" connection. However, there was no way to use the pool without a "main" connection:
|
||||||
|
```javascript
|
||||||
|
const client = await createClient()
|
||||||
|
.on('error', err => console.error(err))
|
||||||
|
.connect();
|
||||||
|
|
||||||
|
await client.ping(
|
||||||
|
client.commandOptions({ isolated: true })
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
In v5 we've extracted this pool logic into its own class—`RedisClientPool`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const pool = await createClientPool()
|
||||||
|
.on('error', err => console.error(err))
|
||||||
|
.connect();
|
||||||
|
|
||||||
|
await pool.ping();
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [pool guide](./pool.md) for more information.
|
||||||
|
|
||||||
|
## Cluster `MULTI`
|
||||||
|
|
||||||
|
In v4, `cluster.multi()` did not support executing commands on replicas, even if they were readonly.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// this might execute on a replica, depending on configuration
|
||||||
|
await cluster.sendCommand('key', true, ['GET', 'key']);
|
||||||
|
|
||||||
|
// this always executes on a master
|
||||||
|
await cluster.multi()
|
||||||
|
.addCommand('key', ['GET', 'key'])
|
||||||
|
.exec();
|
||||||
|
```
|
||||||
|
|
||||||
|
To support executing commands on replicas, `cluster.multi().addCommand` now requires `isReadonly` as the second argument, which matches the signature of `cluster.sendCommand`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await cluster.multi()
|
||||||
|
.addCommand('key', true, ['GET', 'key'])
|
||||||
|
.exec();
|
||||||
|
```
|
||||||
|
|
||||||
|
## `MULTI.execAsPipeline()`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await client.multi()
|
||||||
|
.set('a', 'a')
|
||||||
|
.set('b', 'b')
|
||||||
|
.execAsPipeline();
|
||||||
|
```
|
||||||
|
|
||||||
|
In older versions, if the socket disconnects during the pipeline execution, i.e. after writing `SET a a` and before `SET b b`, the returned promise is rejected, but `SET b b` will still be executed on the server.
|
||||||
|
|
||||||
|
In v5, any unwritten commands (in the same pipeline) will be discarded.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Redis
|
||||||
|
|
||||||
|
- `ACL GETUSER`: `selectors`
|
||||||
|
- `COPY`: `destinationDb` -> `DB`, `replace` -> `REPLACE`, `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `CLIENT KILL`: `enum ClientKillFilters` -> `const CLIENT_KILL_FILTERS` [^enum-to-constants]
|
||||||
|
- `CLUSTER FAILOVER`: `enum FailoverModes` -> `const FAILOVER_MODES` [^enum-to-constants]
|
||||||
|
- `CLIENT TRACKINGINFO`: `flags` in RESP2 - `Set<string>` -> `Array<string>` (to match RESP3 default type mapping)
|
||||||
|
- `CLUSTER INFO`:
|
||||||
|
- `CLUSTER SETSLOT`: `ClusterSlotStates` -> `CLUSTER_SLOT_STATES` [^enum-to-constants]
|
||||||
|
- `CLUSTER RESET`: the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
|
||||||
|
- `CLUSTER FAILOVER`: `enum FailoverModes` -> `const FAILOVER_MODES` [^enum-to-constants], the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
|
||||||
|
- `CLUSTER LINKS`: `createTime` -> `create-time`, `sendBufferAllocated` -> `send-buffer-allocated`, `sendBufferUsed` -> `send-buffer-used` [^map-keys]
|
||||||
|
- `CLUSTER NODES`, `CLUSTER REPLICAS`, `CLUSTER INFO`: returning the raw `VerbatimStringReply`
|
||||||
|
- `EXPIRE`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `EXPIREAT`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `HSCAN`: `tuples` has been renamed to `entries`
|
||||||
|
- `HEXISTS`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `HRANDFIELD_COUNT_WITHVALUES`: `Record<BlobString, BlobString>` -> `Array<{ field: BlobString; value: BlobString; }>` (it can return duplicates).
|
||||||
|
- `HSETNX`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `INFO`:
|
||||||
|
- `LCS IDX`: `length` has been changed to `len`, `matches` has been changed from `Array<{ key1: RangeReply; key2: RangeReply; }>` to `Array<[key1: RangeReply, key2: RangeReply]>`
|
||||||
|
|
||||||
|
|
||||||
|
- `ZINTER`: instead of `client.ZINTER('key', { WEIGHTS: [1] })` use `client.ZINTER({ key: 'key', weight: 1 }])`
|
||||||
|
- `ZINTER_WITHSCORES`: instead of `client.ZINTER_WITHSCORES('key', { WEIGHTS: [1] })` use `client.ZINTER_WITHSCORES({ key: 'key', weight: 1 }])`
|
||||||
|
- `ZUNION`: instead of `client.ZUNION('key', { WEIGHTS: [1] })` use `client.ZUNION({ key: 'key', weight: 1 }])`
|
||||||
|
- `ZUNION_WITHSCORES`: instead of `client.ZUNION_WITHSCORES('key', { WEIGHTS: [1] })` use `client.ZUNION_WITHSCORES({ key: 'key', weight: 1 }])`
|
||||||
|
- `ZMPOP`: `{ elements: Array<{ member: string; score: number; }>; }` -> `{ members: Array<{ value: string; score: number; }>; }` to match other sorted set commands (e.g. `ZRANGE`, `ZSCAN`)
|
||||||
|
|
||||||
|
- `MOVE`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `PEXPIRE`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `PEXPIREAT`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `PFADD`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
|
||||||
|
- `RENAMENX`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `SETNX`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `SCAN`, `HSCAN`, `SSCAN`, and `ZSCAN`: `reply.cursor` will not be converted to number to avoid issues when the number is bigger than `Number.MAX_SAFE_INTEGER`. See [here](https://github.com/redis/node-redis/issues/2561).
|
||||||
|
- `SCRIPT EXISTS`: `Array<boolean>` -> `Array<number>` [^boolean-to-number]
|
||||||
|
- `SISMEMBER`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `SMISMEMBER`: `Array<boolean>` -> `Array<number>` [^boolean-to-number]
|
||||||
|
- `SMOVE`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
|
||||||
|
- `GEOSEARCH_WITH`/`GEORADIUS_WITH`: `GeoReplyWith` -> `GEO_REPLY_WITH` [^enum-to-constants]
|
||||||
|
- `GEORADIUSSTORE` -> `GEORADIUS_STORE`
|
||||||
|
- `GEORADIUSBYMEMBERSTORE` -> `GEORADIUSBYMEMBER_STORE`
|
||||||
|
- `XACK`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `XADD`: the `INCR` option has been removed, use `XADD_INCR` instead
|
||||||
|
- `LASTSAVE`: `Date` -> `number` (unix timestamp)
|
||||||
|
- `HELLO`: `protover` moved from the options object to it's own argument, `auth` -> `AUTH`, `clientName` -> `SETNAME`
|
||||||
|
- `MODULE LIST`: `version` -> `ver` [^map-keys]
|
||||||
|
- `MEMORY STATS`: [^map-keys]
|
||||||
|
- `FUNCTION RESTORE`: the second argument is `{ mode: string; }` instead of `string` [^future-proofing]
|
||||||
|
- `FUNCTION STATS`: `runningScript` -> `running_script`, `durationMs` -> `duration_ms`, `librariesCount` -> `libraries_count`, `functionsCount` -> `functions_count` [^map-keys]
|
||||||
|
|
||||||
|
- `TIME`: `Date` -> `[unixTimestamp: string, microseconds: string]`
|
||||||
|
|
||||||
|
- `XGROUP_CREATECONSUMER`: [^boolean-to-number]
|
||||||
|
- `XGROUP_DESTROY`: [^boolean-to-number]
|
||||||
|
- `XINFO GROUPS`: `lastDeliveredId` -> `last-delivered-id` [^map-keys]
|
||||||
|
- `XINFO STREAM`: `radixTreeKeys` -> `radix-tree-keys`, `radixTreeNodes` -> `radix-tree-nodes`, `lastGeneratedId` -> `last-generated-id`, `maxDeletedEntryId` -> `max-deleted-entry-id`, `entriesAdded` -> `entries-added`, `recordedFirstEntryId` -> `recorded-first-entry-id`, `firstEntry` -> `first-entry`, `lastEntry` -> `last-entry`
|
||||||
|
- `XAUTOCLAIM`, `XCLAIM`, `XRANGE`, `XREVRANGE`: `Array<{ name: string; messages: Array<{ id: string; message: Record<string, string> }>; }>` -> `Record<string, Array<{ id: string; message: Record<string, string> }>>`
|
||||||
|
|
||||||
|
- `COMMAND LIST`: `enum FilterBy` -> `const COMMAND_LIST_FILTER_BY` [^enum-to-constants], the filter argument has been moved from a "top level argument" into ` { FILTERBY: { type: <MODULE|ACLCAT|PATTERN>; value: <value> } }`
|
||||||
|
|
||||||
|
### Bloom
|
||||||
|
|
||||||
|
- `TOPK.QUERY`: `Array<number>` -> `Array<boolean>`
|
||||||
|
|
||||||
|
### Graph
|
||||||
|
|
||||||
|
- `GRAPH.SLOWLOG`: `timestamp` has been changed from `Date` to `number`
|
||||||
|
|
||||||
|
### JSON
|
||||||
|
|
||||||
|
- `JSON.ARRINDEX`: `start` and `end` arguments moved to `{ range: { start: number; end: number; }; }` [^future-proofing]
|
||||||
|
- `JSON.ARRPOP`: `path` and `index` arguments moved to `{ path: string; index: number; }` [^future-proofing]
|
||||||
|
- `JSON.ARRLEN`, `JSON.CLEAR`, `JSON.DEBUG MEMORY`, `JSON.DEL`, `JSON.FORGET`, `JSON.OBJKEYS`, `JSON.OBJLEN`, `JSON.STRAPPEND`, `JSON.STRLEN`, `JSON.TYPE`: `path` argument moved to `{ path: string; }` [^future-proofing]
|
||||||
|
|
||||||
|
### Search
|
||||||
|
|
||||||
|
- `FT.SUGDEL`: [^boolean-to-number]
|
||||||
|
- `FT.CURSOR READ`: `cursor` type changed from `number` to `string` (in and out) to avoid issues when the number is bigger than `Number.MAX_SAFE_INTEGER`. See [here](https://github.com/redis/node-redis/issues/2561).
|
||||||
|
|
||||||
|
### Time Series
|
||||||
|
|
||||||
|
- `TS.ADD`: `boolean` -> `number` [^boolean-to-number]
|
||||||
|
- `TS.[M][REV]RANGE`: `enum TimeSeriesBucketTimestamp` -> `const TIME_SERIES_BUCKET_TIMESTAMP` [^enum-to-constants], `enum TimeSeriesReducers` -> `const TIME_SERIES_REDUCERS` [^enum-to-constants], the `ALIGN` argument has been moved into `AGGREGRATION`
|
||||||
|
- `TS.SYNUPDATE`: `Array<string | Array<string>>` -> `Record<string, Array<string>>`
|
||||||
|
- `TS.M[REV]RANGE[_WITHLABELS]`, `TS.MGET[_WITHLABELS]`: TODO
|
||||||
|
|
||||||
|
[^enum-to-constants]: TODO
|
||||||
|
|
||||||
|
[^map-keys]: To avoid unnecessary transformations and confusion, map keys will not be transformed to "js friendly" names (i.e. `number-of-keys` will not be renamed to `numberOfKeys`). See [here](https://github.com/redis/node-redis/discussions/2506).
|
||||||
|
|
||||||
|
[^future-proofing]: TODO
|
38
docs/v5.md
Normal file
38
docs/v5.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# RESP3 Support
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const client = createClient({
|
||||||
|
RESP: 3
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// by default
|
||||||
|
await client.hGetAll('key'); // Record<string, string>
|
||||||
|
|
||||||
|
await client.withTypeMapping({
|
||||||
|
[TYPES.MAP]: Map
|
||||||
|
}).hGetAll('key'); // Map<string, string>
|
||||||
|
|
||||||
|
await client.withTypeMapping({
|
||||||
|
[TYPES.MAP]: Map,
|
||||||
|
[TYPES.BLOB_STRING]: Buffer
|
||||||
|
}).hGetAll('key'); // Map<string, Buffer>
|
||||||
|
```
|
||||||
|
|
||||||
|
# Sentinel Support
|
||||||
|
|
||||||
|
[TODO](./sentinel.md)
|
||||||
|
|
||||||
|
# `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]
|
||||||
|
```
|
@@ -91,5 +91,5 @@ await client.connect();
|
|||||||
|
|
||||||
// Add your example code here...
|
// Add your example code here...
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
```
|
```
|
||||||
|
@@ -27,4 +27,4 @@ console.log('blpopPromise resolved');
|
|||||||
// {"key":"keyName","element":"value"}
|
// {"key":"keyName","element":"value"}
|
||||||
console.log(`listItem is '${JSON.stringify(listItem)}'`);
|
console.log(`listItem is '${JSON.stringify(listItem)}'`);
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -77,4 +77,4 @@ const info = await client.bf.info('mybloom');
|
|||||||
// }
|
// }
|
||||||
console.log(info);
|
console.log(info);
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -25,4 +25,4 @@ console.log('Afer connectPromise has resolved...');
|
|||||||
// isReady will return True here, client is ready to use.
|
// isReady will return True here, client is ready to use.
|
||||||
console.log(`client.isOpen: ${client.isOpen}, client.isReady: ${client.isReady}`);
|
console.log(`client.isOpen: ${client.isOpen}, client.isReady: ${client.isReady}`);
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -1,25 +1,31 @@
|
|||||||
// Define a custom script that shows example of SET command
|
// Define a custom script that shows example of SET command
|
||||||
// with several modifiers.
|
// with several modifiers.
|
||||||
|
|
||||||
import { createClient } from 'redis';
|
import { createClient } from '../packages/client';
|
||||||
|
|
||||||
const client = createClient();
|
const client = createClient();
|
||||||
|
|
||||||
await client.connect();
|
await client.connect();
|
||||||
await client.del('mykey');
|
await client.del('mykey');
|
||||||
|
|
||||||
let result = await client.set('mykey', 'myvalue', {
|
console.log(
|
||||||
EX: 60,
|
await client.set('mykey', 'myvalue', {
|
||||||
|
expiration: {
|
||||||
|
type: 'EX',
|
||||||
|
value: 60
|
||||||
|
},
|
||||||
GET: true
|
GET: true
|
||||||
});
|
})
|
||||||
|
); // null
|
||||||
|
|
||||||
console.log(result); //null
|
console.log(
|
||||||
|
await client.set('mykey', 'newvalue', {
|
||||||
result = await client.set('mykey', 'newvalue', {
|
expiration: {
|
||||||
EX: 60,
|
type: 'EX',
|
||||||
|
value: 60
|
||||||
|
},
|
||||||
GET: true
|
GET: true
|
||||||
});
|
})
|
||||||
|
); // 'myvalue'
|
||||||
|
|
||||||
console.log(result); //myvalue
|
await client.close();
|
||||||
|
|
||||||
await client.quit();
|
|
||||||
|
@@ -23,4 +23,4 @@ try {
|
|||||||
console.log(`GET command failed: ${e.message}`);
|
console.log(`GET command failed: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -77,4 +77,4 @@ console.log('Count-Min Sketch info:');
|
|||||||
// }
|
// }
|
||||||
console.log(info);
|
console.log(info);
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -76,4 +76,4 @@ const info = await client.cf.info('mycuckoo');
|
|||||||
// }
|
// }
|
||||||
console.log(info);
|
console.log(info);
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -1,15 +1,19 @@
|
|||||||
// This example demonstrates the use of the DUMP and RESTORE commands
|
// This example demonstrates the use of the DUMP and RESTORE commands
|
||||||
|
|
||||||
import { commandOptions, createClient } from 'redis';
|
import { createClient, RESP_TYPES } from 'redis';
|
||||||
|
|
||||||
const client = createClient();
|
const client = await createClient({
|
||||||
await client.connect();
|
commandOptions: {
|
||||||
|
typeMapping: {
|
||||||
|
[RESP_TYPES.BLOB_STRING]: Buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).on('error', err => {
|
||||||
|
console.log('Redis Client Error', err);
|
||||||
|
}).connect();
|
||||||
|
|
||||||
// DUMP a specific key into a local variable
|
// DUMP a specific key into a local variable
|
||||||
const dump = await client.dump(
|
const dump = await client.dump('source');
|
||||||
commandOptions({ returnBuffers: true }),
|
|
||||||
'source'
|
|
||||||
);
|
|
||||||
|
|
||||||
// RESTORE into a new key
|
// RESTORE into a new key
|
||||||
await client.restore('destination', 0, dump);
|
await client.restore('destination', 0, dump);
|
||||||
@@ -19,4 +23,4 @@ await client.restore('destination', 0, dump, {
|
|||||||
REPLACE: true
|
REPLACE: true
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.quit();
|
await client.close();
|
||||||
|
@@ -9,4 +9,4 @@ const serverTime = await client.time();
|
|||||||
// 2022-02-25T12:57:40.000Z { microseconds: 351346 }
|
// 2022-02-25T12:57:40.000Z { microseconds: 351346 }
|
||||||
console.log(serverTime);
|
console.log(serverTime);
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -48,4 +48,4 @@ try {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -24,4 +24,4 @@ await client.connect();
|
|||||||
await client.set('mykey', '5');
|
await client.set('mykey', '5');
|
||||||
console.log(await client.mincr('mykey', 'myotherkey', 10)); // [ 15, 10 ]
|
console.log(await client.mincr('mykey', 'myotherkey', 10)); // [ 15, 10 ]
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -73,4 +73,4 @@ const numPets = await client.json.arrLen('noderedis:jsondata', '$.pets');
|
|||||||
// We now have 4 pets.
|
// We now have 4 pets.
|
||||||
console.log(`We now have ${numPets} pets.`);
|
console.log(`We now have ${numPets} pets.`);
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"redis": "../"
|
"redis": "../packages/client"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -85,4 +85,4 @@ for (const doc of results.documents) {
|
|||||||
console.log(`${doc.id}: ${doc.value.name}, ${doc.value.age} years old.`);
|
console.log(`${doc.id}: ${doc.value.name}, ${doc.value.age} years old.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -145,4 +145,4 @@ console.log(
|
|||||||
// ]
|
// ]
|
||||||
// }
|
// }
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -88,4 +88,4 @@ console.log(JSON.stringify(results, null, 2));
|
|||||||
// }
|
// }
|
||||||
// ]
|
// ]
|
||||||
// }
|
// }
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -12,4 +12,4 @@ for await (const member of client.sScanIterator(setName)) {
|
|||||||
console.log(member);
|
console.log(member);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -28,4 +28,4 @@ for await (const memberWithScore of client.zScanIterator('mysortedset')) {
|
|||||||
console.log(memberWithScore);
|
console.log(memberWithScore);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -47,4 +47,4 @@ console.log(`Length of mystream: ${await client.xLen('mystream')}.`);
|
|||||||
// Should be approximately 1000:
|
// Should be approximately 1000:
|
||||||
console.log(`Length of mytrimmedstream: ${await client.xLen('mytrimmedstream')}.`);
|
console.log(`Length of mytrimmedstream: ${await client.xLen('mytrimmedstream')}.`);
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -119,4 +119,4 @@ try {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -110,4 +110,4 @@ const [ simonCount, lanceCount ] = await client.topK.count('mytopk', [
|
|||||||
console.log(`Count estimate for simon: ${simonCount}.`);
|
console.log(`Count estimate for simon: ${simonCount}.`);
|
||||||
console.log(`Count estimate for lance: ${lanceCount}.`);
|
console.log(`Count estimate for lance: ${lanceCount}.`);
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
@@ -37,4 +37,4 @@ console.log(responses);
|
|||||||
// Clean up fixtures.
|
// Clean up fixtures.
|
||||||
await client.del(['hash1', 'hash2', 'hash3']);
|
await client.del(['hash1', 'hash2', 'hash3']);
|
||||||
|
|
||||||
await client.quit();
|
client.destroy();
|
||||||
|
77
index.ts
77
index.ts
@@ -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)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
4277
package-lock.json
generated
4277
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@@ -1,50 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "redis",
|
"name": "redis-monorepo",
|
||||||
"description": "A modern, high performance Redis client",
|
"private": true,
|
||||||
"version": "4.7.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"main": "./dist/index.js",
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"files": [
|
|
||||||
"dist/"
|
|
||||||
],
|
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./packages/*"
|
"./packages/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run test -ws --if-present",
|
"test": "npm run test -ws --if-present",
|
||||||
"build:client": "npm run build -w ./packages/client",
|
"build": "tsc --build",
|
||||||
"build:test-utils": "npm run build -w ./packages/test-utils",
|
"documentation": "typedoc",
|
||||||
"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",
|
|
||||||
"documentation": "npm run documentation -ws --if-present",
|
|
||||||
"gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot <documentation@bot>'"
|
"gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot <documentation@bot>'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"@redis/bloom": "1.2.0",
|
|
||||||
"@redis/client": "1.6.0",
|
|
||||||
"@redis/graph": "1.1.1",
|
|
||||||
"@redis/json": "1.0.7",
|
|
||||||
"@redis/search": "1.2.0",
|
|
||||||
"@redis/time-series": "1.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node14": "^14.1.0",
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"gh-pages": "^6.0.0",
|
"@types/mocha": "^10.0.6",
|
||||||
"release-it": "^16.1.5",
|
"@types/node": "^20.11.16",
|
||||||
"typescript": "^5.2.2"
|
"gh-pages": "^6.1.1",
|
||||||
},
|
"mocha": "^10.2.0",
|
||||||
"repository": {
|
"nyc": "^15.1.0",
|
||||||
"type": "git",
|
"release-it": "^17.0.3",
|
||||||
"url": "git://github.com/redis/node-redis.git"
|
"tsx": "^4.7.0",
|
||||||
},
|
"typedoc": "^0.25.7",
|
||||||
"bugs": {
|
"typescript": "^5.3.3"
|
||||||
"url": "https://github.com/redis/node-redis/issues"
|
}
|
||||||
},
|
|
||||||
"homepage": "https://github.com/redis/node-redis",
|
|
||||||
"keywords": [
|
|
||||||
"redis"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
.nyc_output/
|
|
||||||
coverage/
|
|
||||||
lib/
|
|
||||||
.nycrc.json
|
|
||||||
.release-it.json
|
|
||||||
tsconfig.json
|
|
@@ -5,6 +5,7 @@
|
|||||||
"tagAnnotation": "Release ${tagName}"
|
"tagAnnotation": "Release ${tagName}"
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
|
"versionArgs": ["--workspaces-update=false"],
|
||||||
"publishArgs": ["--access", "public"]
|
"publishArgs": ["--access", "public"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,17 @@
|
|||||||
# @redis/bloom
|
# @redis/bloom
|
||||||
|
|
||||||
This package provides support for the [RedisBloom](https://redisbloom.io) module, which adds additional probabilistic data structures to Redis. It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RediBloom commands.
|
This package provides support for the [RedisBloom](https://redis.io/docs/data-types/probabilistic/) module, which adds additional probabilistic data structures to Redis.
|
||||||
|
|
||||||
To use these extra commands, your Redis server must have the RedisBloom module installed.
|
Should be used with [`redis`/`@redis/client`](https://github.com/redis/node-redis).
|
||||||
|
|
||||||
|
:warning: To use these extra commands, your Redis server must have the RedisBloom module installed.
|
||||||
|
|
||||||
RedisBloom provides the following probabilistic data structures:
|
RedisBloom provides the following probabilistic data structures:
|
||||||
|
|
||||||
* Bloom Filter: for checking set membership with a high degree of certainty.
|
* Bloom Filter: for checking set membership with a high degree of certainty.
|
||||||
* Cuckoo Filter: for checking set membership with a high degree of certainty.
|
* Cuckoo Filter: for checking set membership with a high degree of certainty.
|
||||||
* Count-Min Sketch: Determine the frequency of events in a stream.
|
* T-Digest: for estimating the quantiles of a stream of data.
|
||||||
* Top-K: Maintain a list of k most frequently seen items.
|
* Top-K: Maintain a list of k most frequently seen items.
|
||||||
|
* Count-Min Sketch: Determine the frequency of events in a stream.
|
||||||
|
|
||||||
For complete examples, see `bloom-filter.js`, `cuckoo-filter.js`, `count-min-sketch.js` and `topk.js` in the Node Redis examples folder.
|
For some examples, see [`bloom-filter.js`](https://github.com/redis/node-redis/tree/master/examples/bloom-filter.js), [`cuckoo-filter.js`](https://github.com/redis/node-redis/tree/master/examples/cuckoo-filter.js), [`count-min-sketch.js`](https://github.com/redis/node-redis/tree/master/examples/count-min-sketch.js) and [`topk.js`](https://github.com/redis/node-redis/tree/master/examples/topk.js) in the [examples folder](https://github.com/redis/node-redis/tree/master/examples).
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './ADD';
|
import ADD from './ADD';
|
||||||
|
|
||||||
describe('BF ADD', () => {
|
describe('BF.ADD', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item'),
|
ADD.transformArguments('key', 'item'),
|
||||||
['BF.ADD', 'key', 'item']
|
['BF.ADD', 'key', 'item']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export function transformArguments(key: string, item: string): Array<string> {
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
|
IS_READ_ONLY: false,
|
||||||
|
transformArguments(key: RedisArgument, item: RedisArgument) {
|
||||||
return ['BF.ADD', key, item];
|
return ['BF.ADD', key, item];
|
||||||
}
|
},
|
||||||
|
transformReply: transformBooleanReply
|
||||||
export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
} as const satisfies Command;
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './CARD';
|
import CARD from './CARD';
|
||||||
|
|
||||||
describe('BF CARD', () => {
|
describe('BF.CARD', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('bloom'),
|
CARD.transformArguments('bloom'),
|
||||||
['BF.CARD', 'bloom']
|
['BF.CARD', 'bloom']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
|
||||||
export const IS_READ_ONLY = true;
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
export function transformArguments(key: string): Array<string> {
|
IS_READ_ONLY: true,
|
||||||
|
transformArguments(key: RedisArgument) {
|
||||||
return ['BF.CARD', key];
|
return ['BF.CARD', key];
|
||||||
}
|
},
|
||||||
|
transformReply: undefined as unknown as () => NumberReply
|
||||||
export declare function transformReply(): number;
|
} as const satisfies Command;
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './EXISTS';
|
import EXISTS from './EXISTS';
|
||||||
|
|
||||||
describe('BF EXISTS', () => {
|
describe('BF.EXISTS', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item'),
|
EXISTS.transformArguments('key', 'item'),
|
||||||
['BF.EXISTS', 'key', 'item']
|
['BF.EXISTS', 'key', 'item']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export const IS_READ_ONLY = true;
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
export function transformArguments(key: string, item: string): Array<string> {
|
IS_READ_ONLY: true,
|
||||||
|
transformArguments(key: RedisArgument, item: RedisArgument) {
|
||||||
return ['BF.EXISTS', key, item];
|
return ['BF.EXISTS', key, item];
|
||||||
}
|
},
|
||||||
|
transformReply: transformBooleanReply
|
||||||
export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
} as const satisfies Command;
|
||||||
|
@@ -1,24 +1,26 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './INFO';
|
import INFO from './INFO';
|
||||||
|
|
||||||
describe('BF INFO', () => {
|
describe('BF.INFO', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('bloom'),
|
INFO.transformArguments('bloom'),
|
||||||
['BF.INFO', 'bloom']
|
['BF.INFO', 'bloom']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('client.bf.info', async client => {
|
testUtils.testWithClient('client.bf.info', async client => {
|
||||||
await client.bf.reserve('key', 0.01, 100);
|
const [, reply] = await Promise.all([
|
||||||
|
client.bf.reserve('key', 0.01, 100),
|
||||||
|
client.bf.info('key')
|
||||||
|
]);
|
||||||
|
|
||||||
const info = await client.bf.info('key');
|
assert.equal(typeof reply, 'object');
|
||||||
assert.equal(typeof info, 'object');
|
assert.equal(reply['Capacity'], 100);
|
||||||
assert.equal(info.capacity, 100);
|
assert.equal(typeof reply['Size'], 'number');
|
||||||
assert.equal(typeof info.size, 'number');
|
assert.equal(typeof reply['Number of filters'], 'number');
|
||||||
assert.equal(typeof info.numberOfFilters, 'number');
|
assert.equal(typeof reply['Number of items inserted'], 'number');
|
||||||
assert.equal(typeof info.numberOfInsertedItems, 'number');
|
assert.equal(typeof reply['Expansion rate'], 'number');
|
||||||
assert.equal(typeof info.expansionRate, 'number');
|
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
@@ -1,38 +1,32 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, Command, UnwrapReply, NullReply, NumberReply, TuplesToMapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
import { transformInfoV2Reply } from '.';
|
||||||
|
|
||||||
export const IS_READ_ONLY = true;
|
export type BfInfoReplyMap = TuplesToMapReply<[
|
||||||
|
[SimpleStringReply<'Capacity'>, NumberReply],
|
||||||
|
[SimpleStringReply<'Size'>, NumberReply],
|
||||||
|
[SimpleStringReply<'Number of filters'>, NumberReply],
|
||||||
|
[SimpleStringReply<'Number of items inserted'>, NumberReply],
|
||||||
|
[SimpleStringReply<'Expansion rate'>, NullReply | NumberReply]
|
||||||
|
]>;
|
||||||
|
|
||||||
export function transformArguments(key: string): Array<string> {
|
export interface BfInfoReply {
|
||||||
|
capacity: NumberReply;
|
||||||
|
size: NumberReply;
|
||||||
|
numberOfFilters: NumberReply;
|
||||||
|
numberOfInsertedItems: NumberReply;
|
||||||
|
expansionRate: NullReply | NumberReply;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
transformArguments(key: RedisArgument) {
|
||||||
return ['BF.INFO', key];
|
return ['BF.INFO', key];
|
||||||
}
|
},
|
||||||
|
transformReply: {
|
||||||
export type InfoRawReply = [
|
2: (reply: UnwrapReply<Resp2Reply<BfInfoReplyMap>>, _, typeMapping?: TypeMapping): BfInfoReplyMap => {
|
||||||
_: string,
|
return transformInfoV2Reply<BfInfoReplyMap>(reply, typeMapping);
|
||||||
capacity: number,
|
},
|
||||||
_: string,
|
3: undefined as unknown as () => BfInfoReplyMap
|
||||||
size: number,
|
}
|
||||||
_: string,
|
} as const satisfies Command;
|
||||||
numberOfFilters: number,
|
|
||||||
_: string,
|
|
||||||
numberOfInsertedItems: number,
|
|
||||||
_: string,
|
|
||||||
expansionRate: number,
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface InfoReply {
|
|
||||||
capacity: number;
|
|
||||||
size: number;
|
|
||||||
numberOfFilters: number;
|
|
||||||
numberOfInsertedItems: number;
|
|
||||||
expansionRate: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transformReply(reply: InfoRawReply): InfoReply {
|
|
||||||
return {
|
|
||||||
capacity: reply[1],
|
|
||||||
size: reply[3],
|
|
||||||
numberOfFilters: reply[5],
|
|
||||||
numberOfInsertedItems: reply[7],
|
|
||||||
expansionRate: reply[9]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@@ -1,54 +1,54 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './INSERT';
|
import INSERT from './INSERT';
|
||||||
|
|
||||||
describe('BF INSERT', () => {
|
describe('BF.INSERT', () => {
|
||||||
describe('transformArguments', () => {
|
describe('transformArguments', () => {
|
||||||
it('simple', () => {
|
it('simple', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item'),
|
INSERT.transformArguments('key', 'item'),
|
||||||
['BF.INSERT', 'key', 'ITEMS', 'item']
|
['BF.INSERT', 'key', 'ITEMS', 'item']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with CAPACITY', () => {
|
it('with CAPACITY', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item', { CAPACITY: 100 }),
|
INSERT.transformArguments('key', 'item', { CAPACITY: 100 }),
|
||||||
['BF.INSERT', 'key', 'CAPACITY', '100', 'ITEMS', 'item']
|
['BF.INSERT', 'key', 'CAPACITY', '100', 'ITEMS', 'item']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with ERROR', () => {
|
it('with ERROR', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item', { ERROR: 0.01 }),
|
INSERT.transformArguments('key', 'item', { ERROR: 0.01 }),
|
||||||
['BF.INSERT', 'key', 'ERROR', '0.01', 'ITEMS', 'item']
|
['BF.INSERT', 'key', 'ERROR', '0.01', 'ITEMS', 'item']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with EXPANSION', () => {
|
it('with EXPANSION', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item', { EXPANSION: 1 }),
|
INSERT.transformArguments('key', 'item', { EXPANSION: 1 }),
|
||||||
['BF.INSERT', 'key', 'EXPANSION', '1', 'ITEMS', 'item']
|
['BF.INSERT', 'key', 'EXPANSION', '1', 'ITEMS', 'item']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with NOCREATE', () => {
|
it('with NOCREATE', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item', { NOCREATE: true }),
|
INSERT.transformArguments('key', 'item', { NOCREATE: true }),
|
||||||
['BF.INSERT', 'key', 'NOCREATE', 'ITEMS', 'item']
|
['BF.INSERT', 'key', 'NOCREATE', 'ITEMS', 'item']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with NONSCALING', () => {
|
it('with NONSCALING', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item', { NONSCALING: true }),
|
INSERT.transformArguments('key', 'item', { NONSCALING: true }),
|
||||||
['BF.INSERT', 'key', 'NONSCALING', 'ITEMS', 'item']
|
['BF.INSERT', 'key', 'NONSCALING', 'ITEMS', 'item']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with CAPACITY, ERROR, EXPANSION, NOCREATE and NONSCALING', () => {
|
it('with CAPACITY, ERROR, EXPANSION, NOCREATE and NONSCALING', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item', {
|
INSERT.transformArguments('key', 'item', {
|
||||||
CAPACITY: 100,
|
CAPACITY: 100,
|
||||||
ERROR: 0.01,
|
ERROR: 0.01,
|
||||||
EXPANSION: 1,
|
EXPANSION: 1,
|
||||||
|
@@ -1,32 +1,34 @@
|
|||||||
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
|
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
|
import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export const FIRST_KEY_INDEX = 1;
|
export interface BfInsertOptions {
|
||||||
|
|
||||||
interface InsertOptions {
|
|
||||||
CAPACITY?: number;
|
CAPACITY?: number;
|
||||||
ERROR?: number;
|
ERROR?: number;
|
||||||
EXPANSION?: number;
|
EXPANSION?: number;
|
||||||
NOCREATE?: true;
|
NOCREATE?: boolean;
|
||||||
NONSCALING?: true;
|
NONSCALING?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformArguments(
|
export default {
|
||||||
key: string,
|
FIRST_KEY_INDEX: 1,
|
||||||
items: RedisCommandArgument | Array<RedisCommandArgument>,
|
IS_READ_ONLY: false,
|
||||||
options?: InsertOptions
|
transformArguments(
|
||||||
): RedisCommandArguments {
|
key: RedisArgument,
|
||||||
|
items: RedisVariadicArgument,
|
||||||
|
options?: BfInsertOptions
|
||||||
|
) {
|
||||||
const args = ['BF.INSERT', key];
|
const args = ['BF.INSERT', key];
|
||||||
|
|
||||||
if (options?.CAPACITY) {
|
if (options?.CAPACITY !== undefined) {
|
||||||
args.push('CAPACITY', options.CAPACITY.toString());
|
args.push('CAPACITY', options.CAPACITY.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.ERROR) {
|
if (options?.ERROR !== undefined) {
|
||||||
args.push('ERROR', options.ERROR.toString());
|
args.push('ERROR', options.ERROR.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.EXPANSION) {
|
if (options?.EXPANSION !== undefined) {
|
||||||
args.push('EXPANSION', options.EXPANSION.toString());
|
args.push('EXPANSION', options.EXPANSION.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ export function transformArguments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
args.push('ITEMS');
|
args.push('ITEMS');
|
||||||
return pushVerdictArguments(args, items);
|
return pushVariadicArguments(args, items);
|
||||||
}
|
},
|
||||||
|
transformReply: transformBooleanArrayReply
|
||||||
export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
} as const satisfies Command;
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './LOADCHUNK';
|
import LOADCHUNK from './LOADCHUNK';
|
||||||
|
import { RESP_TYPES } from '@redis/client';
|
||||||
|
|
||||||
describe('BF LOADCHUNK', () => {
|
describe('BF.LOADCHUNK', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 0, ''),
|
LOADCHUNK.transformArguments('key', 0, ''),
|
||||||
['BF.LOADCHUNK', 'key', '0', '']
|
['BF.LOADCHUNK', 'key', '0', '']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -13,16 +14,22 @@ describe('BF LOADCHUNK', () => {
|
|||||||
testUtils.testWithClient('client.bf.loadChunk', async client => {
|
testUtils.testWithClient('client.bf.loadChunk', async client => {
|
||||||
const [, { iterator, chunk }] = await Promise.all([
|
const [, { iterator, chunk }] = await Promise.all([
|
||||||
client.bf.reserve('source', 0.01, 100),
|
client.bf.reserve('source', 0.01, 100),
|
||||||
client.bf.scanDump(
|
client.bf.scanDump('source', 0)
|
||||||
client.commandOptions({ returnBuffers: true }),
|
|
||||||
'source',
|
|
||||||
0
|
|
||||||
)
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await client.bf.loadChunk('destination', iterator, chunk),
|
await client.bf.loadChunk('destination', iterator, chunk),
|
||||||
'OK'
|
'OK'
|
||||||
);
|
);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
...GLOBAL.SERVERS.OPEN.clientOptions,
|
||||||
|
commandOptions: {
|
||||||
|
typeMapping: {
|
||||||
|
[RESP_TYPES.BLOB_STRING]: Buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +1,10 @@
|
|||||||
import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
|
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
|
||||||
export const FIRST_KEY_INDEX = 1;
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
export function transformArguments(
|
IS_READ_ONLY: false,
|
||||||
key: string,
|
transformArguments(key: RedisArgument, iterator: number, chunk: RedisArgument) {
|
||||||
iteretor: number,
|
return ['BF.LOADCHUNK', key, iterator.toString(), chunk];
|
||||||
chunk: RedisCommandArgument
|
},
|
||||||
): RedisCommandArguments {
|
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||||
return ['BF.LOADCHUNK', key, iteretor.toString(), chunk];
|
} as const satisfies Command;
|
||||||
}
|
|
||||||
|
|
||||||
export declare function transformReply(): 'OK';
|
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './MADD';
|
import MADD from './MADD';
|
||||||
|
|
||||||
describe('BF MADD', () => {
|
describe('BF.MADD', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', ['1', '2']),
|
MADD.transformArguments('key', ['1', '2']),
|
||||||
['BF.MADD', 'key', '1', '2']
|
['BF.MADD', 'key', '1', '2']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,12 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export function transformArguments(key: string, items: Array<string>): Array<string> {
|
export default {
|
||||||
return ['BF.MADD', key, ...items];
|
FIRST_KEY_INDEX: 1,
|
||||||
}
|
IS_READ_ONLY: false,
|
||||||
|
transformArguments(key: RedisArgument, items: RedisVariadicArgument) {
|
||||||
export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
return pushVariadicArguments(['BF.MADD', key], items);
|
||||||
|
},
|
||||||
|
transformReply: transformBooleanArrayReply
|
||||||
|
} as const satisfies Command;
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './MEXISTS';
|
import MEXISTS from './MEXISTS';
|
||||||
|
|
||||||
describe('BF MEXISTS', () => {
|
describe('BF.MEXISTS', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', ['1', '2']),
|
MEXISTS.transformArguments('key', ['1', '2']),
|
||||||
['BF.MEXISTS', 'key', '1', '2']
|
['BF.MEXISTS', 'key', '1', '2']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export const IS_READ_ONLY = true;
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
export function transformArguments(key: string, items: Array<string>): Array<string> {
|
IS_READ_ONLY: true,
|
||||||
return ['BF.MEXISTS', key, ...items];
|
transformArguments(key: RedisArgument, items: RedisVariadicArgument) {
|
||||||
}
|
return pushVariadicArguments(['BF.MEXISTS', key], items);
|
||||||
|
},
|
||||||
export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
transformReply: transformBooleanArrayReply
|
||||||
|
} as const satisfies Command;
|
||||||
|
@@ -1,19 +1,19 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './RESERVE';
|
import RESERVE from './RESERVE';
|
||||||
|
|
||||||
describe('BF RESERVE', () => {
|
describe('BF.RESERVE', () => {
|
||||||
describe('transformArguments', () => {
|
describe('transformArguments', () => {
|
||||||
it('simple', () => {
|
it('simple', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 0.01, 100),
|
RESERVE.transformArguments('key', 0.01, 100),
|
||||||
['BF.RESERVE', 'key', '0.01', '100']
|
['BF.RESERVE', 'key', '0.01', '100']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with EXPANSION', () => {
|
it('with EXPANSION', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 0.01, 100, {
|
RESERVE.transformArguments('key', 0.01, 100, {
|
||||||
EXPANSION: 1
|
EXPANSION: 1
|
||||||
}),
|
}),
|
||||||
['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1']
|
['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1']
|
||||||
@@ -22,7 +22,7 @@ describe('BF RESERVE', () => {
|
|||||||
|
|
||||||
it('with NONSCALING', () => {
|
it('with NONSCALING', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 0.01, 100, {
|
RESERVE.transformArguments('key', 0.01, 100, {
|
||||||
NONSCALING: true
|
NONSCALING: true
|
||||||
}),
|
}),
|
||||||
['BF.RESERVE', 'key', '0.01', '100', 'NONSCALING']
|
['BF.RESERVE', 'key', '0.01', '100', 'NONSCALING']
|
||||||
@@ -31,7 +31,7 @@ describe('BF RESERVE', () => {
|
|||||||
|
|
||||||
it('with EXPANSION and NONSCALING', () => {
|
it('with EXPANSION and NONSCALING', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 0.01, 100, {
|
RESERVE.transformArguments('key', 0.01, 100, {
|
||||||
EXPANSION: 1,
|
EXPANSION: 1,
|
||||||
NONSCALING: true
|
NONSCALING: true
|
||||||
}),
|
}),
|
||||||
|
@@ -1,16 +1,19 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
|
||||||
interface ReserveOptions {
|
export interface BfReserveOptions {
|
||||||
EXPANSION?: number;
|
EXPANSION?: number;
|
||||||
NONSCALING?: true;
|
NONSCALING?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformArguments(
|
export default {
|
||||||
key: string,
|
FIRST_KEY_INDEX: 1,
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
transformArguments(
|
||||||
|
key: RedisArgument,
|
||||||
errorRate: number,
|
errorRate: number,
|
||||||
capacity: number,
|
capacity: number,
|
||||||
options?: ReserveOptions
|
options?: BfReserveOptions
|
||||||
): Array<string> {
|
) {
|
||||||
const args = ['BF.RESERVE', key, errorRate.toString(), capacity.toString()];
|
const args = ['BF.RESERVE', key, errorRate.toString(), capacity.toString()];
|
||||||
|
|
||||||
if (options?.EXPANSION) {
|
if (options?.EXPANSION) {
|
||||||
@@ -22,6 +25,6 @@ export function transformArguments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
}
|
},
|
||||||
|
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||||
export declare function transformReply(): 'OK';
|
} as const satisfies Command;
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './SCANDUMP';
|
import SCANDUMP from './SCANDUMP';
|
||||||
|
|
||||||
describe('BF SCANDUMP', () => {
|
describe('BF.SCANDUMP', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 0),
|
SCANDUMP.transformArguments('key', 0),
|
||||||
['BF.SCANDUMP', 'key', '0']
|
['BF.SCANDUMP', 'key', '0']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -1,24 +1,15 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
|
||||||
export const IS_READ_ONLY = true;
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
export function transformArguments(key: string, iterator: number): Array<string> {
|
IS_READ_ONLY: true,
|
||||||
|
transformArguments(key: RedisArgument, iterator: number) {
|
||||||
return ['BF.SCANDUMP', key, iterator.toString()];
|
return ['BF.SCANDUMP', key, iterator.toString()];
|
||||||
}
|
},
|
||||||
|
transformReply(reply: UnwrapReply<TuplesReply<[NumberReply, BlobStringReply]>>) {
|
||||||
type ScanDumpRawReply = [
|
|
||||||
iterator: number,
|
|
||||||
chunk: string
|
|
||||||
];
|
|
||||||
|
|
||||||
interface ScanDumpReply {
|
|
||||||
iterator: number;
|
|
||||||
chunk: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transformReply([iterator, chunk]: ScanDumpRawReply): ScanDumpReply {
|
|
||||||
return {
|
return {
|
||||||
iterator,
|
iterator: reply[0],
|
||||||
chunk
|
chunk: reply[1]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} as const satisfies Command;
|
||||||
|
@@ -1,13 +1,16 @@
|
|||||||
import * as ADD from './ADD';
|
import type { RedisCommands, TypeMapping } from '@redis/client/dist/lib/RESP/types';
|
||||||
import * as CARD from './CARD';
|
|
||||||
import * as EXISTS from './EXISTS';
|
import ADD from './ADD';
|
||||||
import * as INFO from './INFO';
|
import CARD from './CARD';
|
||||||
import * as INSERT from './INSERT';
|
import EXISTS from './EXISTS';
|
||||||
import * as LOADCHUNK from './LOADCHUNK';
|
import INFO from './INFO';
|
||||||
import * as MADD from './MADD';
|
import INSERT from './INSERT';
|
||||||
import * as MEXISTS from './MEXISTS';
|
import LOADCHUNK from './LOADCHUNK';
|
||||||
import * as RESERVE from './RESERVE';
|
import MADD from './MADD';
|
||||||
import * as SCANDUMP from './SCANDUMP';
|
import MEXISTS from './MEXISTS';
|
||||||
|
import RESERVE from './RESERVE';
|
||||||
|
import SCANDUMP from './SCANDUMP';
|
||||||
|
import { RESP_TYPES } from '@redis/client';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ADD,
|
ADD,
|
||||||
@@ -30,4 +33,32 @@ export default {
|
|||||||
reserve: RESERVE,
|
reserve: RESERVE,
|
||||||
SCANDUMP,
|
SCANDUMP,
|
||||||
scanDump: SCANDUMP
|
scanDump: SCANDUMP
|
||||||
};
|
} as const satisfies RedisCommands;
|
||||||
|
|
||||||
|
export function transformInfoV2Reply<T>(reply: Array<any>, typeMapping?: TypeMapping): T {
|
||||||
|
const mapType = typeMapping ? typeMapping[RESP_TYPES.MAP] : undefined;
|
||||||
|
|
||||||
|
switch (mapType) {
|
||||||
|
case Array: {
|
||||||
|
return reply as unknown as T;
|
||||||
|
}
|
||||||
|
case Map: {
|
||||||
|
const ret = new Map<string, any>();
|
||||||
|
|
||||||
|
for (let i = 0; i < reply.length; i += 2) {
|
||||||
|
ret.set(reply[i].toString(), reply[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret as unknown as T;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const ret = Object.create(null);
|
||||||
|
|
||||||
|
for (let i = 0; i < reply.length; i += 2) {
|
||||||
|
ret[reply[i].toString()] = reply[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret as unknown as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,12 +1,12 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './INCRBY';
|
import INCRBY from './INCRBY';
|
||||||
|
|
||||||
describe('CMS INCRBY', () => {
|
describe('CMS.INCRBY', () => {
|
||||||
describe('transformArguments', () => {
|
describe('transformArguments', () => {
|
||||||
it('single item', () => {
|
it('single item', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', {
|
INCRBY.transformArguments('key', {
|
||||||
item: 'item',
|
item: 'item',
|
||||||
incrementBy: 1
|
incrementBy: 1
|
||||||
}),
|
}),
|
||||||
@@ -16,7 +16,7 @@ describe('CMS INCRBY', () => {
|
|||||||
|
|
||||||
it('multiple items', () => {
|
it('multiple items', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', [{
|
INCRBY.transformArguments('key', [{
|
||||||
item: 'a',
|
item: 'a',
|
||||||
incrementBy: 1
|
incrementBy: 1
|
||||||
}, {
|
}, {
|
||||||
@@ -29,13 +29,14 @@ describe('CMS INCRBY', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('client.cms.incrBy', async client => {
|
testUtils.testWithClient('client.cms.incrBy', async client => {
|
||||||
await client.cms.initByDim('key', 1000, 5);
|
const [, reply] = await Promise.all([
|
||||||
assert.deepEqual(
|
client.cms.initByDim('key', 1000, 5),
|
||||||
await client.cms.incrBy('key', {
|
client.cms.incrBy('key', {
|
||||||
item: 'item',
|
item: 'item',
|
||||||
incrementBy: 1
|
incrementBy: 1
|
||||||
}),
|
})
|
||||||
[1]
|
]);
|
||||||
);
|
|
||||||
|
assert.deepEqual(reply, [1]);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
@@ -1,14 +1,17 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
|
||||||
interface IncrByItem {
|
export interface BfIncrByItem {
|
||||||
item: string;
|
item: RedisArgument;
|
||||||
incrementBy: number;
|
incrementBy: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformArguments(
|
export default {
|
||||||
key: string,
|
FIRST_KEY_INDEX: 1,
|
||||||
items: IncrByItem | Array<IncrByItem>
|
IS_READ_ONLY: false,
|
||||||
): Array<string> {
|
transformArguments(
|
||||||
|
key: RedisArgument,
|
||||||
|
items: BfIncrByItem | Array<BfIncrByItem>
|
||||||
|
) {
|
||||||
const args = ['CMS.INCRBY', key];
|
const args = ['CMS.INCRBY', key];
|
||||||
|
|
||||||
if (Array.isArray(items)) {
|
if (Array.isArray(items)) {
|
||||||
@@ -20,10 +23,10 @@ export function transformArguments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
}
|
},
|
||||||
|
transformReply: undefined as unknown as () => ArrayReply<NumberReply>
|
||||||
|
} as const satisfies Command;
|
||||||
|
|
||||||
function pushIncrByItem(args: Array<string>, { item, incrementBy }: IncrByItem): void {
|
function pushIncrByItem(args: Array<RedisArgument>, { item, incrementBy }: BfIncrByItem): void {
|
||||||
args.push(item, incrementBy.toString());
|
args.push(item, incrementBy.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function transformReply(): Array<number>;
|
|
||||||
|
@@ -1,25 +1,28 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './INFO';
|
import INFO from './INFO';
|
||||||
|
|
||||||
describe('CMS INFO', () => {
|
describe('CMS.INFO', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key'),
|
INFO.transformArguments('key'),
|
||||||
['CMS.INFO', 'key']
|
['CMS.INFO', 'key']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('client.cms.info', async client => {
|
testUtils.testWithClient('client.cms.info', async client => {
|
||||||
await client.cms.initByDim('key', 1000, 5);
|
const width = 1000,
|
||||||
|
depth = 5,
|
||||||
|
[, reply] = await Promise.all([
|
||||||
|
client.cms.initByDim('key', width, depth),
|
||||||
|
client.cms.info('key')
|
||||||
|
]);
|
||||||
|
|
||||||
assert.deepEqual(
|
const expected = Object.create(null);
|
||||||
await client.cms.info('key'),
|
expected['width'] = width;
|
||||||
{
|
expected['depth'] = depth;
|
||||||
width: 1000,
|
expected['count'] = 0;
|
||||||
depth: 5,
|
|
||||||
count: 0
|
assert.deepEqual(reply, expected);
|
||||||
}
|
|
||||||
);
|
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
@@ -1,30 +1,28 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, TuplesToMapReply, NumberReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
import { transformInfoV2Reply } from '../bloom';
|
||||||
|
|
||||||
export const IS_READ_ONLY = true;
|
export type CmsInfoReplyMap = TuplesToMapReply<[
|
||||||
|
[SimpleStringReply<'width'>, NumberReply],
|
||||||
|
[SimpleStringReply<'depth'>, NumberReply],
|
||||||
|
[SimpleStringReply<'count'>, NumberReply]
|
||||||
|
]>;
|
||||||
|
|
||||||
export function transformArguments(key: string): Array<string> {
|
export interface CmsInfoReply {
|
||||||
|
width: NumberReply;
|
||||||
|
depth: NumberReply;
|
||||||
|
count: NumberReply;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
transformArguments(key: RedisArgument) {
|
||||||
return ['CMS.INFO', key];
|
return ['CMS.INFO', key];
|
||||||
}
|
},
|
||||||
|
transformReply: {
|
||||||
export type InfoRawReply = [
|
2: (reply: UnwrapReply<Resp2Reply<CmsInfoReplyMap>>, _, typeMapping?: TypeMapping): CmsInfoReply => {
|
||||||
_: string,
|
return transformInfoV2Reply<CmsInfoReply>(reply, typeMapping);
|
||||||
width: number,
|
},
|
||||||
_: string,
|
3: undefined as unknown as () => CmsInfoReply
|
||||||
depth: number,
|
}
|
||||||
_: string,
|
} as const satisfies Command;
|
||||||
count: number
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface InfoReply {
|
|
||||||
width: number;
|
|
||||||
depth: number;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transformReply(reply: InfoRawReply): InfoReply {
|
|
||||||
return {
|
|
||||||
width: reply[1],
|
|
||||||
depth: reply[3],
|
|
||||||
count: reply[5]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './INITBYDIM';
|
import INITBYDIM from './INITBYDIM';
|
||||||
|
|
||||||
describe('CMS INITBYDIM', () => {
|
describe('CMS.INITBYDIM', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 1000, 5),
|
INITBYDIM.transformArguments('key', 1000, 5),
|
||||||
['CMS.INITBYDIM', 'key', '1000', '5']
|
['CMS.INITBYDIM', 'key', '1000', '5']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
|
||||||
export function transformArguments(key: string, width: number, depth: number): Array<string> {
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
|
IS_READ_ONLY: false,
|
||||||
|
transformArguments(key: RedisArgument, width: number, depth: number) {
|
||||||
return ['CMS.INITBYDIM', key, width.toString(), depth.toString()];
|
return ['CMS.INITBYDIM', key, width.toString(), depth.toString()];
|
||||||
}
|
},
|
||||||
|
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||||
export declare function transformReply(): 'OK';
|
} as const satisfies Command;
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './INITBYPROB';
|
import INITBYPROB from './INITBYPROB';
|
||||||
|
|
||||||
describe('CMS INITBYPROB', () => {
|
describe('CMS.INITBYPROB', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 0.001, 0.01),
|
INITBYPROB.transformArguments('key', 0.001, 0.01),
|
||||||
['CMS.INITBYPROB', 'key', '0.001', '0.01']
|
['CMS.INITBYPROB', 'key', '0.001', '0.01']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
|
||||||
export function transformArguments(key: string, error: number, probability: number): Array<string> {
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
|
IS_READ_ONLY: false,
|
||||||
|
transformArguments(key: RedisArgument, error: number, probability: number) {
|
||||||
return ['CMS.INITBYPROB', key, error.toString(), probability.toString()];
|
return ['CMS.INITBYPROB', key, error.toString(), probability.toString()];
|
||||||
}
|
},
|
||||||
|
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||||
export declare function transformReply(): 'OK';
|
} as const satisfies Command;
|
||||||
|
@@ -1,36 +1,34 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './MERGE';
|
import MERGE from './MERGE';
|
||||||
|
|
||||||
describe('CMS MERGE', () => {
|
describe('CMS.MERGE', () => {
|
||||||
describe('transformArguments', () => {
|
describe('transformArguments', () => {
|
||||||
it('without WEIGHTS', () => {
|
it('without WEIGHTS', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('dest', ['src']),
|
MERGE.transformArguments('destination', ['source']),
|
||||||
['CMS.MERGE', 'dest', '1', 'src']
|
['CMS.MERGE', 'destination', '1', 'source']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with WEIGHTS', () => {
|
it('with WEIGHTS', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('dest', [{
|
MERGE.transformArguments('destination', [{
|
||||||
name: 'src',
|
name: 'source',
|
||||||
weight: 1
|
weight: 1
|
||||||
}]),
|
}]),
|
||||||
['CMS.MERGE', 'dest', '1', 'src', 'WEIGHTS', '1']
|
['CMS.MERGE', 'destination', '1', 'source', 'WEIGHTS', '1']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('client.cms.merge', async client => {
|
testUtils.testWithClient('client.cms.merge', async client => {
|
||||||
await Promise.all([
|
const [, , reply] = await Promise.all([
|
||||||
client.cms.initByDim('src', 1000, 5),
|
client.cms.initByDim('source', 1000, 5),
|
||||||
client.cms.initByDim('dest', 1000, 5),
|
client.cms.initByDim('destination', 1000, 5),
|
||||||
|
client.cms.merge('destination', ['source'])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(reply, 'OK');
|
||||||
await client.cms.merge('dest', ['src']),
|
|
||||||
'OK'
|
|
||||||
);
|
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
@@ -1,37 +1,37 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
|
||||||
interface Sketch {
|
interface BfMergeSketch {
|
||||||
name: string;
|
name: RedisArgument;
|
||||||
weight: number;
|
weight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Sketches = Array<string> | Array<Sketch>;
|
export type BfMergeSketches = Array<RedisArgument> | Array<BfMergeSketch>;
|
||||||
|
|
||||||
export function transformArguments(dest: string, src: Sketches): Array<string> {
|
export default {
|
||||||
const args = [
|
FIRST_KEY_INDEX: 1,
|
||||||
'CMS.MERGE',
|
IS_READ_ONLY: false,
|
||||||
dest,
|
transformArguments(
|
||||||
src.length.toString()
|
destination: RedisArgument,
|
||||||
];
|
source: BfMergeSketches
|
||||||
|
) {
|
||||||
|
let args = ['CMS.MERGE', destination, source.length.toString()];
|
||||||
|
|
||||||
if (isStringSketches(src)) {
|
if (isPlainSketches(source)) {
|
||||||
args.push(...src);
|
args = args.concat(source);
|
||||||
} else {
|
} else {
|
||||||
for (const sketch of src) {
|
const { length } = args;
|
||||||
args.push(sketch.name);
|
args[length + source.length] = 'WEIGHTS';
|
||||||
}
|
for (let i = 0; i < source.length; i++) {
|
||||||
|
args[length + i] = source[i].name;
|
||||||
args.push('WEIGHTS');
|
args[length + source.length + i + 1] = source[i].weight.toString();
|
||||||
for (const sketch of src) {
|
|
||||||
args.push(sketch.weight.toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
}
|
},
|
||||||
|
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
|
||||||
|
} as const satisfies Command;
|
||||||
|
|
||||||
function isStringSketches(src: Sketches): src is Array<string> {
|
function isPlainSketches(src: BfMergeSketches): src is Array<RedisArgument> {
|
||||||
return typeof src[0] === 'string';
|
return typeof src[0] === 'string' || src[0] instanceof Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function transformReply(): 'OK';
|
|
||||||
|
@@ -1,22 +1,21 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments } from './QUERY';
|
import QUERY from './QUERY';
|
||||||
|
|
||||||
describe('CMS QUERY', () => {
|
describe('CMS.QUERY', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item'),
|
QUERY.transformArguments('key', 'item'),
|
||||||
['CMS.QUERY', 'key', 'item']
|
['CMS.QUERY', 'key', 'item']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('client.cms.query', async client => {
|
testUtils.testWithClient('client.cms.query', async client => {
|
||||||
await client.cms.initByDim('key', 1000, 5);
|
const [, reply] = await Promise.all([
|
||||||
|
client.cms.initByDim('key', 1000, 5),
|
||||||
assert.deepEqual(
|
client.cms.query('key', 'item')
|
||||||
await client.cms.query('key', 'item'),
|
]);
|
||||||
[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
assert.deepEqual(reply, [0]);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
@@ -1,15 +1,11 @@
|
|||||||
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
|
import { ArrayReply, NumberReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
|
import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export const FIRST_KEY_INDEX = 1;
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
export const IS_READ_ONLY = true;
|
IS_READ_ONLY: true,
|
||||||
|
transformArguments(key: RedisArgument, items: RedisVariadicArgument) {
|
||||||
export function transformArguments(
|
return pushVariadicArguments(['CMS.QUERY', key], items);
|
||||||
key: string,
|
},
|
||||||
items: string | Array<string>
|
transformReply: undefined as unknown as () => ArrayReply<NumberReply>
|
||||||
): RedisCommandArguments {
|
} as const satisfies Command;
|
||||||
return pushVerdictArguments(['CMS.QUERY', key], items);
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare function transformReply(): Array<number>;
|
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import * as INCRBY from './INCRBY';
|
import type { RedisCommands } from '@redis/client/dist/lib/RESP/types';
|
||||||
import * as INFO from './INFO';
|
import INCRBY from './INCRBY';
|
||||||
import * as INITBYDIM from './INITBYDIM';
|
import INFO from './INFO';
|
||||||
import * as INITBYPROB from './INITBYPROB';
|
import INITBYDIM from './INITBYDIM';
|
||||||
import * as MERGE from './MERGE';
|
import INITBYPROB from './INITBYPROB';
|
||||||
import * as QUERY from './QUERY';
|
import MERGE from './MERGE';
|
||||||
|
import QUERY from './QUERY';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
INCRBY,
|
INCRBY,
|
||||||
@@ -18,4 +19,4 @@ export default {
|
|||||||
merge: MERGE,
|
merge: MERGE,
|
||||||
QUERY,
|
QUERY,
|
||||||
query: QUERY
|
query: QUERY
|
||||||
};
|
} as const satisfies RedisCommands;
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'node:assert';
|
||||||
import testUtils, { GLOBAL } from '../../test-utils';
|
import testUtils, { GLOBAL } from '../../test-utils';
|
||||||
import { transformArguments, transformReply } from './ADD';
|
import ADD from './ADD';
|
||||||
|
|
||||||
describe('CF ADD', () => {
|
describe('CF.ADD', () => {
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments('key', 'item'),
|
ADD.transformArguments('key', 'item'),
|
||||||
['CF.ADD', 'key', 'item']
|
['CF.ADD', 'key', 'item']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
export const FIRST_KEY_INDEX = 1;
|
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export function transformArguments(key: string, item: string): Array<string> {
|
export default {
|
||||||
|
FIRST_KEY_INDEX: 1,
|
||||||
|
IS_READ_ONLY: false,
|
||||||
|
transformArguments(key: RedisArgument, item: RedisArgument) {
|
||||||
return ['CF.ADD', key, item];
|
return ['CF.ADD', key, item];
|
||||||
}
|
},
|
||||||
|
transformReply: transformBooleanReply
|
||||||
export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
} as const satisfies Command;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user