You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-07 13:22:56 +03:00
use dockers for tests, use npm workspaces, add rejson & redisearch modules, fix some bugs
This commit is contained in:
15
packages/client/.eslintrc.json
Normal file
15
packages/client/.eslintrc.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"semi": [2, "always"]
|
||||
}
|
||||
}
|
1
packages/client/.gitignore
vendored
Normal file
1
packages/client/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
documentation/
|
9
packages/client/.npmignore
Normal file
9
packages/client/.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
.nyc_output/
|
||||
coverage/
|
||||
documentation/
|
||||
examples/
|
||||
lib/
|
||||
.nycrc.json
|
||||
dump.rdb
|
||||
index.ts
|
||||
tsconfig.json
|
4
packages/client/.nycrc.json
Normal file
4
packages/client/.nycrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"exclude": ["**/*.spec.ts", "lib/test-utils.ts"]
|
||||
}
|
903
packages/client/CHANGELOG.md
Normal file
903
packages/client/CHANGELOG.md
Normal file
@@ -0,0 +1,903 @@
|
||||
# Changelog
|
||||
|
||||
## v4.0.0
|
||||
|
||||
This version is a major change and refactor, adding modern JavaScript capabilities and multiple breaking changes. See the [migration guide](./docs/v3-to-v4.md) for tips on how to upgrade.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- All functions return Promises by default
|
||||
- Dropped support for Node.js 10.x, the minimum supported Node.js version is now 12.x
|
||||
- `createClient` takes new and different arguments
|
||||
- The `prefix`, `rename_commands` configuration options to `createClient` have been removed
|
||||
- The `enable_offline_queue` configuration option is removed, executing commands on a closed client (without calling `.connect()` or after calling `.disconnect()`) will reject immediately
|
||||
- Login credentials are no longer saved when using `.auth()` directly
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for Promises
|
||||
- Added built-in TypeScript declaration files enabling code completion
|
||||
- Added support for [clustering](./README.md#cluster)
|
||||
- Added idiomatic arguments and responses to [Redis commands](./README.md#redis-commands)
|
||||
- Added full support for [Lua Scripts](./README.md#lua-scripts)
|
||||
- Added support for [SCAN iterators](./README.md#scan-iterator)
|
||||
- Added the ability to extend Node Redis with Redis Module commands
|
||||
|
||||
## v3.0.0 - 09 Feb, 2020
|
||||
|
||||
This version is mainly a release to distribute all the unreleased changes on master since 2017 and additionally removes
|
||||
a lot of old deprecated features and old internals in preparation for an upcoming modernization refactor (v4).
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Dropped support for Node.js < 6
|
||||
- Dropped support for `hiredis` (no longer required)
|
||||
- Removed previously deprecated `drain` event
|
||||
- Removed previously deprecated `idle` event
|
||||
- Removed previously deprecated `parser` option
|
||||
- Removed previously deprecated `max_delay` option
|
||||
- Removed previously deprecated `max_attempts` option
|
||||
- Removed previously deprecated `socket_no_delay` option
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Removed development files from published package (#1370)
|
||||
- Duplicate function now allows db param to be passed (#1311)
|
||||
|
||||
### Features
|
||||
|
||||
- Upgraded to latest `redis-commands` package
|
||||
- Upgraded to latest `redis-parser` package, v3.0.0, which brings performance improvements
|
||||
- Replaced `double-ended-queue` with `denque`, which brings performance improvements
|
||||
- Add timestamps to debug traces
|
||||
- Add `socket_initial_delay` option for `socket.setKeepAlive` (#1396)
|
||||
- Add support for `rediss` protocol in url (#1282)
|
||||
|
||||
## v2.8.0 - 31 Jul, 2017
|
||||
|
||||
Features
|
||||
|
||||
- Accept UPPER_CASE commands in send_command
|
||||
- Add arbitrary commands to the prototype by using `Redis.addCommand(name)`
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed not always copying subscribe unsubscribe arguments
|
||||
- Fixed emitting internal errors while reconnecting with auth
|
||||
- Fixed crashing with invalid url option
|
||||
|
||||
## v2.7.1 - 14 Mar, 2017
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed monitor mode not working in combination with IPv6 (2.6.0 regression)
|
||||
|
||||
## v2.7.0 - 11 Mar, 2017
|
||||
|
||||
Features
|
||||
|
||||
- All returned errors are from now a subclass of `RedisError`.
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed rename_commands not accepting `null` as value
|
||||
- Fixed `AbortError`s and `AggregateError`s not showing the error message in the stack trace
|
||||
|
||||
## v2.6.5 - 15 Jan, 2017
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed parser not being reset in case the redis connection closed ASAP for overcoming of output buffer limits
|
||||
- Fixed parser reset if (p)message_buffer listener is attached
|
||||
|
||||
## v2.6.4 - 12 Jan, 2017
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed monitor mode not working in combination with IPv6, sockets or lua scripts (2.6.0 regression)
|
||||
|
||||
## v2.6.3 - 31 Oct, 2016
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Do not change the tls setting to camel_case
|
||||
- Fix domain handling in combination with the offline queue (2.5.3 regression)
|
||||
|
||||
## v2.6.2 - 16 Jun, 2016
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed individual callbacks of a transaction not being called (2.6.0 regression)
|
||||
|
||||
## v2.6.1 - 02 Jun, 2016
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed invalid function name being exported
|
||||
|
||||
## v2.6.0 - 01 Jun, 2016
|
||||
|
||||
In addition to the pre-releases the following changes exist in v.2.6.0:
|
||||
|
||||
Features
|
||||
|
||||
- Updated [redis-parser](https://github.com/NodeRedis/node-redis-parser) dependency ([changelog](https://github.com/NodeRedis/node-redis-parser/releases/tag/v.2.0.0))
|
||||
- The JS parser is from now on the new default as it is a lot faster than the hiredis parser
|
||||
- This is no BC as there is no changed behavior for the user at all but just a performance improvement. Explicitly requireing the Hiredis parser is still possible.
|
||||
- Added name property to all Redis functions (Node.js >= 4.0)
|
||||
- Improved stack traces in development and debug mode
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Reverted support for `__proto__` (v.2.6.0-2) to prevent and breaking change
|
||||
|
||||
Deprecations
|
||||
|
||||
- The `parser` option is deprecated and should be removed. The built-in Javascript parser is a lot faster than the hiredis parser and has more features
|
||||
|
||||
## v2.6.0-2 - 29 Apr, 2016
|
||||
|
||||
Features
|
||||
|
||||
- Added support for the new [CLIENT REPLY ON|OFF|SKIP](http://redis.io/commands/client-reply) command (Redis v.3.2)
|
||||
- Added support for camelCase
|
||||
- The Node.js landscape default is to use camelCase. node_redis is a bit out of the box here
|
||||
but from now on it is possible to use both, just as you prefer!
|
||||
- If there's any documented variable missing as camelCased, please open a issue for it
|
||||
- Improve error handling significantly
|
||||
- Only emit an error if the error has not already been handled in a callback
|
||||
- Improved unspecific error messages e.g. "Connection gone from end / close event"
|
||||
- Added `args` to command errors to improve identification of the error
|
||||
- Added origin to errors if there's e.g. a connection error
|
||||
- Added ReplyError class. All Redis errors are from now on going to be of that class
|
||||
- Added AbortError class. A subclass of AbortError. All unresolved and by node_redis rejected commands are from now on of that class
|
||||
- Added AggregateError class. If a unresolved and by node_redis rejected command has no callback and
|
||||
this applies to more than a single command, the errors for the commands without callback are aggregated
|
||||
to a single error that is emitted in debug_mode in that case.
|
||||
- Added `message_buffer` / `pmessage_buffer` events. That event is always going to emit a buffer
|
||||
- Listening to the `message` event at the same time is always going to return the same message as string
|
||||
- Added callback option to the duplicate function
|
||||
- Added support for `__proto__` and other reserved keywords as hgetall field
|
||||
- Updated [redis-commands](https://github.com/NodeRedis/redis-commands) dependency ([changelog](https://github.com/NodeRedis/redis-commands/releases/tag/v.1.2.0))
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed v.2.5.0 auth command regression (under special circumstances a reconnect would not authenticate properly)
|
||||
- Fixed v.2.6.0-0 pub sub mode and quit command regressions:
|
||||
- Entering pub sub mode not working if a earlier called and still running command returned an error
|
||||
- Unsubscribe callback not called if unsubscribing from all channels and resubscribing right away
|
||||
- Quit command resulting in an error in some cases
|
||||
- Fixed special handled functions in batch and multi context not working the same as without (e.g. select and info)
|
||||
- Be aware that not all commands work in combination with transactions but they all work with batch
|
||||
- Fixed address always set to 127.0.0.1:6379 in case host / port is set in the `tls` options instead of the general options
|
||||
|
||||
## v2.6.0-1 - 01 Apr, 2016
|
||||
|
||||
A second pre-release with further fixes. This is likely going to be released as 2.6.0 stable without further changes.
|
||||
|
||||
Features
|
||||
|
||||
- Added type validations for client.send_command arguments
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed client.send_command not working properly with every command and every option
|
||||
- Fixed pub sub mode unsubscribing from all channels in combination with the new `string_numbers` option crashing
|
||||
- Fixed pub sub mode unsubscribing from all channels not respected while reconnecting
|
||||
- Fixed pub sub mode events in combination with the `string_numbers` option emitting the number of channels not as number
|
||||
|
||||
## v2.6.0-0 - 27 Mar, 2016
|
||||
|
||||
This is mainly a very important bug fix release with some smaller features.
|
||||
|
||||
Features
|
||||
|
||||
- Monitor and pub sub mode now work together with the offline queue
|
||||
- All commands that were send after a connection loss are now going to be send after reconnecting
|
||||
- Activating monitor mode does now work together with arbitrary commands including pub sub mode
|
||||
- Pub sub mode is completely rewritten and all known issues fixed
|
||||
- Added `string_numbers` option to get back strings instead of numbers
|
||||
- Quit command is from now on always going to end the connection properly
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed calling monitor command while other commands are still running
|
||||
- Fixed monitor and pub sub mode not working together
|
||||
- Fixed monitor mode not working in combination with the offline queue
|
||||
- Fixed pub sub mode not working in combination with the offline queue
|
||||
- Fixed pub sub mode resubscribing not working with non utf8 buffer channels
|
||||
- Fixed pub sub mode crashing if calling unsubscribe / subscribe in various combinations
|
||||
- Fixed pub sub mode emitting unsubscribe even if no channels were unsubscribed
|
||||
- Fixed pub sub mode emitting a message without a message published
|
||||
- Fixed quit command not ending the connection and resulting in further reconnection if called while reconnecting
|
||||
|
||||
The quit command did not end connections earlier if the connection was down at that time and this could have
|
||||
lead to strange situations, therefor this was fixed to end the connection right away in those cases.
|
||||
|
||||
## v2.5.3 - 21 Mar, 2016
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Revert throwing on invalid data types and print a warning instead
|
||||
|
||||
## v2.5.2 - 16 Mar, 2016
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed breaking changes against Redis 2.4 introduced in 2.5.0 / 2.5.1
|
||||
|
||||
## v2.5.1 - 15 Mar, 2016
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed info command not working anymore with optional section argument
|
||||
|
||||
## v2.5.0 - 15 Mar, 2016
|
||||
|
||||
Same changelog as the pre-release
|
||||
|
||||
## v2.5.0-1 - 07 Mar, 2016
|
||||
|
||||
This is a big release with some substantial underlining changes. Therefor this is released as a pre-release and I encourage anyone who's able to, to test this out.
|
||||
|
||||
It took way to long to release this one and the next release cycles will be shorter again.
|
||||
|
||||
This release is also going to deprecate a couple things to prepare for a future v.3 (it'll still take a while to v.3).
|
||||
|
||||
Features
|
||||
|
||||
- The parsers moved into the [redis-parser](https://github.com/NodeRedis/node-redis-parser) module and will be maintained in there from now on
|
||||
- Improve js parser speed significantly for big SUNION/SINTER/LRANGE/ZRANGE
|
||||
- Improve redis-url parsing to also accept the database-number and options as query parameters as suggested in [IANA](http://www.iana.org/assignments/uri-schemes/prov/redis)
|
||||
- Added a `retry_unfulfilled_commands` option
|
||||
- Setting this to 'true' results in retrying all commands that were not fulfilled on a connection loss after the reconnect. Use with caution
|
||||
- Added a `db` option to select the database while connecting (this is [not recommended](https://groups.google.com/forum/#!topic/redis-db/vS5wX8X4Cjg))
|
||||
- Added a `password` option as alias for auth_pass
|
||||
- The client.server_info is from now on updated while using the info command
|
||||
- Gracefuly handle redis protocol errors from now on
|
||||
- Added a `warning` emitter that receives node_redis warnings like auth not required and deprecation messages
|
||||
- Added a `retry_strategy` option that replaces all reconnect options
|
||||
- The reconnecting event from now on also receives:
|
||||
- The error message why the reconnect happened (params.error)
|
||||
- The amount of times the client was connected (params.times_connected)
|
||||
- The total reconnecting time since the last time connected (params.total_retry_time)
|
||||
- Always respect the command execution order no matter if the reply could be returned sync or not (former exceptions: [#937](https://github.com/NodeRedis/node_redis/issues/937#issuecomment-167525939))
|
||||
- redis.createClient is now checking input values stricter and detects more faulty input
|
||||
- Started refactoring internals into individual modules
|
||||
- Pipelining speed improvements
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed explicit undefined as a command callback in a multi context
|
||||
- Fixed hmset failing to detect the first key as buffer or date if the key is of that type
|
||||
- Fixed do not run toString on an array argument and throw a "invalid data" error instead
|
||||
- This is not considered as breaking change, as this is likely a error in your code and if you want to have such a behavior you should handle this beforehand
|
||||
- The same applies to Map / Set and individual Object types
|
||||
- Fixed redis url not accepting the protocol being omitted or protocols other than the redis protocol for convenience
|
||||
- Fixed parsing the db keyspace even if the first database does not begin with a zero
|
||||
- Fixed handling of errors occurring while receiving pub sub messages
|
||||
- Fixed huge string pipelines crashing NodeJS (Pipeline size above 256mb)
|
||||
- Fixed rename_commands and prefix option not working together
|
||||
- Fixed ready being emitted to early in case a slave is still syncing / master down
|
||||
|
||||
Deprecations
|
||||
|
||||
- Using any command with a argument being set to null or undefined is deprecated
|
||||
- From v.3.0.0 on using a command with such an argument will return an error instead
|
||||
- If you want to keep the old behavior please use a precheck in your code that converts the arguments to a string.
|
||||
- Using SET or SETEX with a undefined or null value will from now on also result in converting the value to "null" / "undefined" to have a consistent behavior. This is not considered as breaking change, as it returned an error earlier.
|
||||
- Using .end(flush) without the flush parameter is deprecated and the flush parameter should explicitly be used
|
||||
- From v.3.0.0 on using .end without flush will result in an error
|
||||
- Using .end without flush means that any command that did not yet return is going to silently fail. Therefor this is considered harmful and you should explicitly silence such errors if you are sure you want this
|
||||
- Depending on the return value of a command to detect the backpressure is deprecated
|
||||
- From version 3.0.0 on node_redis might not return true / false as a return value anymore. Please rely on client.should_buffer instead
|
||||
- The `socket_nodelay` option is deprecated and will be removed in v.3.0.0
|
||||
- If you want to buffer commands you should use [.batch or .multi](./README.md) instead. This is necessary to reduce the amount of different options and this is very likely reducing your throughput if set to false.
|
||||
- If you are sure you want to activate the NAGLE algorithm you can still activate it by using client.stream.setNoDelay(false)
|
||||
- The `max_attempts` option is deprecated and will be removed in v.3.0.0. Please use the `retry_strategy` instead
|
||||
- The `retry_max_delay` option is deprecated and will be removed in v.3.0.0. Please use the `retry_strategy` instead
|
||||
- The drain event is deprecated and will be removed in v.3.0.0. Please listen to the stream drain event instead
|
||||
- The idle event is deprecated and will likely be removed in v.3.0.0. If you rely on this feature please open a new ticket in node_redis with your use case
|
||||
- Redis < v. 2.6 is not officially supported anymore and might not work in all cases. Please update to a newer redis version as it is not possible to test for these old versions
|
||||
- Removed non documented command syntax (adding the callback to an arguments array instead of passing it as individual argument)
|
||||
|
||||
## v2.4.2 - 27 Nov, 2015
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed not emitting ready after reconnect with disable_resubscribing ([@maxgalbu](https://github.com/maxgalbu))
|
||||
|
||||
## v2.4.1 - 25 Nov, 2015
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed a js parser regression introduced in 2.4.0 ([@BridgeAR](https://github.com/BridgeAR))
|
||||
|
||||
## v2.4.0 - 25 Nov, 2015
|
||||
|
||||
Features
|
||||
|
||||
- Added `tls` option to initiate a connection to a redis server behind a TLS proxy. Thanks ([@paddybyers](https://github.com/paddybyers))
|
||||
- Added `prefix` option to auto key prefix any command with the provided prefix ([@luin](https://github.com/luin) & [@BridgeAR](https://github.com/BridgeAR))
|
||||
- Added `url` option to pass the connection url with the options object ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Added `client.duplicate([options])` to duplicate the current client and return a new one with the same options ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Improve performance by up to 20% on almost all use cases ([@BridgeAR](https://github.com/BridgeAR))
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed js parser handling big values slow ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- The speed is now on par with the hiredis parser.
|
||||
|
||||
## v2.3.1 - 18 Nov, 2015
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed saving buffers with charsets other than utf-8 while using multi ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Fixed js parser handling big values very slow ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- The speed is up to ~500% faster than before but still up to ~50% slower than the hiredis parser.
|
||||
|
||||
## v2.3.0 - 30 Oct, 2015
|
||||
|
||||
Features
|
||||
|
||||
- Improve speed further for: ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- saving big strings (up to +300%)
|
||||
- using .multi / .batch (up to +50% / on Node.js 0.10.x +300%)
|
||||
- saving small buffers
|
||||
- Increased coverage to 99% ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Refactored manual backpressure control ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Removed the high water mark and low water mark. Such a mechanism should be implemented by a user instead
|
||||
- The `drain` event is from now on only emitted if the stream really had to buffer
|
||||
- Reduced the default connect_timeout to be one hour instead of 24h ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Added .path to redis.createClient(options); ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Ignore info command, if not available on server ([@ivanB1975](https://github.com/ivanB1975))
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed a js parser error that could result in a timeout ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Fixed .multi / .batch used with Node.js 0.10.x not working properly after a reconnect ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Fixed fired but not yet returned commands not being rejected after a connection loss ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Fixed connect_timeout not respected if no connection has ever been established ([@gagle](https://github.com/gagle) & [@benjie](https://github.com/benjie))
|
||||
- Fixed return_buffers in pub sub mode ([@komachi](https://github.com/komachi))
|
||||
|
||||
## v2.2.5 - 18 Oct, 2015
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed undefined options passed to a new instance not accepted (possible with individual .createClient functions) ([@BridgeAR](https://github.com/BridgeAR))
|
||||
|
||||
## v2.2.4 - 17 Oct, 2015
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed unspecific error message for unresolvable commands ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Fixed not allowed command error in pubsub mode not being returned in a provided callback ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Fixed to many commands forbidden in pub sub mode ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Fixed mutation of the arguments array passed to .multi / .batch constructor ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Fixed mutation of the options object passed to createClient ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Fixed error callback in .multi not called if connection in broken mode ([@BridgeAR](https://github.com/BridgeAR))
|
||||
|
||||
## v2.2.3 - 14 Oct, 2015
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed multi not being executed on Node 0.10.x if node_redis not yet ready ([@BridgeAR](https://github.com/BridgeAR))
|
||||
|
||||
## v2.2.2 - 14 Oct, 2015
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed regular commands not being executed after a .multi until .exec was called ([@BridgeAR](https://github.com/BridgeAR))
|
||||
|
||||
## v2.2.1 - 12 Oct, 2015
|
||||
|
||||
No code change
|
||||
|
||||
## v2.2.0 - 12 Oct, 2015 - The peregrino falcon
|
||||
|
||||
The peregrino falcon is the fasted bird on earth and this is what this release is all about: Increased performance for heavy usage by up to **400%** [sic!] and increased overall performance for any command as well. Please check the benchmarks in the [README.md](README.md) for further details.
|
||||
|
||||
Features
|
||||
|
||||
- Added rename_commands options to handle renamed commands from the redis config ([@digmxl](https://github.com/digmxl) & [@BridgeAR](https://github.com/BridgeAR))
|
||||
- Added disable_resubscribing option to prevent a client from resubscribing after reconnecting ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Increased performance ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- exchanging built in queue with [@petkaantonov](https://github.com/petkaantonov)'s [double-ended queue](https://github.com/petkaantonov/deque)
|
||||
- prevent polymorphism
|
||||
- optimize statements
|
||||
- Added _.batch_ command, similar to .multi but without transaction ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- Improved pipelining to minimize the [RTT](http://redis.io/topics/pipelining) further ([@BridgeAR](https://github.com/BridgeAR))
|
||||
|
||||
Bugfixes
|
||||
|
||||
- Fixed a javascript parser regression introduced in 2.0 that could result in timeouts on high load. ([@BridgeAR](https://github.com/BridgeAR))
|
||||
- I was not able to write a regression test for this, since the error seems to only occur under heavy load with special conditions. So please have a look for timeouts with the js parser, if you use it and report all issues and switch to the hiredis parser in the meanwhile. If you're able to come up with a reproducable test case, this would be even better :)
|
||||
- Fixed should_buffer boolean for .exec, .select and .auth commands not being returned and fix a couple special conditions ([@BridgeAR](https://github.com/BridgeAR))
|
||||
|
||||
If you do not rely on transactions but want to reduce the RTT you can use .batch from now on. It'll behave just the same as .multi but it does not have any transaction and therefor won't roll back any failed commands.<br>
|
||||
Both .multi and .batch are from now on going to cache the commands and release them while calling .exec.
|
||||
|
||||
Please consider using .batch instead of looping through a lot of commands one by one. This will significantly improve your performance.
|
||||
|
||||
Here are some stats compared to ioredis 1.9.1 (Lenovo T450s i7-5600U):
|
||||
|
||||
simple set
|
||||
82,496 op/s » ioredis
|
||||
112,617 op/s » node_redis
|
||||
|
||||
simple get
|
||||
82,015 op/s » ioredis
|
||||
105,701 op/s » node_redis
|
||||
|
||||
simple get with pipeline
|
||||
10,233 op/s » ioredis
|
||||
26,541 op/s » node_redis (using .batch)
|
||||
|
||||
lrange 100
|
||||
7,321 op/s » ioredis
|
||||
26,155 op/s » node_redis
|
||||
|
||||
publish
|
||||
90,524 op/s » ioredis
|
||||
112,823 op/s » node_redis
|
||||
|
||||
subscribe
|
||||
43,783 op/s » ioredis
|
||||
61,889 op/s » node_redis
|
||||
|
||||
To conclude: we can proudly say that node_redis is very likely outperforming any other node redis client.
|
||||
|
||||
Known issues
|
||||
|
||||
- The pub sub system has some flaws and those will be addressed in the next minor release
|
||||
|
||||
## v2.1.0 - Oct 02, 2015
|
||||
|
||||
Features:
|
||||
|
||||
- Addded optional flush parameter to `.end`. If set to true, commands fired after using .end are going to be rejected instead of being ignored. (@crispy1989)
|
||||
- Addded: host and port can now be provided in a single options object. E.g. redis.createClient({ host: 'localhost', port: 1337, max_attempts: 5 }); (@BridgeAR)
|
||||
- Speedup common cases (@BridgeAR)
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Fix argument mutation while using the array notation with the multi constructor (@BridgeAR)
|
||||
- Fix multi.hmset key not being type converted if used with an object and key not being a string (@BridgeAR)
|
||||
- Fix parser errors not being catched properly (@BridgeAR)
|
||||
- Fix a crash that could occur if a redis server does not return the info command as usual #541 (@BridgeAR)
|
||||
- Explicitly passing undefined as a callback statement will work again. E.g. client.publish('channel', 'message', undefined); (@BridgeAR)
|
||||
|
||||
## v2.0.1 - Sep 24, 2015
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Fix argument mutation while using the array notation in combination with keys / callbacks ([#866](.)). (@BridgeAR)
|
||||
|
||||
## v2.0.0 - Sep 21, 2015
|
||||
|
||||
This is the biggest release that node_redis had since it was released in 2010. A long list of outstanding bugs has been fixed, so we are very happy to present you redis 2.0 and we highly recommend updating as soon as possible.
|
||||
|
||||
# What's new in 2.0
|
||||
|
||||
- Implemented a "connection is broken" mode if no connection could be established
|
||||
- node_redis no longer throws under any circumstances, preventing it from terminating applications.
|
||||
- Multi error handling is now working properly
|
||||
- Consistent command behavior including multi
|
||||
- Windows support
|
||||
- Improved performance
|
||||
- A lot of code cleanup
|
||||
- Many bug fixes
|
||||
- Better user support!
|
||||
|
||||
## Features:
|
||||
|
||||
- Added a "redis connection is broken" mode after reaching max connection attempts / exceeding connection timeout. (@BridgeAR)
|
||||
- Added NODE_DEBUG=redis env to activate the debug_mode (@BridgeAR)
|
||||
- Added a default connection timeout of 24h instead of never timing out as a default (@BridgeAR)
|
||||
- Added: Network errors and other stream errors will from now on include the error code as `err.code` property (@BridgeAR)
|
||||
- Added: Errors thrown by redis will now include the redis error code as `err.code` property. (@skeggse & @BridgeAR)
|
||||
- Added: Errors thrown by node_redis will now include a `err.command` property for the command used (@BridgeAR)
|
||||
- Added new commands and drop support for deprecated _substr_ (@BridgeAR)
|
||||
- Added new possibilities how to provide the command arguments (@BridgeAR)
|
||||
- The entries in the keyspace of the server_info is now an object instead of a string. (@SinisterLight & @BridgeAR)
|
||||
- Small speedup here and there (e.g. by not using .toLowerCase() anymore) (@BridgeAR)
|
||||
- Full windows support (@bcoe)
|
||||
- Increased coverage by 10% and add a lot of tests to make sure everything works as it should. We now reached 97% :-) (@BridgeAR)
|
||||
- Remove dead code, clean up and refactor very old chunks (@BridgeAR)
|
||||
- Don't flush the offline queue if reconnecting (@BridgeAR)
|
||||
- Emit all errors insteaf of throwing sometimes and sometimes emitting them (@BridgeAR)
|
||||
- _auth_pass_ passwords are now checked to be a valid password (@jcppman & @BridgeAR)
|
||||
|
||||
## Bug fixes:
|
||||
|
||||
- Don't kill the app anymore by randomly throwing errors sync instead of emitting them (@BridgeAR)
|
||||
- Don't catch user errors anymore occuring in callbacks (no try callback anymore & more fixes for the parser) (@BridgeAR)
|
||||
- Early garbage collection of queued items (@dohse)
|
||||
- Fix js parser returning errors as strings (@BridgeAR)
|
||||
- Do not wrap errors into other errors (@BridgeAR)
|
||||
- Authentication failures are now returned in the callback instead of being emitted (@BridgeAR)
|
||||
- Fix a memory leak on reconnect (@rahar)
|
||||
- Using `send_command` directly may now also be called without the args as stated in the [README.md](./README.md) (@BridgeAR)
|
||||
- Fix the multi.exec error handling (@BridgeAR)
|
||||
- Fix commands being inconsistent and behaving wrong (@BridgeAR)
|
||||
- Channel names with spaces are now properly resubscribed after a reconnection (@pbihler)
|
||||
- Do not try to reconnect after the connection timeout has been exceeded (@BridgeAR)
|
||||
- Ensure the execution order is observed if using .eval (@BridgeAR)
|
||||
- Fix commands not being rejected after calling .quit (@BridgeAR)
|
||||
- Fix .auth calling the callback twice if already connected (@BridgeAR)
|
||||
- Fix detect_buffers not working in pub sub mode and while monitoring (@BridgeAR)
|
||||
- Fix channel names always being strings instead of buffers while return_buffers is true (@BridgeAR)
|
||||
- Don't print any debug statements if not asked for (@BridgeAR)
|
||||
- Fix a couple small other bugs
|
||||
|
||||
## Breaking changes:
|
||||
|
||||
1. redis.send_command commands have to be lower case from now on. This does only apply if you use `.send_command` directly instead of the convenient methods like `redis.command`.
|
||||
2. Error messages have changed quite a bit. If you depend on a specific wording please check your application carfully.
|
||||
3. Errors are from now on always either returned if a callback is present or emitted. They won't be thrown (neither sync, nor async).
|
||||
4. The Multi error handling has changed a lot!
|
||||
|
||||
- All errors are from now on errors instead of strings (this only applied to the js parser).
|
||||
- If an error occurs while queueing the commands an EXECABORT error will be returned including the failed commands as `.errors` property instead of an array with errors.
|
||||
- If an error occurs while executing the commands and that command has a callback it'll return the error as first parameter (`err, undefined` instead of `null, undefined`).
|
||||
- All the errors occuring while executing the commands will stay in the result value as error instance (if you used the js parser before they would have been strings). Be aware that the transaction won't be aborted if those error occurr!
|
||||
- If `multi.exec` does not have a callback and an EXECABORT error occurrs, it'll emit that error instead.
|
||||
|
||||
5. If redis can't connect to your redis server it'll give up after a certain point of failures (either max connection attempts or connection timeout exceeded). If that is the case it'll emit an CONNECTION_BROKEN error. You'll have to initiate a new client to try again afterwards.
|
||||
6. The offline queue is not flushed anymore on a reconnect. It'll stay until node_redis gives up trying to reach the server or until you close the connection.
|
||||
7. Before this release node_redis catched user errors and threw them async back. This is not the case anymore! No user behavior of what so ever will be tracked or catched.
|
||||
8. The keyspace of `redis.server_info` (db0...) is from now on an object instead of an string.
|
||||
|
||||
NodeRedis also thanks @qdb, @tobek, @cvibhagool, @frewsxcv, @davidbanham, @serv, @vitaliylag, @chrishamant, @GamingCoder and all other contributors that I may have missed for their contributions!
|
||||
|
||||
From now on we'll push new releases more frequently out and fix further long outstanding things and implement new features.
|
||||
|
||||
<hr>
|
||||
|
||||
## v1.0.0 - Aug 30, 2015
|
||||
|
||||
- Huge issue and pull-request cleanup. Thanks Blain! (@blainsmith)
|
||||
- [#658](https://github.com/NodeRedis/node_redis/pull/658) Client now parses URL-format connection strings (e.g., redis://foo:pass@127.0.0.1:8080) (@kuwabarahiroshi)
|
||||
- [#749](https://github.com/NodeRedis/node_redis/pull/749) Fix reconnection bug when client is in monitoring mode (@danielbprice)
|
||||
- [#786](https://github.com/NodeRedis/node_redis/pull/786) Refactor createClient. Fixes #651 (@BridgeAR)
|
||||
- [#793](https://github.com/NodeRedis/node_redis/pull/793) Refactor tests and improve test coverage (@erinspice, @bcoe)
|
||||
- [#733](https://github.com/NodeRedis/node_redis/pull/733) Fixes detect_buffers functionality in the context of exec. Fixes #732, #263 (@raydog)
|
||||
- [#785](https://github.com/NodeRedis/node_redis/pull/785) Tiny speedup by using 'use strict' (@BridgeAR)
|
||||
- Fix extraneous error output due to pubsub tests (Mikael Kohlmyr)
|
||||
|
||||
## v0.12.1 - Aug 10, 2014
|
||||
|
||||
- Fix IPv6/IPv4 family selection in node 0.11+ (Various)
|
||||
|
||||
## v0.12.0 - Aug 9, 2014
|
||||
|
||||
- Fix unix socket support (Jack Tang)
|
||||
- Improve createClient argument handling (Jack Tang)
|
||||
|
||||
## v0.11.0 - Jul 10, 2014
|
||||
|
||||
- IPv6 Support. (Yann Stephan)
|
||||
- Revert error emitting and go back to throwing errors. (Bryce Baril)
|
||||
- Set socket_keepalive to prevent long-lived client timeouts. (mohit)
|
||||
- Correctly reset retry timer. (ouotuo)
|
||||
- Domains protection from bad user exit. (Jake Verbaten)
|
||||
- Fix reconnection socket logic to prevent misqueued entries. (Iain Proctor)
|
||||
|
||||
## v0.10.3 - May 22, 2014
|
||||
|
||||
- Update command list to match Redis 2.8.9 (Charles Feng)
|
||||
|
||||
## v0.10.2 - May 18, 2014
|
||||
|
||||
- Better binary key handling for HGETALL. (Nick Apperson)
|
||||
- Fix test not resetting `error` handler. (CrypticSwarm)
|
||||
- Fix SELECT error semantics. (Bryan English)
|
||||
|
||||
## v0.10.1 - February 17, 2014
|
||||
|
||||
- Skip plucking redis version from the INFO stream if INFO results weren't provided. (Robert Sköld)
|
||||
|
||||
## v0.10.0 - December 21, 2013
|
||||
|
||||
- Instead of throwing errors asynchronously, emit errors on client. (Bryce Baril)
|
||||
|
||||
## v0.9.2 - December 15, 2013
|
||||
|
||||
- Regenerate commands for new 2.8.x Redis commands. (Marek Ventur)
|
||||
- Correctly time reconnect counts when using 'auth'. (William Hockey)
|
||||
|
||||
## v0.9.1 - November 23, 2013
|
||||
|
||||
- Allow hmset to accept numeric keys. (Alex Stokes)
|
||||
- Fix TypeError for multiple MULTI/EXEC errors. (Kwangsu Kim)
|
||||
|
||||
## v0.9.0 - October 17, 2013
|
||||
|
||||
- Domains support. (Forrest L Norvell)
|
||||
|
||||
## v0.8.6 - October 2, 2013
|
||||
|
||||
- If error is already an Error, don't wrap it in another Error. (Mathieu M-Gosselin)
|
||||
- Fix retry delay logic (Ian Babrou)
|
||||
- Return Errors instead of strings where Errors are expected (Ian Babrou)
|
||||
- Add experimental `.unref()` method to RedisClient (Bryce Baril / Olivier Lalonde)
|
||||
- Strengthen checking of reply to prevent conflating "message" or "pmessage" fields with pub_sub replies. (Bryce Baril)
|
||||
|
||||
## v0.8.5 - September 26, 2013
|
||||
|
||||
- Add `auth_pass` option to connect and immediately authenticate (Henrik Peinar)
|
||||
|
||||
## v0.8.4 - June 24, 2013
|
||||
|
||||
Many contributed features and fixes, including:
|
||||
|
||||
- Ignore password set if not needed. (jbergknoff)
|
||||
- Improved compatibility with 0.10.X for tests and client.end() (Bryce Baril)
|
||||
- Protect connection retries from application exceptions. (Amos Barreto)
|
||||
- Better exception handling for Multi/Exec (Thanasis Polychronakis)
|
||||
- Renamed pubsub mode to subscriber mode (Luke Plaster)
|
||||
- Treat SREM like SADD when passed an array (Martin Ciparelli)
|
||||
- Fix empty unsub/punsub TypeError (Jeff Barczewski)
|
||||
- Only attempt to run a callback if it one was provided (jifeng)
|
||||
|
||||
## v0.8.3 - April 09, 2013
|
||||
|
||||
Many contributed features and fixes, including:
|
||||
|
||||
- Fix some tests for Node.js version 0.9.x+ changes (Roman Ivanilov)
|
||||
- Fix error when commands submitted after idle event handler (roamm)
|
||||
- Bypass Redis for no-op SET/SETEX commands (jifeng)
|
||||
- Fix HMGET + detect_buffers (Joffrey F)
|
||||
- Fix CLIENT LOAD functionality (Jonas Dohse)
|
||||
- Add percentage outputs to diff_multi_bench_output.js (Bryce Baril)
|
||||
- Add retry_max_delay option (Tomasz Durka)
|
||||
- Fix parser off-by-one errors with nested multi-bulk replies (Bryce Baril)
|
||||
- Prevent parser from sinking application-side exceptions (Bryce Baril)
|
||||
- Fix parser incorrect buffer skip when parsing multi-bulk errors (Bryce Baril)
|
||||
- Reverted previous change with throwing on non-string values with HMSET (David Trejo)
|
||||
- Fix command queue sync issue when using pubsub (Tom Leach)
|
||||
- Fix compatibility with two-word Redis commands (Jonas Dohse)
|
||||
- Add EVAL with array syntax (dmoena)
|
||||
- Fix tests due to Redis reply order changes in 2.6.5+ (Bryce Baril)
|
||||
- Added a test for the SLOWLOG command (Nitesh Sinha)
|
||||
- Fix SMEMBERS order dependency in test broken by Redis changes (Garrett Johnson)
|
||||
- Update commands for new Redis commands (David Trejo)
|
||||
- Prevent exception from SELECT on subscriber reconnection (roamm)
|
||||
|
||||
## v0.8.2 - November 11, 2012
|
||||
|
||||
Another version bump because 0.8.1 didn't get applied properly for some mysterious reason.
|
||||
Sorry about that.
|
||||
|
||||
Changed name of "faster" parser to "javascript".
|
||||
|
||||
## v0.8.1 - September 11, 2012
|
||||
|
||||
Important bug fix for null responses (Jerry Sievert)
|
||||
|
||||
## v0.8.0 - September 10, 2012
|
||||
|
||||
Many contributed features and fixes, including:
|
||||
|
||||
- Pure JavaScript reply parser that is usually faster than hiredis (Jerry Sievert)
|
||||
- Remove hiredis as optionalDependency from package.json. It still works if you want it.
|
||||
- Restore client state on reconnect, including select, subscribe, and monitor. (Ignacio Burgueño)
|
||||
- Fix idle event (Trae Robrock)
|
||||
- Many documentation improvements and bug fixes (David Trejo)
|
||||
|
||||
## v0.7.2 - April 29, 2012
|
||||
|
||||
Many contributed fixes. Thank you, contributors.
|
||||
|
||||
- [GH-190] - pub/sub mode fix (Brian Noguchi)
|
||||
- [GH-165] - parser selection fix (TEHEK)
|
||||
- numerous documentation and examples updates
|
||||
- auth errors emit Errors instead of Strings (David Trejo)
|
||||
|
||||
## v0.7.1 - November 15, 2011
|
||||
|
||||
Fix regression in reconnect logic.
|
||||
|
||||
Very much need automated tests for reconnection and queue logic.
|
||||
|
||||
## v0.7.0 - November 14, 2011
|
||||
|
||||
Many contributed fixes. Thanks everybody.
|
||||
|
||||
- [GH-127] - properly re-initialize parser on reconnect
|
||||
- [GH-136] - handle passing undefined as callback (Ian Babrou)
|
||||
- [GH-139] - properly handle exceptions thrown in pub/sub event handlers (Felix Geisendörfer)
|
||||
- [GH-141] - detect closing state on stream error (Felix Geisendörfer)
|
||||
- [GH-142] - re-select database on reconnection (Jean-Hugues Pinson)
|
||||
- [GH-146] - add sort example (Maksim Lin)
|
||||
|
||||
Some more goodies:
|
||||
|
||||
- Fix bugs with node 0.6
|
||||
- Performance improvements
|
||||
- New version of `multi_bench.js` that tests more realistic scenarios
|
||||
- [GH-140] - support optional callback for subscribe commands
|
||||
- Properly flush and error out command queue when connection fails
|
||||
- Initial work on reconnection thresholds
|
||||
|
||||
## v0.6.7 - July 30, 2011
|
||||
|
||||
(accidentally skipped v0.6.6)
|
||||
|
||||
Fix and test for [GH-123]
|
||||
|
||||
Passing an Array as as the last argument should expand as users
|
||||
expect. The old behavior was to coerce the arguments into Strings,
|
||||
which did surprising things with Arrays.
|
||||
|
||||
## v0.6.5 - July 6, 2011
|
||||
|
||||
Contributed changes:
|
||||
|
||||
- Support SlowBuffers (Umair Siddique)
|
||||
- Add Multi to exports (Louis-Philippe Perron)
|
||||
- Fix for drain event calculation (Vladimir Dronnikov)
|
||||
|
||||
Thanks!
|
||||
|
||||
## v0.6.4 - June 30, 2011
|
||||
|
||||
Fix bug with optional callbacks for hmset.
|
||||
|
||||
## v0.6.2 - June 30, 2011
|
||||
|
||||
Bugs fixed:
|
||||
|
||||
- authentication retry while server is loading db (danmaz74) [GH-101]
|
||||
- command arguments processing issue with arrays
|
||||
|
||||
New features:
|
||||
|
||||
- Auto update of new commands from redis.io (Dave Hoover)
|
||||
- Performance improvements and backpressure controls.
|
||||
- Commands now return the true/false value from the underlying socket write(s).
|
||||
- Implement command_queue high water and low water for more better control of queueing.
|
||||
|
||||
See `examples/backpressure_drain.js` for more information.
|
||||
|
||||
## v0.6.1 - June 29, 2011
|
||||
|
||||
Add support and tests for Redis scripting through EXEC command.
|
||||
|
||||
Bug fix for monitor mode. (forddg)
|
||||
|
||||
Auto update of new commands from redis.io (Dave Hoover)
|
||||
|
||||
## v0.6.0 - April 21, 2011
|
||||
|
||||
Lots of bugs fixed.
|
||||
|
||||
- connection error did not properly trigger reconnection logic [GH-85]
|
||||
- client.hmget(key, [val1, val2]) was not expanding properly [GH-66]
|
||||
- client.quit() while in pub/sub mode would throw an error [GH-87]
|
||||
- client.multi(['hmset', 'key', {foo: 'bar'}]) fails [GH-92]
|
||||
- unsubscribe before subscribe would make things very confused [GH-88]
|
||||
- Add BRPOPLPUSH [GH-79]
|
||||
|
||||
## v0.5.11 - April 7, 2011
|
||||
|
||||
Added DISCARD
|
||||
|
||||
I originally didn't think DISCARD would do anything here because of the clever MULTI interface, but somebody
|
||||
pointed out to me that DISCARD can be used to flush the WATCH set.
|
||||
|
||||
## v0.5.10 - April 6, 2011
|
||||
|
||||
Added HVALS
|
||||
|
||||
## v0.5.9 - March 14, 2011
|
||||
|
||||
Fix bug with empty Array arguments - Andy Ray
|
||||
|
||||
## v0.5.8 - March 14, 2011
|
||||
|
||||
Add `MONITOR` command and special monitor command reply parsing.
|
||||
|
||||
## v0.5.7 - February 27, 2011
|
||||
|
||||
Add magical auth command.
|
||||
|
||||
Authentication is now remembered by the client and will be automatically sent to the server
|
||||
on every connection, including any reconnections.
|
||||
|
||||
## v0.5.6 - February 22, 2011
|
||||
|
||||
Fix bug in ready check with `return_buffers` set to `true`.
|
||||
|
||||
Thanks to Dean Mao and Austin Chau.
|
||||
|
||||
## v0.5.5 - February 16, 2011
|
||||
|
||||
Add probe for server readiness.
|
||||
|
||||
When a Redis server starts up, it might take a while to load the dataset into memory.
|
||||
During this time, the server will accept connections, but will return errors for all non-INFO
|
||||
commands. Now node_redis will send an INFO command whenever it connects to a server.
|
||||
If the info command indicates that the server is not ready, the client will keep trying until
|
||||
the server is ready. Once it is ready, the client will emit a "ready" event as well as the
|
||||
"connect" event. The client will queue up all commands sent before the server is ready, just
|
||||
like it did before. When the server is ready, all offline/non-ready commands will be replayed.
|
||||
This should be backward compatible with previous versions.
|
||||
|
||||
To disable this ready check behavior, set `options.no_ready_check` when creating the client.
|
||||
|
||||
As a side effect of this change, the key/val params from the info command are available as
|
||||
`client.server_options`. Further, the version string is decomposed into individual elements
|
||||
in `client.server_options.versions`.
|
||||
|
||||
## v0.5.4 - February 11, 2011
|
||||
|
||||
Fix excess memory consumption from Queue backing store.
|
||||
|
||||
Thanks to Gustaf Sjöberg.
|
||||
|
||||
## v0.5.3 - February 5, 2011
|
||||
|
||||
Fix multi/exec error reply callback logic.
|
||||
|
||||
Thanks to Stella Laurenzo.
|
||||
|
||||
## v0.5.2 - January 18, 2011
|
||||
|
||||
Fix bug where unhandled error replies confuse the parser.
|
||||
|
||||
## v0.5.1 - January 18, 2011
|
||||
|
||||
Fix bug where subscribe commands would not handle redis-server startup error properly.
|
||||
|
||||
## v0.5.0 - December 29, 2010
|
||||
|
||||
Some bug fixes:
|
||||
|
||||
- An important bug fix in reconnection logic. Previously, reply callbacks would be invoked twice after
|
||||
a reconnect.
|
||||
- Changed error callback argument to be an actual Error object.
|
||||
|
||||
New feature:
|
||||
|
||||
- Add friendly syntax for HMSET using an object.
|
||||
|
||||
## v0.4.1 - December 8, 2010
|
||||
|
||||
Remove warning about missing hiredis. You probably do want it though.
|
||||
|
||||
## v0.4.0 - December 5, 2010
|
||||
|
||||
Support for multiple response parsers and hiredis C library from Pieter Noordhuis.
|
||||
Return Strings instead of Buffers by default.
|
||||
Empty nested mb reply bug fix.
|
||||
|
||||
## v0.3.9 - November 30, 2010
|
||||
|
||||
Fix parser bug on failed EXECs.
|
||||
|
||||
## v0.3.8 - November 10, 2010
|
||||
|
||||
Fix for null MULTI response when WATCH condition fails.
|
||||
|
||||
## v0.3.7 - November 9, 2010
|
||||
|
||||
Add "drain" and "idle" events.
|
||||
|
||||
## v0.3.6 - November 3, 2010
|
||||
|
||||
Add all known Redis commands from Redis master, even ones that are coming in 2.2 and beyond.
|
||||
|
||||
Send a friendlier "error" event message on stream errors like connection refused / reset.
|
||||
|
||||
## v0.3.5 - October 21, 2010
|
||||
|
||||
A few bug fixes.
|
||||
|
||||
- Fixed bug with `nil` multi-bulk reply lengths that showed up with `BLPOP` timeouts.
|
||||
- Only emit `end` once when connection goes away.
|
||||
- Fixed bug in `test.js` where driver finished before all tests completed.
|
||||
|
||||
## unversioned wasteland
|
||||
|
||||
See the git history for what happened before.
|
24
packages/client/LICENSE
Normal file
24
packages/client/LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-present Node Redis contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
294
packages/client/README.md
Normal file
294
packages/client/README.md
Normal file
@@ -0,0 +1,294 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/redis/node-redis">
|
||||
<img width="128" src="https://static.invertase.io/assets/node_redis_logo.png" />
|
||||
</a>
|
||||
<h2 align="center">Node Redis</h2>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://coveralls.io/github/redis/node-redis">
|
||||
<img src="https://coveralls.io/repos/github/redis/node-redis/badge.svg" alt="Coverage Status"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/redis/v/next">
|
||||
<img src="https://img.shields.io/npm/dm/redis.svg" alt="Downloads"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/redis/v/next">
|
||||
<img src="https://img.shields.io/npm/v/redis/next.svg" alt="Version"/>
|
||||
</a>
|
||||
<a href="https://discord.gg/XMMVgxUm">
|
||||
<img src="https://img.shields.io/discord/697882427875393627" alt="Chat"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install redis@next
|
||||
```
|
||||
|
||||
> :warning: The new interface is clean and cool, but if you have an existing code base, you'll want to read the [migration guide](./docs/v3-to-v4.md).
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example
|
||||
|
||||
```typescript
|
||||
import { createClient } from 'redis';
|
||||
|
||||
(async () => {
|
||||
const client = createClient();
|
||||
|
||||
client.on('error', (err) => console.log('Redis Client Error', err));
|
||||
|
||||
await client.connect();
|
||||
|
||||
await client.set('key', 'value');
|
||||
const value = await client.get('key');
|
||||
})();
|
||||
```
|
||||
|
||||
The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in the format `redis[s]://[[username][:password]@][host][:port][/db-number]`:
|
||||
|
||||
```typescript
|
||||
createClient({
|
||||
url: 'redis://alice:foobared@awesome.redis.server:6380'
|
||||
});
|
||||
```
|
||||
|
||||
You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in the [client configuration guide](./docs/client-configuration.md).
|
||||
|
||||
### Redis Commands
|
||||
|
||||
There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`, etc.):
|
||||
|
||||
```typescript
|
||||
// raw Redis commands
|
||||
await client.HSET('key', 'field', 'value');
|
||||
await client.HGETALL('key');
|
||||
|
||||
// friendly JavaScript commands
|
||||
await client.hSet('key', 'field', 'value');
|
||||
await client.hGetAll('key');
|
||||
```
|
||||
|
||||
Modifiers to commands are specified using a JavaScript object:
|
||||
|
||||
```typescript
|
||||
await client.set('key', 'value', {
|
||||
EX: 10,
|
||||
NX: true
|
||||
});
|
||||
```
|
||||
|
||||
Replies will be transformed into useful data structures:
|
||||
|
||||
```typescript
|
||||
await client.hGetAll('key'); // { field1: 'value1', field2: 'value2' }
|
||||
await client.hVals('key'); // ['value1', 'value2']
|
||||
```
|
||||
|
||||
### Unsupported Redis Commands
|
||||
|
||||
If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`:
|
||||
|
||||
```typescript
|
||||
await client.sendCommand(['SET', 'key', 'value', 'NX']); // 'OK'
|
||||
|
||||
await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2']
|
||||
```
|
||||
|
||||
### Transactions (Multi/Exec)
|
||||
|
||||
Start a [transaction](https://redis.io/topics/transactions) by calling `.multi()`, then chaining your commands. When you're done, call `.exec()` and you'll get an array back with your results:
|
||||
|
||||
```typescript
|
||||
await client.set('another-key', 'another-value');
|
||||
|
||||
const [setKeyReply, otherKeyValue] = await client
|
||||
.multi()
|
||||
.set('key', 'value')
|
||||
.get('another-key')
|
||||
.exec(); // ['OK', 'another-value']
|
||||
```
|
||||
|
||||
You can also [watch](https://redis.io/topics/transactions#optimistic-locking-using-check-and-set) keys by calling `.watch()`. Your transaction will abort if any of the watched keys change.
|
||||
|
||||
To dig deeper into transactions, check out the [Isolated Execution Guide](./docs/isolated-execution.md).
|
||||
|
||||
### Blocking Commands
|
||||
|
||||
Any command can be run on a new connection by specifying the `isolated` option. The newly created connection is closed when the command's `Promise` is fulfilled.
|
||||
|
||||
This pattern works especially well for blocking commands—such as `BLPOP` and `BLMOVE`:
|
||||
|
||||
```typescript
|
||||
import { commandOptions } from 'redis';
|
||||
|
||||
const blPopPromise = client.blPop(commandOptions({ isolated: true }), 'key', 0);
|
||||
|
||||
await client.lPush('key', ['1', '2']);
|
||||
|
||||
await blPopPromise; // '2'
|
||||
```
|
||||
|
||||
To learn more about isolated execution, check out the [guide](./docs/isolated-execution.md).
|
||||
|
||||
### Pub/Sub
|
||||
|
||||
Subscribing to a channel requires a dedicated stand-alone connection. You can easily get one by `.duplicate()`ing an existing Redis connection.
|
||||
|
||||
```typescript
|
||||
const subscriber = client.duplicate();
|
||||
|
||||
await subscriber.connect();
|
||||
```
|
||||
|
||||
Once you have one, simply subscribe and unsubscribe as needed:
|
||||
|
||||
```typescript
|
||||
await subscriber.subscribe('channel', (message) => {
|
||||
console.log(message); // 'message'
|
||||
});
|
||||
|
||||
await subscriber.pSubscribe('channe*', (message, channel) => {
|
||||
console.log(message, channel); // 'message', 'channel'
|
||||
});
|
||||
|
||||
await subscriber.unsubscribe('channel');
|
||||
|
||||
await subscriber.pUnsubscribe('channe*');
|
||||
```
|
||||
|
||||
Publish a message on a channel:
|
||||
|
||||
```typescript
|
||||
await publisher.publish('channel', 'message');
|
||||
```
|
||||
|
||||
### Scan Iterator
|
||||
|
||||
[`SCAN`](https://redis.io/commands/scan) results can be looped over using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator):
|
||||
|
||||
```typescript
|
||||
for await (const key of client.scanIterator()) {
|
||||
// use the key!
|
||||
await client.get(key);
|
||||
}
|
||||
```
|
||||
|
||||
This works with `HSCAN`, `SSCAN`, and `ZSCAN` too:
|
||||
|
||||
```typescript
|
||||
for await (const { field, value } of client.hScanIterator('hash')) {}
|
||||
for await (const member of client.sScanIterator('set')) {}
|
||||
for await (const { score, member } of client.zScanIterator('sorted-set')) {}
|
||||
```
|
||||
|
||||
You can override the default options by providing a configuration object:
|
||||
|
||||
```typescript
|
||||
client.scanIterator({
|
||||
TYPE: 'string', // `SCAN` only
|
||||
MATCH: 'patter*',
|
||||
COUNT: 100
|
||||
});
|
||||
```
|
||||
|
||||
### Lua Scripts
|
||||
|
||||
Define new functions using [Lua scripts](https://redis.io/commands/eval) which execute on the Redis server:
|
||||
|
||||
```typescript
|
||||
import { createClient, defineScript } from 'redis';
|
||||
|
||||
(async () => {
|
||||
const client = createClient({
|
||||
scripts: {
|
||||
add: defineScript({
|
||||
NUMBER_OF_KEYS: 1,
|
||||
SCRIPT:
|
||||
'local val = redis.pcall("GET", KEYS[1]);' +
|
||||
'return val + ARGV[1];',
|
||||
transformArguments(key: string, toAdd: number): Array<string> {
|
||||
return [key, toAdd.toString()];
|
||||
},
|
||||
transformReply(reply: number): number {
|
||||
return reply;
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
await client.set('key', '1');
|
||||
await client.add('key', 2); // 3
|
||||
})();
|
||||
```
|
||||
|
||||
### Disconnecting
|
||||
|
||||
There are two functions that disconnect a client from the Redis server. In most scenarios you should use `.quit()` to ensure that pending commands are sent to Redis before closing a connection.
|
||||
|
||||
#### `.QUIT()`/`.quit()`
|
||||
|
||||
Gracefully close a client's connection to Redis, by sending the [`QUIT`](https://redis.io/commands/quit) command to the server. Before quitting, the client executes any remaining commands in its queue, and will receive replies from Redis for each of them.
|
||||
|
||||
```typescript
|
||||
const [ping, get, quit] = await Promise.all([
|
||||
client.ping(),
|
||||
client.get('key'),
|
||||
client.quit()
|
||||
]); // ['PONG', null, 'OK']
|
||||
|
||||
try {
|
||||
await client.get('key');
|
||||
} catch (err) {
|
||||
// ClosedClient Error
|
||||
}
|
||||
```
|
||||
|
||||
#### `.disconnect()`
|
||||
|
||||
Forcibly close a client's connection to Redis immediately. Calling `disconnect` will not send further pending commands to the Redis server, or wait for or parse outstanding responses.
|
||||
|
||||
```typescript
|
||||
await client.disconnect();
|
||||
```
|
||||
|
||||
### Auto-Pipelining
|
||||
|
||||
Node Redis will automatically pipeline requests that are made during the same "tick".
|
||||
|
||||
```typescript
|
||||
client.set('Tm9kZSBSZWRpcw==', 'users:1');
|
||||
client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==');
|
||||
```
|
||||
|
||||
Of course, if you don't do something with your Promises you're certain to get [unhandled Promise exceptions](https://nodejs.org/api/process.html#process_event_unhandledrejection). To take advantage of auto-pipelining and handle your Promises, use `Promise.all()`.
|
||||
|
||||
```typescript
|
||||
await Promise.all([
|
||||
client.set('Tm9kZSBSZWRpcw==', 'users:1'),
|
||||
client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==')
|
||||
]);
|
||||
```
|
||||
|
||||
### Clustering
|
||||
|
||||
Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to connect to a Redis Cluster.
|
||||
|
||||
## Supported Redis versions
|
||||
|
||||
Node Redis is supported with the following versions of Redis:
|
||||
|
||||
| Version | Supported |
|
||||
|---------|--------------------|
|
||||
| 6.2.z | :heavy_check_mark: |
|
||||
| 6.0.z | :heavy_check_mark: |
|
||||
| 5.y.z | :heavy_check_mark: |
|
||||
| < 5.0 | :x: |
|
||||
|
||||
> Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support.
|
13
packages/client/docs/FAQ.md
Normal file
13
packages/client/docs/FAQ.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# F.A.Q.
|
||||
|
||||
Nobody has *actually* asked these questions. But, we needed somewhere to put all the important bits and bobs that didn't fit anywhere else. So, here you go!
|
||||
|
||||
## What happens when the network goes down?
|
||||
|
||||
When a socket closed unexpectedly, all the commands that were already sent will reject as they might have been executed on the server. The rest will remain queued in memory until a new socket is established. If the client is closed—either by returning an error from [`reconnectStrategy`](./client-configuration.md#reconnect-strategy) or by manually calling `.disconnect()`—they will be rejected.
|
||||
|
||||
## How are commands batched?
|
||||
|
||||
Commands are pipelined using [`queueMicrotask`](https://nodejs.org/api/globals.html#globals_queuemicrotask_callback).
|
||||
|
||||
If `socket.write()` returns `false`—meaning that ["all or part of the data was queued in user memory"](https://nodejs.org/api/net.html#net_socket_write_data_encoding_callback:~:text=all%20or%20part%20of%20the%20data%20was%20queued%20in%20user%20memory)—the commands will stack in memory until the [`drain`](https://nodejs.org/api/net.html#net_event_drain) event is fired.
|
32
packages/client/docs/client-configuration.md
Normal file
32
packages/client/docs/client-configuration.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# `createClient` configuration
|
||||
|
||||
| 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) |
|
||||
| socket | | Object defining socket connection properties |
|
||||
| socket.host | `'localhost'` | Hostname to connect to |
|
||||
| socket.port | `6379` | Port to connect to |
|
||||
| socket.path | | UNIX Socket to connect to |
|
||||
| socket.connectTimeout | `5000` | The timeout for connecting to the Redis Server (in milliseconds) |
|
||||
| socket.noDelay | `true` | Enable/disable the use of [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
|
||||
| socket.keepAlive | `5000` | Enable/disable the [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality |
|
||||
| socket.tls | | Set to `true` to enable [TLS Configuration](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) |
|
||||
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
|
||||
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
|
||||
| password | | ACL password or the old "--requirepass" password |
|
||||
| database | | Database number to connect to (see [`SELECT`](https://redis.io/commands/select) command) |
|
||||
| modules | | Object defining which [Redis Modules](../../README.md#modules) to include |
|
||||
| scripts | | Object defining Lua Scripts to use with this client (see [Lua Scripts](../README.md#lua-scripts)) |
|
||||
| commandsQueueMaxLength | | Maximum length of the client's internal command queue |
|
||||
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
|
||||
| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](v3-to-v4.md)) |
|
||||
| isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) |
|
||||
|
||||
## Reconnect Strategy
|
||||
|
||||
You can implement a custom reconnect strategy as a function that should:
|
||||
|
||||
- Receives the number of retries attempted so far.
|
||||
- Should return `number | Error`:
|
||||
- `number`: the time in milliseconds to wait before trying to reconnect again.
|
||||
- `Error`: close the client and flush the commands queue.
|
56
packages/client/docs/clustering.md
Normal file
56
packages/client/docs/clustering.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Clustering
|
||||
|
||||
## Basic Example
|
||||
|
||||
Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a regular client instance:
|
||||
|
||||
```typescript
|
||||
import { createCluster } from 'redis';
|
||||
|
||||
(async () => {
|
||||
const cluster = createCluster({
|
||||
rootNodes: [
|
||||
{
|
||||
url: 'redis://10.0.0.1:30001'
|
||||
},
|
||||
{
|
||||
url: 'redis://10.0.0.2:30002'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
cluster.on('error', (err) => console.log('Redis Cluster Error', err));
|
||||
|
||||
await cluster.connect();
|
||||
|
||||
await cluster.set('key', 'value');
|
||||
const value = await cluster.get('key');
|
||||
})();
|
||||
```
|
||||
|
||||
## `createCluster` configuration
|
||||
|
||||
> See the [client configuration](./client-configuration.md) page for the `rootNodes` and `defaults` configuration schemas.
|
||||
|
||||
| Property | Default | Description |
|
||||
|------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| rootNodes | | An array of root nodes that are part of the cluster, which will be used to get the cluster topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster, 3 should be enough to reliably connect and obtain the cluster configuration from the server |
|
||||
| defaults | | The default configuration values for every client in the cluster. Use this for example when specifying an ACL user to connect with |
|
||||
| useReplicas | `false` | When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes |
|
||||
| maxCommandRedirections | `16` | The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors |
|
||||
| modules | | Object defining which [Redis Modules](../../README.md#modules) to include |
|
||||
| scripts | | Object defining Lua Scripts to use with this client (see [Lua Scripts](../README.md#lua-scripts)) |
|
||||
|
||||
## Command Routing
|
||||
|
||||
### Commands that operate on Redis Keys
|
||||
|
||||
Commands such as `GET`, `SET`, etc. will be routed by the first key, for instance `MGET 1 2 3` will be routed by the key `1`.
|
||||
|
||||
### [Server Commands](https://redis.io/commands#server)
|
||||
|
||||
Admin commands such as `MEMORY STATS`, `FLUSHALL`, etc. are not attached to the cluster, and should be executed on a specific node using `.getSlot()` or `.getAllMasters()`.
|
||||
|
||||
### "Forwarded Commands"
|
||||
|
||||
Some commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. The client will send these commands to a random node in order to spread the load across the cluster.
|
67
packages/client/docs/isolated-execution.md
Normal file
67
packages/client/docs/isolated-execution.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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 scences we're using [`generic-pool`](https://www.npmjs.com/package/generic-pool) to provide a pool of connections that can be isolated. Go there to learn more.
|
||||
|
||||
## The Simple Secnario
|
||||
|
||||
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'
|
||||
);
|
||||
```
|
35
packages/client/docs/v3-to-v4.md
Normal file
35
packages/client/docs/v3-to-v4.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# v3 to v4 Migration Guide
|
||||
|
||||
Version 4 of Node Redis is a major refactor. While we have tried to maintain backwards compatibility where possible, several interfaces have changed. Read this guide to understand the differences and how to implement version 4 in your application.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
See the [Change Log](../CHANGELOG.md).
|
||||
|
||||
## Promises
|
||||
|
||||
Node Redis now uses native [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) by default for all functions.
|
||||
|
||||
## Legacy Mode
|
||||
|
||||
Use legacy mode to preserve the backwards compatibility of commands while still getting access to the updated experience:
|
||||
|
||||
```typescript
|
||||
const client = createClient({
|
||||
legacyMode: true
|
||||
});
|
||||
|
||||
// legacy mode
|
||||
client.set('key', 'value', 'NX', (err, reply) => {
|
||||
// ...
|
||||
});
|
||||
|
||||
// version 4 interface is still accessible
|
||||
await client.v4.set('key', 'value', {
|
||||
NX: true
|
||||
});
|
||||
```
|
||||
|
||||
## `createClient`
|
||||
|
||||
The configuration object passed to `createClient` has changed significantly with this release. See the [client configuration guide](./client-configuration.md) for details.
|
75
packages/client/examples/README.md
Normal file
75
packages/client/examples/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Node Redis: Examples
|
||||
|
||||
This folder contains example scripts showing how to use Node Redis in different scenarios.
|
||||
|
||||
| File Name | Description |
|
||||
|-----------------------------|--------------------------------------|
|
||||
| `connect-as-acl-user.js` | Connect to Redis 6 using an ACL user |
|
||||
| `blocking-list-pop.js` | Block until an element is pushed to a list |
|
||||
| `lua-multi-incr.js` | Define a custom lua script that allows you to perform INCRBY on multiple keys |
|
||||
| `command-with-modifiers.js` | Define a script that allows to run a command with several modifiers |
|
||||
| `set-scan.js` | An example script that shows how to use the SSCAN iterator functionality |
|
||||
|
||||
## Contributing
|
||||
|
||||
We'd love to see more examples here. If you have an idea that you'd like to see included here, submit a Pull Request and we'll be sure to review it! Don't forget to check out our [contributing guide](../CONTRIBUTING.md).
|
||||
|
||||
## Setup
|
||||
|
||||
To set up the examples folder so that you can run an example / develop one of your own:
|
||||
|
||||
```
|
||||
$ git clone https://github.com/redis/node-redis.git
|
||||
$ cd node-redis
|
||||
$ npm install && npm run build
|
||||
$ cd examples
|
||||
$ npm install
|
||||
```
|
||||
|
||||
### Coding Guidelines for Examples
|
||||
|
||||
When adding a new example, please follow these guidelines:
|
||||
|
||||
* Add your code in a single JavaScript or TypeScript file per example, directly in the `examples` folder
|
||||
* Do not introduce other dependencies in your example
|
||||
* Give your `.js` file a meaningful name using `-` separators e.g. `adding-to-a-stream.js` / `adding-to-a-stream.ts`
|
||||
* Indent your code using 2 spaces
|
||||
* Use the single line `//` comment style and comment your code
|
||||
* Add a comment at the top of your `.js` / `.ts` file describing what your example does
|
||||
* Add a comment at the top of your `.js` / `.ts` file describing any Redis commands that need to be run to set up data for your example (try and keep this minimal)
|
||||
* Use semicolons
|
||||
* Use `async` and `await`
|
||||
* Use single quotes, `'hello'` not `"hello"`
|
||||
* Place your example code in a single `async` function where possible, named according to the file name e.g. `add-to-stream.js` would contain `const addtoStream = async () => { ... };`, and call this function at the end of the file e.g. `addToStream();`
|
||||
* Unless your example requires a connection string, assume Redis is on the default localhost port 6379 with no password
|
||||
* Use meaningful example data, let's not use `foo`, `bar`, `baz` etc!
|
||||
* Leave an empty line at the end of your `.js` file
|
||||
* Update this `README.md` file to add your example to the table
|
||||
|
||||
Use [connect-as-acl-user.js](connect-as-acl-user.js) as a guide to develop a well formatted example script.
|
||||
|
||||
### Example Template
|
||||
|
||||
Here's a starter template for adding a new example, imagine this is stored in `do-something.js`:
|
||||
|
||||
```javascript
|
||||
// This comment should describe what the example does
|
||||
// and can extend to multiple lines.
|
||||
|
||||
// Set up the data in redis-cli using these commands:
|
||||
// <add your command(s) here, one per line>
|
||||
|
||||
import { createClient } from 'redis';
|
||||
|
||||
async function doSomething() {
|
||||
const client = createClient();
|
||||
|
||||
await client.connect();
|
||||
|
||||
// Add your example code here...
|
||||
|
||||
await client.quit();
|
||||
}
|
||||
|
||||
doSomething();
|
||||
```
|
32
packages/client/examples/blocking-list-pop.js
Normal file
32
packages/client/examples/blocking-list-pop.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// This example shows how to use the blocking LPUSH command.
|
||||
|
||||
// This code shows how to run with isolation the blPop Command to block the script while waiting for a value to be pushed to the list.
|
||||
// The script will be blocked until the LPUSH command is executed.
|
||||
// After which we log the list and quit the client.
|
||||
|
||||
import { createClient, commandOptions } from 'redis';
|
||||
|
||||
async function blockingListPop() {
|
||||
const client = createClient();
|
||||
|
||||
await client.connect();
|
||||
|
||||
const keyName = 'keyName';
|
||||
|
||||
const blpopPromise = client.blPop(
|
||||
commandOptions({ isolated: true }),
|
||||
keyName,
|
||||
0
|
||||
);
|
||||
|
||||
await client.lPush(keyName, 'value');
|
||||
|
||||
await blpopPromise;
|
||||
|
||||
console.log('blpopPromise resolved');
|
||||
console.log(keyName);
|
||||
|
||||
await client.quit();
|
||||
}
|
||||
|
||||
blockingListPop();
|
32
packages/client/examples/command-with-modifiers.js
Normal file
32
packages/client/examples/command-with-modifiers.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// Define a custom script that shows example of SET command
|
||||
// with several modifiers.
|
||||
|
||||
import { createClient } from 'redis';
|
||||
|
||||
async function commandWithModifiers() {
|
||||
const client = createClient();
|
||||
|
||||
await client.connect();
|
||||
await client.del('mykey');
|
||||
|
||||
let result = await client.set('mykey', 'myvalue', {
|
||||
EX: 60,
|
||||
GET: true
|
||||
}
|
||||
);
|
||||
|
||||
console.log(result); //nil
|
||||
|
||||
result = await client.set('mykey', 'newvalue', {
|
||||
EX: 60,
|
||||
GET: true
|
||||
}
|
||||
);
|
||||
|
||||
console.log(result); //myvalue
|
||||
|
||||
await client.quit();
|
||||
}
|
||||
|
||||
commandWithModifiers();
|
||||
|
30
packages/client/examples/connect-as-acl-user.js
Normal file
30
packages/client/examples/connect-as-acl-user.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Connect to Redis 6.x as an ACL user. Attempt to run a command
|
||||
// that the user is allowed to execute, and a command that the
|
||||
// user is not allowed to execute.
|
||||
|
||||
// Create the test user in redis-cli with this command:
|
||||
// acl setuser testuser on >testpassword +ping
|
||||
|
||||
import { createClient } from 'redis';
|
||||
|
||||
async function connectWithACLUser() {
|
||||
const client = createClient({
|
||||
url: 'redis://testuser:testpassword@127.0.0.1:6379'
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
// Returns PONG
|
||||
console.log(`Response from PING command: ${await client.ping()}`);
|
||||
|
||||
try {
|
||||
// This will error as this user is not allowed to run this command...
|
||||
console.log(`Response from GET command: ${await client.get('somekey')}`);
|
||||
} catch (e) {
|
||||
console.log(`GET command failed: ${e.message}`);
|
||||
}
|
||||
|
||||
await client.quit();
|
||||
}
|
||||
|
||||
connectWithACLUser();
|
31
packages/client/examples/lua-multi-incr.js
Normal file
31
packages/client/examples/lua-multi-incr.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Define a custome lua script that accepts two keys and an amount to
|
||||
// increment each of them by
|
||||
|
||||
import { createClient, defineScript } from 'redis';
|
||||
|
||||
async function luaMultiIncr() {
|
||||
const client = createClient({
|
||||
scripts: {
|
||||
mincr: defineScript({
|
||||
NUMBER_OF_KEYS: 2,
|
||||
SCRIPT:
|
||||
'return {' +
|
||||
'redis.pcall("INCRBY", KEYS[1], ARGV[1]),' +
|
||||
'redis.pcall("INCRBY", KEYS[2], ARGV[1])' +
|
||||
'}',
|
||||
transformArguments(key1, key2, increment) {
|
||||
return [key1, key2, increment.toString()];
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
await client.set('mykey', '5');
|
||||
console.log(await client.mincr('mykey', 'myotherkey', 10)); // [ 15, 10 ]
|
||||
|
||||
await client.quit();
|
||||
}
|
||||
|
||||
luaMultiIncr();
|
87
packages/client/examples/package-lock.json
generated
Normal file
87
packages/client/examples/package-lock.json
generated
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "node-redis-examples",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "node-redis-examples",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"redis": "../"
|
||||
}
|
||||
},
|
||||
"..": {
|
||||
"version": "4.0.0-rc.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.0",
|
||||
"generic-pool": "3.8.2",
|
||||
"redis-parser": "3.0.0",
|
||||
"yallist": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@tsconfig/node12": "^1.0.9",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^16.11.1",
|
||||
"@types/sinon": "^10.0.4",
|
||||
"@types/which": "^2.0.1",
|
||||
"@types/yallist": "^4.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.1.0",
|
||||
"@typescript-eslint/parser": "^5.1.0",
|
||||
"eslint": "^8.0.1",
|
||||
"mocha": "^9.1.3",
|
||||
"nyc": "^15.1.0",
|
||||
"release-it": "^14.11.6",
|
||||
"sinon": "^11.1.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"ts-node": "^10.3.0",
|
||||
"typedoc": "^0.22.6",
|
||||
"typedoc-github-wiki-theme": "^0.6.0",
|
||||
"typedoc-plugin-markdown": "^3.11.3",
|
||||
"typescript": "^4.4.4",
|
||||
"which": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"resolved": "..",
|
||||
"link": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"redis": {
|
||||
"version": "file:..",
|
||||
"requires": {
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@tsconfig/node12": "^1.0.9",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^16.11.1",
|
||||
"@types/sinon": "^10.0.4",
|
||||
"@types/which": "^2.0.1",
|
||||
"@types/yallist": "^4.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.1.0",
|
||||
"@typescript-eslint/parser": "^5.1.0",
|
||||
"cluster-key-slot": "1.1.0",
|
||||
"eslint": "^8.0.1",
|
||||
"generic-pool": "3.8.2",
|
||||
"mocha": "^9.1.3",
|
||||
"nyc": "^15.1.0",
|
||||
"redis-parser": "3.0.0",
|
||||
"release-it": "^14.11.6",
|
||||
"sinon": "^11.1.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"ts-node": "^10.3.0",
|
||||
"typedoc": "^0.22.6",
|
||||
"typedoc-github-wiki-theme": "^0.6.0",
|
||||
"typedoc-plugin-markdown": "^3.11.3",
|
||||
"typescript": "^4.4.4",
|
||||
"which": "^2.0.2",
|
||||
"yallist": "4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
packages/client/examples/package.json
Normal file
9
packages/client/examples/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "node-redis-examples",
|
||||
"version": "1.0.0",
|
||||
"description": "node-redis 4 example script",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
|
19
packages/client/examples/set-scan.js
Normal file
19
packages/client/examples/set-scan.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// An example script that shows how to use the SSCAN iterator functionality to retrieve the contents of a Redis set.
|
||||
// Create the set in redis-cli with this command:
|
||||
// sadd setName a b c d e f g h i j k l m n o p q
|
||||
|
||||
import { createClient } from 'redis';
|
||||
|
||||
async function setScan() {
|
||||
const client = createClient();
|
||||
await client.connect();
|
||||
|
||||
const setName = 'setName';
|
||||
for await (const member of client.sScanIterator(setName)) {
|
||||
console.log(member);
|
||||
}
|
||||
|
||||
await client.quit();
|
||||
}
|
||||
|
||||
setScan();
|
10
packages/client/index.ts
Normal file
10
packages/client/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import RedisClient from './lib/client';
|
||||
import RedisCluster from './lib/cluster';
|
||||
|
||||
export const createClient = RedisClient.create;
|
||||
|
||||
export const commandOptions = RedisClient.commandOptions;
|
||||
|
||||
export const createCluster = RedisCluster.create;
|
||||
|
||||
export { defineScript } from './lib/lua-script';
|
298
packages/client/lib/client/commands-queue.ts
Normal file
298
packages/client/lib/client/commands-queue.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import LinkedList from 'yallist';
|
||||
import RedisParser from 'redis-parser';
|
||||
import { AbortError } from '../errors';
|
||||
import { RedisCommandArguments, RedisCommandRawReply } from '../commands';
|
||||
|
||||
export interface QueueCommandOptions {
|
||||
asap?: boolean;
|
||||
chainId?: symbol;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
interface CommandWaitingToBeSent extends CommandWaitingForReply {
|
||||
args: RedisCommandArguments;
|
||||
chainId?: symbol;
|
||||
abort?: {
|
||||
signal: AbortSignal;
|
||||
listener(): void;
|
||||
};
|
||||
}
|
||||
|
||||
interface CommandWaitingForReply {
|
||||
resolve(reply?: unknown): void;
|
||||
reject(err: Error): void;
|
||||
channelsCounter?: number;
|
||||
bufferMode?: boolean;
|
||||
}
|
||||
|
||||
export enum PubSubSubscribeCommands {
|
||||
SUBSCRIBE = 'SUBSCRIBE',
|
||||
PSUBSCRIBE = 'PSUBSCRIBE'
|
||||
}
|
||||
|
||||
export enum PubSubUnsubscribeCommands {
|
||||
UNSUBSCRIBE = 'UNSUBSCRIBE',
|
||||
PUNSUBSCRIBE = 'PUNSUBSCRIBE'
|
||||
}
|
||||
|
||||
export type PubSubListener = (message: string, channel: string) => unknown;
|
||||
|
||||
export type PubSubListenersMap = Map<string, Set<PubSubListener>>;
|
||||
|
||||
export default class RedisCommandsQueue {
|
||||
static #flushQueue<T extends CommandWaitingForReply>(queue: LinkedList<T>, err: Error): void {
|
||||
while (queue.length) {
|
||||
queue.shift()!.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
static #emitPubSubMessage(listeners: Set<PubSubListener>, message: string, channel: string): void {
|
||||
for (const listener of listeners) {
|
||||
listener(message, channel);
|
||||
}
|
||||
}
|
||||
|
||||
readonly #maxLength: number | null | undefined;
|
||||
|
||||
readonly #waitingToBeSent = new LinkedList<CommandWaitingToBeSent>();
|
||||
|
||||
readonly #waitingForReply = new LinkedList<CommandWaitingForReply>();
|
||||
|
||||
readonly #pubSubState = {
|
||||
subscribing: 0,
|
||||
subscribed: 0,
|
||||
unsubscribing: 0
|
||||
};
|
||||
|
||||
readonly #pubSubListeners = {
|
||||
channels: <PubSubListenersMap>new Map(),
|
||||
patterns: <PubSubListenersMap>new Map()
|
||||
};
|
||||
|
||||
readonly #parser = new RedisParser({
|
||||
returnReply: (reply: unknown) => {
|
||||
if ((this.#pubSubState.subscribing || this.#pubSubState.subscribed) && Array.isArray(reply)) {
|
||||
switch (reply[0]) {
|
||||
case 'message':
|
||||
return RedisCommandsQueue.#emitPubSubMessage(
|
||||
this.#pubSubListeners.channels.get(reply[1])!,
|
||||
reply[2],
|
||||
reply[1]
|
||||
);
|
||||
|
||||
case 'pmessage':
|
||||
return RedisCommandsQueue.#emitPubSubMessage(
|
||||
this.#pubSubListeners.patterns.get(reply[1])!,
|
||||
reply[3],
|
||||
reply[2]
|
||||
);
|
||||
|
||||
case 'subscribe':
|
||||
case 'psubscribe':
|
||||
if (--this.#waitingForReply.head!.value.channelsCounter! === 0) {
|
||||
this.#shiftWaitingForReply().resolve();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.#shiftWaitingForReply().resolve(reply);
|
||||
},
|
||||
returnError: (err: Error) => this.#shiftWaitingForReply().reject(err)
|
||||
});
|
||||
|
||||
#chainInExecution: symbol | undefined;
|
||||
|
||||
constructor(maxLength: number | null | undefined) {
|
||||
this.#maxLength = maxLength;
|
||||
}
|
||||
|
||||
addCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: QueueCommandOptions, bufferMode?: boolean): Promise<T> {
|
||||
if (this.#pubSubState.subscribing || this.#pubSubState.subscribed) {
|
||||
return Promise.reject(new Error('Cannot send commands in PubSub mode'));
|
||||
} else if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
|
||||
return Promise.reject(new Error('The queue is full'));
|
||||
} else if (options?.signal?.aborted) {
|
||||
return Promise.reject(new AbortError());
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const node = new LinkedList.Node<CommandWaitingToBeSent>({
|
||||
args,
|
||||
chainId: options?.chainId,
|
||||
bufferMode,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
|
||||
if (options?.signal) {
|
||||
const listener = () => {
|
||||
this.#waitingToBeSent.removeNode(node);
|
||||
node.value.reject(new AbortError());
|
||||
};
|
||||
|
||||
node.value.abort = {
|
||||
signal: options.signal,
|
||||
listener
|
||||
};
|
||||
// AbortSignal type is incorrent
|
||||
(options.signal as any).addEventListener('abort', listener, {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
|
||||
if (options?.asap) {
|
||||
this.#waitingToBeSent.unshiftNode(node);
|
||||
} else {
|
||||
this.#waitingToBeSent.pushNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(command: PubSubSubscribeCommands, channels: string | Array<string>, listener: PubSubListener): Promise<void> {
|
||||
const channelsToSubscribe: Array<string> = [],
|
||||
listeners = command === PubSubSubscribeCommands.SUBSCRIBE ? this.#pubSubListeners.channels : this.#pubSubListeners.patterns;
|
||||
for (const channel of (Array.isArray(channels) ? channels : [channels])) {
|
||||
if (listeners.has(channel)) {
|
||||
listeners.get(channel)!.add(listener);
|
||||
continue;
|
||||
}
|
||||
|
||||
listeners.set(channel, new Set([listener]));
|
||||
channelsToSubscribe.push(channel);
|
||||
}
|
||||
|
||||
if (!channelsToSubscribe.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.#pushPubSubCommand(command, channelsToSubscribe);
|
||||
}
|
||||
|
||||
unsubscribe(command: PubSubUnsubscribeCommands, channels?: string | Array<string>, listener?: PubSubListener): Promise<void> {
|
||||
const listeners = command === PubSubUnsubscribeCommands.UNSUBSCRIBE ? this.#pubSubListeners.channels : this.#pubSubListeners.patterns;
|
||||
if (!channels) {
|
||||
const size = listeners.size;
|
||||
listeners.clear();
|
||||
return this.#pushPubSubCommand(command, size);
|
||||
}
|
||||
|
||||
const channelsToUnsubscribe = [];
|
||||
for (const channel of (Array.isArray(channels) ? channels : [channels])) {
|
||||
const set = listeners.get(channel);
|
||||
if (!set) continue;
|
||||
|
||||
let shouldUnsubscribe = !listener;
|
||||
if (listener) {
|
||||
set.delete(listener);
|
||||
shouldUnsubscribe = set.size === 0;
|
||||
}
|
||||
|
||||
if (shouldUnsubscribe) {
|
||||
channelsToUnsubscribe.push(channel);
|
||||
listeners.delete(channel);
|
||||
}
|
||||
}
|
||||
|
||||
if (!channelsToUnsubscribe.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.#pushPubSubCommand(command, channelsToUnsubscribe);
|
||||
}
|
||||
|
||||
#pushPubSubCommand(command: PubSubSubscribeCommands | PubSubUnsubscribeCommands, channels: number | Array<string>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const isSubscribe = command === PubSubSubscribeCommands.SUBSCRIBE || command === PubSubSubscribeCommands.PSUBSCRIBE,
|
||||
inProgressKey = isSubscribe ? 'subscribing' : 'unsubscribing',
|
||||
commandArgs: Array<string> = [command];
|
||||
|
||||
let channelsCounter: number;
|
||||
if (typeof channels === 'number') { // unsubscribe only
|
||||
channelsCounter = channels;
|
||||
} else {
|
||||
commandArgs.push(...channels);
|
||||
channelsCounter = channels.length;
|
||||
}
|
||||
|
||||
this.#pubSubState[inProgressKey] += channelsCounter;
|
||||
|
||||
this.#waitingToBeSent.push({
|
||||
args: commandArgs,
|
||||
channelsCounter,
|
||||
resolve: () => {
|
||||
this.#pubSubState[inProgressKey] -= channelsCounter;
|
||||
this.#pubSubState.subscribed += channelsCounter * (isSubscribe ? 1 : -1);
|
||||
resolve();
|
||||
},
|
||||
reject: () => {
|
||||
this.#pubSubState[inProgressKey] -= channelsCounter;
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resubscribe(): Promise<any> | undefined {
|
||||
if (!this.#pubSubState.subscribed && !this.#pubSubState.subscribing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#pubSubState.subscribed = this.#pubSubState.subscribing = 0;
|
||||
|
||||
// TODO: acl error on one channel/pattern will reject the whole command
|
||||
return Promise.all([
|
||||
this.#pushPubSubCommand(PubSubSubscribeCommands.SUBSCRIBE, [...this.#pubSubListeners.channels.keys()]),
|
||||
this.#pushPubSubCommand(PubSubSubscribeCommands.PSUBSCRIBE, [...this.#pubSubListeners.patterns.keys()])
|
||||
]);
|
||||
}
|
||||
|
||||
getCommandToSend(): RedisCommandArguments | undefined {
|
||||
const toSend = this.#waitingToBeSent.shift();
|
||||
|
||||
if (toSend) {
|
||||
this.#waitingForReply.push({
|
||||
resolve: toSend.resolve,
|
||||
reject: toSend.reject,
|
||||
channelsCounter: toSend.channelsCounter,
|
||||
bufferMode: toSend.bufferMode
|
||||
});
|
||||
}
|
||||
|
||||
this.#chainInExecution = toSend?.chainId;
|
||||
|
||||
return toSend?.args;
|
||||
}
|
||||
|
||||
parseResponse(data: Buffer): void {
|
||||
this.#parser.setReturnBuffers(!!this.#waitingForReply.head?.value.bufferMode);
|
||||
this.#parser.execute(data);
|
||||
}
|
||||
|
||||
#shiftWaitingForReply(): CommandWaitingForReply {
|
||||
if (!this.#waitingForReply.length) {
|
||||
throw new Error('Got an unexpected reply from Redis');
|
||||
}
|
||||
|
||||
return this.#waitingForReply.shift()!;
|
||||
}
|
||||
|
||||
flushWaitingForReply(err: Error): void {
|
||||
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
|
||||
|
||||
if (!this.#chainInExecution) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) {
|
||||
this.#waitingToBeSent.shift();
|
||||
}
|
||||
|
||||
this.#chainInExecution = undefined;
|
||||
}
|
||||
|
||||
flushAll(err: Error): void {
|
||||
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
|
||||
RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err);
|
||||
}
|
||||
}
|
233
packages/client/lib/client/commands.ts
Normal file
233
packages/client/lib/client/commands.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import CLUSTER_COMMANDS from '../cluster/commands';
|
||||
import * as ACL_CAT from '../commands/ACL_CAT';
|
||||
import * as ACL_DELUSER from '../commands/ACL_DELUSER';
|
||||
import * as ACL_GENPASS from '../commands/ACL_GENPASS';
|
||||
import * as ACL_GETUSER from '../commands/ACL_GETUSER';
|
||||
import * as ACL_LIST from '../commands/ACL_LIST';
|
||||
import * as ACL_LOAD from '../commands/ACL_LOAD';
|
||||
import * as ACL_LOG_RESET from '../commands/ACL_LOG_RESET';
|
||||
import * as ACL_LOG from '../commands/ACL_LOG';
|
||||
import * as ACL_SAVE from '../commands/ACL_SAVE';
|
||||
import * as ACL_SETUSER from '../commands/ACL_SETUSER';
|
||||
import * as ACL_USERS from '../commands/ACL_USERS';
|
||||
import * as ACL_WHOAMI from '../commands/ACL_WHOAMI';
|
||||
import * as ASKING from '../commands/ASKING';
|
||||
import * as AUTH from '../commands/AUTH';
|
||||
import * as BGREWRITEAOF from '../commands/BGREWRITEAOF';
|
||||
import * as BGSAVE from '../commands/BGSAVE';
|
||||
import * as CLIENT_ID from '../commands/CLIENT_ID';
|
||||
import * as CLIENT_INFO from '../commands/CLIENT_INFO';
|
||||
import * as CLUSTER_ADDSLOTS from '../commands/CLUSTER_ADDSLOTS';
|
||||
import * as CLUSTER_FLUSHSLOTS from '../commands/CLUSTER_FLUSHSLOTS';
|
||||
import * as CLUSTER_INFO from '../commands/CLUSTER_INFO';
|
||||
import * as CLUSTER_NODES from '../commands/CLUSTER_NODES';
|
||||
import * as CLUSTER_MEET from '../commands/CLUSTER_MEET';
|
||||
import * as CLUSTER_RESET from '../commands/CLUSTER_RESET';
|
||||
import * as CLUSTER_SETSLOT from '../commands/CLUSTER_SETSLOT';
|
||||
import * as CLUSTER_SLOTS from '../commands/CLUSTER_SLOTS';
|
||||
import * as COMMAND_COUNT from '../commands/COMMAND_COUNT';
|
||||
import * as COMMAND_GETKEYS from '../commands/COMMAND_GETKEYS';
|
||||
import * as COMMAND_INFO from '../commands/COMMAND_INFO';
|
||||
import * as COMMAND from '../commands/COMMAND';
|
||||
import * as CONFIG_GET from '../commands/CONFIG_GET';
|
||||
import * as CONFIG_RESETASTAT from '../commands/CONFIG_RESETSTAT';
|
||||
import * as CONFIG_REWRITE from '../commands/CONFIG_REWRITE';
|
||||
import * as CONFIG_SET from '../commands/CONFIG_SET';
|
||||
import * as DBSIZE from '../commands/DBSIZE';
|
||||
import * as DISCARD from '../commands/DISCARD';
|
||||
import * as ECHO from '../commands/ECHO';
|
||||
import * as FAILOVER from '../commands/FAILOVER';
|
||||
import * as FLUSHALL from '../commands/FLUSHALL';
|
||||
import * as FLUSHDB from '../commands/FLUSHDB';
|
||||
import * as HELLO from '../commands/HELLO';
|
||||
import * as INFO from '../commands/INFO';
|
||||
import * as KEYS from '../commands/KEYS';
|
||||
import * as LASTSAVE from '../commands/LASTSAVE';
|
||||
import * as LOLWUT from '../commands/LOLWUT';
|
||||
import * as MEMOERY_DOCTOR from '../commands/MEMORY_DOCTOR';
|
||||
import * as MEMORY_MALLOC_STATS from '../commands/MEMORY_MALLOC-STATS';
|
||||
import * as MEMORY_PURGE from '../commands/MEMORY_PURGE';
|
||||
import * as MEMORY_STATS from '../commands/MEMORY_STATS';
|
||||
import * as MEMORY_USAGE from '../commands/MEMORY_USAGE';
|
||||
import * as MODULE_LIST from '../commands/MODULE_LIST';
|
||||
import * as MODULE_LOAD from '../commands/MODULE_LOAD';
|
||||
import * as MODULE_UNLOAD from '../commands/MODULE_UNLOAD';
|
||||
import * as MOVE from '../commands/MOVE';
|
||||
import * as PING from '../commands/PING';
|
||||
import * as PUBSUB_CHANNELS from '../commands/PUBSUB_CHANNELS';
|
||||
import * as PUBSUB_NUMPAT from '../commands/PUBSUB_NUMPAT';
|
||||
import * as PUBSUB_NUMSUB from '../commands/PUBSUB_NUMSUB';
|
||||
import * as RANDOMKEY from '../commands/RANDOMKEY';
|
||||
import * as READONLY from '../commands/READONLY';
|
||||
import * as READWRITE from '../commands/READWRITE';
|
||||
import * as REPLICAOF from '../commands/REPLICAOF';
|
||||
import * as RESTORE_ASKING from '../commands/RESTORE-ASKING';
|
||||
import * as ROLE from '../commands/ROLE';
|
||||
import * as SAVE from '../commands/SAVE';
|
||||
import * as SCAN from '../commands/SCAN';
|
||||
import * as SCRIPT_DEBUG from '../commands/SCRIPT_DEBUG';
|
||||
import * as SCRIPT_EXISTS from '../commands/SCRIPT_EXISTS';
|
||||
import * as SCRIPT_FLUSH from '../commands/SCRIPT_FLUSH';
|
||||
import * as SCRIPT_KILL from '../commands/SCRIPT_KILL';
|
||||
import * as SCRIPT_LOAD from '../commands/SCRIPT_LOAD';
|
||||
import * as SHUTDOWN from '../commands/SHUTDOWN';
|
||||
import * as SWAPDB from '../commands/SWAPDB';
|
||||
import * as TIME from '../commands/TIME';
|
||||
import * as UNWATCH from '../commands/UNWATCH';
|
||||
import * as WAIT from '../commands/WAIT';
|
||||
|
||||
export default {
|
||||
...CLUSTER_COMMANDS,
|
||||
ACL_CAT,
|
||||
aclCat: ACL_CAT,
|
||||
ACL_DELUSER,
|
||||
aclDelUser: ACL_DELUSER,
|
||||
ACL_GENPASS,
|
||||
aclGenPass: ACL_GENPASS,
|
||||
ACL_GETUSER,
|
||||
aclGetUser: ACL_GETUSER,
|
||||
ACL_LIST,
|
||||
aclList: ACL_LIST,
|
||||
ACL_LOAD,
|
||||
aclLoad: ACL_LOAD,
|
||||
ACL_LOG_RESET,
|
||||
aclLogReset: ACL_LOG_RESET,
|
||||
ACL_LOG,
|
||||
aclLog: ACL_LOG,
|
||||
ACL_SAVE,
|
||||
aclSave: ACL_SAVE,
|
||||
ACL_SETUSER,
|
||||
aclSetUser: ACL_SETUSER,
|
||||
ACL_USERS,
|
||||
aclUsers: ACL_USERS,
|
||||
ACL_WHOAMI,
|
||||
aclWhoAmI: ACL_WHOAMI,
|
||||
ASKING,
|
||||
asking: ASKING,
|
||||
AUTH,
|
||||
auth: AUTH,
|
||||
BGREWRITEAOF,
|
||||
bgRewriteAof: BGREWRITEAOF,
|
||||
BGSAVE,
|
||||
bgSave: BGSAVE,
|
||||
CLIENT_ID,
|
||||
clientId: CLIENT_ID,
|
||||
CLIENT_INFO,
|
||||
clientInfo: CLIENT_INFO,
|
||||
CLUSTER_ADDSLOTS,
|
||||
clusterAddSlots: CLUSTER_ADDSLOTS,
|
||||
CLUSTER_FLUSHSLOTS,
|
||||
clusterFlushSlots: CLUSTER_FLUSHSLOTS,
|
||||
CLUSTER_INFO,
|
||||
clusterInfo: CLUSTER_INFO,
|
||||
CLUSTER_NODES,
|
||||
clusterNodes: CLUSTER_NODES,
|
||||
CLUSTER_MEET,
|
||||
clusterMeet: CLUSTER_MEET,
|
||||
CLUSTER_RESET,
|
||||
clusterReset: CLUSTER_RESET,
|
||||
CLUSTER_SETSLOT,
|
||||
clusterSetSlot: CLUSTER_SETSLOT,
|
||||
CLUSTER_SLOTS,
|
||||
clusterSlots: CLUSTER_SLOTS,
|
||||
COMMAND_COUNT,
|
||||
commandCount: COMMAND_COUNT,
|
||||
COMMAND_GETKEYS,
|
||||
commandGetKeys: COMMAND_GETKEYS,
|
||||
COMMAND_INFO,
|
||||
commandInfo: COMMAND_INFO,
|
||||
COMMAND,
|
||||
command: COMMAND,
|
||||
CONFIG_GET,
|
||||
configGet: CONFIG_GET,
|
||||
CONFIG_RESETASTAT,
|
||||
configResetStat: CONFIG_RESETASTAT,
|
||||
CONFIG_REWRITE,
|
||||
configRewrite: CONFIG_REWRITE,
|
||||
CONFIG_SET,
|
||||
configSet: CONFIG_SET,
|
||||
DBSIZE,
|
||||
dbSize: DBSIZE,
|
||||
DISCARD,
|
||||
discard: DISCARD,
|
||||
ECHO,
|
||||
echo: ECHO,
|
||||
FAILOVER,
|
||||
failover: FAILOVER,
|
||||
FLUSHALL,
|
||||
flushAll: FLUSHALL,
|
||||
FLUSHDB,
|
||||
flushDb: FLUSHDB,
|
||||
HELLO,
|
||||
hello: HELLO,
|
||||
INFO,
|
||||
info: INFO,
|
||||
KEYS,
|
||||
keys: KEYS,
|
||||
LASTSAVE,
|
||||
lastSave: LASTSAVE,
|
||||
LOLWUT,
|
||||
lolwut: LOLWUT,
|
||||
MEMOERY_DOCTOR,
|
||||
memoryDoctor: MEMOERY_DOCTOR,
|
||||
'MEMORY_MALLOC-STATS': MEMORY_MALLOC_STATS,
|
||||
memoryMallocStats: MEMORY_MALLOC_STATS,
|
||||
MEMORY_PURGE,
|
||||
memoryPurge: MEMORY_PURGE,
|
||||
MEMORY_STATS,
|
||||
memoryStats: MEMORY_STATS,
|
||||
MEMORY_USAGE,
|
||||
memoryUsage: MEMORY_USAGE,
|
||||
MODULE_LIST,
|
||||
moduleList: MODULE_LIST,
|
||||
MODULE_LOAD,
|
||||
moduleLoad: MODULE_LOAD,
|
||||
MODULE_UNLOAD,
|
||||
moduleUnload: MODULE_UNLOAD,
|
||||
MOVE,
|
||||
move: MOVE,
|
||||
PING,
|
||||
ping: PING,
|
||||
PUBSUB_CHANNELS,
|
||||
pubSubChannels: PUBSUB_CHANNELS,
|
||||
PUBSUB_NUMPAT,
|
||||
pubSubNumPat: PUBSUB_NUMPAT,
|
||||
PUBSUB_NUMSUB,
|
||||
pubSubNumSub: PUBSUB_NUMSUB,
|
||||
RANDOMKEY,
|
||||
randomKey: RANDOMKEY,
|
||||
READONLY,
|
||||
readonly: READONLY,
|
||||
READWRITE,
|
||||
readwrite: READWRITE,
|
||||
REPLICAOF,
|
||||
replicaOf: REPLICAOF,
|
||||
'RESTORE-ASKING': RESTORE_ASKING,
|
||||
restoreAsking: RESTORE_ASKING,
|
||||
ROLE,
|
||||
role: ROLE,
|
||||
SAVE,
|
||||
save: SAVE,
|
||||
SCAN,
|
||||
scan: SCAN,
|
||||
SCRIPT_DEBUG,
|
||||
scriptDebug: SCRIPT_DEBUG,
|
||||
SCRIPT_EXISTS,
|
||||
scriptExists: SCRIPT_EXISTS,
|
||||
SCRIPT_FLUSH,
|
||||
scriptFlush: SCRIPT_FLUSH,
|
||||
SCRIPT_KILL,
|
||||
scriptKill: SCRIPT_KILL,
|
||||
SCRIPT_LOAD,
|
||||
scriptLoad: SCRIPT_LOAD,
|
||||
SHUTDOWN,
|
||||
shutdown: SHUTDOWN,
|
||||
SWAPDB,
|
||||
swapDb: SWAPDB,
|
||||
TIME,
|
||||
time: TIME,
|
||||
UNWATCH,
|
||||
unwatch: UNWATCH,
|
||||
WAIT,
|
||||
wait: WAIT
|
||||
};
|
681
packages/client/lib/client/index.spec.ts
Normal file
681
packages/client/lib/client/index.spec.ts
Normal file
@@ -0,0 +1,681 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
|
||||
import RedisClient, { RedisClientType } from '.';
|
||||
import { RedisClientMultiCommandType } from './multi-command';
|
||||
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands';
|
||||
import { AbortError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
|
||||
import { defineScript } from '../lua-script';
|
||||
import { spy } from 'sinon';
|
||||
import { once } from 'events';
|
||||
|
||||
export const SQUARE_SCRIPT = defineScript({
|
||||
NUMBER_OF_KEYS: 0,
|
||||
SCRIPT: 'return ARGV[1] * ARGV[1];',
|
||||
transformArguments(number: number): Array<string> {
|
||||
return [number.toString()];
|
||||
},
|
||||
transformReply(reply: number): number {
|
||||
return reply;
|
||||
}
|
||||
});
|
||||
|
||||
describe('Client', () => {
|
||||
describe('parseURL', () => {
|
||||
it('redis://user:secret@localhost:6379/0', () => {
|
||||
assert.deepEqual(
|
||||
RedisClient.parseURL('redis://user:secret@localhost:6379/0'),
|
||||
{
|
||||
socket: {
|
||||
host: 'localhost',
|
||||
port: 6379
|
||||
},
|
||||
username: 'user',
|
||||
password: 'secret',
|
||||
database: 0
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('rediss://user:secret@localhost:6379/0', () => {
|
||||
assert.deepEqual(
|
||||
RedisClient.parseURL('rediss://user:secret@localhost:6379/0'),
|
||||
{
|
||||
socket: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
tls: true
|
||||
},
|
||||
username: 'user',
|
||||
password: 'secret',
|
||||
database: 0
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Invalid protocol', () => {
|
||||
assert.throws(
|
||||
() => RedisClient.parseURL('redi://user:secret@localhost:6379/0'),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
|
||||
it('Invalid pathname', () => {
|
||||
assert.throws(
|
||||
() => RedisClient.parseURL('redis://user:secret@localhost:6379/NaN'),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
|
||||
it('redis://localhost', () => {
|
||||
assert.deepEqual(
|
||||
RedisClient.parseURL('redis://localhost'),
|
||||
{
|
||||
socket: {
|
||||
host: 'localhost',
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authentication', () => {
|
||||
testUtils.testWithClient('Client should be authenticated', async client => {
|
||||
assert.equal(
|
||||
await client.ping(),
|
||||
'PONG'
|
||||
);
|
||||
}, GLOBAL.SERVERS.PASSWORD);
|
||||
|
||||
testUtils.testWithClient('should not retry connecting if failed due to wrong auth', async client => {
|
||||
let message;
|
||||
if (testUtils.isVersionGreaterThan([6, 2])) {
|
||||
message = 'WRONGPASS invalid username-password pair or user is disabled.';
|
||||
} else if (testUtils.isVersionGreaterThan([6])) {
|
||||
message = 'WRONGPASS invalid username-password pair';
|
||||
} else {
|
||||
message = 'ERR invalid password';
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
client.connect(),
|
||||
{ message }
|
||||
);
|
||||
|
||||
assert.equal(client.isOpen, false);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.PASSWORD,
|
||||
clientOptions: {
|
||||
password: 'wrongpassword'
|
||||
},
|
||||
disableClientSetup: true
|
||||
});
|
||||
|
||||
testUtils.testWithClient('should execute AUTH before SELECT', async client => {
|
||||
assert.equal(
|
||||
(await client.clientInfo()).db,
|
||||
2
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.PASSWORD,
|
||||
clientOptions: {
|
||||
...GLOBAL.SERVERS.PASSWORD.clientOptions,
|
||||
database: 2
|
||||
},
|
||||
minimumDockerVersion: [6, 2]
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacyMode', () => {
|
||||
function sendCommandAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, args: RedisCommandArguments): Promise<RedisCommandRawReply> {
|
||||
return new Promise((resolve, reject) => {
|
||||
(client as any).sendCommand(args, (err: Error | undefined, reply: RedisCommandRawReply) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
resolve(reply);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
testUtils.testWithClient('client.sendCommand should call the callback', async client => {
|
||||
assert.equal(
|
||||
await sendCommandAsync(client, ['PING']),
|
||||
'PONG'
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.sendCommand should work without callback', async client => {
|
||||
client.sendCommand(['PING']);
|
||||
await client.v4.ping(); // make sure the first command was replied
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.v4.sendCommand should return a promise', async client => {
|
||||
assert.equal(
|
||||
await client.v4.sendCommand(['PING']),
|
||||
'PONG'
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: Array<string | Buffer | RedisCommandArguments>): Promise<RedisCommandRawReply> {
|
||||
return new Promise((resolve, reject) => {
|
||||
(client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
resolve(reply);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
testUtils.testWithClient('client.{command} should accept vardict arguments', async client => {
|
||||
assert.equal(
|
||||
await setAsync(client, 'a', 'b'),
|
||||
'OK'
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.{command} should accept arguments array', async client => {
|
||||
assert.equal(
|
||||
await setAsync(client, ['a', 'b']),
|
||||
'OK'
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.{command} should accept mix of strings and array of strings', async client => {
|
||||
assert.equal(
|
||||
await setAsync(client, ['a'], 'b', ['XX']),
|
||||
null
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
function multiExecAsync<M extends RedisModules, S extends RedisScripts>(multi: RedisClientMultiCommandType<M, S>): Promise<Array<RedisCommandRawReply>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
(multi as any).exec((err: Error | undefined, replies: Array<RedisCommandRawReply>) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
resolve(replies);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => {
|
||||
assert.deepEqual(
|
||||
await multiExecAsync(
|
||||
client.multi().ping()
|
||||
),
|
||||
['PONG']
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => {
|
||||
client.multi()
|
||||
.ping()
|
||||
.exec();
|
||||
await client.v4.ping(); // make sure the first command was replied
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.multi.ping.v4.ping.v4.exec should return a promise', async client => {
|
||||
assert.deepEqual(
|
||||
await client.multi()
|
||||
.ping()
|
||||
.v4.ping()
|
||||
.v4.exec(),
|
||||
['PONG', 'PONG']
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.{script} should return a promise', async client => {
|
||||
assert.equal(
|
||||
await client.square(2),
|
||||
4
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
legacyMode: true,
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
testUtils.testWithClient('connect, ready, end', async client => {
|
||||
await Promise.all([
|
||||
once(client, 'connect'),
|
||||
once(client, 'ready'),
|
||||
client.connect()
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
once(client, 'end'),
|
||||
client.disconnect()
|
||||
]);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
disableClientSetup: true
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendCommand', () => {
|
||||
testUtils.testWithClient('PING', async client => {
|
||||
assert.equal(await client.sendCommand(['PING']), 'PONG');
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('bufferMode', async client => {
|
||||
assert.deepEqual(
|
||||
await client.sendCommand(['PING'], undefined, true),
|
||||
Buffer.from('PONG')
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
describe('AbortController', () => {
|
||||
before(function () {
|
||||
if (!global.AbortController) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithClient('success', async client => {
|
||||
await client.sendCommand(['PING'], {
|
||||
signal: new AbortController().signal
|
||||
});
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('AbortError', client => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
return assert.rejects(
|
||||
client.sendCommand(['PING'], {
|
||||
signal: controller.signal
|
||||
}),
|
||||
AbortError
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi', () => {
|
||||
testUtils.testWithClient('simple', async client => {
|
||||
assert.deepEqual(
|
||||
await client.multi()
|
||||
.ping()
|
||||
.set('key', 'value')
|
||||
.get('key')
|
||||
.exec(),
|
||||
['PONG', 'OK', 'value']
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('should reject the whole chain on error', client => {
|
||||
return assert.rejects(
|
||||
client.multi()
|
||||
.ping()
|
||||
.addCommand(['INVALID COMMAND'])
|
||||
.ping()
|
||||
.exec()
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('with script', async client => {
|
||||
assert.deepEqual(
|
||||
await client.multi()
|
||||
.square(2)
|
||||
.exec(),
|
||||
[4]
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithClient('WatchError', async client => {
|
||||
await client.watch('key');
|
||||
|
||||
await client.set(
|
||||
RedisClient.commandOptions({
|
||||
isolated: true
|
||||
}),
|
||||
'key',
|
||||
'1'
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
client.multi()
|
||||
.decr('key')
|
||||
.exec(),
|
||||
WatchError
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('execAsPipeline', async client => {
|
||||
assert.deepEqual(
|
||||
await client.multi()
|
||||
.ping()
|
||||
.exec(true),
|
||||
['PONG']
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('scripts', async client => {
|
||||
assert.equal(
|
||||
await client.square(2),
|
||||
4
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testUtils.testWithClient('modules', async client => {
|
||||
assert.equal(
|
||||
await client.module.echo('message'),
|
||||
'message'
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
modules: {
|
||||
module: {
|
||||
echo: {
|
||||
transformArguments(message: string): Array<string> {
|
||||
return ['ECHO', message];
|
||||
},
|
||||
transformReply(reply: string): string {
|
||||
return reply;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// testUtils.testWithClient('executeIsolated', async client => {
|
||||
// await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
|
||||
|
||||
// assert.equal(
|
||||
// await client.executeIsolated(isolatedClient =>
|
||||
// isolatedClient.sendCommand(['CLIENT', 'GETNAME'])
|
||||
// ),
|
||||
// null
|
||||
// );
|
||||
// }, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
async function killClient<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>): Promise<void> {
|
||||
const onceErrorPromise = once(client, 'error');
|
||||
await client.sendCommand(['QUIT']);
|
||||
await Promise.all([
|
||||
onceErrorPromise,
|
||||
assert.rejects(client.ping(), SocketClosedUnexpectedlyError)
|
||||
]);
|
||||
}
|
||||
|
||||
testUtils.testWithClient('should reconnect when socket disconnects', async client => {
|
||||
await killClient(client);
|
||||
await assert.doesNotReject(client.ping());
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('should remember selected db', async client => {
|
||||
await client.select(1);
|
||||
await killClient(client);
|
||||
assert.equal(
|
||||
(await client.clientInfo()).db,
|
||||
1
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
minimumDockerVersion: [6, 2] // CLIENT INFO
|
||||
});
|
||||
|
||||
testUtils.testWithClient('scanIterator', async client => {
|
||||
const promises = [],
|
||||
keys = new Set();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const key = i.toString();
|
||||
keys.add(key);
|
||||
promises.push(client.set(key, ''));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const results = new Set();
|
||||
for await (const key of client.scanIterator()) {
|
||||
results.add(key);
|
||||
}
|
||||
|
||||
assert.deepEqual(keys, results);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('hScanIterator', async client => {
|
||||
const hash: Record<string, string> = {};
|
||||
for (let i = 0; i < 100; i++) {
|
||||
hash[i.toString()] = i.toString();
|
||||
}
|
||||
|
||||
await client.hSet('key', hash);
|
||||
|
||||
const results: Record<string, string> = {};
|
||||
for await (const { field, value } of client.hScanIterator('key')) {
|
||||
results[field] = value;
|
||||
}
|
||||
|
||||
assert.deepEqual(hash, results);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('sScanIterator', async client => {
|
||||
const members = new Set<string>();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
members.add(i.toString());
|
||||
}
|
||||
|
||||
await client.sAdd('key', Array.from(members));
|
||||
|
||||
const results = new Set<string>();
|
||||
for await (const key of client.sScanIterator('key')) {
|
||||
results.add(key);
|
||||
}
|
||||
|
||||
assert.deepEqual(members, results);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('zScanIterator', async client => {
|
||||
const members = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
members.push({
|
||||
score: 1,
|
||||
value: i.toString()
|
||||
});
|
||||
}
|
||||
|
||||
await client.zAdd('key', members);
|
||||
|
||||
const map = new Map();
|
||||
for await (const member of client.zScanIterator('key')) {
|
||||
map.set(member.value, member.score);
|
||||
}
|
||||
|
||||
type MemberTuple = [string, number];
|
||||
|
||||
function sort(a: MemberTuple, b: MemberTuple) {
|
||||
return Number(b[0]) - Number(a[0]);
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
[...map.entries()].sort(sort),
|
||||
members.map<MemberTuple>(member => [member.value, member.score]).sort(sort)
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('PubSub', async publisher => {
|
||||
const subscriber = publisher.duplicate();
|
||||
|
||||
await subscriber.connect();
|
||||
|
||||
try {
|
||||
const channelListener1 = spy(),
|
||||
channelListener2 = spy(),
|
||||
patternListener = spy();
|
||||
|
||||
await Promise.all([
|
||||
subscriber.subscribe('channel', channelListener1),
|
||||
subscriber.subscribe('channel', channelListener2),
|
||||
subscriber.pSubscribe('channel*', patternListener)
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(channelListener1),
|
||||
waitTillBeenCalled(channelListener2),
|
||||
waitTillBeenCalled(patternListener),
|
||||
publisher.publish('channel', 'message')
|
||||
]);
|
||||
|
||||
assert.ok(channelListener1.calledOnceWithExactly('message', 'channel'));
|
||||
assert.ok(channelListener2.calledOnceWithExactly('message', 'channel'));
|
||||
assert.ok(patternListener.calledOnceWithExactly('message', 'channel'));
|
||||
|
||||
await subscriber.unsubscribe('channel', channelListener1);
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(channelListener2),
|
||||
waitTillBeenCalled(patternListener),
|
||||
publisher.publish('channel', 'message')
|
||||
]);
|
||||
|
||||
assert.ok(channelListener1.calledOnce);
|
||||
assert.ok(channelListener2.calledTwice);
|
||||
assert.ok(channelListener2.secondCall.calledWithExactly('message', 'channel'));
|
||||
assert.ok(patternListener.calledTwice);
|
||||
assert.ok(patternListener.secondCall.calledWithExactly('message', 'channel'));
|
||||
|
||||
await subscriber.unsubscribe('channel');
|
||||
await Promise.all([
|
||||
waitTillBeenCalled(patternListener),
|
||||
publisher.publish('channel', 'message')
|
||||
]);
|
||||
|
||||
assert.ok(channelListener1.calledOnce);
|
||||
assert.ok(channelListener2.calledTwice);
|
||||
assert.ok(patternListener.calledThrice);
|
||||
assert.ok(patternListener.thirdCall.calledWithExactly('message', 'channel'));
|
||||
|
||||
await subscriber.pUnsubscribe();
|
||||
await publisher.publish('channel', 'message');
|
||||
|
||||
assert.ok(channelListener1.calledOnce);
|
||||
assert.ok(channelListener2.calledTwice);
|
||||
assert.ok(patternListener.calledThrice);
|
||||
|
||||
// should be able to send commands when unsubsribed from all channels (see #1652)
|
||||
await assert.doesNotReject(subscriber.ping());
|
||||
} finally {
|
||||
await subscriber.disconnect();
|
||||
}
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithClient('ConnectionTimeoutError', async client => {
|
||||
const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
|
||||
start = process.hrtime.bigint();
|
||||
|
||||
while (process.hrtime.bigint() - start < 1_000_000) {
|
||||
// block the event loop for 1ms, to make sure the connection will timeout
|
||||
}
|
||||
|
||||
await promise;
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
clientOptions: {
|
||||
socket: {
|
||||
connectTimeout: 1
|
||||
}
|
||||
},
|
||||
disableClientSetup: true
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.quit', async client => {
|
||||
await client.connect();
|
||||
|
||||
const pingPromise = client.ping(),
|
||||
quitPromise = client.quit();
|
||||
assert.equal(client.isOpen, false);
|
||||
|
||||
const [ping] = await Promise.all([
|
||||
pingPromise,
|
||||
assert.doesNotReject(quitPromise),
|
||||
assert.rejects(client.ping(), ClientClosedError)
|
||||
]);
|
||||
|
||||
assert.equal(ping, 'PONG');
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
disableClientSetup: true
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.disconnect', async client => {
|
||||
await client.connect();
|
||||
|
||||
const pingPromise = client.ping(),
|
||||
disconnectPromise = client.disconnect();
|
||||
assert.equal(client.isOpen, false);
|
||||
await Promise.all([
|
||||
assert.rejects(pingPromise, DisconnectsClientError),
|
||||
assert.doesNotReject(disconnectPromise),
|
||||
assert.rejects(client.ping(), ClientClosedError)
|
||||
]);
|
||||
}, {
|
||||
...GLOBAL.SERVERS.OPEN,
|
||||
disableClientSetup: true
|
||||
});
|
||||
});
|
539
packages/client/lib/client/index.ts
Normal file
539
packages/client/lib/client/index.ts
Normal file
@@ -0,0 +1,539 @@
|
||||
import COMMANDS from './commands';
|
||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
||||
import RedisSocket, { RedisSocketOptions, RedisNetSocketOptions, RedisTlsSocketOptions } from './socket';
|
||||
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
|
||||
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
|
||||
import { RedisMultiQueuedCommand } from '../multi-command';
|
||||
import EventEmitter from 'events';
|
||||
import { CommandOptions, commandOptions, isCommandOptions } from '../command-options';
|
||||
import { ScanOptions, ZMember } from '../commands/generic-transformers';
|
||||
import { ScanCommandOptions } from '../commands/SCAN';
|
||||
import { HScanTuple } from '../commands/HSCAN';
|
||||
import { extendWithCommands, extendWithModulesAndScripts, transformCommandArguments, transformCommandReply } from '../commander';
|
||||
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
||||
import { ClientClosedError, DisconnectsClientError } from '../errors';
|
||||
import { URL } from 'url';
|
||||
|
||||
export interface RedisClientOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
|
||||
url?: string;
|
||||
socket?: RedisSocketOptions;
|
||||
username?: string;
|
||||
password?: string;
|
||||
database?: number;
|
||||
commandsQueueMaxLength?: number;
|
||||
readonly?: boolean;
|
||||
legacyMode?: boolean;
|
||||
isolationPoolOptions?: PoolOptions;
|
||||
}
|
||||
|
||||
export type RedisClientCommandSignature<C extends RedisCommand> =
|
||||
(...args: Parameters<C['transformArguments']> | [options: CommandOptions<ClientCommandOptions>, ...rest: Parameters<C['transformArguments']>]) => Promise<RedisCommandReply<C>>;
|
||||
|
||||
type WithCommands = {
|
||||
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
|
||||
};
|
||||
|
||||
export type WithModules<M extends RedisModules> = {
|
||||
[P in keyof M as M[P] extends never ? never : P]: {
|
||||
[C in keyof M[P]]: RedisClientCommandSignature<M[P][C]>;
|
||||
};
|
||||
};
|
||||
|
||||
export type WithScripts<S extends RedisScripts> = {
|
||||
[P in keyof S as S[P] extends never ? never : P]: RedisClientCommandSignature<S[P]>;
|
||||
};
|
||||
|
||||
export type RedisClientType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
|
||||
RedisClient<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
|
||||
|
||||
export type InstantiableRedisClient<M extends RedisModules, S extends RedisScripts> =
|
||||
new (...args: ConstructorParameters<typeof RedisClient>) => RedisClientType<M, S>;
|
||||
|
||||
export interface ClientCommandOptions extends QueueCommandOptions {
|
||||
isolated?: boolean;
|
||||
}
|
||||
|
||||
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
|
||||
|
||||
export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
|
||||
static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> {
|
||||
return commandOptions(options);
|
||||
}
|
||||
|
||||
static extend<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(plugins?: RedisPlugins<M, S>): InstantiableRedisClient<M, S> {
|
||||
const Client = <any>extendWithModulesAndScripts({
|
||||
BaseClass: RedisClient,
|
||||
modules: plugins?.modules,
|
||||
modulesCommandsExecutor: RedisClient.prototype.commandsExecutor,
|
||||
scripts: plugins?.scripts,
|
||||
scriptsExecutor: RedisClient.prototype.scriptsExecutor
|
||||
});
|
||||
|
||||
if (Client !== RedisClient) {
|
||||
Client.prototype.Multi = RedisClientMultiCommand.extend(plugins);
|
||||
}
|
||||
|
||||
return Client;
|
||||
}
|
||||
|
||||
static create<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
|
||||
return new (RedisClient.extend(options))(options);
|
||||
}
|
||||
|
||||
static parseURL(url: string): RedisClientOptions<Record<string, never>, Record<string, never>> {
|
||||
// https://www.iana.org/assignments/uri-schemes/prov/redis
|
||||
const { hostname, port, protocol, username, password, pathname } = new URL(url),
|
||||
parsed: RedisClientOptions<Record<string, never>, Record<string, never>> = {
|
||||
socket: {
|
||||
host: hostname
|
||||
}
|
||||
};
|
||||
|
||||
if (protocol === 'rediss:') {
|
||||
(parsed.socket as RedisTlsSocketOptions).tls = true;
|
||||
} else if (protocol !== 'redis:') {
|
||||
throw new TypeError('Invalid protocol');
|
||||
}
|
||||
|
||||
if (port) {
|
||||
(parsed.socket as RedisNetSocketOptions).port = Number(port);
|
||||
}
|
||||
|
||||
if (username) {
|
||||
parsed.username = username;
|
||||
}
|
||||
|
||||
if (password) {
|
||||
parsed.password = password;
|
||||
}
|
||||
|
||||
if (pathname.length > 1) {
|
||||
const database = Number(pathname.substring(1));
|
||||
if (isNaN(database)) {
|
||||
throw new TypeError('Invalid pathname');
|
||||
}
|
||||
|
||||
parsed.database = database;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
readonly #options?: RedisClientOptions<M, S>;
|
||||
readonly #socket: RedisSocket;
|
||||
readonly #queue: RedisCommandsQueue;
|
||||
readonly #isolationPool: Pool<RedisClientType<M, S>>;
|
||||
readonly #v4: Record<string, any> = {};
|
||||
#selectedDB = 0;
|
||||
|
||||
get options(): RedisClientOptions<M, S> | undefined {
|
||||
return this.#options;
|
||||
}
|
||||
|
||||
get isOpen(): boolean {
|
||||
return this.#socket.isOpen;
|
||||
}
|
||||
|
||||
get v4(): Record<string, any> {
|
||||
if (!this.#options?.legacyMode) {
|
||||
throw new Error('the client is not in "legacy mode"');
|
||||
}
|
||||
|
||||
return this.#v4;
|
||||
}
|
||||
|
||||
constructor(options?: RedisClientOptions<M, S>) {
|
||||
super();
|
||||
this.#options = this.#initiateOptions(options);
|
||||
this.#socket = this.#initiateSocket();
|
||||
this.#queue = this.#initiateQueue();
|
||||
this.#isolationPool = createPool({
|
||||
create: async () => {
|
||||
const duplicate = this.duplicate({
|
||||
isolationPoolOptions: undefined
|
||||
});
|
||||
await duplicate.connect();
|
||||
return duplicate;
|
||||
},
|
||||
destroy: client => client.disconnect()
|
||||
}, options?.isolationPoolOptions);
|
||||
this.#legacyMode();
|
||||
}
|
||||
|
||||
#initiateOptions(options?: RedisClientOptions<M, S>): RedisClientOptions<M, S> | undefined {
|
||||
if (options?.url) {
|
||||
const parsed = RedisClient.parseURL(options.url);
|
||||
if (options.socket) {
|
||||
parsed.socket = Object.assign(options.socket, parsed.socket);
|
||||
}
|
||||
|
||||
Object.assign(options, parsed);
|
||||
}
|
||||
|
||||
if (options?.database) {
|
||||
this.#selectedDB = options.database;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
#initiateSocket(): RedisSocket {
|
||||
const socketInitiator = async (): Promise<void> => {
|
||||
const promises = [];
|
||||
|
||||
if (this.#selectedDB !== 0) {
|
||||
promises.push(
|
||||
this.#queue.addCommand(
|
||||
['SELECT', this.#selectedDB.toString()],
|
||||
{ asap: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.#options?.readonly) {
|
||||
promises.push(
|
||||
this.#queue.addCommand(
|
||||
COMMANDS.READONLY.transformArguments(),
|
||||
{ asap: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.#options?.username || this.#options?.password) {
|
||||
promises.push(
|
||||
this.#queue.addCommand(
|
||||
COMMANDS.AUTH.transformArguments({
|
||||
username: this.#options.username,
|
||||
password: this.#options.password ?? ''
|
||||
}),
|
||||
{ asap: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const resubscribePromise = this.#queue.resubscribe();
|
||||
if (resubscribePromise) {
|
||||
promises.push(resubscribePromise);
|
||||
}
|
||||
|
||||
if (promises.length) {
|
||||
this.#tick(true);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
};
|
||||
|
||||
return new RedisSocket(socketInitiator, this.#options?.socket)
|
||||
.on('data', data => this.#queue.parseResponse(data))
|
||||
.on('error', err => {
|
||||
this.emit('error', err);
|
||||
this.#queue.flushWaitingForReply(err);
|
||||
})
|
||||
.on('connect', () => this.emit('connect'))
|
||||
.on('ready', () => {
|
||||
this.emit('ready');
|
||||
this.#tick();
|
||||
})
|
||||
.on('reconnecting', () => this.emit('reconnecting'))
|
||||
.on('drain', () => this.#tick())
|
||||
.on('end', () => this.emit('end'));
|
||||
}
|
||||
|
||||
#initiateQueue(): RedisCommandsQueue {
|
||||
return new RedisCommandsQueue(this.#options?.commandsQueueMaxLength);
|
||||
}
|
||||
|
||||
#legacyMode(): void {
|
||||
if (!this.#options?.legacyMode) return;
|
||||
|
||||
(this as any).#v4.sendCommand = this.#sendCommand.bind(this);
|
||||
(this as any).sendCommand = (...args: Array<unknown>): void => {
|
||||
const callback = typeof args[args.length - 1] === 'function' ?
|
||||
args[args.length - 1] as ClientLegacyCallback :
|
||||
undefined,
|
||||
actualArgs = !callback ? args : args.slice(0, -1);
|
||||
this.#sendCommand(actualArgs.flat() as Array<string>)
|
||||
.then((reply: RedisCommandRawReply) => {
|
||||
if (!callback) return;
|
||||
|
||||
// https://github.com/NodeRedis/node-redis#commands:~:text=minimal%20parsing
|
||||
|
||||
callback(null, reply);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (!callback) {
|
||||
this.emit('error', err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
for (const name of Object.keys(COMMANDS)) {
|
||||
this.#defineLegacyCommand(name);
|
||||
}
|
||||
|
||||
// hard coded commands
|
||||
this.#defineLegacyCommand('SELECT');
|
||||
this.#defineLegacyCommand('select');
|
||||
this.#defineLegacyCommand('SUBSCRIBE');
|
||||
this.#defineLegacyCommand('subscribe');
|
||||
this.#defineLegacyCommand('PSUBSCRIBE');
|
||||
this.#defineLegacyCommand('pSubscribe');
|
||||
this.#defineLegacyCommand('UNSUBSCRIBE');
|
||||
this.#defineLegacyCommand('unsubscribe');
|
||||
this.#defineLegacyCommand('PUNSUBSCRIBE');
|
||||
this.#defineLegacyCommand('pUnsubscribe');
|
||||
this.#defineLegacyCommand('QUIT');
|
||||
this.#defineLegacyCommand('quit');
|
||||
}
|
||||
|
||||
#defineLegacyCommand(name: string): void {
|
||||
(this as any).#v4[name] = (this as any)[name].bind(this);
|
||||
(this as any)[name] = (...args: Array<unknown>): void => {
|
||||
(this as any).sendCommand(name, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
duplicate(overrides?: Partial<RedisClientOptions<M, S>>): RedisClientType<M, S> {
|
||||
return new (Object.getPrototypeOf(this).constructor)({
|
||||
...this.#options,
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
await this.#socket.connect();
|
||||
}
|
||||
|
||||
async commandsExecutor(command: RedisCommand, args: Array<unknown>): Promise<RedisCommandReply<typeof command>> {
|
||||
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
|
||||
|
||||
return transformCommandReply(
|
||||
command,
|
||||
await this.#sendCommand(redisArgs, options, command.BUFFER_MODE),
|
||||
redisArgs.preserve
|
||||
);
|
||||
}
|
||||
|
||||
sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
|
||||
return this.#sendCommand(args, options, bufferMode);
|
||||
}
|
||||
|
||||
// using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
|
||||
#sendCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: ClientCommandOptions, bufferMode?: boolean): Promise<T> {
|
||||
if (!this.#socket.isOpen) {
|
||||
return Promise.reject(new ClientClosedError());
|
||||
}
|
||||
|
||||
if (options?.isolated) {
|
||||
return this.executeIsolated(isolatedClient =>
|
||||
isolatedClient.sendCommand(args, {
|
||||
...options,
|
||||
isolated: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const promise = this.#queue.addCommand<T>(args, options, bufferMode);
|
||||
this.#tick();
|
||||
return promise;
|
||||
}
|
||||
|
||||
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> {
|
||||
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
|
||||
|
||||
return transformCommandReply(
|
||||
script,
|
||||
await this.executeScript(script, redisArgs, options, script.BUFFER_MODE),
|
||||
redisArgs.preserve
|
||||
);
|
||||
}
|
||||
|
||||
async executeScript(script: RedisScript, args: RedisCommandArguments, options?: ClientCommandOptions, bufferMode?: boolean): Promise<RedisCommandReply<typeof script>> {
|
||||
try {
|
||||
return await this.#sendCommand([
|
||||
'EVALSHA',
|
||||
script.SHA1,
|
||||
script.NUMBER_OF_KEYS.toString(),
|
||||
...args
|
||||
], options, bufferMode);
|
||||
} catch (err: any) {
|
||||
if (!err?.message?.startsWith?.('NOSCRIPT')) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
return await this.#sendCommand([
|
||||
'EVAL',
|
||||
script.SCRIPT,
|
||||
script.NUMBER_OF_KEYS.toString(),
|
||||
...args
|
||||
], options, bufferMode);
|
||||
}
|
||||
}
|
||||
|
||||
async SELECT(db: number): Promise<void>;
|
||||
async SELECT(options: CommandOptions<ClientCommandOptions>, db: number): Promise<void>;
|
||||
async SELECT(options?: any, db?: any): Promise<void> {
|
||||
if (!isCommandOptions(options)) {
|
||||
db = options;
|
||||
options = null;
|
||||
}
|
||||
|
||||
await this.#sendCommand(['SELECT', db.toString()], options);
|
||||
this.#selectedDB = db;
|
||||
}
|
||||
|
||||
select = this.SELECT;
|
||||
|
||||
SUBSCRIBE(channels: string | Array<string>, listener: PubSubListener): Promise<void> {
|
||||
return this.#subscribe(PubSubSubscribeCommands.SUBSCRIBE, channels, listener);
|
||||
}
|
||||
|
||||
subscribe = this.SUBSCRIBE;
|
||||
|
||||
PSUBSCRIBE(patterns: string | Array<string>, listener: PubSubListener): Promise<void> {
|
||||
return this.#subscribe(PubSubSubscribeCommands.PSUBSCRIBE, patterns, listener);
|
||||
}
|
||||
|
||||
pSubscribe = this.PSUBSCRIBE;
|
||||
|
||||
#subscribe(command: PubSubSubscribeCommands, channels: string | Array<string>, listener: PubSubListener): Promise<void> {
|
||||
const promise = this.#queue.subscribe(command, channels, listener);
|
||||
this.#tick();
|
||||
return promise;
|
||||
}
|
||||
|
||||
UNSUBSCRIBE(channels?: string | Array<string>, listener?: PubSubListener): Promise<void> {
|
||||
return this.#unsubscribe(PubSubUnsubscribeCommands.UNSUBSCRIBE, channels, listener);
|
||||
}
|
||||
|
||||
unsubscribe = this.UNSUBSCRIBE;
|
||||
|
||||
PUNSUBSCRIBE(patterns?: string | Array<string>, listener?: PubSubListener): Promise<void> {
|
||||
return this.#unsubscribe(PubSubUnsubscribeCommands.PUNSUBSCRIBE, patterns, listener);
|
||||
}
|
||||
|
||||
pUnsubscribe = this.PUNSUBSCRIBE;
|
||||
|
||||
#unsubscribe(command: PubSubUnsubscribeCommands, channels?: string | Array<string>, listener?: PubSubListener): Promise<void> {
|
||||
const promise = this.#queue.unsubscribe(command, channels, listener);
|
||||
this.#tick();
|
||||
return promise;
|
||||
}
|
||||
|
||||
QUIT(): Promise<void> {
|
||||
return this.#socket.quit(() => {
|
||||
const quitPromise = this.#queue.addCommand(['QUIT']);
|
||||
this.#tick();
|
||||
return Promise.all([
|
||||
quitPromise,
|
||||
this.#destroyIsolationPool()
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
quit = this.QUIT;
|
||||
|
||||
#tick(force = false): void {
|
||||
if (this.#socket.writableNeedDrain || (!force && !this.#socket.isReady)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#socket.cork();
|
||||
|
||||
while (!this.#socket.writableNeedDrain) {
|
||||
const args = this.#queue.getCommandToSend();
|
||||
if (args === undefined) break;
|
||||
|
||||
this.#socket.writeCommand(args);
|
||||
}
|
||||
}
|
||||
|
||||
executeIsolated<T>(fn: (client: RedisClientType<M, S>) => T | Promise<T>): Promise<T> {
|
||||
return this.#isolationPool.use(fn);
|
||||
}
|
||||
|
||||
multi(): RedisClientMultiCommandType<M, S> {
|
||||
return new (this as any).Multi(
|
||||
this.multiExecutor.bind(this),
|
||||
this.#options?.legacyMode
|
||||
);
|
||||
}
|
||||
|
||||
multiExecutor(commands: Array<RedisMultiQueuedCommand>, chainId?: symbol): Promise<Array<RedisCommandRawReply>> {
|
||||
const promise = Promise.all(
|
||||
commands.map(({ args }) => {
|
||||
return this.#queue.addCommand(args, RedisClient.commandOptions({
|
||||
chainId
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
this.#tick();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async* scanIterator(options?: ScanCommandOptions): AsyncIterable<string> {
|
||||
let cursor = 0;
|
||||
do {
|
||||
const reply = await (this as any).scan(cursor, options);
|
||||
cursor = reply.cursor;
|
||||
for (const key of reply.keys) {
|
||||
yield key;
|
||||
}
|
||||
} while (cursor !== 0);
|
||||
}
|
||||
|
||||
async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<HScanTuple> {
|
||||
let cursor = 0;
|
||||
do {
|
||||
const reply = await (this as any).hScan(key, cursor, options);
|
||||
cursor = reply.cursor;
|
||||
for (const tuple of reply.tuples) {
|
||||
yield tuple;
|
||||
}
|
||||
} while (cursor !== 0);
|
||||
}
|
||||
|
||||
async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable<string> {
|
||||
let cursor = 0;
|
||||
do {
|
||||
const reply = await (this as any).sScan(key, cursor, options);
|
||||
cursor = reply.cursor;
|
||||
for (const member of reply.members) {
|
||||
yield member;
|
||||
}
|
||||
} while (cursor !== 0);
|
||||
}
|
||||
|
||||
async* zScanIterator(key: string, options?: ScanOptions): AsyncIterable<ZMember> {
|
||||
let cursor = 0;
|
||||
do {
|
||||
const reply = await (this as any).zScan(key, cursor, options);
|
||||
cursor = reply.cursor;
|
||||
for (const member of reply.members) {
|
||||
yield member;
|
||||
}
|
||||
} while (cursor !== 0);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
this.#queue.flushAll(new DisconnectsClientError());
|
||||
this.#socket.disconnect();
|
||||
await this.#destroyIsolationPool();
|
||||
}
|
||||
|
||||
async #destroyIsolationPool(): Promise<void> {
|
||||
await this.#isolationPool.drain();
|
||||
await this.#isolationPool.clear();
|
||||
}
|
||||
}
|
||||
|
||||
extendWithCommands({
|
||||
BaseClass: RedisClient,
|
||||
commands: COMMANDS,
|
||||
executor: RedisClient.prototype.commandsExecutor
|
||||
});
|
||||
(RedisClient.prototype as any).Multi = RedisClientMultiCommand;
|
132
packages/client/lib/client/multi-command.ts
Normal file
132
packages/client/lib/client/multi-command.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import COMMANDS from './commands';
|
||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
||||
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
||||
import { extendWithCommands, extendWithModulesAndScripts } from '../commander';
|
||||
|
||||
type RedisClientMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
|
||||
(...args: Parameters<C['transformArguments']>) => RedisClientMultiCommandType<M, S>;
|
||||
|
||||
type WithCommands<M extends RedisModules, S extends RedisScripts> = {
|
||||
[P in keyof typeof COMMANDS]: RedisClientMultiCommandSignature<(typeof COMMANDS)[P], M, S>
|
||||
};
|
||||
|
||||
type WithModules<M extends RedisModules, S extends RedisScripts> = {
|
||||
[P in keyof M as M[P] extends never ? never : P]: {
|
||||
[C in keyof M[P]]: RedisClientMultiCommandSignature<M[P][C], M, S>;
|
||||
};
|
||||
};
|
||||
|
||||
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
|
||||
[P in keyof S as S[P] extends never ? never : P]: RedisClientMultiCommandSignature<S[P], M, S>
|
||||
};
|
||||
|
||||
export type RedisClientMultiCommandType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
|
||||
RedisClientMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
|
||||
|
||||
export type RedisClientMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
|
||||
|
||||
export default class RedisClientMultiCommand {
|
||||
readonly #multi = new RedisMultiCommand();
|
||||
readonly #executor: RedisClientMultiExecutor;
|
||||
|
||||
static extend<M extends RedisModules, S extends RedisScripts>(
|
||||
plugins?: RedisPlugins<M, S>
|
||||
): new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisClientMultiCommandType<M, S> {
|
||||
return <any>extendWithModulesAndScripts({
|
||||
BaseClass: RedisClientMultiCommand,
|
||||
modules: plugins?.modules,
|
||||
modulesCommandsExecutor: RedisClientMultiCommand.prototype.commandsExecutor,
|
||||
scripts: plugins?.scripts,
|
||||
scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor
|
||||
});
|
||||
}
|
||||
|
||||
readonly v4: Record<string, any> = {};
|
||||
|
||||
constructor(executor: RedisClientMultiExecutor, legacyMode = false) {
|
||||
this.#executor = executor;
|
||||
if (legacyMode) {
|
||||
this.#legacyMode();
|
||||
}
|
||||
}
|
||||
|
||||
#legacyMode(): void {
|
||||
this.v4.addCommand = this.addCommand.bind(this);
|
||||
(this as any).addCommand = (...args: Array<string | Buffer | Array<string | Buffer>>): this => {
|
||||
this.#multi.addCommand(args.flat());
|
||||
return this;
|
||||
};
|
||||
this.v4.exec = this.exec.bind(this);
|
||||
(this as any).exec = (callback?: (err: Error | null, replies?: Array<unknown>) => unknown): void => {
|
||||
this.v4.exec()
|
||||
.then((reply: Array<unknown>) => {
|
||||
if (!callback) return;
|
||||
|
||||
callback(null, reply);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (!callback) {
|
||||
// this.emit('error', err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
for (const name of Object.keys(COMMANDS)) {
|
||||
this.#defineLegacyCommand(name);
|
||||
}
|
||||
}
|
||||
|
||||
#defineLegacyCommand(name: string): void {
|
||||
(this as any).v4[name] = (this as any)[name].bind(this.v4);
|
||||
(this as any)[name] = (...args: Array<unknown>): void => (this as any).addCommand(name, args);
|
||||
}
|
||||
|
||||
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
|
||||
return this.addCommand(
|
||||
command.transformArguments(...args),
|
||||
command.transformReply
|
||||
);
|
||||
}
|
||||
|
||||
addCommand(args: RedisCommandArguments, transformReply?: RedisCommand['transformReply']): this {
|
||||
this.#multi.addCommand(args, transformReply);
|
||||
return this;
|
||||
}
|
||||
|
||||
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
|
||||
this.#multi.addScript(script, args);
|
||||
return this;
|
||||
}
|
||||
|
||||
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
|
||||
if (execAsPipeline) {
|
||||
return this.execAsPipeline();
|
||||
}
|
||||
|
||||
const commands = this.#multi.exec();
|
||||
if (!commands) return [];
|
||||
|
||||
return this.#multi.handleExecReplies(
|
||||
await this.#executor(commands, RedisMultiCommand.generateChainId())
|
||||
);
|
||||
}
|
||||
|
||||
EXEC = this.exec;
|
||||
|
||||
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
|
||||
if (!this.#multi.queue.length) return [];
|
||||
|
||||
return this.#multi.transformReplies(
|
||||
await this.#executor(this.#multi.queue)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extendWithCommands({
|
||||
BaseClass: RedisClientMultiCommand,
|
||||
commands: COMMANDS,
|
||||
executor: RedisClientMultiCommand.prototype.commandsExecutor
|
||||
});
|
38
packages/client/lib/client/socket.spec.ts
Normal file
38
packages/client/lib/client/socket.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { SinonFakeTimers, useFakeTimers, spy } from 'sinon';
|
||||
import RedisSocket from './socket';
|
||||
|
||||
describe('Socket', () => {
|
||||
describe('reconnectStrategy', () => {
|
||||
let clock: SinonFakeTimers;
|
||||
beforeEach(() => clock = useFakeTimers());
|
||||
afterEach(() => clock.uninstall());
|
||||
|
||||
it('custom strategy', () => {
|
||||
const reconnectStrategy = spy((retries: number): number | Error => {
|
||||
assert.equal(retries + 1, reconnectStrategy.callCount);
|
||||
|
||||
if (retries === 50) {
|
||||
return Error('50');
|
||||
}
|
||||
|
||||
const time = retries * 2;
|
||||
queueMicrotask(() => clock.tick(time));
|
||||
return time;
|
||||
});
|
||||
|
||||
const socket = new RedisSocket(undefined, {
|
||||
host: 'error',
|
||||
reconnectStrategy
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
// ignore errors
|
||||
});
|
||||
|
||||
return assert.rejects(socket.connect(), {
|
||||
message: '50'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
267
packages/client/lib/client/socket.ts
Normal file
267
packages/client/lib/client/socket.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import EventEmitter from 'events';
|
||||
import net from 'net';
|
||||
import tls from 'tls';
|
||||
import { encodeCommand } from '../commander';
|
||||
import { RedisCommandArguments } from '../commands';
|
||||
import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError } from '../errors';
|
||||
import { promiseTimeout } from '../utils';
|
||||
|
||||
export interface RedisSocketCommonOptions {
|
||||
connectTimeout?: number;
|
||||
noDelay?: boolean;
|
||||
keepAlive?: number | false;
|
||||
reconnectStrategy?(retries: number): number | Error;
|
||||
}
|
||||
|
||||
export interface RedisNetSocketOptions extends RedisSocketCommonOptions {
|
||||
port?: number;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
export interface RedisUnixSocketOptions extends RedisSocketCommonOptions {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface RedisTlsSocketOptions extends RedisNetSocketOptions, tls.SecureContextOptions {
|
||||
tls: true;
|
||||
}
|
||||
|
||||
export type RedisSocketOptions = RedisNetSocketOptions | RedisUnixSocketOptions | RedisTlsSocketOptions;
|
||||
|
||||
interface CreateSocketReturn<T> {
|
||||
connectEvent: string;
|
||||
socket: T;
|
||||
}
|
||||
|
||||
export type RedisSocketInitiator = () => Promise<void>;
|
||||
|
||||
export default class RedisSocket extends EventEmitter {
|
||||
static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions {
|
||||
options ??= {};
|
||||
if (!RedisSocket.#isUnixSocket(options)) {
|
||||
(options as RedisNetSocketOptions).port ??= 6379;
|
||||
(options as RedisNetSocketOptions).host ??= '127.0.0.1';
|
||||
}
|
||||
|
||||
options.connectTimeout ??= 5000;
|
||||
options.keepAlive ??= 5000;
|
||||
options.noDelay ??= true;
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
static #defaultReconnectStrategy(retries: number): number {
|
||||
return Math.min(retries * 50, 500);
|
||||
}
|
||||
|
||||
static #isUnixSocket(options: RedisSocketOptions): options is RedisUnixSocketOptions {
|
||||
return Object.prototype.hasOwnProperty.call(options, 'path');
|
||||
}
|
||||
|
||||
static #isTlsSocket(options: RedisSocketOptions): options is RedisTlsSocketOptions {
|
||||
return (options as RedisTlsSocketOptions).tls === true;
|
||||
}
|
||||
|
||||
readonly #initiator?: RedisSocketInitiator;
|
||||
|
||||
readonly #options: RedisSocketOptions;
|
||||
|
||||
#socket?: net.Socket | tls.TLSSocket;
|
||||
|
||||
#isOpen = false;
|
||||
|
||||
get isOpen(): boolean {
|
||||
return this.#isOpen;
|
||||
}
|
||||
|
||||
#isReady = false;
|
||||
|
||||
get isReady(): boolean {
|
||||
return this.#isReady;
|
||||
}
|
||||
|
||||
// `writable.writableNeedDrain` was added in v15.2.0 and therefore can't be used
|
||||
// https://nodejs.org/api/stream.html#stream_writable_writableneeddrain
|
||||
#writableNeedDrain = false;
|
||||
|
||||
get writableNeedDrain(): boolean {
|
||||
return this.#writableNeedDrain;
|
||||
}
|
||||
|
||||
constructor(initiator?: RedisSocketInitiator, options?: RedisSocketOptions) {
|
||||
super();
|
||||
|
||||
this.#initiator = initiator;
|
||||
this.#options = RedisSocket.#initiateOptions(options);
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (this.#isOpen) {
|
||||
throw new Error('Socket already opened');
|
||||
}
|
||||
|
||||
return this.#connect();
|
||||
}
|
||||
|
||||
async #connect(hadError?: boolean): Promise<void> {
|
||||
this.#isOpen = true;
|
||||
this.#socket = await this.#retryConnection(0, hadError);
|
||||
this.#writableNeedDrain = false;
|
||||
|
||||
if (!this.#isOpen) {
|
||||
this.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('connect');
|
||||
|
||||
if (this.#initiator) {
|
||||
try {
|
||||
await this.#initiator();
|
||||
} catch (err) {
|
||||
this.#socket.destroy();
|
||||
this.#socket = undefined;
|
||||
this.#isOpen = false;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!this.#isOpen) return;
|
||||
}
|
||||
|
||||
this.#isReady = true;
|
||||
|
||||
this.emit('ready');
|
||||
}
|
||||
|
||||
async #retryConnection(retries: number, hadError?: boolean): Promise<net.Socket | tls.TLSSocket> {
|
||||
if (retries > 0 || hadError) {
|
||||
this.emit('reconnecting');
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.#createSocket();
|
||||
} catch (err) {
|
||||
this.emit('error', err);
|
||||
|
||||
if (!this.#isOpen) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const retryIn = (this.#options?.reconnectStrategy ?? RedisSocket.#defaultReconnectStrategy)(retries);
|
||||
if (retryIn instanceof Error) {
|
||||
throw retryIn;
|
||||
}
|
||||
|
||||
await promiseTimeout(retryIn);
|
||||
return this.#retryConnection(retries + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#createSocket(): Promise<net.Socket | tls.TLSSocket> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const {connectEvent, socket} = RedisSocket.#isTlsSocket(this.#options) ?
|
||||
this.#createTlsSocket() :
|
||||
this.#createNetSocket();
|
||||
|
||||
if (this.#options.connectTimeout) {
|
||||
socket.setTimeout(this.#options.connectTimeout, () => socket.destroy(new ConnectionTimeoutError()));
|
||||
}
|
||||
|
||||
socket
|
||||
.setNoDelay(this.#options.noDelay)
|
||||
.setKeepAlive(this.#options.keepAlive !== false, this.#options.keepAlive || 0)
|
||||
.once('error', reject)
|
||||
.once(connectEvent, () => {
|
||||
socket
|
||||
.setTimeout(0)
|
||||
.off('error', reject)
|
||||
.once('error', (err: Error) => this.#onSocketError(err))
|
||||
.once('close', hadError => {
|
||||
if (!hadError && this.#isOpen) {
|
||||
this.#onSocketError(new SocketClosedUnexpectedlyError());
|
||||
}
|
||||
})
|
||||
.on('drain', () => {
|
||||
this.#writableNeedDrain = false;
|
||||
this.emit('drain');
|
||||
})
|
||||
.on('data', (data: Buffer) => this.emit('data', data));
|
||||
|
||||
resolve(socket);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#createNetSocket(): CreateSocketReturn<net.Socket> {
|
||||
return {
|
||||
connectEvent: 'connect',
|
||||
socket: net.connect(this.#options as net.NetConnectOpts) // TODO
|
||||
};
|
||||
}
|
||||
|
||||
#createTlsSocket(): CreateSocketReturn<tls.TLSSocket> {
|
||||
return {
|
||||
connectEvent: 'secureConnect',
|
||||
socket: tls.connect(this.#options as tls.ConnectionOptions) // TODO
|
||||
};
|
||||
}
|
||||
|
||||
#onSocketError(err: Error): void {
|
||||
this.#isReady = false;
|
||||
this.emit('error', err);
|
||||
|
||||
this.#connect(true).catch(() => {
|
||||
// the error was already emitted, silently ignore it
|
||||
});
|
||||
}
|
||||
|
||||
writeCommand(args: RedisCommandArguments): void {
|
||||
if (!this.#socket) {
|
||||
throw new ClientClosedError();
|
||||
}
|
||||
|
||||
for (const toWrite of encodeCommand(args)) {
|
||||
this.#writableNeedDrain = !this.#socket.write(toWrite);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (!this.#socket) {
|
||||
throw new ClientClosedError();
|
||||
} else {
|
||||
this.#isOpen = this.#isReady = false;
|
||||
}
|
||||
|
||||
this.#socket.destroy();
|
||||
this.#socket = undefined;
|
||||
this.emit('end');
|
||||
}
|
||||
|
||||
async quit(fn: () => Promise<unknown>): Promise<void> {
|
||||
if (!this.#isOpen) {
|
||||
throw new ClientClosedError();
|
||||
}
|
||||
|
||||
this.#isOpen = false;
|
||||
await fn();
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
#isCorked = false;
|
||||
|
||||
cork(): void {
|
||||
if (!this.#socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#isCorked) {
|
||||
this.#socket.cork();
|
||||
this.#isCorked = true;
|
||||
|
||||
queueMicrotask(() => {
|
||||
this.#socket?.uncork();
|
||||
this.#isCorked = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
224
packages/client/lib/cluster/cluster-slots.ts
Normal file
224
packages/client/lib/cluster/cluster-slots.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import calculateSlot from 'cluster-key-slot';
|
||||
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
|
||||
import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES';
|
||||
import { RedisClusterClientOptions, RedisClusterOptions } from '.';
|
||||
import { RedisModules, RedisScripts } from '../commands';
|
||||
|
||||
export interface ClusterNode<M extends RedisModules, S extends RedisScripts> {
|
||||
id: string;
|
||||
client: RedisClientType<M, S>;
|
||||
}
|
||||
|
||||
interface SlotNodes<M extends RedisModules, S extends RedisScripts> {
|
||||
master: ClusterNode<M, S>;
|
||||
replicas: Array<ClusterNode<M, S>>;
|
||||
clientIterator: IterableIterator<RedisClientType<M, S>> | undefined;
|
||||
}
|
||||
|
||||
type OnError = (err: unknown) => void;
|
||||
|
||||
export default class RedisClusterSlots<M extends RedisModules, S extends RedisScripts> {
|
||||
readonly #options: RedisClusterOptions<M, S>;
|
||||
readonly #Client: InstantiableRedisClient<M, S>;
|
||||
readonly #onError: OnError;
|
||||
readonly #nodeByUrl = new Map<string, ClusterNode<M, S>>();
|
||||
readonly #slots: Array<SlotNodes<M, S>> = [];
|
||||
|
||||
constructor(options: RedisClusterOptions<M, S>, onError: OnError) {
|
||||
this.#options = options;
|
||||
this.#Client = RedisClient.extend(options);
|
||||
this.#onError = onError;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
for (const rootNode of this.#options.rootNodes) {
|
||||
if (await this.#discoverNodes(this.#clientOptionsDefaults(rootNode))) return;
|
||||
}
|
||||
|
||||
throw new Error('None of the root nodes is available');
|
||||
}
|
||||
|
||||
async discover(startWith: RedisClientType<M, S>): Promise<void> {
|
||||
if (await this.#discoverNodes(startWith.options)) return;
|
||||
|
||||
for (const { client } of this.#nodeByUrl.values()) {
|
||||
if (client === startWith) continue;
|
||||
|
||||
if (await this.#discoverNodes(client.options)) return;
|
||||
}
|
||||
|
||||
throw new Error('None of the cluster nodes is available');
|
||||
}
|
||||
|
||||
async #discoverNodes(clientOptions?: RedisClusterClientOptions): Promise<boolean> {
|
||||
const client = new this.#Client(clientOptions);
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
await this.#reset(await client.clusterNodes());
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.#onError(err);
|
||||
return false;
|
||||
} finally {
|
||||
if (client.isOpen) {
|
||||
await client.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #reset(masters: Array<RedisClusterMasterNode>): Promise<void> {
|
||||
// Override this.#slots and add not existing clients to this.#nodeByUrl
|
||||
const promises: Array<Promise<void>> = [],
|
||||
clientsInUse = new Set<string>();
|
||||
for (const master of masters) {
|
||||
const slot = {
|
||||
master: this.#initiateClientForNode(master, false, clientsInUse, promises),
|
||||
replicas: this.#options.useReplicas ?
|
||||
master.replicas.map(replica => this.#initiateClientForNode(replica, true, clientsInUse, promises)) :
|
||||
[],
|
||||
clientIterator: undefined // will be initiated in use
|
||||
};
|
||||
|
||||
for (const { from, to } of master.slots) {
|
||||
for (let i = from; i < to; i++) {
|
||||
this.#slots[i] = slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unused clients from this.#nodeByUrl using clientsInUse
|
||||
for (const [url, { client }] of this.#nodeByUrl.entries()) {
|
||||
if (clientsInUse.has(url)) continue;
|
||||
|
||||
promises.push(client.disconnect());
|
||||
this.#nodeByUrl.delete(url);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
#clientOptionsDefaults(options: RedisClusterClientOptions): RedisClusterClientOptions {
|
||||
if (!this.#options.defaults) return options;
|
||||
|
||||
const merged = Object.assign({}, this.#options.defaults, options);
|
||||
|
||||
if (options.socket && this.#options.defaults.socket) {
|
||||
Object.assign({}, this.#options.defaults.socket, options.socket);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
#initiateClientForNode(nodeData: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set<string>, promises: Array<Promise<void>>): ClusterNode<M, S> {
|
||||
const url = `${nodeData.host}:${nodeData.port}`;
|
||||
clientsInUse.add(url);
|
||||
|
||||
let node = this.#nodeByUrl.get(url);
|
||||
if (!node) {
|
||||
node = {
|
||||
id: nodeData.id,
|
||||
client: new this.#Client(
|
||||
this.#clientOptionsDefaults({
|
||||
socket: {
|
||||
host: nodeData.host,
|
||||
port: nodeData.port
|
||||
},
|
||||
readonly
|
||||
})
|
||||
)
|
||||
};
|
||||
promises.push(node.client.connect());
|
||||
this.#nodeByUrl.set(url, node);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
getSlotMaster(slot: number): ClusterNode<M, S> {
|
||||
return this.#slots[slot].master;
|
||||
}
|
||||
|
||||
*#slotClientIterator(slotNumber: number): IterableIterator<RedisClientType<M, S>> {
|
||||
const slot = this.#slots[slotNumber];
|
||||
yield slot.master.client;
|
||||
|
||||
for (const replica of slot.replicas) {
|
||||
yield replica.client;
|
||||
}
|
||||
}
|
||||
|
||||
#getSlotClient(slotNumber: number): RedisClientType<M, S> {
|
||||
const slot = this.#slots[slotNumber];
|
||||
if (!slot.clientIterator) {
|
||||
slot.clientIterator = this.#slotClientIterator(slotNumber);
|
||||
}
|
||||
|
||||
const {done, value} = slot.clientIterator.next();
|
||||
if (done) {
|
||||
slot.clientIterator = undefined;
|
||||
return this.#getSlotClient(slotNumber);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
#randomClientIterator?: IterableIterator<ClusterNode<M, S>>;
|
||||
|
||||
#getRandomClient(): RedisClientType<M, S> {
|
||||
if (!this.#nodeByUrl.size) {
|
||||
throw new Error('Cluster is not connected');
|
||||
}
|
||||
|
||||
if (!this.#randomClientIterator) {
|
||||
this.#randomClientIterator = this.#nodeByUrl.values();
|
||||
}
|
||||
|
||||
const {done, value} = this.#randomClientIterator.next();
|
||||
if (done) {
|
||||
this.#randomClientIterator = undefined;
|
||||
return this.#getRandomClient();
|
||||
}
|
||||
|
||||
return value.client;
|
||||
}
|
||||
|
||||
getClient(firstKey?: string | Buffer, isReadonly?: boolean): RedisClientType<M, S> {
|
||||
if (!firstKey) {
|
||||
return this.#getRandomClient();
|
||||
}
|
||||
|
||||
const slot = calculateSlot(firstKey);
|
||||
if (!isReadonly || !this.#options.useReplicas) {
|
||||
return this.getSlotMaster(slot).client;
|
||||
}
|
||||
|
||||
return this.#getSlotClient(slot);
|
||||
}
|
||||
|
||||
getMasters(): Array<ClusterNode<M, S>> {
|
||||
const masters = [];
|
||||
|
||||
for (const node of this.#nodeByUrl.values()) {
|
||||
if (node.client.options?.readonly) continue;
|
||||
|
||||
masters.push(node);
|
||||
}
|
||||
|
||||
return masters;
|
||||
}
|
||||
|
||||
getNodeByUrl(url: string): ClusterNode<M, S> | undefined {
|
||||
return this.#nodeByUrl.get(url);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
await Promise.all(
|
||||
[...this.#nodeByUrl.values()].map(({ client }) => client.disconnect())
|
||||
);
|
||||
|
||||
this.#nodeByUrl.clear();
|
||||
this.#slots.splice(0);
|
||||
}
|
||||
}
|
535
packages/client/lib/cluster/commands.ts
Normal file
535
packages/client/lib/cluster/commands.ts
Normal file
@@ -0,0 +1,535 @@
|
||||
|
||||
import * as APPEND from '../commands/APPEND';
|
||||
import * as BITCOUNT from '../commands/BITCOUNT';
|
||||
import * as BITFIELD from '../commands/BITFIELD';
|
||||
import * as BITOP from '../commands/BITOP';
|
||||
import * as BITPOS from '../commands/BITPOS';
|
||||
import * as BLMOVE from '../commands/BLMOVE';
|
||||
import * as BLPOP from '../commands/BLPOP';
|
||||
import * as BRPOP from '../commands/BRPOP';
|
||||
import * as BRPOPLPUSH from '../commands/BRPOPLPUSH';
|
||||
import * as BZPOPMAX from '../commands/BZPOPMAX';
|
||||
import * as BZPOPMIN from '../commands/BZPOPMIN';
|
||||
import * as COPY from '../commands/COPY';
|
||||
import * as DECR from '../commands/DECR';
|
||||
import * as DECRBY from '../commands/DECRBY';
|
||||
import * as DEL from '../commands/DEL';
|
||||
import * as DUMP from '../commands/DUMP';
|
||||
import * as EVAL from '../commands/EVAL';
|
||||
import * as EVALSHA from '../commands/EVALSHA';
|
||||
import * as EXISTS from '../commands/EXISTS';
|
||||
import * as EXPIRE from '../commands/EXPIRE';
|
||||
import * as EXPIREAT from '../commands/EXPIREAT';
|
||||
import * as GEOADD from '../commands/GEOADD';
|
||||
import * as GEODIST from '../commands/GEODIST';
|
||||
import * as GEOHASH from '../commands/GEOHASH';
|
||||
import * as GEOPOS from '../commands/GEOPOS';
|
||||
import * as GEOSEARCH_WITH from '../commands/GEOSEARCH_WITH';
|
||||
import * as GEOSEARCH from '../commands/GEOSEARCH';
|
||||
import * as GEOSEARCHSTORE from '../commands/GEOSEARCHSTORE';
|
||||
import * as GET_BUFFER from '../commands/GET_BUFFER';
|
||||
import * as GET from '../commands/GET';
|
||||
import * as GETBIT from '../commands/GETBIT';
|
||||
import * as GETDEL from '../commands/GETDEL';
|
||||
import * as GETEX from '../commands/GETEX';
|
||||
import * as GETRANGE from '../commands/GETRANGE';
|
||||
import * as GETSET from '../commands/GETSET';
|
||||
import * as HDEL from '../commands/HDEL';
|
||||
import * as HEXISTS from '../commands/HEXISTS';
|
||||
import * as HGET from '../commands/HGET';
|
||||
import * as HGETALL from '../commands/HGETALL';
|
||||
import * as HINCRBY from '../commands/HINCRBY';
|
||||
import * as HINCRBYFLOAT from '../commands/HINCRBYFLOAT';
|
||||
import * as HKEYS from '../commands/HKEYS';
|
||||
import * as HLEN from '../commands/HLEN';
|
||||
import * as HMGET from '../commands/HMGET';
|
||||
import * as HRANDFIELD_COUNT_WITHVALUES from '../commands/HRANDFIELD_COUNT_WITHVALUES';
|
||||
import * as HRANDFIELD_COUNT from '../commands/HRANDFIELD_COUNT';
|
||||
import * as HRANDFIELD from '../commands/HRANDFIELD';
|
||||
import * as HSCAN from '../commands/HSCAN';
|
||||
import * as HSET from '../commands/HSET';
|
||||
import * as HSETNX from '../commands/HSETNX';
|
||||
import * as HSTRLEN from '../commands/HSTRLEN';
|
||||
import * as HVALS from '../commands/HVALS';
|
||||
import * as INCR from '../commands/INCR';
|
||||
import * as INCRBY from '../commands/INCRBY';
|
||||
import * as INCRBYFLOAT from '../commands/INCRBYFLOAT';
|
||||
import * as LINDEX from '../commands/LINDEX';
|
||||
import * as LINSERT from '../commands/LINSERT';
|
||||
import * as LLEN from '../commands/LLEN';
|
||||
import * as LMOVE from '../commands/LMOVE';
|
||||
import * as LPOP_COUNT from '../commands/LPOP_COUNT';
|
||||
import * as LPOP from '../commands/LPOP';
|
||||
import * as LPOS_COUNT from '../commands/LPOS_COUNT';
|
||||
import * as LPOS from '../commands/LPOS';
|
||||
import * as LPUSH from '../commands/LPUSH';
|
||||
import * as LPUSHX from '../commands/LPUSHX';
|
||||
import * as LRANGE from '../commands/LRANGE';
|
||||
import * as LREM from '../commands/LREM';
|
||||
import * as LSET from '../commands/LSET';
|
||||
import * as LTRIM from '../commands/LTRIM';
|
||||
import * as MGET from '../commands/MGET';
|
||||
import * as MIGRATE from '../commands/MIGRATE';
|
||||
import * as MSET from '../commands/MSET';
|
||||
import * as MSETNX from '../commands/MSETNX';
|
||||
import * as PERSIST from '../commands/PERSIST';
|
||||
import * as PEXPIRE from '../commands/PEXPIRE';
|
||||
import * as PEXPIREAT from '../commands/PEXPIREAT';
|
||||
import * as PFADD from '../commands/PFADD';
|
||||
import * as PFCOUNT from '../commands/PFCOUNT';
|
||||
import * as PFMERGE from '../commands/PFMERGE';
|
||||
import * as PSETEX from '../commands/PSETEX';
|
||||
import * as PTTL from '../commands/PTTL';
|
||||
import * as PUBLISH from '../commands/PUBLISH';
|
||||
import * as RENAME from '../commands/RENAME';
|
||||
import * as RENAMENX from '../commands/RENAMENX';
|
||||
import * as RPOP_COUNT from '../commands/RPOP_COUNT';
|
||||
import * as RPOP from '../commands/RPOP';
|
||||
import * as RPOPLPUSH from '../commands/RPOPLPUSH';
|
||||
import * as RPUSH from '../commands/RPUSH';
|
||||
import * as RPUSHX from '../commands/RPUSHX';
|
||||
import * as SADD from '../commands/SADD';
|
||||
import * as SCARD from '../commands/SCARD';
|
||||
import * as SDIFF from '../commands/SDIFF';
|
||||
import * as SDIFFSTORE from '../commands/SDIFFSTORE';
|
||||
import * as SET from '../commands/SET';
|
||||
import * as SETBIT from '../commands/SETBIT';
|
||||
import * as SETEX from '../commands/SETEX';
|
||||
import * as SETNX from '../commands/SETNX';
|
||||
import * as SETRANGE from '../commands/SETRANGE';
|
||||
import * as SINTER from '../commands/SINTER';
|
||||
import * as SINTERSTORE from '../commands/SINTERSTORE';
|
||||
import * as SISMEMBER from '../commands/SISMEMBER';
|
||||
import * as SMEMBERS from '../commands/SMEMBERS';
|
||||
import * as SMISMEMBER from '../commands/SMISMEMBER';
|
||||
import * as SMOVE from '../commands/SMOVE';
|
||||
import * as SORT from '../commands/SORT';
|
||||
import * as SPOP from '../commands/SPOP';
|
||||
import * as SRANDMEMBER_COUNT from '../commands/SRANDMEMBER_COUNT';
|
||||
import * as SRANDMEMBER from '../commands/SRANDMEMBER';
|
||||
import * as SREM from '../commands/SREM';
|
||||
import * as SSCAN from '../commands/SSCAN';
|
||||
import * as STRLEN from '../commands/STRLEN';
|
||||
import * as SUNION from '../commands/SUNION';
|
||||
import * as SUNIONSTORE from '../commands/SUNIONSTORE';
|
||||
import * as TOUCH from '../commands/TOUCH';
|
||||
import * as TTL from '../commands/TTL';
|
||||
import * as TYPE from '../commands/TYPE';
|
||||
import * as UNLINK from '../commands/UNLINK';
|
||||
import * as WATCH from '../commands/WATCH';
|
||||
import * as XACK from '../commands/XACK';
|
||||
import * as XADD from '../commands/XADD';
|
||||
import * as XAUTOCLAIM_JUSTID from '../commands/XAUTOCLAIM_JUSTID';
|
||||
import * as XAUTOCLAIM from '../commands/XAUTOCLAIM';
|
||||
import * as XCLAIM from '../commands/XCLAIM';
|
||||
import * as XCLAIM_JUSTID from '../commands/XCLAIM_JUSTID';
|
||||
import * as XDEL from '../commands/XDEL';
|
||||
import * as XGROUP_CREATE from '../commands/XGROUP_CREATE';
|
||||
import * as XGROUP_CREATECONSUMER from '../commands/XGROUP_CREATECONSUMER';
|
||||
import * as XGROUP_DELCONSUMER from '../commands/XGROUP_DELCONSUMER';
|
||||
import * as XGROUP_DESTROY from '../commands/XGROUP_DESTROY';
|
||||
import * as XGROUP_SETID from '../commands/XGROUP_SETID';
|
||||
import * as XINFO_CONSUMERS from '../commands/XINFO_CONSUMERS';
|
||||
import * as XINFO_GROUPS from '../commands/XINFO_GROUPS';
|
||||
import * as XINFO_STREAM from '../commands/XINFO_STREAM';
|
||||
import * as XLEN from '../commands/XLEN';
|
||||
import * as XPENDING_RANGE from '../commands/XPENDING_RANGE';
|
||||
import * as XPENDING from '../commands/XPENDING';
|
||||
import * as XRANGE from '../commands/XRANGE';
|
||||
import * as XREAD from '../commands/XREAD';
|
||||
import * as XREADGROUP from '../commands/XREADGROUP';
|
||||
import * as XREVRANGE from '../commands/XREVRANGE';
|
||||
import * as XTRIM from '../commands/XTRIM';
|
||||
import * as ZADD from '../commands/ZADD';
|
||||
import * as ZCARD from '../commands/ZCARD';
|
||||
import * as ZCOUNT from '../commands/ZCOUNT';
|
||||
import * as ZDIFF_WITHSCORES from '../commands/ZDIFF_WITHSCORES';
|
||||
import * as ZDIFF from '../commands/ZDIFF';
|
||||
import * as ZDIFFSTORE from '../commands/ZDIFFSTORE';
|
||||
import * as ZINCRBY from '../commands/ZINCRBY';
|
||||
import * as ZINTER_WITHSCORES from '../commands/ZINTER_WITHSCORES';
|
||||
import * as ZINTER from '../commands/ZINTER';
|
||||
import * as ZINTERSTORE from '../commands/ZINTERSTORE';
|
||||
import * as ZLEXCOUNT from '../commands/ZLEXCOUNT';
|
||||
import * as ZMSCORE from '../commands/ZMSCORE';
|
||||
import * as ZPOPMAX_COUNT from '../commands/ZPOPMAX_COUNT';
|
||||
import * as ZPOPMAX from '../commands/ZPOPMAX';
|
||||
import * as ZPOPMIN_COUNT from '../commands/ZPOPMIN_COUNT';
|
||||
import * as ZPOPMIN from '../commands/ZPOPMIN';
|
||||
import * as ZRANDMEMBER_COUNT_WITHSCORES from '../commands/ZRANDMEMBER_COUNT_WITHSCORES';
|
||||
import * as ZRANDMEMBER_COUNT from '../commands/ZRANDMEMBER_COUNT';
|
||||
import * as ZRANDMEMBER from '../commands/ZRANDMEMBER';
|
||||
import * as ZRANGE_WITHSCORES from '../commands/ZRANGE_WITHSCORES';
|
||||
import * as ZRANGE from '../commands/ZRANGE';
|
||||
import * as ZRANGEBYLEX from '../commands/ZRANGEBYLEX';
|
||||
import * as ZRANGEBYSCORE_WITHSCORES from '../commands/ZRANGEBYSCORE_WITHSCORES';
|
||||
import * as ZRANGEBYSCORE from '../commands/ZRANGEBYSCORE';
|
||||
import * as ZRANGESTORE from '../commands/ZRANGESTORE';
|
||||
import * as ZRANK from '../commands/ZRANK';
|
||||
import * as ZREM from '../commands/ZREM';
|
||||
import * as ZREMRANGEBYLEX from '../commands/ZREMRANGEBYLEX';
|
||||
import * as ZREMRANGEBYRANK from '../commands/ZREMRANGEBYRANK';
|
||||
import * as ZREMRANGEBYSCORE from '../commands/ZREMRANGEBYSCORE';
|
||||
import * as ZREVRANK from '../commands/ZREVRANK';
|
||||
import * as ZSCAN from '../commands/ZSCAN';
|
||||
import * as ZSCORE from '../commands/ZSCORE';
|
||||
import * as ZUNION_WITHSCORES from '../commands/ZUNION_WITHSCORES';
|
||||
import * as ZUNION from '../commands/ZUNION';
|
||||
import * as ZUNIONSTORE from '../commands/ZUNIONSTORE';
|
||||
|
||||
export default {
|
||||
APPEND,
|
||||
append: APPEND,
|
||||
BITCOUNT,
|
||||
bitCount: BITCOUNT,
|
||||
BITFIELD,
|
||||
bitField: BITFIELD,
|
||||
BITOP,
|
||||
bitOp: BITOP,
|
||||
BITPOS,
|
||||
bitPos: BITPOS,
|
||||
BLMOVE,
|
||||
blMove: BLMOVE,
|
||||
BLPOP,
|
||||
blPop: BLPOP,
|
||||
BRPOP,
|
||||
brPop: BRPOP,
|
||||
BRPOPLPUSH,
|
||||
brPopLPush: BRPOPLPUSH,
|
||||
BZPOPMAX,
|
||||
bzPopMax: BZPOPMAX,
|
||||
BZPOPMIN,
|
||||
bzPopMin: BZPOPMIN,
|
||||
COPY,
|
||||
copy: COPY,
|
||||
DECR,
|
||||
decr: DECR,
|
||||
DECRBY,
|
||||
decrBy: DECRBY,
|
||||
DEL,
|
||||
del: DEL,
|
||||
DUMP,
|
||||
dump: DUMP,
|
||||
EVAL,
|
||||
eval: EVAL,
|
||||
EVALSHA,
|
||||
evalSha: EVALSHA,
|
||||
EXISTS,
|
||||
exists: EXISTS,
|
||||
EXPIRE,
|
||||
expire: EXPIRE,
|
||||
EXPIREAT,
|
||||
expireAt: EXPIREAT,
|
||||
GEOADD,
|
||||
geoAdd: GEOADD,
|
||||
GEODIST,
|
||||
geoDist: GEODIST,
|
||||
GEOHASH,
|
||||
geoHash: GEOHASH,
|
||||
GEOPOS,
|
||||
geoPos: GEOPOS,
|
||||
GEOSEARCH_WITH,
|
||||
geoSearchWith: GEOSEARCH_WITH,
|
||||
GEOSEARCH,
|
||||
geoSearch: GEOSEARCH,
|
||||
GEOSEARCHSTORE,
|
||||
geoSearchStore: GEOSEARCHSTORE,
|
||||
GET_BUFFER,
|
||||
getBuffer: GET_BUFFER,
|
||||
GET,
|
||||
get: GET,
|
||||
GETBIT,
|
||||
getBit: GETBIT,
|
||||
GETDEL,
|
||||
getDel: GETDEL,
|
||||
GETEX,
|
||||
getEx: GETEX,
|
||||
GETRANGE,
|
||||
getRange: GETRANGE,
|
||||
GETSET,
|
||||
getSet: GETSET,
|
||||
HDEL,
|
||||
hDel: HDEL,
|
||||
HEXISTS,
|
||||
hExists: HEXISTS,
|
||||
HGET,
|
||||
hGet: HGET,
|
||||
HGETALL,
|
||||
hGetAll: HGETALL,
|
||||
HINCRBY,
|
||||
hIncrBy: HINCRBY,
|
||||
HINCRBYFLOAT,
|
||||
hIncrByFloat: HINCRBYFLOAT,
|
||||
HKEYS,
|
||||
hKeys: HKEYS,
|
||||
HLEN,
|
||||
hLen: HLEN,
|
||||
HMGET,
|
||||
hmGet: HMGET,
|
||||
HRANDFIELD_COUNT_WITHVALUES,
|
||||
hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES,
|
||||
HRANDFIELD_COUNT,
|
||||
hRandFieldCount: HRANDFIELD_COUNT,
|
||||
HRANDFIELD,
|
||||
hRandField: HRANDFIELD,
|
||||
HSCAN,
|
||||
hScan: HSCAN,
|
||||
HSET,
|
||||
hSet: HSET,
|
||||
HSETNX,
|
||||
hSetNX: HSETNX,
|
||||
HSTRLEN,
|
||||
hStrLen: HSTRLEN,
|
||||
HVALS,
|
||||
hVals: HVALS,
|
||||
INCR,
|
||||
incr: INCR,
|
||||
INCRBY,
|
||||
incrBy: INCRBY,
|
||||
INCRBYFLOAT,
|
||||
incrByFloat: INCRBYFLOAT,
|
||||
LINDEX,
|
||||
lIndex: LINDEX,
|
||||
LINSERT,
|
||||
lInsert: LINSERT,
|
||||
LLEN,
|
||||
lLen: LLEN,
|
||||
LMOVE,
|
||||
lMove: LMOVE,
|
||||
LPOP_COUNT,
|
||||
lPopCount: LPOP_COUNT,
|
||||
LPOP,
|
||||
lPop: LPOP,
|
||||
LPOS_COUNT,
|
||||
lPosCount: LPOS_COUNT,
|
||||
LPOS,
|
||||
lPos: LPOS,
|
||||
LPUSH,
|
||||
lPush: LPUSH,
|
||||
LPUSHX,
|
||||
lPushX: LPUSHX,
|
||||
LRANGE,
|
||||
lRange: LRANGE,
|
||||
LREM,
|
||||
lRem: LREM,
|
||||
LSET,
|
||||
lSet: LSET,
|
||||
LTRIM,
|
||||
lTrim: LTRIM,
|
||||
MGET,
|
||||
mGet: MGET,
|
||||
MIGRATE,
|
||||
migrate: MIGRATE,
|
||||
MSET,
|
||||
mSet: MSET,
|
||||
MSETNX,
|
||||
mSetNX: MSETNX,
|
||||
PERSIST,
|
||||
persist: PERSIST,
|
||||
PEXPIRE,
|
||||
pExpire: PEXPIRE,
|
||||
PEXPIREAT,
|
||||
pExpireAt: PEXPIREAT,
|
||||
PFADD,
|
||||
pfAdd: PFADD,
|
||||
PFCOUNT,
|
||||
pfCount: PFCOUNT,
|
||||
PFMERGE,
|
||||
pfMerge: PFMERGE,
|
||||
PSETEX,
|
||||
pSetEx: PSETEX,
|
||||
PTTL,
|
||||
pTTL: PTTL,
|
||||
PUBLISH,
|
||||
publish: PUBLISH,
|
||||
RENAME,
|
||||
rename: RENAME,
|
||||
RENAMENX,
|
||||
renameNX: RENAMENX,
|
||||
RPOP_COUNT,
|
||||
rPopCount: RPOP_COUNT,
|
||||
RPOP,
|
||||
rPop: RPOP,
|
||||
RPOPLPUSH,
|
||||
rPopLPush: RPOPLPUSH,
|
||||
RPUSH,
|
||||
rPush: RPUSH,
|
||||
RPUSHX,
|
||||
rPushX: RPUSHX,
|
||||
SADD,
|
||||
sAdd: SADD,
|
||||
SCARD,
|
||||
sCard: SCARD,
|
||||
SDIFF,
|
||||
sDiff: SDIFF,
|
||||
SDIFFSTORE,
|
||||
sDiffStore: SDIFFSTORE,
|
||||
SINTER,
|
||||
sInter: SINTER,
|
||||
SINTERSTORE,
|
||||
sInterStore: SINTERSTORE,
|
||||
SET,
|
||||
set: SET,
|
||||
SETBIT,
|
||||
setBit: SETBIT,
|
||||
SETEX,
|
||||
setEx: SETEX,
|
||||
SETNX,
|
||||
setNX: SETNX,
|
||||
SETRANGE,
|
||||
setRange: SETRANGE,
|
||||
SISMEMBER,
|
||||
sIsMember: SISMEMBER,
|
||||
SMEMBERS,
|
||||
sMembers: SMEMBERS,
|
||||
SMISMEMBER,
|
||||
smIsMember: SMISMEMBER,
|
||||
SMOVE,
|
||||
sMove: SMOVE,
|
||||
SORT,
|
||||
sort: SORT,
|
||||
SPOP,
|
||||
sPop: SPOP,
|
||||
SRANDMEMBER_COUNT,
|
||||
sRandMemberCount: SRANDMEMBER_COUNT,
|
||||
SRANDMEMBER,
|
||||
sRandMember: SRANDMEMBER,
|
||||
SREM,
|
||||
sRem: SREM,
|
||||
SSCAN,
|
||||
sScan: SSCAN,
|
||||
STRLEN,
|
||||
strLen: STRLEN,
|
||||
SUNION,
|
||||
sUnion: SUNION,
|
||||
SUNIONSTORE,
|
||||
sUnionStore: SUNIONSTORE,
|
||||
TOUCH,
|
||||
touch: TOUCH,
|
||||
TTL,
|
||||
ttl: TTL,
|
||||
TYPE,
|
||||
type: TYPE,
|
||||
UNLINK,
|
||||
unlink: UNLINK,
|
||||
WATCH,
|
||||
watch: WATCH,
|
||||
XACK,
|
||||
xAck: XACK,
|
||||
XADD,
|
||||
xAdd: XADD,
|
||||
XAUTOCLAIM_JUSTID,
|
||||
xAutoClaimJustId: XAUTOCLAIM_JUSTID,
|
||||
XAUTOCLAIM,
|
||||
xAutoClaim: XAUTOCLAIM,
|
||||
XCLAIM,
|
||||
xClaim: XCLAIM,
|
||||
XCLAIM_JUSTID,
|
||||
xClaimJustId: XCLAIM_JUSTID,
|
||||
XDEL,
|
||||
xDel: XDEL,
|
||||
XGROUP_CREATE,
|
||||
xGroupCreate: XGROUP_CREATE,
|
||||
XGROUP_CREATECONSUMER,
|
||||
xGroupCreateConsumer: XGROUP_CREATECONSUMER,
|
||||
XGROUP_DELCONSUMER,
|
||||
xGroupDelConsumer: XGROUP_DELCONSUMER,
|
||||
XGROUP_DESTROY,
|
||||
xGroupDestroy: XGROUP_DESTROY,
|
||||
XGROUP_SETID,
|
||||
xGroupSetId: XGROUP_SETID,
|
||||
XINFO_CONSUMERS,
|
||||
xInfoConsumers: XINFO_CONSUMERS,
|
||||
XINFO_GROUPS,
|
||||
xInfoGroups: XINFO_GROUPS,
|
||||
XINFO_STREAM,
|
||||
xInfoStream: XINFO_STREAM,
|
||||
XLEN,
|
||||
xLen: XLEN,
|
||||
XPENDING_RANGE,
|
||||
xPendingRange: XPENDING_RANGE,
|
||||
XPENDING,
|
||||
xPending: XPENDING,
|
||||
XRANGE,
|
||||
xRange: XRANGE,
|
||||
XREAD,
|
||||
xRead: XREAD,
|
||||
XREADGROUP,
|
||||
xReadGroup: XREADGROUP,
|
||||
XREVRANGE,
|
||||
xRevRange: XREVRANGE,
|
||||
XTRIM,
|
||||
xTrim: XTRIM,
|
||||
ZADD,
|
||||
zAdd: ZADD,
|
||||
ZCARD,
|
||||
zCard: ZCARD,
|
||||
ZCOUNT,
|
||||
zCount: ZCOUNT,
|
||||
ZDIFF_WITHSCORES,
|
||||
zDiffWithScores: ZDIFF_WITHSCORES,
|
||||
ZDIFF,
|
||||
zDiff: ZDIFF,
|
||||
ZDIFFSTORE,
|
||||
zDiffStore: ZDIFFSTORE,
|
||||
ZINCRBY,
|
||||
zIncrBy: ZINCRBY,
|
||||
ZINTER_WITHSCORES,
|
||||
zInterWithScores: ZINTER_WITHSCORES,
|
||||
ZINTER,
|
||||
zInter: ZINTER,
|
||||
ZINTERSTORE,
|
||||
zInterStore: ZINTERSTORE,
|
||||
ZLEXCOUNT,
|
||||
zLexCount: ZLEXCOUNT,
|
||||
ZMSCORE,
|
||||
zmScore: ZMSCORE,
|
||||
ZPOPMAX_COUNT,
|
||||
zPopMaxCount: ZPOPMAX_COUNT,
|
||||
ZPOPMAX,
|
||||
zPopMax: ZPOPMAX,
|
||||
ZPOPMIN_COUNT,
|
||||
zPopMinCount: ZPOPMIN_COUNT,
|
||||
ZPOPMIN,
|
||||
zPopMin: ZPOPMIN,
|
||||
ZRANDMEMBER_COUNT_WITHSCORES,
|
||||
zRandMemberCountWithScores: ZRANDMEMBER_COUNT_WITHSCORES,
|
||||
ZRANDMEMBER_COUNT,
|
||||
zRandMemberCount: ZRANDMEMBER_COUNT,
|
||||
ZRANDMEMBER,
|
||||
zRandMember: ZRANDMEMBER,
|
||||
ZRANGE_WITHSCORES,
|
||||
zRangeWithScores: ZRANGE_WITHSCORES,
|
||||
ZRANGE,
|
||||
zRange: ZRANGE,
|
||||
ZRANGEBYLEX,
|
||||
zRangeByLex: ZRANGEBYLEX,
|
||||
ZRANGEBYSCORE_WITHSCORES,
|
||||
zRangeByScoreWithScores: ZRANGEBYSCORE_WITHSCORES,
|
||||
ZRANGEBYSCORE,
|
||||
zRangeByScore: ZRANGEBYSCORE,
|
||||
ZRANGESTORE,
|
||||
zRangeStore: ZRANGESTORE,
|
||||
ZRANK,
|
||||
zRank: ZRANK,
|
||||
ZREM,
|
||||
zRem: ZREM,
|
||||
ZREMRANGEBYLEX,
|
||||
zRemRangeByLex: ZREMRANGEBYLEX,
|
||||
ZREMRANGEBYRANK,
|
||||
zRemRangeByRank: ZREMRANGEBYRANK,
|
||||
ZREMRANGEBYSCORE,
|
||||
zRemRangeByScore: ZREMRANGEBYSCORE,
|
||||
ZREVRANK,
|
||||
zRevRank: ZREVRANK,
|
||||
ZSCAN,
|
||||
zScan: ZSCAN,
|
||||
ZSCORE,
|
||||
zScore: ZSCORE,
|
||||
ZUNION_WITHSCORES,
|
||||
zUnionWithScores: ZUNION_WITHSCORES,
|
||||
ZUNION,
|
||||
zUnion: ZUNION,
|
||||
ZUNIONSTORE,
|
||||
zUnionStore: ZUNIONSTORE
|
||||
};
|
93
packages/client/lib/cluster/index.spec.ts
Normal file
93
packages/client/lib/cluster/index.spec.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import calculateSlot from 'cluster-key-slot';
|
||||
import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
|
||||
import { SQUARE_SCRIPT } from '../client/index.spec';
|
||||
|
||||
describe('Cluster', () => {
|
||||
testUtils.testWithCluster('sendCommand', async cluster => {
|
||||
await cluster.connect();
|
||||
|
||||
try {
|
||||
await cluster.publish('channel', 'message');
|
||||
await cluster.set('a', 'b');
|
||||
await cluster.set('a{a}', 'bb');
|
||||
await cluster.set('aa', 'bb');
|
||||
await cluster.get('aa');
|
||||
await cluster.get('aa');
|
||||
await cluster.get('aa');
|
||||
await cluster.get('aa');
|
||||
} finally {
|
||||
await cluster.disconnect();
|
||||
}
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('multi', async cluster => {
|
||||
const key = 'key';
|
||||
assert.deepEqual(
|
||||
await cluster.multi()
|
||||
.set(key, 'value')
|
||||
.get(key)
|
||||
.exec(),
|
||||
['OK', 'value']
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('scripts', async cluster => {
|
||||
assert.equal(
|
||||
await cluster.square(2),
|
||||
4
|
||||
);
|
||||
}, {
|
||||
...GLOBAL.CLUSTERS.OPEN,
|
||||
clusterConfiguration: {
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// testUtils.testWithCluster('should handle live resharding', async cluster => {
|
||||
// const key = 'key',
|
||||
// value = 'value';
|
||||
// await cluster.set(key, value);
|
||||
|
||||
// const slot = calculateSlot(key),
|
||||
// from = cluster.getSlotMaster(slot),
|
||||
// to = cluster.getMasters().find(node => node.id !== from.id);
|
||||
|
||||
// await to!.client.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, from.id);
|
||||
|
||||
// // should be able to get the key from the original node before it was migrated
|
||||
// assert.equal(
|
||||
// await cluster.get(key),
|
||||
// value
|
||||
// );
|
||||
|
||||
// await from.client.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, to!.id);
|
||||
|
||||
// // should be able to get the key from the original node using the "ASKING" command
|
||||
// assert.equal(
|
||||
// await cluster.get(key),
|
||||
// value
|
||||
// );
|
||||
|
||||
// const { port: toPort } = <any>to!.client.options!.socket;
|
||||
|
||||
// await from.client.migrate(
|
||||
// '127.0.0.1',
|
||||
// toPort,
|
||||
// key,
|
||||
// 0,
|
||||
// 10
|
||||
// );
|
||||
|
||||
// // should be able to get the key from the new node
|
||||
// assert.equal(
|
||||
// await cluster.get(key),
|
||||
// value
|
||||
// );
|
||||
// }, {
|
||||
// serverArguments: []
|
||||
// });
|
||||
});
|
206
packages/client/lib/cluster/index.ts
Normal file
206
packages/client/lib/cluster/index.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import COMMANDS from './commands';
|
||||
import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
||||
import { ClientCommandOptions, RedisClientCommandSignature, RedisClientOptions, RedisClientType, WithModules, WithScripts } from '../client';
|
||||
import RedisClusterSlots, { ClusterNode } from './cluster-slots';
|
||||
import { extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, extendWithCommands } from '../commander';
|
||||
import { EventEmitter } from 'events';
|
||||
import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command';
|
||||
import { RedisMultiQueuedCommand } from '../multi-command';
|
||||
|
||||
export type RedisClusterClientOptions = Omit<RedisClientOptions<Record<string, never>, Record<string, never>>, 'modules' | 'scripts'>;
|
||||
|
||||
export interface RedisClusterOptions<M extends RedisModules, S extends RedisScripts> extends RedisPlugins<M, S> {
|
||||
rootNodes: Array<RedisClusterClientOptions>;
|
||||
defaults?: Partial<RedisClusterClientOptions>;
|
||||
useReplicas?: boolean;
|
||||
maxCommandRedirections?: number;
|
||||
}
|
||||
|
||||
type WithCommands = {
|
||||
[P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>;
|
||||
};
|
||||
|
||||
export type RedisClusterType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
|
||||
RedisCluster<M, S> & WithCommands & WithModules<M> & WithScripts<S>;
|
||||
|
||||
export default class RedisCluster<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> extends EventEmitter {
|
||||
static extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: RedisCommandArguments): string | Buffer | undefined {
|
||||
if (command.FIRST_KEY_INDEX === undefined) {
|
||||
return undefined;
|
||||
} else if (typeof command.FIRST_KEY_INDEX === 'number') {
|
||||
return redisArgs[command.FIRST_KEY_INDEX];
|
||||
}
|
||||
|
||||
return command.FIRST_KEY_INDEX(...originalArgs);
|
||||
}
|
||||
|
||||
static create<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
|
||||
return new (<any>extendWithModulesAndScripts({
|
||||
BaseClass: RedisCluster,
|
||||
modules: options?.modules,
|
||||
modulesCommandsExecutor: RedisCluster.prototype.commandsExecutor,
|
||||
scripts: options?.scripts,
|
||||
scriptsExecutor: RedisCluster.prototype.scriptsExecutor
|
||||
}))(options);
|
||||
}
|
||||
|
||||
readonly #options: RedisClusterOptions<M, S>;
|
||||
readonly #slots: RedisClusterSlots<M, S>;
|
||||
readonly #Multi: new (...args: ConstructorParameters<typeof RedisClusterMultiCommand>) => RedisClusterMultiCommandType<M, S>;
|
||||
|
||||
constructor(options: RedisClusterOptions<M, S>) {
|
||||
super();
|
||||
|
||||
this.#options = options;
|
||||
this.#slots = new RedisClusterSlots(options, err => this.emit('error', err));
|
||||
this.#Multi = RedisClusterMultiCommand.extend(options);
|
||||
}
|
||||
|
||||
duplicate(overrides?: Partial<RedisClusterOptions<M, S>>): RedisClusterType<M, S> {
|
||||
return new (Object.getPrototypeOf(this).constructor)({
|
||||
...this.#options,
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
return this.#slots.connect();
|
||||
}
|
||||
|
||||
async commandsExecutor(command: RedisCommand, args: Array<unknown>): Promise<RedisCommandReply<typeof command>> {
|
||||
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
|
||||
|
||||
return transformCommandReply(
|
||||
command,
|
||||
await this.sendCommand(
|
||||
RedisCluster.extractFirstKey(command, args, redisArgs),
|
||||
command.IS_READ_ONLY,
|
||||
redisArgs,
|
||||
options,
|
||||
command.BUFFER_MODE
|
||||
),
|
||||
redisArgs.preserve
|
||||
);
|
||||
}
|
||||
|
||||
async sendCommand<C extends RedisCommand>(
|
||||
firstKey: string | Buffer | undefined,
|
||||
isReadonly: boolean | undefined,
|
||||
args: RedisCommandArguments,
|
||||
options?: ClientCommandOptions,
|
||||
bufferMode?: boolean,
|
||||
redirections = 0
|
||||
): Promise<RedisCommandReply<C>> {
|
||||
const client = this.#slots.getClient(firstKey, isReadonly);
|
||||
|
||||
try {
|
||||
return await client.sendCommand(args, options, bufferMode);
|
||||
} catch (err: any) {
|
||||
const shouldRetry = await this.#handleCommandError(err, client, redirections);
|
||||
if (shouldRetry === true) {
|
||||
return this.sendCommand(firstKey, isReadonly, args, options, bufferMode, redirections + 1);
|
||||
} else if (shouldRetry) {
|
||||
return shouldRetry.sendCommand(args, options, bufferMode);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async scriptsExecutor(script: RedisScript, args: Array<unknown>): Promise<RedisCommandReply<typeof script>> {
|
||||
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
|
||||
|
||||
return transformCommandReply(
|
||||
script,
|
||||
await this.executeScript(
|
||||
script,
|
||||
args,
|
||||
redisArgs,
|
||||
options
|
||||
),
|
||||
redisArgs.preserve
|
||||
);
|
||||
}
|
||||
|
||||
async executeScript(
|
||||
script: RedisScript,
|
||||
originalArgs: Array<unknown>,
|
||||
redisArgs: RedisCommandArguments,
|
||||
options?: ClientCommandOptions,
|
||||
redirections = 0
|
||||
): Promise<RedisCommandReply<typeof script>> {
|
||||
const client = this.#slots.getClient(
|
||||
RedisCluster.extractFirstKey(script, originalArgs, redisArgs),
|
||||
script.IS_READ_ONLY
|
||||
);
|
||||
|
||||
try {
|
||||
return await client.executeScript(script, redisArgs, options, script.BUFFER_MODE);
|
||||
} catch (err: any) {
|
||||
const shouldRetry = await this.#handleCommandError(err, client, redirections);
|
||||
if (shouldRetry === true) {
|
||||
return this.executeScript(script, originalArgs, redisArgs, options, redirections + 1);
|
||||
} else if (shouldRetry) {
|
||||
return shouldRetry.executeScript(script, redisArgs, options, script.BUFFER_MODE);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async #handleCommandError(err: Error, client: RedisClientType<M, S>, redirections: number): Promise<boolean | RedisClientType<M, S>> {
|
||||
if (redirections > (this.#options.maxCommandRedirections ?? 16)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (err.message.startsWith('ASK')) {
|
||||
const url = err.message.substring(err.message.lastIndexOf(' ') + 1);
|
||||
let node = this.#slots.getNodeByUrl(url);
|
||||
if (!node) {
|
||||
await this.#slots.discover(client);
|
||||
node = this.#slots.getNodeByUrl(url);
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`Cannot find node ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
await node.client.asking();
|
||||
return node.client;
|
||||
} else if (err.message.startsWith('MOVED')) {
|
||||
await this.#slots.discover(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
multi(routing?: string | Buffer): RedisClusterMultiCommandType<M, S> {
|
||||
return new this.#Multi(
|
||||
async (commands: Array<RedisMultiQueuedCommand>, firstKey?: string | Buffer, chainId?: symbol) => {
|
||||
return this.#slots
|
||||
.getClient(firstKey)
|
||||
.multiExecutor(commands, chainId);
|
||||
},
|
||||
routing
|
||||
);
|
||||
}
|
||||
|
||||
getMasters(): Array<ClusterNode<M, S>> {
|
||||
return this.#slots.getMasters();
|
||||
}
|
||||
|
||||
getSlotMaster(slot: number): ClusterNode<M, S> {
|
||||
return this.#slots.getSlotMaster(slot);
|
||||
}
|
||||
|
||||
disconnect(): Promise<void> {
|
||||
return this.#slots.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
extendWithCommands({
|
||||
BaseClass: RedisCluster,
|
||||
commands: COMMANDS,
|
||||
executor: RedisCluster.prototype.commandsExecutor
|
||||
});
|
112
packages/client/lib/cluster/multi-command.ts
Normal file
112
packages/client/lib/cluster/multi-command.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import COMMANDS from './commands';
|
||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
||||
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
||||
import { extendWithCommands, extendWithModulesAndScripts } from '../commander';
|
||||
import RedisCluster from '.';
|
||||
|
||||
type RedisClusterMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
|
||||
(...args: Parameters<C['transformArguments']>) => RedisClusterMultiCommandType<M, S>;
|
||||
|
||||
type WithCommands<M extends RedisModules, S extends RedisScripts> = {
|
||||
[P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, S>
|
||||
};
|
||||
|
||||
type WithModules<M extends RedisModules, S extends RedisScripts> = {
|
||||
[P in keyof M as M[P] extends never ? never : P]: {
|
||||
[C in keyof M[P]]: RedisClusterMultiCommandSignature<M[P][C], M, S>;
|
||||
};
|
||||
};
|
||||
|
||||
type WithScripts<M extends RedisModules, S extends RedisScripts> = {
|
||||
[P in keyof S as S[P] extends never ? never : P]: RedisClusterMultiCommandSignature<S[P], M, S>
|
||||
};
|
||||
|
||||
export type RedisClusterMultiCommandType<M extends RedisModules = Record<string, never>, S extends RedisScripts = Record<string, never>> =
|
||||
RedisClusterMultiCommand & WithCommands<M, S> & WithModules<M, S> & WithScripts<M, S>;
|
||||
|
||||
export type RedisClusterMultiExecutor = (queue: Array<RedisMultiQueuedCommand>, firstKey?: string | Buffer, chainId?: symbol) => Promise<Array<RedisCommandRawReply>>;
|
||||
|
||||
export default class RedisClusterMultiCommand {
|
||||
readonly #multi = new RedisMultiCommand();
|
||||
readonly #executor: RedisClusterMultiExecutor;
|
||||
#firstKey: string | Buffer | undefined;
|
||||
|
||||
static extend<M extends RedisModules, S extends RedisScripts>(
|
||||
plugins?: RedisPlugins<M, S>
|
||||
): new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisClusterMultiCommandType<M, S> {
|
||||
return <any>extendWithModulesAndScripts({
|
||||
BaseClass: RedisClusterMultiCommand,
|
||||
modules: plugins?.modules,
|
||||
modulesCommandsExecutor: RedisClusterMultiCommand.prototype.commandsExecutor,
|
||||
scripts: plugins?.scripts,
|
||||
scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor
|
||||
});
|
||||
}
|
||||
|
||||
constructor(executor: RedisClusterMultiExecutor, firstKey?: string | Buffer) {
|
||||
this.#executor = executor;
|
||||
this.#firstKey = firstKey;
|
||||
}
|
||||
|
||||
commandsExecutor(command: RedisCommand, args: Array<unknown>): this {
|
||||
const transformedArguments = command.transformArguments(...args);
|
||||
if (!this.#firstKey) {
|
||||
this.#firstKey = RedisCluster.extractFirstKey(command, args, transformedArguments);
|
||||
}
|
||||
|
||||
return this.addCommand(
|
||||
undefined,
|
||||
transformedArguments,
|
||||
command.transformReply
|
||||
);
|
||||
}
|
||||
|
||||
addCommand(
|
||||
firstKey: string | Buffer | undefined,
|
||||
args: RedisCommandArguments,
|
||||
transformReply?: RedisCommand['transformReply']
|
||||
): this {
|
||||
if (!this.#firstKey) {
|
||||
this.#firstKey = firstKey;
|
||||
}
|
||||
|
||||
this.#multi.addCommand(args, transformReply);
|
||||
return this;
|
||||
}
|
||||
|
||||
scriptsExecutor(script: RedisScript, args: Array<unknown>): this {
|
||||
const transformedArguments = this.#multi.addScript(script, args);
|
||||
if (!this.#firstKey) {
|
||||
this.#firstKey = RedisCluster.extractFirstKey(script, args, transformedArguments);
|
||||
}
|
||||
|
||||
return this.addCommand(undefined, transformedArguments);
|
||||
}
|
||||
|
||||
async exec(execAsPipeline = false): Promise<Array<RedisCommandRawReply>> {
|
||||
if (execAsPipeline) {
|
||||
return this.execAsPipeline();
|
||||
}
|
||||
|
||||
const commands = this.#multi.exec();
|
||||
if (!commands) return [];
|
||||
|
||||
return this.#multi.handleExecReplies(
|
||||
await this.#executor(commands, this.#firstKey, RedisMultiCommand.generateChainId())
|
||||
);
|
||||
}
|
||||
|
||||
EXEC = this.exec;
|
||||
|
||||
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
|
||||
return this.#multi.transformReplies(
|
||||
await this.#executor(this.#multi.queue, this.#firstKey)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extendWithCommands({
|
||||
BaseClass: RedisClusterMultiCommand,
|
||||
commands: COMMANDS,
|
||||
executor: RedisClusterMultiCommand.prototype.commandsExecutor
|
||||
});
|
14
packages/client/lib/command-options.ts
Normal file
14
packages/client/lib/command-options.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const symbol = Symbol('Command Options');
|
||||
|
||||
export type CommandOptions<T> = T & {
|
||||
readonly [symbol]: true;
|
||||
};
|
||||
|
||||
export function commandOptions<T>(options: T): CommandOptions<T> {
|
||||
(options as any)[symbol] = true;
|
||||
return options as CommandOptions<T>;
|
||||
}
|
||||
|
||||
export function isCommandOptions<T>(options: any): options is CommandOptions<T> {
|
||||
return options && options[symbol] === true;
|
||||
}
|
44
packages/client/lib/commander.spec.ts
Normal file
44
packages/client/lib/commander.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describe } from 'mocha';
|
||||
import { encodeCommand } from './commander';
|
||||
|
||||
function encodeCommandToString(...args: Parameters<typeof encodeCommand>): string {
|
||||
const arr = [];
|
||||
for (const item of encodeCommand(...args)) {
|
||||
arr.push(item.toString());
|
||||
}
|
||||
|
||||
return arr.join('');
|
||||
}
|
||||
|
||||
describe('Commander', () => {
|
||||
describe('encodeCommand (see #1628)', () => {
|
||||
it('1 byte', () => {
|
||||
assert.equal(
|
||||
encodeCommandToString(['a', 'z']),
|
||||
'*2\r\n$1\r\na\r\n$1\r\nz\r\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('2 bytes', () => {
|
||||
assert.equal(
|
||||
encodeCommandToString(['א', 'ת']),
|
||||
'*2\r\n$2\r\nא\r\n$2\r\nת\r\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('4 bytes', () => {
|
||||
assert.equal(
|
||||
encodeCommandToString(['🐣', '🐤']),
|
||||
'*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('with a buffer', () => {
|
||||
assert.equal(
|
||||
encodeCommandToString([Buffer.from('string')]),
|
||||
'*1\r\n$6\r\nstring\r\n'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
115
packages/client/lib/commander.ts
Normal file
115
packages/client/lib/commander.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
|
||||
import { CommandOptions, isCommandOptions } from './command-options';
|
||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisCommands, RedisModules, RedisScript, RedisScripts } from './commands';
|
||||
|
||||
type Instantiable<T = any> = new(...args: Array<any>) => T;
|
||||
|
||||
interface ExtendWithCommandsConfig<T extends Instantiable> {
|
||||
BaseClass: T;
|
||||
commands: RedisCommands;
|
||||
executor(command: RedisCommand, args: Array<unknown>): unknown;
|
||||
}
|
||||
|
||||
export function extendWithCommands<T extends Instantiable>({ BaseClass, commands, executor }: ExtendWithCommandsConfig<T>): void {
|
||||
for (const [name, command] of Object.entries(commands)) {
|
||||
BaseClass.prototype[name] = function (...args: Array<unknown>): unknown {
|
||||
return executor.call(this, command, args);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface ExtendWithModulesAndScriptsConfig<T extends Instantiable> {
|
||||
BaseClass: T;
|
||||
modules?: RedisModules;
|
||||
modulesCommandsExecutor(this: InstanceType<T>, command: RedisCommand, args: Array<unknown>): unknown;
|
||||
scripts?: RedisScripts;
|
||||
scriptsExecutor(this: InstanceType<T>, script: RedisScript, args: Array<unknown>): unknown;
|
||||
}
|
||||
|
||||
export function extendWithModulesAndScripts<T extends Instantiable>(config: ExtendWithModulesAndScriptsConfig<T>): T {
|
||||
let Commander: T | undefined;
|
||||
|
||||
if (config.modules) {
|
||||
Commander = class extends config.BaseClass {
|
||||
constructor(...args: Array<any>) {
|
||||
super(...args);
|
||||
|
||||
for (const module of Object.keys(config.modules!)) {
|
||||
this[module] = new this[module](this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const [moduleName, module] of Object.entries(config.modules)) {
|
||||
Commander.prototype[moduleName] = class {
|
||||
readonly self: T;
|
||||
|
||||
constructor(self: InstanceType<T>) {
|
||||
this.self = self;
|
||||
}
|
||||
};
|
||||
|
||||
for (const [commandName, command] of Object.entries(module)) {
|
||||
Commander.prototype[moduleName].prototype[commandName] = function (...args: Array<unknown>): unknown {
|
||||
return config.modulesCommandsExecutor.call(this.self, command, args);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.scripts) {
|
||||
Commander ??= class extends config.BaseClass {};
|
||||
|
||||
for (const [name, script] of Object.entries(config.scripts)) {
|
||||
Commander.prototype[name] = function (...args: Array<unknown>): unknown {
|
||||
return config.scriptsExecutor.call(this, script, args);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (Commander ?? config.BaseClass) as any;
|
||||
}
|
||||
|
||||
export function transformCommandArguments<T = unknown>(
|
||||
command: RedisCommand,
|
||||
args: Array<unknown>
|
||||
): {
|
||||
args: RedisCommandArguments;
|
||||
options: CommandOptions<T> | undefined;
|
||||
} {
|
||||
let options;
|
||||
if (isCommandOptions<T>(args[0])) {
|
||||
options = args[0];
|
||||
args = args.slice(1);
|
||||
}
|
||||
|
||||
return {
|
||||
args: command.transformArguments(...args),
|
||||
options
|
||||
};
|
||||
}
|
||||
|
||||
const DELIMITER = '\r\n';
|
||||
|
||||
export function* encodeCommand(args: RedisCommandArguments): IterableIterator<string | Buffer> {
|
||||
yield `*${args.length}${DELIMITER}`;
|
||||
|
||||
for (const arg of args) {
|
||||
const byteLength = typeof arg === 'string' ? Buffer.byteLength(arg): arg.length;
|
||||
yield `$${byteLength.toString()}${DELIMITER}`;
|
||||
yield arg;
|
||||
yield DELIMITER;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformCommandReply(
|
||||
command: RedisCommand,
|
||||
rawReply: RedisCommandRawReply,
|
||||
preserved: unknown
|
||||
): RedisCommandReply<typeof command> {
|
||||
if (!command.transformReply) {
|
||||
return rawReply;
|
||||
}
|
||||
|
||||
return command.transformReply(rawReply, preserved);
|
||||
}
|
23
packages/client/lib/commands/ACL_CAT.spec.ts
Normal file
23
packages/client/lib/commands/ACL_CAT.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_CAT';
|
||||
|
||||
describe('ACL CAT', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'CAT']
|
||||
);
|
||||
});
|
||||
|
||||
it('with categoryName', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('dangerous'),
|
||||
['ACL', 'CAT', 'dangerous']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
11
packages/client/lib/commands/ACL_CAT.ts
Normal file
11
packages/client/lib/commands/ACL_CAT.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export function transformArguments(categoryName?: string): Array<string> {
|
||||
const args = ['ACL', 'CAT'];
|
||||
|
||||
if (categoryName) {
|
||||
args.push(categoryName);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<string>;
|
30
packages/client/lib/commands/ACL_DELUSER.spec.ts
Normal file
30
packages/client/lib/commands/ACL_DELUSER.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './ACL_DELUSER';
|
||||
|
||||
describe('ACL DELUSER', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('string', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('username'),
|
||||
['ACL', 'DELUSER', 'username']
|
||||
);
|
||||
});
|
||||
|
||||
it('array', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(['1', '2']),
|
||||
['ACL', 'DELUSER', '1', '2']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclDelUser', async client => {
|
||||
assert.equal(
|
||||
await client.aclDelUser('dosenotexists'),
|
||||
0
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
8
packages/client/lib/commands/ACL_DELUSER.ts
Normal file
8
packages/client/lib/commands/ACL_DELUSER.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments } from './generic-transformers';
|
||||
|
||||
export function transformArguments(username: string | Array<string>): RedisCommandArguments {
|
||||
return pushVerdictArguments(['ACL', 'DELUSER'], username);
|
||||
}
|
||||
|
||||
export declare const transformReply: (reply: number) => number;
|
23
packages/client/lib/commands/ACL_GENPASS.spec.ts
Normal file
23
packages/client/lib/commands/ACL_GENPASS.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_GENPASS';
|
||||
|
||||
describe('ACL GENPASS', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'GENPASS']
|
||||
);
|
||||
});
|
||||
|
||||
it('with bits', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(128),
|
||||
['ACL', 'GENPASS', '128']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
11
packages/client/lib/commands/ACL_GENPASS.ts
Normal file
11
packages/client/lib/commands/ACL_GENPASS.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export function transformArguments(bits?: number): Array<string> {
|
||||
const args = ['ACL', 'GENPASS'];
|
||||
|
||||
if (bits) {
|
||||
args.push(bits.toString());
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
32
packages/client/lib/commands/ACL_GETUSER.spec.ts
Normal file
32
packages/client/lib/commands/ACL_GETUSER.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './ACL_GETUSER';
|
||||
|
||||
describe('ACL GETUSER', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('username'),
|
||||
['ACL', 'GETUSER', 'username']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.aclGetUser', async client => {
|
||||
assert.deepEqual(
|
||||
await client.aclGetUser('default'),
|
||||
{
|
||||
passwords: [],
|
||||
commands: '+@all',
|
||||
keys: ['*'],
|
||||
...(testUtils.isVersionGreaterThan([6, 2]) ? {
|
||||
flags: ['on', 'allkeys', 'allchannels', 'allcommands', 'nopass'],
|
||||
channels: ['*']
|
||||
} : {
|
||||
flags: ['on', 'allkeys', 'allcommands', 'nopass'],
|
||||
channels: undefined
|
||||
})
|
||||
}
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
34
packages/client/lib/commands/ACL_GETUSER.ts
Normal file
34
packages/client/lib/commands/ACL_GETUSER.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export function transformArguments(username: string): Array<string> {
|
||||
return ['ACL', 'GETUSER', username];
|
||||
}
|
||||
|
||||
type AclGetUserRawReply = [
|
||||
_: string,
|
||||
flags: Array<string>,
|
||||
_: string,
|
||||
passwords: Array<string>,
|
||||
_: string,
|
||||
commands: string,
|
||||
_: string,
|
||||
keys: Array<string>,
|
||||
_: string,
|
||||
channels: Array<string>
|
||||
];
|
||||
|
||||
interface AclUser {
|
||||
flags: Array<string>;
|
||||
passwords: Array<string>;
|
||||
commands: string;
|
||||
keys: Array<string>;
|
||||
channels: Array<string>
|
||||
}
|
||||
|
||||
export function transformReply(reply: AclGetUserRawReply): AclUser {
|
||||
return {
|
||||
flags: reply[1],
|
||||
passwords: reply[3],
|
||||
commands: reply[5],
|
||||
keys: reply[7],
|
||||
channels: reply[9]
|
||||
};
|
||||
}
|
14
packages/client/lib/commands/ACL_LIST.spec.ts
Normal file
14
packages/client/lib/commands/ACL_LIST.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_LIST';
|
||||
|
||||
describe('ACL LIST', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'LIST']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_LIST.ts
Normal file
5
packages/client/lib/commands/ACL_LIST.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'LIST'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<string>;
|
14
packages/client/lib/commands/ACL_LOAD.spec.ts
Normal file
14
packages/client/lib/commands/ACL_LOAD.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_SAVE';
|
||||
|
||||
describe('ACL SAVE', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'SAVE']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_LOAD.ts
Normal file
5
packages/client/lib/commands/ACL_LOAD.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'LOAD'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
53
packages/client/lib/commands/ACL_LOG.spec.ts
Normal file
53
packages/client/lib/commands/ACL_LOG.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments, transformReply } from './ACL_LOG';
|
||||
|
||||
describe('ACL LOG', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'LOG']
|
||||
);
|
||||
});
|
||||
|
||||
it('with count', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(10),
|
||||
['ACL', 'LOG', '10']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('transformReply', () => {
|
||||
assert.deepEqual(
|
||||
transformReply([[
|
||||
'count',
|
||||
1,
|
||||
'reason',
|
||||
'auth',
|
||||
'context',
|
||||
'toplevel',
|
||||
'object',
|
||||
'AUTH',
|
||||
'username',
|
||||
'someuser',
|
||||
'age-seconds',
|
||||
'4.096',
|
||||
'client-info',
|
||||
'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
|
||||
]]),
|
||||
[{
|
||||
count: 1,
|
||||
reason: 'auth',
|
||||
context: 'toplevel',
|
||||
object: 'AUTH',
|
||||
username: 'someuser',
|
||||
ageSeconds: 4.096,
|
||||
clientInfo: 'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
|
||||
}]
|
||||
);
|
||||
});
|
||||
});
|
48
packages/client/lib/commands/ACL_LOG.ts
Normal file
48
packages/client/lib/commands/ACL_LOG.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export function transformArguments(count?: number): Array<string> {
|
||||
const args = ['ACL', 'LOG'];
|
||||
|
||||
if (count) {
|
||||
args.push(count.toString());
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
type AclLogRawReply = [
|
||||
_: string,
|
||||
count: number,
|
||||
_: string,
|
||||
reason: string,
|
||||
_: string,
|
||||
context: string,
|
||||
_: string,
|
||||
object: string,
|
||||
_: string,
|
||||
username: string,
|
||||
_: string,
|
||||
ageSeconds: string,
|
||||
_: string,
|
||||
clientInfo: string
|
||||
];
|
||||
|
||||
interface AclLog {
|
||||
count: number;
|
||||
reason: string;
|
||||
context: string;
|
||||
object: string;
|
||||
username: string;
|
||||
ageSeconds: number;
|
||||
clientInfo: string;
|
||||
}
|
||||
|
||||
export function transformReply(reply: Array<AclLogRawReply>): Array<AclLog> {
|
||||
return reply.map(log => ({
|
||||
count: log[1],
|
||||
reason: log[3],
|
||||
context: log[5],
|
||||
object: log[7],
|
||||
username: log[9],
|
||||
ageSeconds: Number(log[11]),
|
||||
clientInfo: log[13]
|
||||
}));
|
||||
}
|
14
packages/client/lib/commands/ACL_LOG_RESET.spec.ts
Normal file
14
packages/client/lib/commands/ACL_LOG_RESET.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_LOG_RESET';
|
||||
|
||||
describe('ACL LOG RESET', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'LOG', 'RESET']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_LOG_RESET.ts
Normal file
5
packages/client/lib/commands/ACL_LOG_RESET.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'LOG', 'RESET'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
14
packages/client/lib/commands/ACL_SAVE.spec.ts
Normal file
14
packages/client/lib/commands/ACL_SAVE.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_LOAD';
|
||||
|
||||
describe('ACL LOAD', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'LOAD']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_SAVE.ts
Normal file
5
packages/client/lib/commands/ACL_SAVE.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'SAVE'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
23
packages/client/lib/commands/ACL_SETUSER.spec.ts
Normal file
23
packages/client/lib/commands/ACL_SETUSER.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_SETUSER';
|
||||
|
||||
describe('ACL SETUSER', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
describe('transformArguments', () => {
|
||||
it('string', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('username', 'allkeys'),
|
||||
['ACL', 'SETUSER', 'username', 'allkeys']
|
||||
);
|
||||
});
|
||||
|
||||
it('array', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('username', ['allkeys', 'allchannels']),
|
||||
['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
8
packages/client/lib/commands/ACL_SETUSER.ts
Normal file
8
packages/client/lib/commands/ACL_SETUSER.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments } from './generic-transformers';
|
||||
|
||||
export function transformArguments(username: string, rule: string | Array<string>): RedisCommandArguments {
|
||||
return pushVerdictArguments(['ACL', 'SETUSER', username], rule);
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
14
packages/client/lib/commands/ACL_USERS.spec.ts
Normal file
14
packages/client/lib/commands/ACL_USERS.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_USERS';
|
||||
|
||||
describe('ACL USERS', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'USERS']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_USERS.ts
Normal file
5
packages/client/lib/commands/ACL_USERS.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'USERS'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<string>;
|
14
packages/client/lib/commands/ACL_WHOAMI.spec.ts
Normal file
14
packages/client/lib/commands/ACL_WHOAMI.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils from '../test-utils';
|
||||
import { transformArguments } from './ACL_WHOAMI';
|
||||
|
||||
describe('ACL WHOAMI', () => {
|
||||
testUtils.isVersionGreaterThanHook([6]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ACL', 'WHOAMI']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ACL_WHOAMI.ts
Normal file
5
packages/client/lib/commands/ACL_WHOAMI.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'WHOAMI'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
11
packages/client/lib/commands/APPEND.spec.ts
Normal file
11
packages/client/lib/commands/APPEND.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './APPEND';
|
||||
|
||||
describe('APPEND', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 'value'),
|
||||
['APPEND', 'key', 'value']
|
||||
);
|
||||
});
|
||||
});
|
7
packages/client/lib/commands/APPEND.ts
Normal file
7
packages/client/lib/commands/APPEND.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(key: string, value: string): Array<string> {
|
||||
return ['APPEND', key, value];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
11
packages/client/lib/commands/ASKING.spec.ts
Normal file
11
packages/client/lib/commands/ASKING.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './ASKING';
|
||||
|
||||
describe('ASKING', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['ASKING']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/ASKING.ts
Normal file
5
packages/client/lib/commands/ASKING.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ASKING'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
25
packages/client/lib/commands/AUTH.spec.ts
Normal file
25
packages/client/lib/commands/AUTH.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './AUTH';
|
||||
|
||||
describe('AUTH', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('password only', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments({
|
||||
password: 'password'
|
||||
}),
|
||||
['AUTH', 'password']
|
||||
);
|
||||
});
|
||||
|
||||
it('username & password', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments({
|
||||
username: 'username',
|
||||
password: 'password'
|
||||
}),
|
||||
['AUTH', 'username', 'password']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
14
packages/client/lib/commands/AUTH.ts
Normal file
14
packages/client/lib/commands/AUTH.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface AuthOptions {
|
||||
username?: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export function transformArguments({username, password}: AuthOptions): Array<string> {
|
||||
if (!username) {
|
||||
return ['AUTH', password];
|
||||
}
|
||||
|
||||
return ['AUTH', username, password];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
11
packages/client/lib/commands/BGREWRITEAOF.spec.ts
Normal file
11
packages/client/lib/commands/BGREWRITEAOF.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './BGREWRITEAOF';
|
||||
|
||||
describe('BGREWRITEAOF', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['BGREWRITEAOF']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/BGREWRITEAOF.ts
Normal file
5
packages/client/lib/commands/BGREWRITEAOF.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['BGREWRITEAOF'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
23
packages/client/lib/commands/BGSAVE.spec.ts
Normal file
23
packages/client/lib/commands/BGSAVE.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { describe } from 'mocha';
|
||||
import { transformArguments } from './BGSAVE';
|
||||
|
||||
describe('BGSAVE', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['BGSAVE']
|
||||
);
|
||||
});
|
||||
|
||||
it('with SCHEDULE', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments({
|
||||
SCHEDULE: true
|
||||
}),
|
||||
['BGSAVE', 'SCHEDULE']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
15
packages/client/lib/commands/BGSAVE.ts
Normal file
15
packages/client/lib/commands/BGSAVE.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
interface BgSaveOptions {
|
||||
SCHEDULE?: true;
|
||||
}
|
||||
|
||||
export function transformArguments(options?: BgSaveOptions): Array<string> {
|
||||
const args = ['BGSAVE'];
|
||||
|
||||
if (options?.SCHEDULE) {
|
||||
args.push('SCHEDULE');
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
31
packages/client/lib/commands/BITCOUNT.spec.ts
Normal file
31
packages/client/lib/commands/BITCOUNT.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITCOUNT';
|
||||
|
||||
describe('BITCOUNT', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key'),
|
||||
['BITCOUNT', 'key']
|
||||
);
|
||||
});
|
||||
|
||||
it('with range', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', {
|
||||
start: 0,
|
||||
end: 1
|
||||
}),
|
||||
['BITCOUNT', 'key', '0', '1']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bitCount', async client => {
|
||||
assert.equal(
|
||||
await client.bitCount('key'),
|
||||
0
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
23
packages/client/lib/commands/BITCOUNT.ts
Normal file
23
packages/client/lib/commands/BITCOUNT.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export const IS_READ_ONLY = true;
|
||||
|
||||
interface BitCountRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export function transformArguments(key: string, range?: BitCountRange): Array<string> {
|
||||
const args = ['BITCOUNT', key];
|
||||
|
||||
if (range) {
|
||||
args.push(
|
||||
range.start.toString(),
|
||||
range.end.toString()
|
||||
);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): number;
|
42
packages/client/lib/commands/BITFIELD.spec.ts
Normal file
42
packages/client/lib/commands/BITFIELD.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITFIELD';
|
||||
|
||||
describe('BITFIELD', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', [{
|
||||
operation: 'OVERFLOW',
|
||||
behavior: 'WRAP'
|
||||
}, {
|
||||
operation: 'GET',
|
||||
type: 'i8',
|
||||
offset: 0
|
||||
}, {
|
||||
operation: 'OVERFLOW',
|
||||
behavior: 'SAT'
|
||||
}, {
|
||||
operation: 'SET',
|
||||
type: 'i16',
|
||||
offset: 1,
|
||||
value: 0
|
||||
}, {
|
||||
operation: 'OVERFLOW',
|
||||
behavior: 'FAIL'
|
||||
}, {
|
||||
operation: 'INCRBY',
|
||||
type: 'i32',
|
||||
offset: 2,
|
||||
increment: 1
|
||||
}]),
|
||||
['BITFIELD', 'key', 'OVERFLOW', 'WRAP', 'GET', 'i8', '0', 'OVERFLOW', 'SAT', 'SET', 'i16', '1', '0', 'OVERFLOW', 'FAIL', 'INCRBY', 'i32', '2', '1']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bitField', async client => {
|
||||
assert.deepEqual(
|
||||
await client.bitField('key', []),
|
||||
[]
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
82
packages/client/lib/commands/BITFIELD.ts
Normal file
82
packages/client/lib/commands/BITFIELD.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export const IS_READ_ONLY = true;
|
||||
|
||||
type BitFieldType = string; // TODO 'i[1-64]' | 'u[1-63]'
|
||||
|
||||
interface BitFieldOperation<S extends string> {
|
||||
operation: S;
|
||||
}
|
||||
|
||||
interface BitFieldGetOperation extends BitFieldOperation<'GET'> {
|
||||
type: BitFieldType;
|
||||
offset: number | string;
|
||||
}
|
||||
|
||||
interface BitFieldSetOperation extends BitFieldOperation<'SET'> {
|
||||
type: BitFieldType;
|
||||
offset: number | string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface BitFieldIncrByOperation extends BitFieldOperation<'INCRBY'> {
|
||||
type: BitFieldType;
|
||||
offset: number | string;
|
||||
increment: number;
|
||||
}
|
||||
|
||||
interface BitFieldOverflowOperation extends BitFieldOperation<'OVERFLOW'> {
|
||||
behavior: string;
|
||||
}
|
||||
|
||||
type BitFieldOperations = Array<
|
||||
BitFieldGetOperation |
|
||||
BitFieldSetOperation |
|
||||
BitFieldIncrByOperation |
|
||||
BitFieldOverflowOperation
|
||||
>;
|
||||
|
||||
export function transformArguments(key: string, operations: BitFieldOperations): Array<string> {
|
||||
const args = ['BITFIELD', key];
|
||||
|
||||
for (const options of operations) {
|
||||
switch (options.operation) {
|
||||
case 'GET':
|
||||
args.push(
|
||||
'GET',
|
||||
options.type,
|
||||
options.offset.toString()
|
||||
);
|
||||
break;
|
||||
|
||||
case 'SET':
|
||||
args.push(
|
||||
'SET',
|
||||
options.type,
|
||||
options.offset.toString(),
|
||||
options.value.toString()
|
||||
);
|
||||
break;
|
||||
|
||||
case 'INCRBY':
|
||||
args.push(
|
||||
'INCRBY',
|
||||
options.type,
|
||||
options.offset.toString(),
|
||||
options.increment.toString()
|
||||
);
|
||||
break;
|
||||
|
||||
case 'OVERFLOW':
|
||||
args.push(
|
||||
'OVERFLOW',
|
||||
options.behavior
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): Array<number | null>;
|
35
packages/client/lib/commands/BITOP.spec.ts
Normal file
35
packages/client/lib/commands/BITOP.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITOP';
|
||||
|
||||
describe('BITOP', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('single key', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('AND', 'destKey', 'key'),
|
||||
['BITOP', 'AND', 'destKey', 'key']
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple keys', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('AND', 'destKey', ['1', '2']),
|
||||
['BITOP', 'AND', 'destKey', '1', '2']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bitOp', async client => {
|
||||
assert.equal(
|
||||
await client.bitOp('AND', 'destKey', 'key'),
|
||||
0
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('cluster.bitOp', async cluster => {
|
||||
assert.equal(
|
||||
await cluster.bitOp('AND', '{tag}destKey', '{tag}key'),
|
||||
0
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
});
|
12
packages/client/lib/commands/BITOP.ts
Normal file
12
packages/client/lib/commands/BITOP.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments } from './generic-transformers';
|
||||
|
||||
export const FIRST_KEY_INDEX = 2;
|
||||
|
||||
type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT';
|
||||
|
||||
export function transformArguments(operation: BitOperations, destKey: string, key: string | Array<string>): RedisCommandArguments {
|
||||
return pushVerdictArguments(['BITOP', operation, destKey], key);
|
||||
}
|
||||
|
||||
export declare function transformReply(): number;
|
42
packages/client/lib/commands/BITPOS.spec.ts
Normal file
42
packages/client/lib/commands/BITPOS.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BITPOS';
|
||||
|
||||
describe('BITPOS', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('simple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 1),
|
||||
['BITPOS', 'key', '1']
|
||||
);
|
||||
});
|
||||
|
||||
it('with start', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 1, 1),
|
||||
['BITPOS', 'key', '1', '1']
|
||||
);
|
||||
});
|
||||
|
||||
it('with start, end', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 1, 1, -1),
|
||||
['BITPOS', 'key', '1', '1', '-1']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bitPos', async client => {
|
||||
assert.equal(
|
||||
await client.bitPos('key', 1, 1),
|
||||
-1
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('cluster.bitPos', async cluster => {
|
||||
assert.equal(
|
||||
await cluster.bitPos('key', 1, 1),
|
||||
-1
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
});
|
21
packages/client/lib/commands/BITPOS.ts
Normal file
21
packages/client/lib/commands/BITPOS.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { BitValue } from './generic-transformers';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export const IS_READ_ONLY = true;
|
||||
|
||||
export function transformArguments(key: string, bit: BitValue, start?: number, end?: number): Array<string> {
|
||||
const args = ['BITPOS', key, bit.toString()];
|
||||
|
||||
if (typeof start === 'number') {
|
||||
args.push(start.toString());
|
||||
}
|
||||
|
||||
if (typeof end === 'number') {
|
||||
args.push(end.toString());
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): number;
|
43
packages/client/lib/commands/BLMOVE.spec.ts
Normal file
43
packages/client/lib/commands/BLMOVE.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BLMOVE';
|
||||
import { commandOptions } from '../../index';
|
||||
|
||||
describe('BLMOVE', () => {
|
||||
testUtils.isVersionGreaterThanHook([6, 2]);
|
||||
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('source', 'destination', 'LEFT', 'RIGHT', 0),
|
||||
['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.blMove', async client => {
|
||||
const [blMoveReply] = await Promise.all([
|
||||
client.blMove(commandOptions({
|
||||
isolated: true
|
||||
}), 'source', 'destination', 'LEFT', 'RIGHT', 0),
|
||||
client.lPush('source', 'element')
|
||||
]);
|
||||
|
||||
assert.equal(
|
||||
blMoveReply,
|
||||
'element'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('cluster.blMove', async cluster => {
|
||||
const [blMoveReply] = await Promise.all([
|
||||
cluster.blMove(commandOptions({
|
||||
isolated: true
|
||||
}), '{tag}source', '{tag}destination', 'LEFT', 'RIGHT', 0),
|
||||
cluster.lPush('{tag}source', 'element')
|
||||
]);
|
||||
|
||||
assert.equal(
|
||||
blMoveReply,
|
||||
'element'
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
});
|
22
packages/client/lib/commands/BLMOVE.ts
Normal file
22
packages/client/lib/commands/BLMOVE.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { LMoveSide } from './LMOVE';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(
|
||||
source: string,
|
||||
destination: string,
|
||||
sourceDirection: LMoveSide,
|
||||
destinationDirection: LMoveSide,
|
||||
timeout: number
|
||||
): Array<string> {
|
||||
return [
|
||||
'BLMOVE',
|
||||
source,
|
||||
destination,
|
||||
sourceDirection,
|
||||
destinationDirection,
|
||||
timeout.toString()
|
||||
];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string | null;
|
79
packages/client/lib/commands/BLPOP.spec.ts
Normal file
79
packages/client/lib/commands/BLPOP.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments, transformReply } from './BLPOP';
|
||||
import { commandOptions } from '../../index';
|
||||
|
||||
describe('BLPOP', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 0),
|
||||
['BLPOP', 'key', '0']
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(['key1', 'key2'], 0),
|
||||
['BLPOP', 'key1', 'key2', '0']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformReply', () => {
|
||||
it('null', () => {
|
||||
assert.equal(
|
||||
transformReply(null),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('member', () => {
|
||||
assert.deepEqual(
|
||||
transformReply(['key', 'element']),
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.blPop', async client => {
|
||||
const [ blPopReply ] = await Promise.all([
|
||||
client.blPop(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
1
|
||||
),
|
||||
client.lPush('key', 'element'),
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
blPopReply,
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('cluster.blPop', async cluster => {
|
||||
const [ blPopReply ] = await Promise.all([
|
||||
cluster.blPop(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
1
|
||||
),
|
||||
cluster.lPush('key', 'element'),
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
blPopReply,
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
});
|
26
packages/client/lib/commands/BLPOP.ts
Normal file
26
packages/client/lib/commands/BLPOP.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments } from './generic-transformers';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(keys: string | Buffer | Array<string | Buffer>, timeout: number): RedisCommandArguments {
|
||||
const args = pushVerdictArguments(['BLPOP'], keys);
|
||||
|
||||
args.push(timeout.toString());
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
type BLPOPReply = null | {
|
||||
key: string;
|
||||
element: string;
|
||||
};
|
||||
|
||||
export function transformReply(reply: null | [string, string]): BLPOPReply {
|
||||
if (reply === null) return null;
|
||||
|
||||
return {
|
||||
key: reply[0],
|
||||
element: reply[1]
|
||||
};
|
||||
}
|
79
packages/client/lib/commands/BRPOP.spec.ts
Normal file
79
packages/client/lib/commands/BRPOP.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments, transformReply } from './BRPOP';
|
||||
import { commandOptions } from '../../index';
|
||||
|
||||
describe('BRPOP', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 0),
|
||||
['BRPOP', 'key', '0']
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(['key1', 'key2'], 0),
|
||||
['BRPOP', 'key1', 'key2', '0']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformReply', () => {
|
||||
it('null', () => {
|
||||
assert.equal(
|
||||
transformReply(null),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('member', () => {
|
||||
assert.deepEqual(
|
||||
transformReply(['key', 'element']),
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.brPop', async client => {
|
||||
const [ brPopReply ] = await Promise.all([
|
||||
client.brPop(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
1
|
||||
),
|
||||
client.lPush('key', 'element'),
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
brPopReply,
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('cluster.brPop', async cluster => {
|
||||
const [ brPopReply ] = await Promise.all([
|
||||
cluster.brPop(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
1
|
||||
),
|
||||
cluster.lPush('key', 'element'),
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
brPopReply,
|
||||
{
|
||||
key: 'key',
|
||||
element: 'element'
|
||||
}
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
});
|
26
packages/client/lib/commands/BRPOP.ts
Normal file
26
packages/client/lib/commands/BRPOP.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments } from './generic-transformers';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(key: string | Array<string>, timeout: number): RedisCommandArguments {
|
||||
const args = pushVerdictArguments(['BRPOP'], key);
|
||||
|
||||
args.push(timeout.toString());
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
type BRPOPReply = null | {
|
||||
key: string;
|
||||
element: string;
|
||||
};
|
||||
|
||||
export function transformReply(reply: null | [string, string]): BRPOPReply {
|
||||
if (reply === null) return null;
|
||||
|
||||
return {
|
||||
key: reply[0],
|
||||
element: reply[1]
|
||||
};
|
||||
}
|
47
packages/client/lib/commands/BRPOPLPUSH.spec.ts
Normal file
47
packages/client/lib/commands/BRPOPLPUSH.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './BRPOPLPUSH';
|
||||
import { commandOptions } from '../../index';
|
||||
|
||||
describe('BRPOPLPUSH', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('source', 'destination', 0),
|
||||
['BRPOPLPUSH', 'source', 'destination', '0']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.brPopLPush', async client => {
|
||||
const [ popReply ] = await Promise.all([
|
||||
client.brPopLPush(
|
||||
commandOptions({ isolated: true }),
|
||||
'source',
|
||||
'destination',
|
||||
0
|
||||
),
|
||||
client.lPush('source', 'element')
|
||||
]);
|
||||
|
||||
assert.equal(
|
||||
popReply,
|
||||
'element'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
|
||||
testUtils.testWithCluster('cluster.brPopLPush', async cluster => {
|
||||
const [ popReply ] = await Promise.all([
|
||||
cluster.brPopLPush(
|
||||
commandOptions({ isolated: true }),
|
||||
'{tag}source',
|
||||
'{tag}destination',
|
||||
0
|
||||
),
|
||||
cluster.lPush('{tag}source', 'element')
|
||||
]);
|
||||
|
||||
assert.equal(
|
||||
popReply,
|
||||
'element'
|
||||
);
|
||||
}, GLOBAL.CLUSTERS.OPEN);
|
||||
});
|
7
packages/client/lib/commands/BRPOPLPUSH.ts
Normal file
7
packages/client/lib/commands/BRPOPLPUSH.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(source: string, destination: string, timeout: number): Array<string> {
|
||||
return ['BRPOPLPUSH', source, destination, timeout.toString()];
|
||||
}
|
||||
|
||||
export declare function transformReply(): number | null;
|
65
packages/client/lib/commands/BZPOPMAX.spec.ts
Normal file
65
packages/client/lib/commands/BZPOPMAX.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments, transformReply } from './BZPOPMAX';
|
||||
import { commandOptions } from '../../index';
|
||||
|
||||
describe('BZPOPMAX', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 0),
|
||||
['BZPOPMAX', 'key', '0']
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(['1', '2'], 0),
|
||||
['BZPOPMAX', '1', '2', '0']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformReply', () => {
|
||||
it('null', () => {
|
||||
assert.equal(
|
||||
transformReply(null),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('member', () => {
|
||||
assert.deepEqual(
|
||||
transformReply(['key', 'value', '1']),
|
||||
{
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
score: 1
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bzPopMax', async client => {
|
||||
const [ bzPopMaxReply ] = await Promise.all([
|
||||
client.bzPopMax(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
0
|
||||
),
|
||||
client.zAdd('key', [{
|
||||
value: '1',
|
||||
score: 1
|
||||
}])
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
bzPopMaxReply,
|
||||
{
|
||||
key: 'key',
|
||||
value: '1',
|
||||
score: 1
|
||||
}
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
28
packages/client/lib/commands/BZPOPMAX.ts
Normal file
28
packages/client/lib/commands/BZPOPMAX.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments, transformReplyNumberInfinity, ZMember } from './generic-transformers';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(key: string | Array<string>, timeout: number): RedisCommandArguments {
|
||||
const args = pushVerdictArguments(['BZPOPMAX'], key);
|
||||
|
||||
args.push(timeout.toString());
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
interface ZMemberWithKey extends ZMember {
|
||||
key: string;
|
||||
}
|
||||
|
||||
type BZPopMaxReply = ZMemberWithKey | null;
|
||||
|
||||
export function transformReply(reply: [key: string, value: string, score: string] | null): BZPopMaxReply | null {
|
||||
if (!reply) return null;
|
||||
|
||||
return {
|
||||
key: reply[0],
|
||||
value: reply[1],
|
||||
score: transformReplyNumberInfinity(reply[2])
|
||||
};
|
||||
}
|
65
packages/client/lib/commands/BZPOPMIN.spec.ts
Normal file
65
packages/client/lib/commands/BZPOPMIN.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments, transformReply } from './BZPOPMIN';
|
||||
import { commandOptions } from '../../index';
|
||||
|
||||
describe('BZPOPMIN', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments('key', 0),
|
||||
['BZPOPMIN', 'key', '0']
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(['1', '2'], 0),
|
||||
['BZPOPMIN', '1', '2', '0']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformReply', () => {
|
||||
it('null', () => {
|
||||
assert.equal(
|
||||
transformReply(null),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('member', () => {
|
||||
assert.deepEqual(
|
||||
transformReply(['key', 'value', '1']),
|
||||
{
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
score: 1
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.bzPopMin', async client => {
|
||||
const [ bzPopMinReply ] = await Promise.all([
|
||||
client.bzPopMin(
|
||||
commandOptions({ isolated: true }),
|
||||
'key',
|
||||
0
|
||||
),
|
||||
client.zAdd('key', [{
|
||||
value: '1',
|
||||
score: 1
|
||||
}])
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
bzPopMinReply,
|
||||
{
|
||||
key: 'key',
|
||||
value: '1',
|
||||
score: 1
|
||||
}
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
28
packages/client/lib/commands/BZPOPMIN.ts
Normal file
28
packages/client/lib/commands/BZPOPMIN.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { RedisCommandArguments } from '.';
|
||||
import { pushVerdictArguments, transformReplyNumberInfinity, ZMember } from './generic-transformers';
|
||||
|
||||
export const FIRST_KEY_INDEX = 1;
|
||||
|
||||
export function transformArguments(key: string | Array<string>, timeout: number): RedisCommandArguments {
|
||||
const args = pushVerdictArguments(['BZPOPMIN'], key);
|
||||
|
||||
args.push(timeout.toString());
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
interface ZMemberWithKey extends ZMember {
|
||||
key: string;
|
||||
}
|
||||
|
||||
type BZPopMinReply = ZMemberWithKey | null;
|
||||
|
||||
export function transformReply(reply: [key: string, value: string, score: string] | null): BZPopMinReply | null {
|
||||
if (!reply) return null;
|
||||
|
||||
return {
|
||||
key: reply[0],
|
||||
value: reply[1],
|
||||
score: transformReplyNumberInfinity(reply[2])
|
||||
};
|
||||
}
|
19
packages/client/lib/commands/CLIENT_ID.spec.ts
Normal file
19
packages/client/lib/commands/CLIENT_ID.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import testUtils, { GLOBAL } from '../test-utils';
|
||||
import { transformArguments } from './CLIENT_ID';
|
||||
|
||||
describe('CLIENT ID', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['CLIENT', 'ID']
|
||||
);
|
||||
});
|
||||
|
||||
testUtils.testWithClient('client.clientId', async client => {
|
||||
assert.equal(
|
||||
typeof (await client.clientId()),
|
||||
'number'
|
||||
);
|
||||
}, GLOBAL.SERVERS.OPEN);
|
||||
});
|
7
packages/client/lib/commands/CLIENT_ID.ts
Normal file
7
packages/client/lib/commands/CLIENT_ID.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const IS_READ_ONLY = true;
|
||||
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['CLIENT', 'ID'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): number;
|
42
packages/client/lib/commands/CLIENT_INFO.spec.ts
Normal file
42
packages/client/lib/commands/CLIENT_INFO.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments, transformReply } from './CLIENT_INFO';
|
||||
|
||||
describe('CLIENT INFO', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['CLIENT', 'INFO']
|
||||
);
|
||||
});
|
||||
|
||||
it('transformReply', () => {
|
||||
assert.deepEqual(
|
||||
transformReply('id=526512 addr=127.0.0.1:36244 laddr=127.0.0.1:6379 fd=8 name= age=11213 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default redir=-1\n'),
|
||||
{
|
||||
id: 526512,
|
||||
addr: '127.0.0.1:36244',
|
||||
laddr: '127.0.0.1:6379',
|
||||
fd: 8,
|
||||
name: '',
|
||||
age: 11213,
|
||||
idle: 0,
|
||||
flags: 'N',
|
||||
db: 0,
|
||||
sub: 0,
|
||||
psub: 0,
|
||||
multi: -1,
|
||||
qbuf: 26,
|
||||
qbufFree: 40928,
|
||||
argvMem: 10,
|
||||
obl: 0,
|
||||
oll: 0,
|
||||
omem: 0,
|
||||
totMem: 61466,
|
||||
events: 'r',
|
||||
cmd: 'client',
|
||||
user: 'default',
|
||||
redir: -1
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
85
packages/client/lib/commands/CLIENT_INFO.ts
Normal file
85
packages/client/lib/commands/CLIENT_INFO.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['CLIENT', 'INFO'];
|
||||
}
|
||||
|
||||
interface ClientInfoReply {
|
||||
id: number;
|
||||
addr: string;
|
||||
laddr: string;
|
||||
fd: number;
|
||||
name: string;
|
||||
age: number;
|
||||
idle: number;
|
||||
flags: string;
|
||||
db: number;
|
||||
sub: number;
|
||||
psub: number;
|
||||
multi: number;
|
||||
qbuf: number;
|
||||
qbufFree: number;
|
||||
argvMem: number;
|
||||
obl: number;
|
||||
oll: number;
|
||||
omem: number;
|
||||
totMem: number;
|
||||
events: string;
|
||||
cmd: string;
|
||||
user: string;
|
||||
redir: number;
|
||||
}
|
||||
|
||||
const REGEX = /=([^\s]*)/g;
|
||||
|
||||
export function transformReply(reply: string): ClientInfoReply {
|
||||
const [
|
||||
[, id],
|
||||
[, addr],
|
||||
[, laddr],
|
||||
[, fd],
|
||||
[, name],
|
||||
[, age],
|
||||
[, idle],
|
||||
[, flags],
|
||||
[, db],
|
||||
[, sub],
|
||||
[, psub],
|
||||
[, multi],
|
||||
[, qbuf],
|
||||
[, qbufFree],
|
||||
[, argvMem],
|
||||
[, obl],
|
||||
[, oll],
|
||||
[, omem],
|
||||
[, totMem],
|
||||
[, events],
|
||||
[, cmd],
|
||||
[, user],
|
||||
[, redir]
|
||||
] = [...reply.matchAll(REGEX)];
|
||||
|
||||
return {
|
||||
id: Number(id),
|
||||
addr,
|
||||
laddr,
|
||||
fd: Number(fd),
|
||||
name,
|
||||
age: Number(age),
|
||||
idle: Number(idle),
|
||||
flags,
|
||||
db: Number(db),
|
||||
sub: Number(sub),
|
||||
psub: Number(psub),
|
||||
multi: Number(multi),
|
||||
qbuf: Number(qbuf),
|
||||
qbufFree: Number(qbufFree),
|
||||
argvMem: Number(argvMem),
|
||||
obl: Number(obl),
|
||||
oll: Number(oll),
|
||||
omem: Number(omem),
|
||||
totMem: Number(totMem),
|
||||
events,
|
||||
cmd,
|
||||
user,
|
||||
redir: Number(redir)
|
||||
};
|
||||
}
|
20
packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts
Normal file
20
packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './CLUSTER_ADDSLOTS';
|
||||
|
||||
describe('CLUSTER ADDSLOTS', () => {
|
||||
describe('transformArguments', () => {
|
||||
it('single', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(0),
|
||||
['CLUSTER', 'ADDSLOTS', '0']
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments([0, 1]),
|
||||
['CLUSTER', 'ADDSLOTS', '0', '1']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
13
packages/client/lib/commands/CLUSTER_ADDSLOTS.ts
Normal file
13
packages/client/lib/commands/CLUSTER_ADDSLOTS.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function transformArguments(slots: number | Array<number>): Array<string> {
|
||||
const args = ['CLUSTER', 'ADDSLOTS'];
|
||||
|
||||
if (typeof slots === 'number') {
|
||||
args.push(slots.toString());
|
||||
} else {
|
||||
args.push(...slots.map(String));
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
11
packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts
Normal file
11
packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './CLUSTER_FLUSHSLOTS';
|
||||
|
||||
describe('CLUSTER FLUSHSLOTS', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['CLUSTER', 'FLUSHSLOTS']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts
Normal file
5
packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['CLUSTER', 'FLUSHSLOTS'];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
11
packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts
Normal file
11
packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './CLUSTER_GETKEYSINSLOT';
|
||||
|
||||
describe('CLUSTER GETKEYSINSLOT', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(0, 10),
|
||||
['CLUSTER', 'GETKEYSINSLOT', '0', '10']
|
||||
);
|
||||
});
|
||||
});
|
5
packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts
Normal file
5
packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function transformArguments(slot: number, count: number): Array<string> {
|
||||
return ['CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()];
|
||||
}
|
||||
|
||||
export declare function transformReply(): string;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user