From 4e6d018d77c259f7ff7cc9137153b4ffc4cdbba0 Mon Sep 17 00:00:00 2001
From: Leibale Eidelman
Date: Thu, 2 Sep 2021 10:04:48 -0400
Subject: [PATCH 001/490] V4 (#1624)
* init v4
* add .gitignore to benchmark
* spawn redis-servers for tests,
add some tests,
fix client auth on connect
* add tests coverage report
* add tests workflow, replace nyc text reporter with text-summary
* run tests with node 16.x & redis 6.x only (for now)
* add socket events on client,
stop reconnectiong when manually calling disconnect,
remove abort signal listener when a command is written on the socket
* add isOpen boolean getter on client, add maxLength option to command queue, add test for client.multi
* move to use CommonJS
* add MULTI and EXEC commands to when executing multi command, make client.multi return type innerit the module commands, clean some tests, exclute spec files from coverage report
* missing file from commit 61edd4f1b5436640cadb842f783edce36b07aa80
* exclude spec files from coverage report
* add support for options in a command function (.get, .set, ...), add support for the SELECT command, implement a couple of commands, fix client socket reconnection strategy, add support for using replicas (RO) in cluster, and more..
* fix client.blPop test
* use which to find redis-server path
* change command options to work with Symbol rather then WeakSet
* implement more commands
* Add support for lua scripts in client & muilti, fix client socket initiator, implement simple cluster nodes discovery strategy
* replace `callbackify` with `legacyMode`
* add the SCAN command and client.scanIterator
* rename scanIterator
* init benchmark workflow
* fix benchmark workflow
* fix benchmark workflow
* fix benchmark workflow
* push coverage report to Coveralls
* fix Coveralls
* generator lcov (for Coveralls)
* fix .nycrc.json
* PubSub
* add support for all set commands (including sScanIterator)
* support pipeline
* fix KEEPTTL in SET
* remove console.log
* add HyperLogLog commands
* update README.md (thanks to @guyroyse)
* add support for most of the "keys commands"
* fix EXPIREAT.spec.ts
* add support for date in both EXPIREAT & EXPIRE
* add tests
* better cluster nodes discorvery strategy after MOVED error, add PubSub test
* fix PubSub UNSUBSCRIBE/PUNSUBSCRIBE without channel and/or listener
* fix PubSub
* add release-it to dev dependencies
* Release 4.0.0-next.0
* fix .npmignore
* Release 4.0.0-next.1
* fix links in README.md
* fix .npmignore
* Release 4.0.0-next.2
* add support for all sorted set commands
* add support for most stream commands
* add missing file from commit 53de279afe58f8359b6704c0390e32632787b346
* lots of todo commends
* make PubSub test more stable
* clean ZPOPMAX
* add support for lua scripts and modules in cluster, spawn cluster for tests, add some cluster tests, fix pubsub listener arguments
* GET.spec.ts
* add support for List commands, fix some Sorted Set commands, add some cluster commands, spawn cluster for testing, add support for command options in cluster, and more
* add missing file from commit faab94fab2262e26b44159646f7c99090da789a5
* clean ZRANK and ZREVRANK
* add XREAD and XREADGROUP commands
* remove unused files
* implement a couple of more commands, make cluster random iterator be per node (instead of per slot)
* Release 4.0.0-next.3
* app spec files to npmignore
* fix some code analyzers (LGTM, deepsource, codeclimate) issues
* fix CLUSTER_NODES, add some tests
* add HSCAN, clean some commands, add tests for generic transformers
* add missing files from 0feb35a1fbf423c017831f2f3dd02f55e58be6a2
* update README.md (thanks to @guyroyse)
* handle ASK errors, add some commands and tests
* Release 4.0.0-next.4
* replace "modern" with "v4"
* remove unused imports
* add all ACL subcommands, all MODULE subcommands, and some other commands
* remove 2 unused imports
* fix BITFIELD command
* fix XTRIM spec file
* clean code
* fix package.json types field
* better modules support, fix some bugs in legacy mode, add some tests
* remove unused function
* add test for hScanIterator
* change node mimimum version to 12 (latest LTS)
* update tsconfig.json to support node 12, run tests on Redis 5 & 6 and on all node live versions
* remove future node releases :P
* remove "lib" from ts compiler options
* Update tsconfig.json
* fix build
* run some tests only on supported redis versions, use coveralls parallel mode
* fix tests
* Do not use "timers/promises", fix "isRedisVersionGreaterThan"
* skip AbortController tests when not available
* use 'fs'.promises instead of 'fs/promises'
* add some missing commands
* run GETDEL tests only if the redis version is greater than 6.2
* implement some GEO commands, improve scan generic transformer, expose RPUSHX
* fix GEOSEARCH & GEOSEARCHSTORE
* use socket.setNoDelay and queueMicrotask to improve latency
* commands-queue.ts: String length / byte length counting issue (#1630)
* Update commands-queue.ts
Hopefully fixing #1628
* Reverted 2fa5ea6, and implemented test for byte length check
* Changed back to Buffer.byteLength, due to issue author input. Updated test to look for 4 bytes.
* Fixed. There were two places that length was calculated.
* Removed redundant string assignment
* add 2 bytes test as well
Co-authored-by: Leibale Eidelman
* fix scripts in multi
* do not hide bugs in redis
* fix for e7bf09644b28c57287bcf84d3433265abdac2c71
* remove unused import
* implement WATCH command, fix ZRANGESTORE & GEOSEARCHSTORE tests
* update README.md
Co-authored-by: @GuyRoyse
* use typedoc to auto generate documentation
* run "npm install" before "npm run documentation"
* clean documentation workflow
* fix WATCH spec file
* increase "CLUSTER_NODE_TIMEOUT" to 5000ms to avoid "CLUSTERDOWN" errors in tests
* pull cluster state every 100 ms
* await meetPromises before pulling the cluster state
* enhance the way commanders (client/multi/cluster) get extended with modules and scripts
* add test for socket retry strategy
* implement more commands
* set GETEX minimum version to 6.2
* remove unused imports
* add support for multi in cluster
* upgrade dependencies
* Release 4.0.0-next.5
* remove unused imports
* improve benchmarking
* use the same Multi with duplicated clients
* exclude some files from the documentation, add some exports, clean code
* fix #1636 - handle null in multi.exec
* remove unused import
* add supoprt for tuples in HSET
* add FIRST_KEY_INDEX to HSET
* add a bunch of missing commands, fix MSET and HELLO, add some tests
* add FIRST_KEY_INDEX to MSET and MSETNX
* upgrade actions
* fix coverallsapp/github-action version
* Update documentation.yml
* Update documentation.yml
* clean code
* remove unused imports
* use "npm ci" instead of "npm install"
* fix `self` binding on client modules, use connection pool for `duplicateConnection`
* add client.executeIsolated, rename "duplicateConnection" to "isolated", update README.md (thanks to @GuyRoyse and @SimonPrickett)
* update README (thanks to @GuyRoyse), add some tests
* try to fix "cluster is down" errors in tests
* try to fix "cluster is down" errors in tests
* upgrade dependencies
* update package-lock
* Release 4.0.0-next.6
* fix #1636 - fix WatchError
* fix for f1bf0beebf29bae7e87bc961a7aa73c1af9acba2 - remove .only from multi tests
* Release 4.0.0-next.7
* update README and other markdown files
Co-authored-by: @GuyRoyse & @SimonPrickett
* Doc updates. (#1640)
* update docs, upgrade dependencies
* fix README
* Release 4.0.0-rc.0
* Update README.md
* update docs, add `connectTimeout` options, fix tls
Co-authored-by: Guy Royse
* npm update, "fix" some tests, clean code
* fix AssertionError import
* fix #1642 - fix XREAD, XREADGROUP and XTRIM
* fix #1644 - add the QUIT command
* add socket.noDelay and socket.keepAlive configurations
* Update README.md (#1645)
* Update README.md
Fixed issue with how connection string was specified.
Now you can have user@host without having to specify a password, which just makes more sense
* Update client-configuration.md as well
Co-authored-by: Leibale Eidelman
* update socket.reconnectStrategy description
* fix borken link in v3-to-v4.md
* increase test coverage, fix bug in cluster redirection strategy, implement CLIENT_ID, remove unused EXEC command
Co-authored-by: Nova
Co-authored-by: Simon Prickett
Co-authored-by: Guy Royse
---
.deepsource.toml | 9 -
.eslintignore | 4 -
.eslintrc | 109 -
.github/FUNDING.yml | 1 -
.github/ISSUE_TEMPLATE.md | 19 +-
.github/PULL_REQUEST_TEMPLATE.md | 7 +-
.github/workflows/benchmark.yml | 37 +-
.github/workflows/codeql-analysis.yml | 67 -
.github/workflows/documentation.yml | 31 +
.github/workflows/linting.yml | 31 -
.github/workflows/tests.yml | 51 +-
.github/workflows/tests_windows.yml | 49 -
.gitignore | 25 +-
.npmignore | 36 +-
.nycrc.json | 4 +
.prettierrc | 11 -
CHANGELOG.md | 23 +
CODE_OF_CONDUCT.md | 60 -
CONTRIBUTING.md | 92 +-
LICENSE | 2 +-
README.md | 1072 +-
benchmark/.gitignore | 1 +
benchmark/index.js | 81 +
benchmark/package-lock.json | 926 ++
benchmark/package.json | 17 +
benchmarks/diff_multi_bench_output.js | 95 -
benchmarks/multi_bench.js | 291 -
docs/FAQ.md | 13 +
docs/client-configuration.md | 30 +
docs/isolated-execution.md | 67 +
docs/v3-to-v4.md | 35 +
examples/auth.js | 7 -
examples/backpressure_drain.js | 34 -
examples/eval.js | 14 -
examples/extend.js | 26 -
examples/file.js | 38 -
examples/mget.js | 7 -
examples/monitor.js | 12 -
examples/multi.js | 49 -
examples/multi2.js | 31 -
examples/psubscribe.js | 33 -
examples/pub_sub.js | 42 -
examples/scan.js | 51 -
examples/simple.js | 26 -
examples/sort.js | 19 -
examples/streams.js | 47 -
examples/subqueries.js | 17 -
examples/subquery.js | 17 -
examples/unix_socket.js | 32 -
examples/web_server.js | 33 -
index.js | 1039 --
index.ts | 10 +
lib/client.spec.ts | 562 +
lib/client.ts | 468 +
lib/cluster-slots.ts | 221 +
lib/cluster.spec.ts | 115 +
lib/cluster.ts | 202 +
lib/command-options.ts | 14 +
lib/command.js | 16 -
lib/commander.spec.ts | 28 +
lib/commander.ts | 109 +
lib/commands-queue.ts | 333 +
lib/commands.js | 105 -
lib/commands/ACL_CAT.spec.ts | 23 +
lib/commands/ACL_CAT.ts | 13 +
lib/commands/ACL_DELUSER.spec.ts | 30 +
lib/commands/ACL_DELUSER.ts | 7 +
lib/commands/ACL_GENPASS.spec.ts | 23 +
lib/commands/ACL_GENPASS.ts | 13 +
lib/commands/ACL_GETUSER.spec.ts | 27 +
lib/commands/ACL_GETUSER.ts | 34 +
lib/commands/ACL_LIST.spec.ts | 14 +
lib/commands/ACL_LIST.ts | 7 +
lib/commands/ACL_LOAD.spec.ts | 14 +
lib/commands/ACL_LOAD.ts | 7 +
lib/commands/ACL_LOG.spec.ts | 53 +
lib/commands/ACL_LOG.ts | 48 +
lib/commands/ACL_LOG_RESET.spec.ts | 14 +
lib/commands/ACL_LOG_RESET.ts | 7 +
lib/commands/ACL_SAVE.spec.ts | 14 +
lib/commands/ACL_SAVE.ts | 7 +
lib/commands/ACL_SETUSER.spec.ts | 23 +
lib/commands/ACL_SETUSER.ts | 7 +
lib/commands/ACL_USERS.spec.ts | 14 +
lib/commands/ACL_USERS.ts | 7 +
lib/commands/ACL_WHOAMI.spec.ts | 14 +
lib/commands/ACL_WHOAMI.ts | 7 +
lib/commands/APPEND.spec.ts | 11 +
lib/commands/APPEND.ts | 9 +
lib/commands/ASKING.spec.ts | 11 +
lib/commands/ASKING.ts | 7 +
lib/commands/AUTH.spec.ts | 25 +
lib/commands/AUTH.ts | 16 +
lib/commands/BGREWRITEAOF.spec.ts | 11 +
lib/commands/BGREWRITEAOF.ts | 7 +
lib/commands/BGSAVE.spec.ts | 23 +
lib/commands/BGSAVE.ts | 17 +
lib/commands/BITCOUNT.spec.ts | 31 +
lib/commands/BITCOUNT.ts | 25 +
lib/commands/BITFIELD.spec.ts | 42 +
lib/commands/BITFIELD.ts | 84 +
lib/commands/BITOP.spec.ts | 35 +
lib/commands/BITOP.ts | 11 +
lib/commands/BITPOS.spec.ts | 42 +
lib/commands/BITPOS.ts | 21 +
lib/commands/BLMOVE.spec.ts | 43 +
lib/commands/BLMOVE.ts | 23 +
lib/commands/BLPOP.spec.ts | 79 +
lib/commands/BLPOP.ts | 25 +
lib/commands/BRPOP.spec.ts | 79 +
lib/commands/BRPOP.ts | 25 +
lib/commands/BRPOPLPUSH.spec.ts | 47 +
lib/commands/BRPOPLPUSH.ts | 9 +
lib/commands/BZPOPMAX.spec.ts | 66 +
lib/commands/BZPOPMAX.ts | 27 +
lib/commands/BZPOPMIN.spec.ts | 65 +
lib/commands/BZPOPMIN.ts | 27 +
lib/commands/CLIENT_ID.spec.ts | 19 +
lib/commands/CLIENT_ID.ts | 9 +
lib/commands/CLIENT_INFO.spec.ts | 42 +
lib/commands/CLIENT_INFO.ts | 85 +
lib/commands/CLUSTER_ADDSLOTS.spec.ts | 20 +
lib/commands/CLUSTER_ADDSLOTS.ts | 15 +
lib/commands/CLUSTER_FLUSHSLOTS.spec.ts | 11 +
lib/commands/CLUSTER_FLUSHSLOTS.ts | 7 +
lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts | 11 +
lib/commands/CLUSTER_GETKEYSINSLOT.ts | 7 +
lib/commands/CLUSTER_INFO.spec.ts | 64 +
lib/commands/CLUSTER_INFO.ts | 47 +
lib/commands/CLUSTER_MEET.spec.ts | 11 +
lib/commands/CLUSTER_MEET.ts | 7 +
lib/commands/CLUSTER_NODES.spec.ts | 116 +
lib/commands/CLUSTER_NODES.ts | 96 +
lib/commands/CLUSTER_RESET.spec.ts | 27 +
lib/commands/CLUSTER_RESET.ts | 15 +
lib/commands/CLUSTER_SETSLOT.spec.ts | 20 +
lib/commands/CLUSTER_SETSLOT.ts | 20 +
lib/commands/CONFIG_GET.spec.ts | 11 +
lib/commands/CONFIG_GET.ts | 7 +
lib/commands/CONFIG_RESETSTAT.spec.ts | 11 +
lib/commands/CONFIG_RESETSTAT.ts | 7 +
lib/commands/CONFIG_REWRITE.spec.ts | 11 +
lib/commands/CONFIG_REWRITE.ts | 7 +
lib/commands/CONFIG_SET.spec.ts | 11 +
lib/commands/CONFIG_SET.ts | 7 +
lib/commands/COPY.spec.ts | 67 +
lib/commands/COPY.ts | 24 +
lib/commands/DBSIZE.spec.ts | 26 +
lib/commands/DBSIZE.ts | 9 +
lib/commands/DECR.spec.ts | 19 +
lib/commands/DECR.ts | 9 +
lib/commands/DECRBY.spec.ts | 19 +
lib/commands/DECRBY.ts | 9 +
lib/commands/DEL.spec.ts | 28 +
lib/commands/DEL.ts | 7 +
lib/commands/DISCARD.spec.ts | 11 +
lib/commands/DISCARD.ts | 7 +
lib/commands/DUMP.spec.ts | 11 +
lib/commands/DUMP.ts | 7 +
lib/commands/ECHO.spec.ts | 26 +
lib/commands/ECHO.ts | 9 +
lib/commands/EVAL.spec.ts | 29 +
lib/commands/EVAL.ts | 9 +
lib/commands/EVALSHA.spec.ts | 14 +
lib/commands/EVALSHA.ts | 9 +
lib/commands/EXISTS.spec.ts | 28 +
lib/commands/EXISTS.ts | 11 +
lib/commands/EXPIRE.spec.ts | 19 +
lib/commands/EXPIRE.ts | 7 +
lib/commands/EXPIREAT.spec.ts | 29 +
lib/commands/EXPIREAT.ts | 11 +
lib/commands/FAILOVER.spec.ts | 72 +
lib/commands/FAILOVER.ts | 35 +
lib/commands/FLUSHALL.spec.ts | 35 +
lib/commands/FLUSHALL.ts | 18 +
lib/commands/FLUSHDB.spec.ts | 36 +
lib/commands/FLUSHDB.ts | 14 +
lib/commands/GEOADD.spec.ts | 95 +
lib/commands/GEOADD.ts | 49 +
lib/commands/GEODIST.spec.ts | 35 +
lib/commands/GEODIST.ts | 24 +
lib/commands/GEOHASH.spec.ts | 35 +
lib/commands/GEOHASH.ts | 11 +
lib/commands/GEOPOS.spec.ts | 35 +
lib/commands/GEOPOS.ts | 21 +
lib/commands/GEOSEARCH.spec.ts | 37 +
lib/commands/GEOSEARCH.ts | 16 +
lib/commands/GEOSEARCHSTORE.spec.ts | 74 +
lib/commands/GEOSEARCHSTORE.ts | 39 +
lib/commands/GEOSEARCH_WITH.spec.ts | 42 +
lib/commands/GEOSEARCH_WITH.ts | 23 +
lib/commands/GET.spec.ts | 26 +
lib/commands/GET.ts | 11 +
lib/commands/GETBIT.spec.ts | 26 +
lib/commands/GETBIT.ts | 11 +
lib/commands/GETDEL.spec.ts | 29 +
lib/commands/GETDEL.ts | 9 +
lib/commands/GETEX.spec.ts | 96 +
lib/commands/GETEX.ts | 35 +
lib/commands/GETRANGE.spec.ts | 27 +
lib/commands/GETRANGE.ts | 11 +
lib/commands/GETSET.spec.ts | 26 +
lib/commands/GETSET.ts | 9 +
lib/commands/HDEL.spec.ts | 28 +
lib/commands/HDEL.ts | 9 +
lib/commands/HELLO.spec.ts | 77 +
lib/commands/HELLO.ts | 64 +
lib/commands/HEXISTS.spec.ts | 19 +
lib/commands/HEXISTS.ts | 9 +
lib/commands/HGET.spec.ts | 19 +
lib/commands/HGET.ts | 9 +
lib/commands/HGETALL.spec.ts | 41 +
lib/commands/HGETALL.ts | 9 +
lib/commands/HINCRBY.spec.ts | 19 +
lib/commands/HINCRBY.ts | 9 +
lib/commands/HINCRBYFLOAT.spec.ts | 19 +
lib/commands/HINCRBYFLOAT.ts | 9 +
lib/commands/HKEYS.spec.ts | 19 +
lib/commands/HKEYS.ts | 9 +
lib/commands/HLEN.spec.ts | 19 +
lib/commands/HLEN.ts | 9 +
lib/commands/HMGET.spec.ts | 28 +
lib/commands/HMGET.ts | 11 +
lib/commands/HRANDFIELD.spec.ts | 21 +
lib/commands/HRANDFIELD.ts | 9 +
lib/commands/HRANDFIELD_COUNT.spec.ts | 21 +
lib/commands/HRANDFIELD_COUNT.ts | 13 +
.../HRANDFIELD_COUNT_WITHVALUES.spec.ts | 21 +
lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts | 13 +
lib/commands/HSCAN.spec.ts | 77 +
lib/commands/HSCAN.ts | 37 +
lib/commands/HSET.spec.ts | 44 +
lib/commands/HSET.ts | 41 +
lib/commands/HSETNX.spec.ts | 19 +
lib/commands/HSETNX.ts | 9 +
lib/commands/HSTRLEN.spec.ts | 19 +
lib/commands/HSTRLEN.ts | 9 +
lib/commands/HVALS.spec.ts | 19 +
lib/commands/HVALS.ts | 9 +
lib/commands/INCR.spec.ts | 19 +
lib/commands/INCR.ts | 9 +
lib/commands/INCRBY.spec.ts | 19 +
lib/commands/INCRBY.ts | 9 +
lib/commands/INCRBYFLOAT.spec.ts | 19 +
lib/commands/INCRBYFLOAT.ts | 9 +
lib/commands/INFO.spec.ts | 20 +
lib/commands/INFO.ts | 15 +
lib/commands/KEYS.spec.ts | 11 +
lib/commands/KEYS.ts | 7 +
lib/commands/LASTSAVE.spec.ts | 20 +
lib/commands/LASTSAVE.ts | 9 +
lib/commands/LINDEX.spec.ts | 26 +
lib/commands/LINDEX.ts | 11 +
lib/commands/LINSERT.spec.ts | 26 +
lib/commands/LINSERT.ts | 22 +
lib/commands/LLEN.spec.ts | 26 +
lib/commands/LLEN.ts | 11 +
lib/commands/LMOVE.spec.ts | 28 +
lib/commands/LMOVE.ts | 22 +
lib/commands/LOLWUT.spec.ts | 43 +
lib/commands/LOLWUT.ts | 19 +
lib/commands/LPOP.spec.ts | 26 +
lib/commands/LPOP.ts | 9 +
lib/commands/LPOP_COUNT.spec.ts | 28 +
lib/commands/LPOP_COUNT.ts | 9 +
lib/commands/LPOS.spec.ts | 58 +
lib/commands/LPOS.ts | 26 +
lib/commands/LPOS_COUNT.spec.ts | 58 +
lib/commands/LPOS_COUNT.ts | 22 +
lib/commands/LPUSH.spec.ts | 35 +
lib/commands/LPUSH.ts | 8 +
lib/commands/LPUSHX.spec.ts | 35 +
lib/commands/LPUSHX.ts | 9 +
lib/commands/LRANGE.spec.ts | 27 +
lib/commands/LRANGE.ts | 16 +
lib/commands/LREM.spec.ts | 27 +
lib/commands/LREM.ts | 14 +
lib/commands/LSET.spec.ts | 28 +
lib/commands/LSET.ts | 14 +
lib/commands/LTRIM.spec.ts | 26 +
lib/commands/LTRIM.ts | 14 +
lib/commands/MEMORY_DOCTOR.spec.ts | 26 +
lib/commands/MEMORY_DOCTOR.ts | 7 +
lib/commands/MEMORY_MALLOC-STATS.spec.ts | 26 +
lib/commands/MEMORY_MALLOC-STATS.ts | 7 +
lib/commands/MEMORY_PURGE.spec.ts | 26 +
lib/commands/MEMORY_PURGE.ts | 7 +
lib/commands/MEMORY_STATS.spec.ts | 108 +
lib/commands/MEMORY_STATS.ts | 93 +
lib/commands/MEMORY_USAGE.spec.ts | 37 +
lib/commands/MEMORY_USAGE.ts | 21 +
lib/commands/MGET.spec.ts | 26 +
lib/commands/MGET.ts | 11 +
lib/commands/MIGRATE.spec.ts | 76 +
lib/commands/MIGRATE.ts | 65 +
lib/commands/MODULE_LIST.spec.ts | 11 +
lib/commands/MODULE_LIST.ts | 7 +
lib/commands/MODULE_LOAD.spec.ts | 20 +
lib/commands/MODULE_LOAD.ts | 13 +
lib/commands/MODULE_UNLOAD.spec.ts | 11 +
lib/commands/MODULE_UNLOAD.ts | 7 +
lib/commands/MOVE.spec.ts | 19 +
lib/commands/MOVE.ts | 7 +
lib/commands/MSET.spec.ts | 42 +
lib/commands/MSET.ts | 19 +
lib/commands/MSETNX.spec.ts | 42 +
lib/commands/MSETNX.ts | 19 +
lib/commands/PERSIST.spec.ts | 19 +
lib/commands/PERSIST.ts | 9 +
lib/commands/PEXPIRE.spec.ts | 19 +
lib/commands/PEXPIRE.ts | 9 +
lib/commands/PEXPIREAT.spec.ts | 29 +
lib/commands/PEXPIREAT.ts | 13 +
lib/commands/PFADD.spec.ts | 28 +
lib/commands/PFADD.ts | 9 +
lib/commands/PFCOUNT.spec.ts | 28 +
lib/commands/PFCOUNT.ts | 9 +
lib/commands/PFMERGE.spec.ts | 28 +
lib/commands/PFMERGE.ts | 9 +
lib/commands/PING.spec.ts | 18 +
lib/commands/PING.ts | 7 +
lib/commands/PSETEX.spec.ts | 26 +
lib/commands/PSETEX.ts | 14 +
lib/commands/PTTL.spec.ts | 19 +
lib/commands/PTTL.ts | 11 +
lib/commands/PUBLISH.spec.ts | 19 +
lib/commands/PUBLISH.ts | 7 +
lib/commands/PUBSUB_CHANNELS.spec.ts | 35 +
lib/commands/PUBSUB_CHANNELS.ts | 15 +
lib/commands/PUBSUB_NUMPAT.spec.ts | 26 +
lib/commands/PUBSUB_NUMPAT.ts | 9 +
lib/commands/PUBSUB_NUMSUB.spec.ts | 42 +
lib/commands/PUBSUB_NUMSUB.ts | 23 +
lib/commands/RANDOMKEY.spec.ts | 19 +
lib/commands/RANDOMKEY.ts | 9 +
lib/commands/READONLY.spec.ts | 11 +
lib/commands/READONLY.ts | 7 +
lib/commands/READWRITE.spec.ts | 11 +
lib/commands/READWRITE.ts | 7 +
lib/commands/RENAME.spec.ts | 21 +
lib/commands/RENAME.ts | 9 +
lib/commands/RENAMENX.spec.ts | 21 +
lib/commands/RENAMENX.ts | 9 +
lib/commands/REPLICAOF.spec.ts | 11 +
lib/commands/REPLICAOF.ts | 7 +
lib/commands/RESTORE-ASKING.spec.ts | 11 +
lib/commands/RESTORE-ASKING.ts | 7 +
lib/commands/ROLE.spec.ts | 69 +
lib/commands/ROLE.ts | 75 +
lib/commands/RPOP.spec.ts | 26 +
lib/commands/RPOP.ts | 9 +
lib/commands/RPOPLPUSH.spec.ts | 26 +
lib/commands/RPOPLPUSH.ts | 9 +
lib/commands/RPOP_COUNT.spec.ts | 28 +
lib/commands/RPOP_COUNT.ts | 9 +
lib/commands/RPUSH.spec.ts | 35 +
lib/commands/RPUSH.ts | 9 +
lib/commands/RPUSHX.spec.ts | 35 +
lib/commands/RPUSHX.ts | 9 +
lib/commands/SADD.spec.ts | 28 +
lib/commands/SADD.ts | 9 +
lib/commands/SAVE.spec.ts | 11 +
lib/commands/SAVE.ts | 7 +
lib/commands/SCAN.spec.ts | 84 +
lib/commands/SCAN.ts | 28 +
lib/commands/SCARD.spec.ts | 19 +
lib/commands/SCARD.ts | 9 +
lib/commands/SCRIPT_DEBUG.spec.ts | 26 +
lib/commands/SCRIPT_DEBUG.ts | 7 +
lib/commands/SCRIPT_EXISTS.spec.ts | 35 +
lib/commands/SCRIPT_EXISTS.ts | 7 +
lib/commands/SCRIPT_FLUSH.spec.ts | 35 +
lib/commands/SCRIPT_FLUSH.ts | 13 +
lib/commands/SCRIPT_KILL.spec.ts | 11 +
lib/commands/SCRIPT_KILL.ts | 7 +
lib/commands/SCRIPT_LOAD.spec.ts | 30 +
lib/commands/SCRIPT_LOAD.ts | 7 +
lib/commands/SDIFF.spec.ts | 28 +
lib/commands/SDIFF.ts | 9 +
lib/commands/SDIFFSTORE.spec.ts | 28 +
lib/commands/SDIFFSTORE.ts | 9 +
lib/commands/SET.spec.ts | 121 +
lib/commands/SET.ts | 75 +
lib/commands/SETBIT.spec.ts | 26 +
lib/commands/SETBIT.ts | 9 +
lib/commands/SETEX.spec.ts | 26 +
lib/commands/SETEX.ts | 14 +
lib/commands/SETNX .spec.ts | 26 +
lib/commands/SETNX.ts | 9 +
lib/commands/SETRANGE.spec.ts | 26 +
lib/commands/SETRANGE.ts | 9 +
lib/commands/SHUTDOWN.spec.ts | 27 +
lib/commands/SHUTDOWN.ts | 13 +
lib/commands/SINTER.spec.ts | 28 +
lib/commands/SINTER.ts | 9 +
lib/commands/SINTERSTORE.spec.ts | 28 +
lib/commands/SINTERSTORE.ts | 9 +
lib/commands/SISMEMBER.spec.ts | 21 +
lib/commands/SISMEMBER.ts | 9 +
lib/commands/SMEMBERS.spec.ts | 19 +
lib/commands/SMEMBERS.ts | 9 +
lib/commands/SMISMEMBER.spec.ts | 21 +
lib/commands/SMISMEMBER.ts | 9 +
lib/commands/SMOVE.spec.ts | 19 +
lib/commands/SMOVE.ts | 9 +
lib/commands/SORT.spec.ts | 106 +
lib/commands/SORT.ts | 57 +
lib/commands/SPOP.spec.ts | 28 +
lib/commands/SPOP.ts | 15 +
lib/commands/SRANDMEMBER.spec.ts | 19 +
lib/commands/SRANDMEMBER.ts | 9 +
lib/commands/SRANDMEMBER_COUNT.spec.ts | 19 +
lib/commands/SRANDMEMBER_COUNT.ts | 13 +
lib/commands/SREM.spec.ts | 28 +
lib/commands/SREM.ts | 9 +
lib/commands/SSCAN.spec.ts | 74 +
lib/commands/SSCAN.ts | 24 +
lib/commands/STRLEN.spec.ts | 26 +
lib/commands/STRLEN.ts | 11 +
lib/commands/SUNION.spec.ts | 28 +
lib/commands/SUNION.ts | 11 +
lib/commands/SUNIONSTORE.spec.ts | 28 +
lib/commands/SUNIONSTORE.ts | 9 +
lib/commands/SWAPDB.spec.ts | 19 +
lib/commands/SWAPDB.ts | 7 +
lib/commands/TIME.spec.ts | 18 +
lib/commands/TIME.ts | 15 +
lib/commands/TOUCH.spec.ts | 28 +
lib/commands/TOUCH.ts | 9 +
lib/commands/TTL.spec.ts | 19 +
lib/commands/TTL.ts | 11 +
lib/commands/TYPE.spec.ts | 19 +
lib/commands/TYPE.ts | 11 +
lib/commands/UNLINK.spec.ts | 28 +
lib/commands/UNLINK.ts | 9 +
lib/commands/UNWATCH.spec.ts | 26 +
lib/commands/UNWATCH.ts | 7 +
lib/commands/WAIT.spec.ts | 19 +
lib/commands/WAIT.ts | 9 +
lib/commands/WATCH.spec.ts | 20 +
lib/commands/WATCH.ts | 7 +
lib/commands/XACK.spec.ts | 28 +
lib/commands/XACK.ts | 9 +
lib/commands/XADD.spec.ts | 118 +
lib/commands/XADD.ts | 48 +
lib/commands/XAUTOCLAIM.spec.ts | 42 +
lib/commands/XAUTOCLAIM.ts | 36 +
lib/commands/XAUTOCLAIM_JUSTID.spec.ts | 31 +
lib/commands/XAUTOCLAIM_JUSTID.ts | 22 +
lib/commands/XCLAIM.spec.ts | 90 +
lib/commands/XCLAIM.ts | 46 +
lib/commands/XCLAIM_JUSTID.spec.ts | 23 +
lib/commands/XCLAIM_JUSTID.ts | 13 +
lib/commands/XDEL.spec.ts | 28 +
lib/commands/XDEL.ts | 9 +
lib/commands/XGROUP_CREATE.spec.ts | 32 +
lib/commands/XGROUP_CREATE.ts | 19 +
lib/commands/XGROUP_CREATECONSUMER.spec.ts | 25 +
lib/commands/XGROUP_CREATECONSUMER.ts | 9 +
lib/commands/XGROUP_DELCONSUMER.spec.ts | 23 +
lib/commands/XGROUP_DELCONSUMER.ts | 9 +
lib/commands/XGROUP_DESTROY.spec.ts | 23 +
lib/commands/XGROUP_DESTROY.ts | 9 +
lib/commands/XGROUP_SETID.spec.ts | 23 +
lib/commands/XGROUP_SETID.ts | 9 +
lib/commands/XINFO_CONSUMERS.spec.ts | 41 +
lib/commands/XINFO_CONSUMERS.ts | 21 +
lib/commands/XINFO_GROUPS.spec.ts | 48 +
lib/commands/XINFO_GROUPS.ts | 23 +
lib/commands/XINFO_STREAM.spec.ts | 72 +
lib/commands/XINFO_STREAM.ts | 63 +
lib/commands/XLEN.spec.ts | 19 +
lib/commands/XLEN.ts | 11 +
lib/commands/XPENDING.spec.ts | 30 +
lib/commands/XPENDING.ts | 23 +
lib/commands/XPENDING_RANGE.spec.ts | 53 +
lib/commands/XPENDING_RANGE.ts | 35 +
lib/commands/XRANGE.spec.ts | 30 +
lib/commands/XRANGE.ts | 21 +
lib/commands/XREAD.spec.ts | 87 +
lib/commands/XREAD.ts | 43 +
lib/commands/XREADGROUP.spec.ts | 137 +
lib/commands/XREADGROUP.ts | 57 +
lib/commands/XREVRANGE.spec.ts | 30 +
lib/commands/XREVRANGE.ts | 17 +
lib/commands/XTRIM.spec.ts | 49 +
lib/commands/XTRIM.ts | 26 +
lib/commands/ZADD.spec.ts | 127 +
lib/commands/ZADD.ts | 66 +
lib/commands/ZCARD.spec.ts | 19 +
lib/commands/ZCARD.ts | 11 +
lib/commands/ZCOUNT.spec.ts | 19 +
lib/commands/ZCOUNT.ts | 16 +
lib/commands/ZDIFF.spec.ts | 30 +
lib/commands/ZDIFF.ts | 11 +
lib/commands/ZDIFFSTORE.spec.ts | 30 +
lib/commands/ZDIFFSTORE.ts | 9 +
lib/commands/ZDIFF_WITHSCORES.spec.ts | 30 +
lib/commands/ZDIFF_WITHSCORES.ts | 13 +
lib/commands/ZINCRBY.spec.ts | 19 +
lib/commands/ZINCRBY.ts | 14 +
lib/commands/ZINTER.spec.ts | 58 +
lib/commands/ZINTER.ts | 29 +
lib/commands/ZINTERSTORE.spec.ts | 56 +
lib/commands/ZINTERSTORE.ts | 27 +
lib/commands/ZINTER_WITHSCORES.spec.ts | 58 +
lib/commands/ZINTER_WITHSCORES.ts | 13 +
lib/commands/ZLEXCOUNT.spec.ts | 19 +
lib/commands/ZLEXCOUNT.ts | 16 +
lib/commands/ZMSCORE.spec.ts | 30 +
lib/commands/ZMSCORE.ts | 11 +
lib/commands/ZPOPMAX.spec.ts | 41 +
lib/commands/ZPOPMAX.ts | 19 +
lib/commands/ZPOPMAX_COUNT.spec.ts | 19 +
lib/commands/ZPOPMAX_COUNT.ts | 13 +
lib/commands/ZPOPMIN.spec.ts | 41 +
lib/commands/ZPOPMIN.ts | 19 +
lib/commands/ZPOPMIN_COUNT.spec.ts | 19 +
lib/commands/ZPOPMIN_COUNT.ts | 13 +
lib/commands/ZRANDMEMBER.spec.ts | 21 +
lib/commands/ZRANDMEMBER.ts | 11 +
lib/commands/ZRANDMEMBER_COUNT.spec.ts | 21 +
lib/commands/ZRANDMEMBER_COUNT.ts | 13 +
.../ZRANDMEMBER_COUNT_WITHSCORES.spec.ts | 21 +
lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts | 13 +
lib/commands/ZRANGE.spec.ts | 74 +
lib/commands/ZRANGE.ts | 45 +
lib/commands/ZRANGESTORE.spec.ts | 82 +
lib/commands/ZRANGESTORE.ts | 55 +
lib/commands/ZRANGE_WITHSCORES.spec.ts | 65 +
lib/commands/ZRANGE_WITHSCORES.ts | 13 +
lib/commands/ZRANK.spec.ts | 19 +
lib/commands/ZRANK.ts | 11 +
lib/commands/ZREM.spec.ts | 28 +
lib/commands/ZREM.ts | 9 +
lib/commands/ZREMRANGEBYLEX.spec.ts | 19 +
lib/commands/ZREMRANGEBYLEX.ts | 9 +
lib/commands/ZREMRANGEBYRANK.spec.ts | 19 +
lib/commands/ZREMRANGEBYRANK.ts | 9 +
lib/commands/ZREMRANGEBYSCORE.spec.ts | 19 +
lib/commands/ZREMRANGEBYSCORE.ts | 9 +
lib/commands/ZREVRANK.spec.ts | 19 +
lib/commands/ZREVRANK.ts | 11 +
lib/commands/ZSCAN.spec.ts | 77 +
lib/commands/ZSCAN.ts | 32 +
lib/commands/ZSCORE.spec.ts | 19 +
lib/commands/ZSCORE.ts | 11 +
lib/commands/ZUNION.spec.ts | 48 +
lib/commands/ZUNION.ts | 26 +
lib/commands/ZUNIONSTORE.spec.ts | 56 +
lib/commands/ZUNIONSTORE.ts | 24 +
lib/commands/ZUNION_WITHSCORES.spec.ts | 48 +
lib/commands/ZUNION_WITHSCORES.ts | 13 +
lib/commands/generic-transformers.spec.ts | 623 ++
lib/commands/generic-transformers.ts | 381 +
lib/commands/index.ts | 755 ++
lib/createClient.js | 88 -
lib/customErrors.js | 58 -
lib/debug.js | 13 -
lib/errors.ts | 23 +
lib/extendedApi.js | 113 -
lib/individualCommands.js | 629 --
lib/lua-script.ts | 28 +
lib/multi-command.spec.ts | 127 +
lib/multi-command.ts | 210 +
lib/multi.js | 187 -
lib/socket.spec.ts | 38 +
lib/socket.ts | 254 +
lib/test-utils.ts | 373 +
lib/ts-declarations/cluster-key-slot.d.ts | 3 +
lib/ts-declarations/redis-parser.d.ts | 13 +
lib/utils.js | 134 -
lib/utils.ts | 3 +
package-lock.json | 9965 +++++++++++++++++
package.json | 74 +-
test/auth.spec.js | 354 -
test/batch.spec.js | 350 -
test/commands/blpop.spec.js | 77 -
test/commands/client.spec.js | 155 -
test/commands/dbsize.spec.js | 95 -
test/commands/del.spec.js | 57 -
test/commands/eval.spec.js | 210 -
test/commands/exists.spec.js | 40 -
test/commands/expire.spec.js | 42 -
test/commands/flushdb.spec.js | 105 -
test/commands/geoadd.spec.js | 35 -
test/commands/get.spec.js | 95 -
test/commands/getset.spec.js | 105 -
test/commands/hgetall.spec.js | 83 -
test/commands/hincrby.spec.js | 40 -
test/commands/hlen.spec.js | 38 -
test/commands/hmget.spec.js | 71 -
test/commands/hmset.spec.js | 117 -
test/commands/hset.spec.js | 85 -
test/commands/incr.spec.js | 76 -
test/commands/info.spec.js | 79 -
test/commands/keys.spec.js | 69 -
test/commands/mget.spec.js | 70 -
test/commands/monitor.spec.js | 215 -
test/commands/mset.spec.js | 111 -
test/commands/msetnx.spec.js | 38 -
test/commands/randomkey.test.js | 35 -
test/commands/rename.spec.js | 38 -
test/commands/renamenx.spec.js | 41 -
test/commands/rpush.spec.js | 36 -
test/commands/sadd.spec.js | 62 -
test/commands/scard.spec.js | 31 -
test/commands/script.spec.js | 55 -
test/commands/sdiff.spec.js | 47 -
test/commands/sdiffstore.spec.js | 47 -
test/commands/select.spec.js | 126 -
test/commands/set.spec.js | 178 -
test/commands/setex.spec.js | 37 -
test/commands/setnx.spec.js | 37 -
test/commands/sinter.spec.js | 63 -
test/commands/sinterstore.spec.js | 48 -
test/commands/sismember.spec.js | 35 -
test/commands/slowlog.spec.js | 41 -
test/commands/smembers.spec.js | 38 -
test/commands/smove.spec.js | 40 -
test/commands/sort.spec.js | 130 -
test/commands/spop.spec.js | 38 -
test/commands/srem.spec.js | 69 -
test/commands/sunion.spec.js | 46 -
test/commands/sunionstore.spec.js | 49 -
test/commands/ttl.spec.js | 37 -
test/commands/type.spec.js | 56 -
test/commands/watch.spec.js | 54 -
test/commands/zadd.spec.js | 52 -
test/commands/zscan.spec.js | 50 -
test/commands/zscore.spec.js | 35 -
test/conect.slave.spec.js | 99 -
test/conf/faulty.cert | 19 -
test/conf/password.conf | 6 -
test/conf/redis.conf | 5 -
test/conf/redis.js.org.cert | 19 -
test/conf/redis.js.org.key | 27 -
test/conf/rename.conf | 8 -
test/conf/slave.conf | 7 -
test/conf/stunnel.conf.template | 11 -
test/connection.spec.js | 637 --
test/custom_errors.spec.js | 89 -
test/detect_buffers.spec.js | 268 -
test/errors.js | 6 -
test/good_traces.spec.js | 59 -
test/helper.js | 220 -
test/lib/config.js | 37 -
test/lib/good-traces.js | 20 -
test/lib/redis-process.js | 100 -
test/lib/stunnel-process.js | 88 -
test/lib/unref.js | 17 -
test/multi.spec.js | 737 --
test/node_redis.spec.js | 1043 --
test/prefix.spec.js | 118 -
test/pubsub.spec.js | 679 --
test/rename.spec.js | 147 -
test/return_buffers.spec.js | 297 -
test/tls.spec.js | 151 -
test/unify_options.spec.js | 241 -
test/utils.spec.js | 185 -
tsconfig.json | 37 +
661 files changed, 28837 insertions(+), 14549 deletions(-)
delete mode 100644 .deepsource.toml
delete mode 100644 .eslintignore
delete mode 100644 .eslintrc
delete mode 100644 .github/FUNDING.yml
delete mode 100644 .github/workflows/codeql-analysis.yml
create mode 100644 .github/workflows/documentation.yml
delete mode 100644 .github/workflows/linting.yml
delete mode 100644 .github/workflows/tests_windows.yml
create mode 100644 .nycrc.json
delete mode 100644 .prettierrc
delete mode 100644 CODE_OF_CONDUCT.md
create mode 100644 benchmark/.gitignore
create mode 100644 benchmark/index.js
create mode 100644 benchmark/package-lock.json
create mode 100644 benchmark/package.json
delete mode 100755 benchmarks/diff_multi_bench_output.js
delete mode 100644 benchmarks/multi_bench.js
create mode 100644 docs/FAQ.md
create mode 100644 docs/client-configuration.md
create mode 100644 docs/isolated-execution.md
create mode 100644 docs/v3-to-v4.md
delete mode 100644 examples/auth.js
delete mode 100644 examples/backpressure_drain.js
delete mode 100644 examples/eval.js
delete mode 100644 examples/extend.js
delete mode 100644 examples/file.js
delete mode 100644 examples/mget.js
delete mode 100644 examples/monitor.js
delete mode 100644 examples/multi.js
delete mode 100644 examples/multi2.js
delete mode 100644 examples/psubscribe.js
delete mode 100644 examples/pub_sub.js
delete mode 100644 examples/scan.js
delete mode 100644 examples/simple.js
delete mode 100644 examples/sort.js
delete mode 100644 examples/streams.js
delete mode 100644 examples/subqueries.js
delete mode 100644 examples/subquery.js
delete mode 100644 examples/unix_socket.js
delete mode 100644 examples/web_server.js
delete mode 100644 index.js
create mode 100644 index.ts
create mode 100644 lib/client.spec.ts
create mode 100644 lib/client.ts
create mode 100644 lib/cluster-slots.ts
create mode 100644 lib/cluster.spec.ts
create mode 100644 lib/cluster.ts
create mode 100644 lib/command-options.ts
delete mode 100644 lib/command.js
create mode 100644 lib/commander.spec.ts
create mode 100644 lib/commander.ts
create mode 100644 lib/commands-queue.ts
delete mode 100644 lib/commands.js
create mode 100644 lib/commands/ACL_CAT.spec.ts
create mode 100644 lib/commands/ACL_CAT.ts
create mode 100644 lib/commands/ACL_DELUSER.spec.ts
create mode 100644 lib/commands/ACL_DELUSER.ts
create mode 100644 lib/commands/ACL_GENPASS.spec.ts
create mode 100644 lib/commands/ACL_GENPASS.ts
create mode 100644 lib/commands/ACL_GETUSER.spec.ts
create mode 100644 lib/commands/ACL_GETUSER.ts
create mode 100644 lib/commands/ACL_LIST.spec.ts
create mode 100644 lib/commands/ACL_LIST.ts
create mode 100644 lib/commands/ACL_LOAD.spec.ts
create mode 100644 lib/commands/ACL_LOAD.ts
create mode 100644 lib/commands/ACL_LOG.spec.ts
create mode 100644 lib/commands/ACL_LOG.ts
create mode 100644 lib/commands/ACL_LOG_RESET.spec.ts
create mode 100644 lib/commands/ACL_LOG_RESET.ts
create mode 100644 lib/commands/ACL_SAVE.spec.ts
create mode 100644 lib/commands/ACL_SAVE.ts
create mode 100644 lib/commands/ACL_SETUSER.spec.ts
create mode 100644 lib/commands/ACL_SETUSER.ts
create mode 100644 lib/commands/ACL_USERS.spec.ts
create mode 100644 lib/commands/ACL_USERS.ts
create mode 100644 lib/commands/ACL_WHOAMI.spec.ts
create mode 100644 lib/commands/ACL_WHOAMI.ts
create mode 100644 lib/commands/APPEND.spec.ts
create mode 100644 lib/commands/APPEND.ts
create mode 100644 lib/commands/ASKING.spec.ts
create mode 100644 lib/commands/ASKING.ts
create mode 100644 lib/commands/AUTH.spec.ts
create mode 100644 lib/commands/AUTH.ts
create mode 100644 lib/commands/BGREWRITEAOF.spec.ts
create mode 100644 lib/commands/BGREWRITEAOF.ts
create mode 100644 lib/commands/BGSAVE.spec.ts
create mode 100644 lib/commands/BGSAVE.ts
create mode 100644 lib/commands/BITCOUNT.spec.ts
create mode 100644 lib/commands/BITCOUNT.ts
create mode 100644 lib/commands/BITFIELD.spec.ts
create mode 100644 lib/commands/BITFIELD.ts
create mode 100644 lib/commands/BITOP.spec.ts
create mode 100644 lib/commands/BITOP.ts
create mode 100644 lib/commands/BITPOS.spec.ts
create mode 100644 lib/commands/BITPOS.ts
create mode 100644 lib/commands/BLMOVE.spec.ts
create mode 100644 lib/commands/BLMOVE.ts
create mode 100644 lib/commands/BLPOP.spec.ts
create mode 100644 lib/commands/BLPOP.ts
create mode 100644 lib/commands/BRPOP.spec.ts
create mode 100644 lib/commands/BRPOP.ts
create mode 100644 lib/commands/BRPOPLPUSH.spec.ts
create mode 100644 lib/commands/BRPOPLPUSH.ts
create mode 100644 lib/commands/BZPOPMAX.spec.ts
create mode 100644 lib/commands/BZPOPMAX.ts
create mode 100644 lib/commands/BZPOPMIN.spec.ts
create mode 100644 lib/commands/BZPOPMIN.ts
create mode 100644 lib/commands/CLIENT_ID.spec.ts
create mode 100644 lib/commands/CLIENT_ID.ts
create mode 100644 lib/commands/CLIENT_INFO.spec.ts
create mode 100644 lib/commands/CLIENT_INFO.ts
create mode 100644 lib/commands/CLUSTER_ADDSLOTS.spec.ts
create mode 100644 lib/commands/CLUSTER_ADDSLOTS.ts
create mode 100644 lib/commands/CLUSTER_FLUSHSLOTS.spec.ts
create mode 100644 lib/commands/CLUSTER_FLUSHSLOTS.ts
create mode 100644 lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts
create mode 100644 lib/commands/CLUSTER_GETKEYSINSLOT.ts
create mode 100644 lib/commands/CLUSTER_INFO.spec.ts
create mode 100644 lib/commands/CLUSTER_INFO.ts
create mode 100644 lib/commands/CLUSTER_MEET.spec.ts
create mode 100644 lib/commands/CLUSTER_MEET.ts
create mode 100644 lib/commands/CLUSTER_NODES.spec.ts
create mode 100644 lib/commands/CLUSTER_NODES.ts
create mode 100644 lib/commands/CLUSTER_RESET.spec.ts
create mode 100644 lib/commands/CLUSTER_RESET.ts
create mode 100644 lib/commands/CLUSTER_SETSLOT.spec.ts
create mode 100644 lib/commands/CLUSTER_SETSLOT.ts
create mode 100644 lib/commands/CONFIG_GET.spec.ts
create mode 100644 lib/commands/CONFIG_GET.ts
create mode 100644 lib/commands/CONFIG_RESETSTAT.spec.ts
create mode 100644 lib/commands/CONFIG_RESETSTAT.ts
create mode 100644 lib/commands/CONFIG_REWRITE.spec.ts
create mode 100644 lib/commands/CONFIG_REWRITE.ts
create mode 100644 lib/commands/CONFIG_SET.spec.ts
create mode 100644 lib/commands/CONFIG_SET.ts
create mode 100644 lib/commands/COPY.spec.ts
create mode 100644 lib/commands/COPY.ts
create mode 100644 lib/commands/DBSIZE.spec.ts
create mode 100644 lib/commands/DBSIZE.ts
create mode 100644 lib/commands/DECR.spec.ts
create mode 100644 lib/commands/DECR.ts
create mode 100644 lib/commands/DECRBY.spec.ts
create mode 100644 lib/commands/DECRBY.ts
create mode 100644 lib/commands/DEL.spec.ts
create mode 100644 lib/commands/DEL.ts
create mode 100644 lib/commands/DISCARD.spec.ts
create mode 100644 lib/commands/DISCARD.ts
create mode 100644 lib/commands/DUMP.spec.ts
create mode 100644 lib/commands/DUMP.ts
create mode 100644 lib/commands/ECHO.spec.ts
create mode 100644 lib/commands/ECHO.ts
create mode 100644 lib/commands/EVAL.spec.ts
create mode 100644 lib/commands/EVAL.ts
create mode 100644 lib/commands/EVALSHA.spec.ts
create mode 100644 lib/commands/EVALSHA.ts
create mode 100644 lib/commands/EXISTS.spec.ts
create mode 100644 lib/commands/EXISTS.ts
create mode 100644 lib/commands/EXPIRE.spec.ts
create mode 100644 lib/commands/EXPIRE.ts
create mode 100644 lib/commands/EXPIREAT.spec.ts
create mode 100644 lib/commands/EXPIREAT.ts
create mode 100644 lib/commands/FAILOVER.spec.ts
create mode 100644 lib/commands/FAILOVER.ts
create mode 100644 lib/commands/FLUSHALL.spec.ts
create mode 100644 lib/commands/FLUSHALL.ts
create mode 100644 lib/commands/FLUSHDB.spec.ts
create mode 100644 lib/commands/FLUSHDB.ts
create mode 100644 lib/commands/GEOADD.spec.ts
create mode 100644 lib/commands/GEOADD.ts
create mode 100644 lib/commands/GEODIST.spec.ts
create mode 100644 lib/commands/GEODIST.ts
create mode 100644 lib/commands/GEOHASH.spec.ts
create mode 100644 lib/commands/GEOHASH.ts
create mode 100644 lib/commands/GEOPOS.spec.ts
create mode 100644 lib/commands/GEOPOS.ts
create mode 100644 lib/commands/GEOSEARCH.spec.ts
create mode 100644 lib/commands/GEOSEARCH.ts
create mode 100644 lib/commands/GEOSEARCHSTORE.spec.ts
create mode 100644 lib/commands/GEOSEARCHSTORE.ts
create mode 100644 lib/commands/GEOSEARCH_WITH.spec.ts
create mode 100644 lib/commands/GEOSEARCH_WITH.ts
create mode 100644 lib/commands/GET.spec.ts
create mode 100644 lib/commands/GET.ts
create mode 100644 lib/commands/GETBIT.spec.ts
create mode 100644 lib/commands/GETBIT.ts
create mode 100644 lib/commands/GETDEL.spec.ts
create mode 100644 lib/commands/GETDEL.ts
create mode 100644 lib/commands/GETEX.spec.ts
create mode 100644 lib/commands/GETEX.ts
create mode 100644 lib/commands/GETRANGE.spec.ts
create mode 100644 lib/commands/GETRANGE.ts
create mode 100644 lib/commands/GETSET.spec.ts
create mode 100644 lib/commands/GETSET.ts
create mode 100644 lib/commands/HDEL.spec.ts
create mode 100644 lib/commands/HDEL.ts
create mode 100644 lib/commands/HELLO.spec.ts
create mode 100644 lib/commands/HELLO.ts
create mode 100644 lib/commands/HEXISTS.spec.ts
create mode 100644 lib/commands/HEXISTS.ts
create mode 100644 lib/commands/HGET.spec.ts
create mode 100644 lib/commands/HGET.ts
create mode 100644 lib/commands/HGETALL.spec.ts
create mode 100644 lib/commands/HGETALL.ts
create mode 100644 lib/commands/HINCRBY.spec.ts
create mode 100644 lib/commands/HINCRBY.ts
create mode 100644 lib/commands/HINCRBYFLOAT.spec.ts
create mode 100644 lib/commands/HINCRBYFLOAT.ts
create mode 100644 lib/commands/HKEYS.spec.ts
create mode 100644 lib/commands/HKEYS.ts
create mode 100644 lib/commands/HLEN.spec.ts
create mode 100644 lib/commands/HLEN.ts
create mode 100644 lib/commands/HMGET.spec.ts
create mode 100644 lib/commands/HMGET.ts
create mode 100644 lib/commands/HRANDFIELD.spec.ts
create mode 100644 lib/commands/HRANDFIELD.ts
create mode 100644 lib/commands/HRANDFIELD_COUNT.spec.ts
create mode 100644 lib/commands/HRANDFIELD_COUNT.ts
create mode 100644 lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts
create mode 100644 lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts
create mode 100644 lib/commands/HSCAN.spec.ts
create mode 100644 lib/commands/HSCAN.ts
create mode 100644 lib/commands/HSET.spec.ts
create mode 100644 lib/commands/HSET.ts
create mode 100644 lib/commands/HSETNX.spec.ts
create mode 100644 lib/commands/HSETNX.ts
create mode 100644 lib/commands/HSTRLEN.spec.ts
create mode 100644 lib/commands/HSTRLEN.ts
create mode 100644 lib/commands/HVALS.spec.ts
create mode 100644 lib/commands/HVALS.ts
create mode 100644 lib/commands/INCR.spec.ts
create mode 100644 lib/commands/INCR.ts
create mode 100644 lib/commands/INCRBY.spec.ts
create mode 100644 lib/commands/INCRBY.ts
create mode 100644 lib/commands/INCRBYFLOAT.spec.ts
create mode 100644 lib/commands/INCRBYFLOAT.ts
create mode 100644 lib/commands/INFO.spec.ts
create mode 100644 lib/commands/INFO.ts
create mode 100644 lib/commands/KEYS.spec.ts
create mode 100644 lib/commands/KEYS.ts
create mode 100644 lib/commands/LASTSAVE.spec.ts
create mode 100644 lib/commands/LASTSAVE.ts
create mode 100644 lib/commands/LINDEX.spec.ts
create mode 100644 lib/commands/LINDEX.ts
create mode 100644 lib/commands/LINSERT.spec.ts
create mode 100644 lib/commands/LINSERT.ts
create mode 100644 lib/commands/LLEN.spec.ts
create mode 100644 lib/commands/LLEN.ts
create mode 100644 lib/commands/LMOVE.spec.ts
create mode 100644 lib/commands/LMOVE.ts
create mode 100644 lib/commands/LOLWUT.spec.ts
create mode 100644 lib/commands/LOLWUT.ts
create mode 100644 lib/commands/LPOP.spec.ts
create mode 100644 lib/commands/LPOP.ts
create mode 100644 lib/commands/LPOP_COUNT.spec.ts
create mode 100644 lib/commands/LPOP_COUNT.ts
create mode 100644 lib/commands/LPOS.spec.ts
create mode 100644 lib/commands/LPOS.ts
create mode 100644 lib/commands/LPOS_COUNT.spec.ts
create mode 100644 lib/commands/LPOS_COUNT.ts
create mode 100644 lib/commands/LPUSH.spec.ts
create mode 100644 lib/commands/LPUSH.ts
create mode 100644 lib/commands/LPUSHX.spec.ts
create mode 100644 lib/commands/LPUSHX.ts
create mode 100644 lib/commands/LRANGE.spec.ts
create mode 100644 lib/commands/LRANGE.ts
create mode 100644 lib/commands/LREM.spec.ts
create mode 100644 lib/commands/LREM.ts
create mode 100644 lib/commands/LSET.spec.ts
create mode 100644 lib/commands/LSET.ts
create mode 100644 lib/commands/LTRIM.spec.ts
create mode 100644 lib/commands/LTRIM.ts
create mode 100644 lib/commands/MEMORY_DOCTOR.spec.ts
create mode 100644 lib/commands/MEMORY_DOCTOR.ts
create mode 100644 lib/commands/MEMORY_MALLOC-STATS.spec.ts
create mode 100644 lib/commands/MEMORY_MALLOC-STATS.ts
create mode 100644 lib/commands/MEMORY_PURGE.spec.ts
create mode 100644 lib/commands/MEMORY_PURGE.ts
create mode 100644 lib/commands/MEMORY_STATS.spec.ts
create mode 100644 lib/commands/MEMORY_STATS.ts
create mode 100644 lib/commands/MEMORY_USAGE.spec.ts
create mode 100644 lib/commands/MEMORY_USAGE.ts
create mode 100644 lib/commands/MGET.spec.ts
create mode 100644 lib/commands/MGET.ts
create mode 100644 lib/commands/MIGRATE.spec.ts
create mode 100644 lib/commands/MIGRATE.ts
create mode 100644 lib/commands/MODULE_LIST.spec.ts
create mode 100644 lib/commands/MODULE_LIST.ts
create mode 100644 lib/commands/MODULE_LOAD.spec.ts
create mode 100644 lib/commands/MODULE_LOAD.ts
create mode 100644 lib/commands/MODULE_UNLOAD.spec.ts
create mode 100644 lib/commands/MODULE_UNLOAD.ts
create mode 100644 lib/commands/MOVE.spec.ts
create mode 100644 lib/commands/MOVE.ts
create mode 100644 lib/commands/MSET.spec.ts
create mode 100644 lib/commands/MSET.ts
create mode 100644 lib/commands/MSETNX.spec.ts
create mode 100644 lib/commands/MSETNX.ts
create mode 100644 lib/commands/PERSIST.spec.ts
create mode 100644 lib/commands/PERSIST.ts
create mode 100644 lib/commands/PEXPIRE.spec.ts
create mode 100644 lib/commands/PEXPIRE.ts
create mode 100644 lib/commands/PEXPIREAT.spec.ts
create mode 100644 lib/commands/PEXPIREAT.ts
create mode 100644 lib/commands/PFADD.spec.ts
create mode 100644 lib/commands/PFADD.ts
create mode 100644 lib/commands/PFCOUNT.spec.ts
create mode 100644 lib/commands/PFCOUNT.ts
create mode 100644 lib/commands/PFMERGE.spec.ts
create mode 100644 lib/commands/PFMERGE.ts
create mode 100644 lib/commands/PING.spec.ts
create mode 100644 lib/commands/PING.ts
create mode 100644 lib/commands/PSETEX.spec.ts
create mode 100644 lib/commands/PSETEX.ts
create mode 100644 lib/commands/PTTL.spec.ts
create mode 100644 lib/commands/PTTL.ts
create mode 100644 lib/commands/PUBLISH.spec.ts
create mode 100644 lib/commands/PUBLISH.ts
create mode 100644 lib/commands/PUBSUB_CHANNELS.spec.ts
create mode 100644 lib/commands/PUBSUB_CHANNELS.ts
create mode 100644 lib/commands/PUBSUB_NUMPAT.spec.ts
create mode 100644 lib/commands/PUBSUB_NUMPAT.ts
create mode 100644 lib/commands/PUBSUB_NUMSUB.spec.ts
create mode 100644 lib/commands/PUBSUB_NUMSUB.ts
create mode 100644 lib/commands/RANDOMKEY.spec.ts
create mode 100644 lib/commands/RANDOMKEY.ts
create mode 100644 lib/commands/READONLY.spec.ts
create mode 100644 lib/commands/READONLY.ts
create mode 100644 lib/commands/READWRITE.spec.ts
create mode 100644 lib/commands/READWRITE.ts
create mode 100644 lib/commands/RENAME.spec.ts
create mode 100644 lib/commands/RENAME.ts
create mode 100644 lib/commands/RENAMENX.spec.ts
create mode 100644 lib/commands/RENAMENX.ts
create mode 100644 lib/commands/REPLICAOF.spec.ts
create mode 100644 lib/commands/REPLICAOF.ts
create mode 100644 lib/commands/RESTORE-ASKING.spec.ts
create mode 100644 lib/commands/RESTORE-ASKING.ts
create mode 100644 lib/commands/ROLE.spec.ts
create mode 100644 lib/commands/ROLE.ts
create mode 100644 lib/commands/RPOP.spec.ts
create mode 100644 lib/commands/RPOP.ts
create mode 100644 lib/commands/RPOPLPUSH.spec.ts
create mode 100644 lib/commands/RPOPLPUSH.ts
create mode 100644 lib/commands/RPOP_COUNT.spec.ts
create mode 100644 lib/commands/RPOP_COUNT.ts
create mode 100644 lib/commands/RPUSH.spec.ts
create mode 100644 lib/commands/RPUSH.ts
create mode 100644 lib/commands/RPUSHX.spec.ts
create mode 100644 lib/commands/RPUSHX.ts
create mode 100644 lib/commands/SADD.spec.ts
create mode 100644 lib/commands/SADD.ts
create mode 100644 lib/commands/SAVE.spec.ts
create mode 100644 lib/commands/SAVE.ts
create mode 100644 lib/commands/SCAN.spec.ts
create mode 100644 lib/commands/SCAN.ts
create mode 100644 lib/commands/SCARD.spec.ts
create mode 100644 lib/commands/SCARD.ts
create mode 100644 lib/commands/SCRIPT_DEBUG.spec.ts
create mode 100644 lib/commands/SCRIPT_DEBUG.ts
create mode 100644 lib/commands/SCRIPT_EXISTS.spec.ts
create mode 100644 lib/commands/SCRIPT_EXISTS.ts
create mode 100644 lib/commands/SCRIPT_FLUSH.spec.ts
create mode 100644 lib/commands/SCRIPT_FLUSH.ts
create mode 100644 lib/commands/SCRIPT_KILL.spec.ts
create mode 100644 lib/commands/SCRIPT_KILL.ts
create mode 100644 lib/commands/SCRIPT_LOAD.spec.ts
create mode 100644 lib/commands/SCRIPT_LOAD.ts
create mode 100644 lib/commands/SDIFF.spec.ts
create mode 100644 lib/commands/SDIFF.ts
create mode 100644 lib/commands/SDIFFSTORE.spec.ts
create mode 100644 lib/commands/SDIFFSTORE.ts
create mode 100644 lib/commands/SET.spec.ts
create mode 100644 lib/commands/SET.ts
create mode 100644 lib/commands/SETBIT.spec.ts
create mode 100644 lib/commands/SETBIT.ts
create mode 100644 lib/commands/SETEX.spec.ts
create mode 100644 lib/commands/SETEX.ts
create mode 100644 lib/commands/SETNX .spec.ts
create mode 100644 lib/commands/SETNX.ts
create mode 100644 lib/commands/SETRANGE.spec.ts
create mode 100644 lib/commands/SETRANGE.ts
create mode 100644 lib/commands/SHUTDOWN.spec.ts
create mode 100644 lib/commands/SHUTDOWN.ts
create mode 100644 lib/commands/SINTER.spec.ts
create mode 100644 lib/commands/SINTER.ts
create mode 100644 lib/commands/SINTERSTORE.spec.ts
create mode 100644 lib/commands/SINTERSTORE.ts
create mode 100644 lib/commands/SISMEMBER.spec.ts
create mode 100644 lib/commands/SISMEMBER.ts
create mode 100644 lib/commands/SMEMBERS.spec.ts
create mode 100644 lib/commands/SMEMBERS.ts
create mode 100644 lib/commands/SMISMEMBER.spec.ts
create mode 100644 lib/commands/SMISMEMBER.ts
create mode 100644 lib/commands/SMOVE.spec.ts
create mode 100644 lib/commands/SMOVE.ts
create mode 100644 lib/commands/SORT.spec.ts
create mode 100644 lib/commands/SORT.ts
create mode 100644 lib/commands/SPOP.spec.ts
create mode 100644 lib/commands/SPOP.ts
create mode 100644 lib/commands/SRANDMEMBER.spec.ts
create mode 100644 lib/commands/SRANDMEMBER.ts
create mode 100644 lib/commands/SRANDMEMBER_COUNT.spec.ts
create mode 100644 lib/commands/SRANDMEMBER_COUNT.ts
create mode 100644 lib/commands/SREM.spec.ts
create mode 100644 lib/commands/SREM.ts
create mode 100644 lib/commands/SSCAN.spec.ts
create mode 100644 lib/commands/SSCAN.ts
create mode 100644 lib/commands/STRLEN.spec.ts
create mode 100644 lib/commands/STRLEN.ts
create mode 100644 lib/commands/SUNION.spec.ts
create mode 100644 lib/commands/SUNION.ts
create mode 100644 lib/commands/SUNIONSTORE.spec.ts
create mode 100644 lib/commands/SUNIONSTORE.ts
create mode 100644 lib/commands/SWAPDB.spec.ts
create mode 100644 lib/commands/SWAPDB.ts
create mode 100644 lib/commands/TIME.spec.ts
create mode 100644 lib/commands/TIME.ts
create mode 100644 lib/commands/TOUCH.spec.ts
create mode 100644 lib/commands/TOUCH.ts
create mode 100644 lib/commands/TTL.spec.ts
create mode 100644 lib/commands/TTL.ts
create mode 100644 lib/commands/TYPE.spec.ts
create mode 100644 lib/commands/TYPE.ts
create mode 100644 lib/commands/UNLINK.spec.ts
create mode 100644 lib/commands/UNLINK.ts
create mode 100644 lib/commands/UNWATCH.spec.ts
create mode 100644 lib/commands/UNWATCH.ts
create mode 100644 lib/commands/WAIT.spec.ts
create mode 100644 lib/commands/WAIT.ts
create mode 100644 lib/commands/WATCH.spec.ts
create mode 100644 lib/commands/WATCH.ts
create mode 100644 lib/commands/XACK.spec.ts
create mode 100644 lib/commands/XACK.ts
create mode 100644 lib/commands/XADD.spec.ts
create mode 100644 lib/commands/XADD.ts
create mode 100644 lib/commands/XAUTOCLAIM.spec.ts
create mode 100644 lib/commands/XAUTOCLAIM.ts
create mode 100644 lib/commands/XAUTOCLAIM_JUSTID.spec.ts
create mode 100644 lib/commands/XAUTOCLAIM_JUSTID.ts
create mode 100644 lib/commands/XCLAIM.spec.ts
create mode 100644 lib/commands/XCLAIM.ts
create mode 100644 lib/commands/XCLAIM_JUSTID.spec.ts
create mode 100644 lib/commands/XCLAIM_JUSTID.ts
create mode 100644 lib/commands/XDEL.spec.ts
create mode 100644 lib/commands/XDEL.ts
create mode 100644 lib/commands/XGROUP_CREATE.spec.ts
create mode 100644 lib/commands/XGROUP_CREATE.ts
create mode 100644 lib/commands/XGROUP_CREATECONSUMER.spec.ts
create mode 100644 lib/commands/XGROUP_CREATECONSUMER.ts
create mode 100644 lib/commands/XGROUP_DELCONSUMER.spec.ts
create mode 100644 lib/commands/XGROUP_DELCONSUMER.ts
create mode 100644 lib/commands/XGROUP_DESTROY.spec.ts
create mode 100644 lib/commands/XGROUP_DESTROY.ts
create mode 100644 lib/commands/XGROUP_SETID.spec.ts
create mode 100644 lib/commands/XGROUP_SETID.ts
create mode 100644 lib/commands/XINFO_CONSUMERS.spec.ts
create mode 100644 lib/commands/XINFO_CONSUMERS.ts
create mode 100644 lib/commands/XINFO_GROUPS.spec.ts
create mode 100644 lib/commands/XINFO_GROUPS.ts
create mode 100644 lib/commands/XINFO_STREAM.spec.ts
create mode 100644 lib/commands/XINFO_STREAM.ts
create mode 100644 lib/commands/XLEN.spec.ts
create mode 100644 lib/commands/XLEN.ts
create mode 100644 lib/commands/XPENDING.spec.ts
create mode 100644 lib/commands/XPENDING.ts
create mode 100644 lib/commands/XPENDING_RANGE.spec.ts
create mode 100644 lib/commands/XPENDING_RANGE.ts
create mode 100644 lib/commands/XRANGE.spec.ts
create mode 100644 lib/commands/XRANGE.ts
create mode 100644 lib/commands/XREAD.spec.ts
create mode 100644 lib/commands/XREAD.ts
create mode 100644 lib/commands/XREADGROUP.spec.ts
create mode 100644 lib/commands/XREADGROUP.ts
create mode 100644 lib/commands/XREVRANGE.spec.ts
create mode 100644 lib/commands/XREVRANGE.ts
create mode 100644 lib/commands/XTRIM.spec.ts
create mode 100644 lib/commands/XTRIM.ts
create mode 100644 lib/commands/ZADD.spec.ts
create mode 100644 lib/commands/ZADD.ts
create mode 100644 lib/commands/ZCARD.spec.ts
create mode 100644 lib/commands/ZCARD.ts
create mode 100644 lib/commands/ZCOUNT.spec.ts
create mode 100644 lib/commands/ZCOUNT.ts
create mode 100644 lib/commands/ZDIFF.spec.ts
create mode 100644 lib/commands/ZDIFF.ts
create mode 100644 lib/commands/ZDIFFSTORE.spec.ts
create mode 100644 lib/commands/ZDIFFSTORE.ts
create mode 100644 lib/commands/ZDIFF_WITHSCORES.spec.ts
create mode 100644 lib/commands/ZDIFF_WITHSCORES.ts
create mode 100644 lib/commands/ZINCRBY.spec.ts
create mode 100644 lib/commands/ZINCRBY.ts
create mode 100644 lib/commands/ZINTER.spec.ts
create mode 100644 lib/commands/ZINTER.ts
create mode 100644 lib/commands/ZINTERSTORE.spec.ts
create mode 100644 lib/commands/ZINTERSTORE.ts
create mode 100644 lib/commands/ZINTER_WITHSCORES.spec.ts
create mode 100644 lib/commands/ZINTER_WITHSCORES.ts
create mode 100644 lib/commands/ZLEXCOUNT.spec.ts
create mode 100644 lib/commands/ZLEXCOUNT.ts
create mode 100644 lib/commands/ZMSCORE.spec.ts
create mode 100644 lib/commands/ZMSCORE.ts
create mode 100644 lib/commands/ZPOPMAX.spec.ts
create mode 100644 lib/commands/ZPOPMAX.ts
create mode 100644 lib/commands/ZPOPMAX_COUNT.spec.ts
create mode 100644 lib/commands/ZPOPMAX_COUNT.ts
create mode 100644 lib/commands/ZPOPMIN.spec.ts
create mode 100644 lib/commands/ZPOPMIN.ts
create mode 100644 lib/commands/ZPOPMIN_COUNT.spec.ts
create mode 100644 lib/commands/ZPOPMIN_COUNT.ts
create mode 100644 lib/commands/ZRANDMEMBER.spec.ts
create mode 100644 lib/commands/ZRANDMEMBER.ts
create mode 100644 lib/commands/ZRANDMEMBER_COUNT.spec.ts
create mode 100644 lib/commands/ZRANDMEMBER_COUNT.ts
create mode 100644 lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts
create mode 100644 lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts
create mode 100644 lib/commands/ZRANGE.spec.ts
create mode 100644 lib/commands/ZRANGE.ts
create mode 100644 lib/commands/ZRANGESTORE.spec.ts
create mode 100644 lib/commands/ZRANGESTORE.ts
create mode 100644 lib/commands/ZRANGE_WITHSCORES.spec.ts
create mode 100644 lib/commands/ZRANGE_WITHSCORES.ts
create mode 100644 lib/commands/ZRANK.spec.ts
create mode 100644 lib/commands/ZRANK.ts
create mode 100644 lib/commands/ZREM.spec.ts
create mode 100644 lib/commands/ZREM.ts
create mode 100644 lib/commands/ZREMRANGEBYLEX.spec.ts
create mode 100644 lib/commands/ZREMRANGEBYLEX.ts
create mode 100644 lib/commands/ZREMRANGEBYRANK.spec.ts
create mode 100644 lib/commands/ZREMRANGEBYRANK.ts
create mode 100644 lib/commands/ZREMRANGEBYSCORE.spec.ts
create mode 100644 lib/commands/ZREMRANGEBYSCORE.ts
create mode 100644 lib/commands/ZREVRANK.spec.ts
create mode 100644 lib/commands/ZREVRANK.ts
create mode 100644 lib/commands/ZSCAN.spec.ts
create mode 100644 lib/commands/ZSCAN.ts
create mode 100644 lib/commands/ZSCORE.spec.ts
create mode 100644 lib/commands/ZSCORE.ts
create mode 100644 lib/commands/ZUNION.spec.ts
create mode 100644 lib/commands/ZUNION.ts
create mode 100644 lib/commands/ZUNIONSTORE.spec.ts
create mode 100644 lib/commands/ZUNIONSTORE.ts
create mode 100644 lib/commands/ZUNION_WITHSCORES.spec.ts
create mode 100644 lib/commands/ZUNION_WITHSCORES.ts
create mode 100644 lib/commands/generic-transformers.spec.ts
create mode 100644 lib/commands/generic-transformers.ts
create mode 100644 lib/commands/index.ts
delete mode 100644 lib/createClient.js
delete mode 100644 lib/customErrors.js
delete mode 100644 lib/debug.js
create mode 100644 lib/errors.ts
delete mode 100644 lib/extendedApi.js
delete mode 100644 lib/individualCommands.js
create mode 100644 lib/lua-script.ts
create mode 100644 lib/multi-command.spec.ts
create mode 100644 lib/multi-command.ts
delete mode 100644 lib/multi.js
create mode 100644 lib/socket.spec.ts
create mode 100644 lib/socket.ts
create mode 100644 lib/test-utils.ts
create mode 100644 lib/ts-declarations/cluster-key-slot.d.ts
create mode 100644 lib/ts-declarations/redis-parser.d.ts
delete mode 100644 lib/utils.js
create mode 100644 lib/utils.ts
create mode 100644 package-lock.json
delete mode 100644 test/auth.spec.js
delete mode 100644 test/batch.spec.js
delete mode 100644 test/commands/blpop.spec.js
delete mode 100644 test/commands/client.spec.js
delete mode 100644 test/commands/dbsize.spec.js
delete mode 100644 test/commands/del.spec.js
delete mode 100644 test/commands/eval.spec.js
delete mode 100644 test/commands/exists.spec.js
delete mode 100644 test/commands/expire.spec.js
delete mode 100644 test/commands/flushdb.spec.js
delete mode 100644 test/commands/geoadd.spec.js
delete mode 100644 test/commands/get.spec.js
delete mode 100644 test/commands/getset.spec.js
delete mode 100644 test/commands/hgetall.spec.js
delete mode 100644 test/commands/hincrby.spec.js
delete mode 100644 test/commands/hlen.spec.js
delete mode 100644 test/commands/hmget.spec.js
delete mode 100644 test/commands/hmset.spec.js
delete mode 100644 test/commands/hset.spec.js
delete mode 100644 test/commands/incr.spec.js
delete mode 100644 test/commands/info.spec.js
delete mode 100644 test/commands/keys.spec.js
delete mode 100644 test/commands/mget.spec.js
delete mode 100644 test/commands/monitor.spec.js
delete mode 100644 test/commands/mset.spec.js
delete mode 100644 test/commands/msetnx.spec.js
delete mode 100644 test/commands/randomkey.test.js
delete mode 100644 test/commands/rename.spec.js
delete mode 100644 test/commands/renamenx.spec.js
delete mode 100644 test/commands/rpush.spec.js
delete mode 100644 test/commands/sadd.spec.js
delete mode 100644 test/commands/scard.spec.js
delete mode 100644 test/commands/script.spec.js
delete mode 100644 test/commands/sdiff.spec.js
delete mode 100644 test/commands/sdiffstore.spec.js
delete mode 100644 test/commands/select.spec.js
delete mode 100644 test/commands/set.spec.js
delete mode 100644 test/commands/setex.spec.js
delete mode 100644 test/commands/setnx.spec.js
delete mode 100644 test/commands/sinter.spec.js
delete mode 100644 test/commands/sinterstore.spec.js
delete mode 100644 test/commands/sismember.spec.js
delete mode 100644 test/commands/slowlog.spec.js
delete mode 100644 test/commands/smembers.spec.js
delete mode 100644 test/commands/smove.spec.js
delete mode 100644 test/commands/sort.spec.js
delete mode 100644 test/commands/spop.spec.js
delete mode 100644 test/commands/srem.spec.js
delete mode 100644 test/commands/sunion.spec.js
delete mode 100644 test/commands/sunionstore.spec.js
delete mode 100644 test/commands/ttl.spec.js
delete mode 100644 test/commands/type.spec.js
delete mode 100644 test/commands/watch.spec.js
delete mode 100644 test/commands/zadd.spec.js
delete mode 100644 test/commands/zscan.spec.js
delete mode 100644 test/commands/zscore.spec.js
delete mode 100644 test/conect.slave.spec.js
delete mode 100644 test/conf/faulty.cert
delete mode 100644 test/conf/password.conf
delete mode 100644 test/conf/redis.conf
delete mode 100644 test/conf/redis.js.org.cert
delete mode 100644 test/conf/redis.js.org.key
delete mode 100644 test/conf/rename.conf
delete mode 100644 test/conf/slave.conf
delete mode 100644 test/conf/stunnel.conf.template
delete mode 100644 test/connection.spec.js
delete mode 100644 test/custom_errors.spec.js
delete mode 100644 test/detect_buffers.spec.js
delete mode 100644 test/errors.js
delete mode 100644 test/good_traces.spec.js
delete mode 100644 test/helper.js
delete mode 100644 test/lib/config.js
delete mode 100644 test/lib/good-traces.js
delete mode 100644 test/lib/redis-process.js
delete mode 100644 test/lib/stunnel-process.js
delete mode 100644 test/lib/unref.js
delete mode 100644 test/multi.spec.js
delete mode 100644 test/node_redis.spec.js
delete mode 100644 test/prefix.spec.js
delete mode 100644 test/pubsub.spec.js
delete mode 100644 test/rename.spec.js
delete mode 100644 test/return_buffers.spec.js
delete mode 100644 test/tls.spec.js
delete mode 100644 test/unify_options.spec.js
delete mode 100644 test/utils.spec.js
create mode 100644 tsconfig.json
diff --git a/.deepsource.toml b/.deepsource.toml
deleted file mode 100644
index 34bfad29479..00000000000
--- a/.deepsource.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-version = 1
-exclude_patterns = ["examples/**"]
-
-[[analyzers]]
-name = "javascript"
-enabled = true
-
- [analyzers.meta]
- environment = ["nodejs"]
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index fd16de30886..00000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,4 +0,0 @@
-node_modules/**
-coverage/**
-**.md
-**.log
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 81e5e9b9cfd..00000000000
--- a/.eslintrc
+++ /dev/null
@@ -1,109 +0,0 @@
-env:
- node: true
- es6: false
-
-rules:
- # Possible Errors
- # http://eslint.org/docs/rules/#possible-errors
- comma-dangle: [2, "only-multiline"]
- no-constant-condition: 2
- no-control-regex: 2
- no-debugger: 2
- no-dupe-args: 2
- no-dupe-keys: 2
- no-duplicate-case: 2
- no-empty: 2
- no-empty-character-class: 2
- no-ex-assign: 2
- no-extra-boolean-cast : 2
- no-extra-parens: [2, "functions"]
- no-extra-semi: 2
- no-func-assign: 2
- no-invalid-regexp: 2
- no-irregular-whitespace: 2
- no-negated-in-lhs: 2
- no-obj-calls: 2
- no-regex-spaces: 2
- no-sparse-arrays: 2
- no-inner-declarations: 2
- no-unexpected-multiline: 2
- no-unreachable: 2
- use-isnan: 2
- valid-typeof: 2
-
- # Best Practices
- # http://eslint.org/docs/rules/#best-practices
- array-callback-return: 2
- block-scoped-var: 2
- dot-notation: 2
- eqeqeq: 2
- no-else-return: 2
- no-extend-native: 2
- no-floating-decimal: 2
- no-extra-bind: 2
- no-fallthrough: 2
- no-labels: 2
- no-lone-blocks: 2
- no-loop-func: 2
- no-multi-spaces: 2
- no-multi-str: 2
- no-native-reassign: 2
- no-new-wrappers: 2
- no-octal: 2
- no-proto: 2
- no-redeclare: 2
- no-return-assign: 2
- no-self-assign: 2
- no-self-compare: 2
- no-sequences: 2
- no-throw-literal: 2
- no-useless-call: 2
- no-useless-concat: 2
- no-useless-escape: 2
- no-void: 2
- no-unmodified-loop-condition: 2
- yoda: 2
-
- # Strict Mode
- # http://eslint.org/docs/rules/#strict-mode
- strict: [2, "global"]
-
- # Variables
- # http://eslint.org/docs/rules/#variables
- no-delete-var: 2
- no-shadow-restricted-names: 2
- no-undef: 2
- no-unused-vars: [2, {"args": "none"}]
-
- # http://eslint.org/docs/rules/#nodejs-and-commonjs
- no-mixed-requires: 2
- no-new-require: 2
- no-path-concat: 2
-
- # Stylistic Issues
- # http://eslint.org/docs/rules/#stylistic-issues
- comma-spacing: 2
- eol-last: 2
- indent: [2, 4, {SwitchCase: 2}]
- keyword-spacing: 2
- max-len: [2, 200, 2]
- new-parens: 2
- no-mixed-spaces-and-tabs: 2
- no-multiple-empty-lines: [2, {max: 2}]
- no-trailing-spaces: 2
- quotes: [2, "single", "avoid-escape"]
- semi: 2
- space-before-blocks: [2, "always"]
- space-before-function-paren: [2, "always"]
- space-in-parens: [2, "never"]
- space-infix-ops: 2
- space-unary-ops: 2
-
-globals:
- it: true
- describe: true
- xdescribe: true
- before: true
- after: true
- beforeEach: true
- afterEach: true
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 1893f87aadd..00000000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1 +0,0 @@
-open_collective: node-redis
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index c74eb12b803..9b181d43149 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -5,30 +5,17 @@ labels: needs-triage
### Issue
-
-
> Describe your issue here
-
---
### Environment
- **Node.js Version**: `VERSION_HERE`
-
+
- - **Redis Version**: `VERSION_HERE`
-
+ - **Redis Server Version**: `VERSION_HERE`
+
- **Platform**: `PLATFORM_HERE`
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 98e3d312605..d4f8b8f2d9b 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,13 +1,10 @@
-
-
### Description
-
+
-> Description your pull request here
-
+> Describe your pull request here
---
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index 3ec398bb627..b6e5802a914 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -1,6 +1,9 @@
-name: Benchmarking
+name: Benchmark
-on: [pull_request]
+on:
+ push:
+ branches:
+ - v4
jobs:
benchmark:
@@ -9,8 +12,8 @@ jobs:
strategy:
fail-fast: false
matrix:
- node-version: [10.x, 12.x, 14.x, 15.x]
- redis-version: [5.x, 6.x]
+ node-version: [16.x]
+ redis-version: [6.x]
steps:
- uses: actions/checkout@v2.3.4
@@ -18,21 +21,25 @@ jobs:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2.1.5
+ uses: actions/setup-node@v2.3.0
with:
node-version: ${{ matrix.node-version }}
- name: Setup Redis
- uses: shogo82148/actions-setup-redis@v1.9.7
+ uses: shogo82148/actions-setup-redis@v1.12.0
with:
redis-version: ${{ matrix.redis-version }}
- auto-start: "true"
- - run: npm i --no-audit --prefer-offline
- - name: Run Benchmark
- run: npm run benchmark > benchmark-output.txt && cat benchmark-output.txt
- - name: Upload Benchmark Result
- uses: actions/upload-artifact@v2.2.2
- with:
- name: benchmark-output.txt
- path: benchmark-output.txt
+ - name: Install Packages
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+
+ - name: Install Benchmark Packages
+ run: npm ci
+ working-directory: ./benchmark
+
+ - name: Benchmark
+ run: npm run start
+ working-directory: ./benchmark
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index 98c615d0c55..00000000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,67 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
- push:
- branches: [ master ]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: [ master ]
- schedule:
- - cron: '35 0 * * 4'
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- language: [ 'javascript' ]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
- # Learn more:
- # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v2
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v1
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
- # queries: ./path/to/local/query, your-org/your-repo/queries@main
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v1
-
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 https://git.io/JvXDl
-
- # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
- # and modify them (or add more) to build your code if your project
- # uses a compiled language
-
- #- run: |
- # make bootstrap
- # make release
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
new file mode 100644
index 00000000000..16ca16b5608
--- /dev/null
+++ b/.github/workflows/documentation.yml
@@ -0,0 +1,31 @@
+name: Documentation
+
+on:
+ push:
+ branches:
+ - v4
+
+jobs:
+ documentation:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2.3.4
+ with:
+ fetch-depth: 1
+
+ - name: Use Node.js
+ uses: actions/setup-node@v2.3.0
+
+ - name: Install Packages
+ run: npm ci
+
+ - name: Generate Documentation
+ run: npm run documentation
+
+ - name: Upload Documentation to Wiki
+ uses: SwiftDocOrg/github-wiki-publish-action@v1
+ with:
+ path: documentation
+ env:
+ GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }}
diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
deleted file mode 100644
index d110707ee09..00000000000
--- a/.github/workflows/linting.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: Linting
-
-on: [pull_request]
-
-jobs:
- eslint:
- name: ESLint
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2.3.4
- with:
- fetch-depth: 1
- - uses: actions/setup-node@v2.1.5
- with:
- node-version: 12
- - run: npm i --no-audit --prefer-offline
- - name: Test Code Linting
- run: npm run lint
- - name: Save Code Linting Report JSON
- run: npm run lint:report
- continue-on-error: true
- - name: Annotate Code Linting Results
- uses: ataylorme/eslint-annotate-action@1.1.2
- with:
- repo-token: "${{ secrets.GITHUB_TOKEN }}"
- report-json: "eslint-report.json"
- - name: Upload ESLint report
- uses: actions/upload-artifact@v2.2.2
- with:
- name: eslint-report.json
- path: eslint-report.json
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 06b0e57ec3e..028600f1a17 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,16 +1,18 @@
name: Tests
-on: [push]
+on:
+ push:
+ branches:
+ - v4
jobs:
- testing:
- name: Test
+ tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
- node-version: [10.x, 12.x, 14.x, 15.x]
- redis-version: [4.x, 5.x, 6.x]
+ node-version: [12.x, 14.x, 16.x]
+ redis-version: [5.x, 6.x]
steps:
- uses: actions/checkout@v2.3.4
@@ -18,35 +20,38 @@ jobs:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2.1.5
+ uses: actions/setup-node@v2.3.0
with:
node-version: ${{ matrix.node-version }}
- name: Setup Redis
- uses: shogo82148/actions-setup-redis@v1.9.7
+ uses: shogo82148/actions-setup-redis@v1.12.0
with:
redis-version: ${{ matrix.redis-version }}
auto-start: "false"
- - name: Disable IPv6
- run: sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6';
-
- - name: Setup Stunnel
- run: sudo apt-get install stunnel4
-
- name: Install Packages
- run: npm i --no-audit --prefer-offline
+ run: npm ci
- name: Run Tests
- run: npm test
+ run: npm run test
- - name: Submit Coverage
- run: npm run coveralls
- env:
- COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
+ - name: Generate lcov
+ run: ./node_modules/.bin/nyc report -r lcov
- - name: Upload Coverage Report
- uses: actions/upload-artifact@v2.2.2
+ - name: Coveralls
+ uses: coverallsapp/github-action@1.1.3
with:
- name: coverage
- path: coverage
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ flag-name: Node ${{ matrix.node-version }} Redis ${{ matrix.redis-version }}
+ parallel: true
+
+ finish:
+ needs: tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Coveralls Finished
+ uses: coverallsapp/github-action@1.1.3
+ with:
+ github-token: ${{ secrets.github_token }}
+ parallel-finished: true
diff --git a/.github/workflows/tests_windows.yml b/.github/workflows/tests_windows.yml
deleted file mode 100644
index 7a2e00a9c93..00000000000
--- a/.github/workflows/tests_windows.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-name: Tests Windows
-
-on: [push]
-
-jobs:
- testing-windows:
- name: Test Windows
- runs-on: windows-latest
- strategy:
- fail-fast: false
- matrix:
- node-version: [10.x, 12.x, 14.x, 15.x]
- steps:
- - uses: actions/checkout@v2.3.4
- with:
- fetch-depth: 1
-
- - name: Install Redis
- uses: crazy-max/ghaction-chocolatey@v1.4.0
- with:
- args: install redis-64 --version=3.0.503 --no-progress
-
- - name: Start Redis
- run: |
- redis-server --service-install
- redis-server --service-start
- redis-cli config set stop-writes-on-bgsave-error no
-
- - name: Setup Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2.1.5
- with:
- node-version: ${{ matrix.node-version }}
-
- - name: Install Packages
- run: npm i --no-audit --prefer-offline
-
- - name: Run Tests
- run: npm test
-
- - name: Submit Coverage
- run: npm run coveralls
- env:
- COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
-
- - name: Upload Coverage Report
- uses: actions/upload-artifact@v2.2.2
- with:
- name: coverage
- path: coverage
diff --git a/.gitignore b/.gitignore
index 64b4143dc6f..0bdff14c7ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,17 +1,8 @@
-node_modules
-.tern-port
-.nyc_output
-coverage
-*.log
-*.rdb
-stunnel.conf
-stunnel.pid
-*.out
-package-lock.json
-
-# IntelliJ IDEs
-.idea
-# VisualStudioCode IDEs
-.vscode
-.vs
-eslint-report.json
+.vscode/
+.idea/
+node_modules/
+dist/
+.nyc_output/
+coverage/
+dump.rdb
+documentation/
diff --git a/.npmignore b/.npmignore
index 605ed4543cc..fa2d8841124 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,23 +1,17 @@
-examples/
-benchmarks/
-test/
-.nyc_output/
+.vscode/
+.idea/
+node_modules/
+.nyc_output
coverage/
-.github/
-.eslintignore
-.eslintrc
-.tern-port
-*.log
-*.rdb
-*.out
-*.yml
-.vscode
-.idea
+dump.rdb
+documentation/
CONTRIBUTING.md
-CODE_OF_CONDUCT.md
-.travis.yml
-appveyor.yml
-package-lock.json
-.prettierrc
-eslint-report.json
-.deepsource.toml
+tsconfig.json
+.nycrc.json
+benchmark/
+.github/
+scripts/
+lib/
+index.ts
+*.spec.*
+dist/lib/test-utils.*
diff --git a/.nycrc.json b/.nycrc.json
new file mode 100644
index 00000000000..925d954248c
--- /dev/null
+++ b/.nycrc.json
@@ -0,0 +1,4 @@
+{
+ "extends": "@istanbuljs/nyc-config-typescript",
+ "exclude": ["**/*.spec.ts", "lib/test-utils.ts"]
+}
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index 1ca516a86c0..00000000000
--- a/.prettierrc
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "arrowParens": "avoid",
- "trailingComma": "all",
- "useTabs": false,
- "semi": true,
- "singleQuote": false,
- "bracketSpacing": true,
- "jsxBracketSameLine": false,
- "tabWidth": 2,
- "printWidth": 100
-}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 186da332a45..d0095714010 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
# 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
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 2adee6acb34..00000000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making
-participation in our project and our community a harassment-free experience for everyone, regardless of age, body size,
-disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race,
-religion, or sexual identity and orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment include:
-
-- Using welcoming and inclusive language
-- Being respectful of differing viewpoints and experiences
-- Gracefully accepting constructive criticism
-- Focusing on what is best for the community
-- Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-- The use of sexualized language or imagery and unwelcome sexual attention or advances
-- Trolling, insulting/derogatory comments, and personal or political attacks
-- Public or private harassment
-- Publishing others' private information, such as a physical or electronic address, without explicit permission
-- Other conduct which could reasonably be considered inappropriate in a professional setting
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take
-appropriate and fair corrective action in response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,
- issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any
- contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
-
-## Scope
-
-This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the
-project or its community. Examples of representing a project or community include using an official project e-mail address,
-posting via an official social media account, or acting as an appointed representative at an online or offline event.
-Representation of a project may be further defined and clarified by project maintainers.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at
-`redis[AT]invertase.io`. The project team will review and investigate all complaints, and will respond in a way that it
-deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the
-reporter of an incident. Further details of specific enforcement policies may be posted separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent
-repercussions as determined by other members of the project's leadership.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at
-[http://contributor-covenant.org/version/1/4][version]
-
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d9fac6a450c..fbad5205081 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,110 +2,70 @@
First, thank you for considering contributing to Node Redis! It's people like you that make the open source community such a great community! 😊
-We welcome any type of contribution, not just code. You can help with;
+We welcome any type of contribution, not just code. You can help with:
-- **QA**: file bug reports, the more details you can give the better (e.g. platform versions, screenshots sdk versions & logs)
-- **Docs**: improve reference coverage, add more examples, fix typos or anything else you can spot.
-- **Code**: take a look at the open issues and help triage them.
-- **Donations**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/node-redis).
+- **QA**: file bug reports, the more details you can give the better (e.g. platform versions, screenshots, SDK versions, logs)
+- **Docs**: improve reference coverage, add more examples, fix typos or anything else you can spot
+- **Code**: take a look at the open issues and help triage them
---
## Project Guidelines
-As maintainers of this project, we want to ensure that the project lives and continues to grow. Not blocked by any
-singular person's time.
+As maintainers of this project, we want to ensure that the project lives and continues to grow. Progress should not be blocked by any one person's availability.
-One of the simplest ways of doing this is by encouraging a larger set of shallow contributors. Through this we hope to
-mitigate the problems of a project that needs updates but there is no-one who has the power to do so.
+One of the simplest ways of doing this is by encouraging a larger set of contributors. Using this approach we hope to mitigate the challenges of maintaining a project that needs regular updates.
-### Continuous Deployment
+### Getting Comfortable Contributing
-
+It is normal for your first pull request to be a potential fix for a problem but moving on from there to helping the project's direction can be difficult.
-Coming soon.
+We try to help contributors cross that barrier by identifying good first step issues (labelled `good-first-issue`). These issues are considered appropriate for first time contributors. Generally, these should be non-critical issues that are well defined. Established contributors will not work on these, to make space for others.
-### How can we help you get comfortable contributing?
+New contributors may consider picking up issues labelled `needs-triage` or `help-wanted`. These may not necessarily require code changes but rather help with debugging and finding the cause of the issue whether it's a bug or a user's incorrect setup of the library or project.
-It is normal for a first pull request to be a potential fix for a problem but moving on from there to helping the
-project's direction can be difficult.
+We keep all project discussion inside GitHub issues. This ensures that valuable information can be searched easily. GitHub issues are the go to tool for questions about how to use the library, or how the project is run.
-We try to help contributors cross that barrier by offering good first step issues (labelled `good-first-issue`). These
-issues can be fixed without feeling like you are stepping on toes. Generally, these should be non-critical issues that
-are well defined. They will be purposely avoided by mature contributors to the project, to make space for others.
+### Expectations of Contributors
-Additionally issues labelled `needs-triage` or `help-wanted` can also be picked up, these may not necessarily require
-code changes but rather help with debugging and finding the cause of the issue whether it's a bug or a users incorrect
-setup of the library or project.
+You shouldn't feel bad for not contributing to open source. We want contributors like yourself to provide ideas, keep the ship shipping and to take some of the load from others. It is non-obligatory; we’re here to get things done in an enjoyable way. :trophy:
-We aim to keep all project discussion inside GitHub issues. This is to make sure valuable discussion is accessible via
-search. If you have questions about how to use the library, or how the project is running - GitHub issues are the goto
-tool for this project.
+We only ask that you follow the conduct guidelines set out in our [Code of Conduct](https://redis.com/community/community-guidelines-code-of-conduct/) throughout your contribution journey.
-### Our expectations on you as a contributor
-You shouldn't feel bad for not contributing to open source. We want contributors like yourself to provide ideas, keep
-the ship shipping and to take some of the load from others. It is non-obligatory; we’re here to get things done in an
-enjoyable way. :trophy:
+#### Special Thanks
-We only ask that you follow the conduct guidelines set out in our [Code of Conduct](/CODE_OF_CONDUCT.md) throughout your
-contribution journey.
-
-### What about if you have problems that cannot be discussed in public?
-
-You can reach out to us directly via email (`redis[AT]invertase.io`) or direct message us on
-[Twitter](https://twitter.com/NodeRedis) if you'd like to discuss something privately.
-
-#### Project Maintainers
-
-- Mike Diarmid ([Salakar](https://github.com/Salakar)) @ [Invertase](https://github.com/invertase)
- - Twitter: [@mikediarmid](https://twitter.com/mikediarmid)
-- Elliot Hesp ([Ehesp](https://github.com/Ehesp)) @ [Invertase](https://github.com/invertase)
- - Twitter: [@elliothesp](https://twitter.com/elliothesp)
-- Ruben Bridgewater ([BridgeAR](https://github.com/BridgeAR))
- - Twitter: [@BridgeAR](https://twitter.com/BridgeAR)
-
-Huge thanks to the original author of Node Redis, [Matthew Ranney](https://github.com/mranney) and also to
-[Ruben Bridgewater](https://github.com/BridgeAR) for handing over this project over to new maintainers so it could be
-continuously maintained.
+A huge thank you to the original author of Node Redis, [Matthew Ranney](https://github.com/mranney).
---
## Code Guidelines
-### Your First Contribution
-
-Working on your first Pull Request? You can learn how from this _free_ series,
-[How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
-
### Testing Code
Node Redis has a full test suite with coverage setup.
-To run the tests, run `npm install` to install all dependencies, and then run `npm test`. To check detailed coverage locally run the `npm run coverage` command after
-testing and open the generated `./coverage/index.html` in your browser.
+To run the tests, run `npm install` to install dependencies, then run `npm test`.
Note that the test suite assumes that a few tools are installed in your environment, such as:
+
- redis (make sure redis-server is not running when starting the tests, it's part of the test-suite to start it and you'll end up with a "port already in use" error)
- stunnel (for TLS tests)
-### Submitting code for review
+### Submitting Code for Review
-The bigger the pull request, the longer it will take to review and merge. Where possible try to break down large pull
-requests into smaller chunks that are easier to review and merge. It is also always helpful to have some context for
-your pull request. What was the purpose? Why does it matter to you? What problem are you trying to solve? Tag in any linked issues.
+The bigger the pull request, the longer it will take to review and merge. Where possible try to break down large pull requests into smaller chunks that are easier to review and merge. It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? What problem are you trying to solve? Tag in any relevant issues.
-To aid review we also ask that you fill out the pull request template as much as possible.
+To assist reviewers, we ask that you fill out the pull request template as much as possible.
> Use a `draft` pull request if your pull request is not complete or ready for review.
-### Code review process
+### Code Review Process
-Pull Requests to the protected branches require two or more peer-review approvals and passing status checks to be able
-to be merged.
+Pull Requests to the protected branches require peer-review approvals and passing status checks to be able to be merged.
-When reviewing a Pull Request please check the following steps on top of the existing automated checks:
+When reviewing a Pull Request please check the following steps as well as the existing automated checks:
-- Does the it provide or update the docs if docs changes are required?
+- Does your Pull Request provide or update the docs if docs changes are required?
- Have the tests been updated or new tests been added to test any newly implemented or changed functionality?
-- Is the testing coverage ok and not worse than previously?
+- Is the test coverage at the same level as before (preferably more!)?
diff --git a/LICENSE b/LICENSE
index db86cc4de7f..5cb3bb4180b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-MIT License
+ MIT License
Copyright (c) 2016-present Node Redis contributors.
diff --git a/README.md b/README.md
index 10f0c044886..acc229b69c2 100644
--- a/README.md
+++ b/README.md
@@ -1,1008 +1,290 @@
-
-
+
+
Node Redis
- A high performance Node.js Redis client.
----
-
-
-
-
-
-
-
-
-
-
-
+
---
## Installation
```bash
-npm install redis
-```
-
-## Usage
-
-#### Example
-
-```js
-const redis = require("redis");
-const client = redis.createClient();
-
-client.on("error", function(error) {
- console.error(error);
-});
-
-client.set("key", "value", redis.print);
-client.get("key", redis.print);
-```
-
-Note that the API is entirely asynchronous. To get data back from the server,
-you'll need to use a callback.
-
-### Promises
-
-Node Redis currently doesn't natively support promises (this is coming in v4), however you can wrap the methods you
-want to use with promises using the built-in Node.js `util.promisify` method on Node.js >= v8;
-
-```js
-const { promisify } = require("util");
-const getAsync = promisify(client.get).bind(client);
-
-getAsync.then(console.log).catch(console.error);
+npm install redis@next
```
-### Commands
-
-This library is a 1 to 1 mapping of the [Redis commands](https://redis.io/commands).
-
-Each Redis command is exposed as a function on the `client` object.
-All functions take either an `args` Array plus optional `callback` Function or
-a variable number of individual arguments followed by an optional callback.
-Examples:
-
-```js
-client.hmset(["key", "foo", "bar"], function(err, res) {
- // ...
-});
-
-// Works the same as
-client.hmset("key", ["foo", "bar"], function(err, res) {
- // ...
-});
-
-// Or
-client.hmset("key", "foo", "bar", function(err, res) {
- // ...
-});
-```
-
-Care should be taken with user input if arrays are possible (via body-parser, query string or other method), as single arguments could be unintentionally interpreted as multiple args.
-
-Note that in either form the `callback` is optional:
-
-```js
-client.set("foo", "bar");
-client.set(["hello", "world"]);
-```
-
-If the key is missing, reply will be null. Only if the [Redis Command
-Reference](http://redis.io/commands) states something else it will not be null.
-
-```js
-client.get("missing_key", function(err, reply) {
- // reply is null when the key is missing
- console.log(reply);
-});
-```
-
-Minimal parsing is done on the replies. Commands that return a integer return
-JavaScript Numbers, arrays return JavaScript Array. `HGETALL` returns an Object
-keyed by the hash keys. All strings will either be returned as string or as
-buffer depending on your setting. Please be aware that sending null, undefined
-and Boolean values will result in the value coerced to a string!
-
-## API
-
-### Connection and other Events
-
-`client` will emit some events about the state of the connection to the Redis server.
-
-#### `"ready"`
-
-`client` will emit `ready` once a connection is established. Commands issued
-before the `ready` event are queued, then replayed just before this event is
-emitted.
-
-#### `"connect"`
-
-`client` will emit `connect` as soon as the stream is connected to the server.
-
-#### `"reconnecting"`
-
-`client` will emit `reconnecting` when trying to reconnect to the Redis server
-after losing the connection. Listeners are passed an object containing `delay`
-(in ms from the previous try) and `attempt` (the attempt #) attributes.
-
-#### `"error"`
-
-`client` will emit `error` when encountering an error connecting to the Redis
-server or when any other in Node Redis occurs. If you use a command without
-callback and encounter a ReplyError it is going to be emitted to the error
-listener.
-
-So please attach the error listener to Node Redis.
-
-#### `"end"`
-
-`client` will emit `end` when an established Redis server connection has closed.
-
-#### `"warning"`
+> :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).
-`client` will emit `warning` when password was set but none is needed and if a
-deprecated option / function / similar is used.
-
-### redis.createClient()
-
-If you have `redis-server` running on the same machine as node, then the
-defaults for port and host are probably fine and you don't need to supply any
-arguments. `createClient()` returns a `RedisClient` object. Otherwise,
-`createClient()` accepts these arguments:
-
-- `redis.createClient([options])`
-- `redis.createClient(unix_socket[, options])`
-- `redis.createClient(redis_url[, options])`
-- `redis.createClient(port[, host][, options])`
-
-**Tip:** If the Redis server runs on the same machine as the client consider
-using unix sockets if possible to increase throughput.
-
-**Note:** Using `'rediss://...` for the protocol in a `redis_url` will enable a TLS socket connection. However, additional TLS options will need to be passed in `options`, if required.
-
-#### `options` object properties
+## Usage
-| Property | Default | Description |
-| -------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| host | 127.0.0.1 | IP address of the Redis server |
-| port | 6379 | Port of the Redis server |
-| path | null | The UNIX socket string of the Redis server |
-| url | null | The URL of the Redis server. Format: `[redis[s]:]//[[user][:password@]][host][:port][/db-number][?db=db-number[&password=bar[&option=value]]]` (More info avaliable at [IANA](http://www.iana.org/assignments/uri-schemes/prov/redis)). |
-| string_numbers | null | Set to `true`, Node Redis will return Redis number values as Strings instead of javascript Numbers. Useful if you need to handle big numbers (above `Number.MAX_SAFE_INTEGER === 2^53`). Hiredis is incapable of this behavior, so setting this option to `true` will result in the built-in javascript parser being used no matter the value of the `parser` option. |
-| return_buffers | false | If set to `true`, then all replies will be sent to callbacks as Buffers instead of Strings. |
-| detect_buffers | false | If set to `true`, then replies will be sent to callbacks as Buffers. This option lets you switch between Buffers and Strings on a per-command basis, whereas `return_buffers` applies to every command on a client. **Note**: This doesn't work properly with the pubsub mode. A subscriber has to either always return Strings or Buffers. |
-| socket_keepalive | true | If set to `true`, the keep-alive functionality is enabled on the underlying socket. |
-| socket_initial_delay | 0 | Initial Delay in milliseconds, and this will also behave the interval keep alive message sending to Redis. |
-| no_ready_check | false | When a connection is established to the Redis server, the server might still be loading the database from disk. While loading, the server will not respond to any commands. To work around this, Node Redis has a "ready check" which sends the `INFO` command to the server. The response from the `INFO` command indicates whether the server is ready for more commands. When ready, `node_redis` emits a `ready` event. Setting `no_ready_check` to `true` will inhibit this check. |
-| enable_offline_queue | true | By default, if there is no active connection to the Redis server, commands are added to a queue and are executed once the connection has been established. Setting `enable_offline_queue` to `false` will disable this feature and the callback will be executed immediately with an error, or an error will be emitted if no callback is specified. |
-| retry_unfulfilled_commands | false | If set to `true`, all commands that were unfulfilled while the connection is lost will be retried after the connection has been reestablished. Use this with caution if you use state altering commands (e.g. `incr`). This is especially useful if you use blocking commands. |
-| password | null | If set, client will run Redis auth command on connect. Alias `auth_pass` **Note** Node Redis < 2.5 must use `auth_pass` |
-| user | null | The ACL user (only valid when `password` is set) |
-| db | null | If set, client will run Redis `select` command on connect. |
-| family | IPv4 | You can force using IPv6 if you set the family to 'IPv6'. See Node.js [net](https://nodejs.org/api/net.html) or [dns](https://nodejs.org/api/dns.html) modules on how to use the family type. |
-| disable_resubscribing | false | If set to `true`, a client won't resubscribe after disconnecting. |
-| rename_commands | null | Passing an object with renamed commands to use instead of the original functions. For example, if you renamed the command KEYS to "DO-NOT-USE" then the rename_commands object would be: `{ KEYS : "DO-NOT-USE" }` . See the [Redis security topics](http://redis.io/topics/security) for more info. |
-| tls | null | An object containing options to pass to [tls.connect](http://nodejs.org/api/tls.html#tls_tls_connect_port_host_options_callback) to set up a TLS connection to Redis (if, for example, it is set up to be accessible via a tunnel). |
-| prefix | null | A string used to prefix all used keys (e.g. `namespace:test`). Please be aware that the `keys` command will not be prefixed. The `keys` command has a "pattern" as argument and no key and it would be impossible to determine the existing keys in Redis if this would be prefixed. |
-| retry_strategy | function | A function that receives an options object as parameter including the retry `attempt`, the `total_retry_time` indicating how much time passed since the last time connected, the `error` why the connection was lost and the number of `times_connected` in total. If you return a number from this function, the retry will happen exactly after that time in milliseconds. If you return a non-number, no further retry will happen and all offline commands are flushed with errors. Return an error to return that specific error to all offline commands. Example below. |
-| connect_timeout | 3600000 | In milliseconds. This should only be the timeout for connecting to redis, but for now it interferes with `retry_strategy` and stops it from reconnecting after this timeout. |
+### Basic Example
-**`detect_buffers` example:**
+```typescript
+import { createClient } from 'redis';
-```js
-const redis = require("redis");
-const client = redis.createClient({ detect_buffers: true });
+(async () => {
+ const client = createClient();
-client.set("foo_rand000000000000", "OK");
+ client.on('error', (err) => console.log('Redis Client Error', err));
-// This will return a JavaScript String
-client.get("foo_rand000000000000", function(err, reply) {
- console.log(reply.toString()); // Will print `OK`
-});
+ await client.connect();
-// This will return a Buffer since original key is specified as a Buffer
-client.get(new Buffer("foo_rand000000000000"), function(err, reply) {
- console.log(reply.toString()); // Will print ``
-});
+ await client.set('key', 'value');
+ const value = await client.get('key');
+})();
```
-**`retry_strategy` example:**
+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]`:
-```js
-const client = redis.createClient({
- retry_strategy: function(options) {
- if (options.error && options.error.code === "ECONNREFUSED") {
- // End reconnecting on a specific error and flush all commands with
- // a individual error
- return new Error("The server refused the connection");
- }
- if (options.total_retry_time > 1000 * 60 * 60) {
- // End reconnecting after a specific timeout and flush all commands
- // with a individual error
- return new Error("Retry time exhausted");
- }
- if (options.attempt > 10) {
- // End reconnecting with built in error
- return undefined;
+```typescript
+createClient({
+ socket: {
+ url: 'redis://alice:foobared@awesome.redis.server:6380'
}
- // reconnect after
- return Math.min(options.attempt * 100, 3000);
- },
});
```
-### client.auth(password[, callback])
-
-When connecting to a Redis server that requires authentication, the `AUTH`
-command must be sent as the first command after connecting. This can be tricky
-to coordinate with reconnections, the ready check, etc. To make this easier,
-`client.auth()` stashes `password` and will send it after each connection,
-including reconnections. `callback` is invoked only once, after the response to
-the very first `AUTH` command sent.
-NOTE: Your call to `client.auth()` should not be inside the ready handler. If
-you are doing this wrong, `client` will emit an error that looks
-something like this `Error: Ready check failed: ERR operation not permitted`.
-
-### client.quit(callback)
-
-This sends the quit command to the redis server and ends cleanly right after all
-running commands were properly handled. If this is called while reconnecting
-(and therefore no connection to the redis server exists) it is going to end the
-connection right away instead of resulting in further reconnections! All offline
-commands are going to be flushed with an error in that case.
+You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in in the [Wiki](https://github.com/NodeRedis/node-redis/wiki/lib.socket#RedisSocketOptions).
-### client.end(flush)
+### Redis Commands
-Forcibly close the connection to the Redis server. Note that this does not wait
-until all replies have been parsed. If you want to exit cleanly, call
-`client.quit()` as mentioned above.
+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.):
-You should set flush to true, if you are not absolutely sure you do not care
-about any other commands. If you set flush to false all still running commands
-will silently fail.
+```typescript
+// raw Redis commands
+await client.HSET('key', 'field', 'value');
+await client.HGETALL('key');
-This example closes the connection to the Redis server before the replies have
-been read. You probably don't want to do this:
-
-```js
-const redis = require("redis");
-const client = redis.createClient();
-
-client.set("hello", "world", function(err) {
- // This will either result in an error (flush parameter is set to true)
- // or will silently fail and this callback will not be called at all (flush set to false)
- console.error(err);
-});
-
-// No further commands will be processed
-client.end(true);
-
-client.get("hello", function(err) {
- console.error(err); // => 'The connection has already been closed.'
-});
+// friendly JavaScript commands
+await client.hSet('key', 'field', 'value');
+await client.hGetAll('key');
```
-`client.end()` without the flush parameter set to true should NOT be used in production!
-
-### Error Handling
-
-Currently the following `Error` subclasses exist:
-
-- `RedisError`: _All errors_ returned by the client
-- `ReplyError` subclass of `RedisError`: All errors returned by **Redis** itself
-- `AbortError` subclass of `RedisError`: All commands that could not finish due
- to what ever reason
-- `ParserError` subclass of `RedisError`: Returned in case of a parser error
- (this should not happen)
-- `AggregateError` subclass of `AbortError`: Emitted in case multiple unresolved
- commands without callback got rejected in debug_mode instead of lots of
- `AbortError`s.
-
-All error classes are exported by the module.
-
-#### Example
-
-```js
-const assert = require("assert");
-
-const redis = require("redis");
-const { AbortError, AggregateError, ReplyError } = require("redis");
+Modifiers to commands are specified using a JavaScript object:
-const client = redis.createClient();
-
-client.on("error", function(err) {
- assert(err instanceof Error);
- assert(err instanceof AbortError);
- assert(err instanceof AggregateError);
-
- // The set and get are aggregated in here
- assert.strictEqual(err.errors.length, 2);
- assert.strictEqual(err.code, "NR_CLOSED");
-});
-
-client.set("foo", "bar", "baz", function(err, res) {
- // Too many arguments
- assert(err instanceof ReplyError); // => true
- assert.strictEqual(err.command, "SET");
- assert.deepStrictEqual(err.args, ["foo", 123, "bar"]);
-
- redis.debug_mode = true;
-
- client.set("foo", "bar");
- client.get("foo");
-
- process.nextTick(function() {
- // Force closing the connection while the command did not yet return
- client.end(true);
- redis.debug_mode = false;
- });
+```typescript
+await client.set('key', 'value', {
+ EX: 10,
+ NX: true
});
```
-Every `ReplyError` contains the `command` name in all-caps and the arguments (`args`).
-
-If Node Redis emits a library error because of another error, the triggering
-error is added to the returned error as `origin` attribute.
-
-**_Error codes_**
-
-Node Redis returns a `NR_CLOSED` error code if the clients connection dropped.
-If a command unresolved command got rejected a `UNCERTAIN_STATE` code is
-returned. A `CONNECTION_BROKEN` error code is used in case Node Redis gives up
-to reconnect.
-
-### client.unref()
-
-Call `unref()` on the underlying socket connection to the Redis server, allowing
-the program to exit once no more commands are pending.
-
-This is an **experimental** feature, and only supports a subset of the Redis
-protocol. Any commands where client state is saved on the Redis server, e.g.
-`*SUBSCRIBE` or the blocking `BL*` commands will _NOT_ work with `.unref()`.
-
-```js
-const redis = require("redis");
-const client = redis.createClient();
+Replies will be transformed into useful data structures:
-/*
- * Calling unref() will allow this program to exit immediately after the get
- * command finishes. Otherwise the client would hang as long as the
- * client-server connection is alive.
- */
-client.unref();
-
-client.get("foo", function(err, value) {
- if (err) throw err;
- console.log(value);
-});
+```typescript
+await client.hGetAll('key'); // { field1: 'value1', field2: 'value2' }
+await client.hVals('key'); // ['value1', 'value2']
```
-### Hash Commands
-
-Most Redis commands take a single String or an Array of Strings as arguments,
-and replies are sent back as a single String or an Array of Strings. When
-dealing with hash values, there are a couple of useful exceptions to this.
-
-#### client.hgetall(hash, callback)
+### Unsupported Redis Commands
-The reply from an `HGETALL` command will be converted into a JavaScript Object. That way you can interact with the
-responses using JavaScript syntax.
+If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`:
-**Example:**
+```typescript
+await client.sendCommand(['SET', 'key', 'value', 'NX']); // 'OK'
-```js
-client.hmset("key", "foo", "bar", "hello", "world");
-
-client.hgetall("key", function(err, value) {
- console.log(value.foo); // > "bar"
- console.log(value.hello); // > "world"
-});
+await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2']
```
-#### client.hmset(hash, key1, val1, ...keyN, valN, [callback])
+### Transactions (Multi/Exec)
-Multiple values may also be set by supplying more arguments.
+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:
-**Example:**
+```typescript
+await client.set('another-key', 'another-value');
-```js
-// key
-// 1) foo => bar
-// 2) hello => world
-client.HMSET("key", "foo", "bar", "hello", "world");
+const [ setKeyReply, otherKeyValue ] = await client.multi()
+ .set('key', 'value')
+ .get('another-key')
+ .exec()
+]); // ['OK', 'another-value']
```
-### PubSub
+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.
-#### Example
+To dig deeper into transactions, check out the [Isolated Execution Guide](./docs/isolated-execution.md).
-This example opens two client connections, subscribes to a channel on one of them, and publishes to that
-channel on the other.
+### Blocking Commands
-```js
-const redis = require("redis");
+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.
-const subscriber = redis.createClient();
-const publisher = redis.createClient();
+This pattern works especially well for blocking commands—such as `BLPOP` and `BLMOVE`:
-let messageCount = 0;
+```typescript
+import { commandOptions } from 'redis';
-subscriber.on("subscribe", function(channel, count) {
- publisher.publish("a channel", "a message");
- publisher.publish("a channel", "another message");
-});
+const blPopPromise = client.blPop(
+ commandOptions({ isolated: true }),
+ 'key'
+);
-subscriber.on("message", function(channel, message) {
- messageCount += 1;
+await client.lPush('key', ['1', '2']);
- console.log("Subscriber received message in channel '" + channel + "': " + message);
-
- if (messageCount === 2) {
- subscriber.unsubscribe();
- subscriber.quit();
- publisher.quit();
- }
-});
-
-subscriber.subscribe("a channel");
+await blPopPromise; // '2'
```
-When a client issues a `SUBSCRIBE` or `PSUBSCRIBE`, that connection is put into
-a `"subscriber"` mode. At that point, the only valid commands are those that modify the subscription
-set, and quit (also ping on some redis versions). When
-the subscription set is empty, the connection is put back into regular mode.
-
-If you need to send regular commands to Redis while in subscriber mode, just
-open another connection with a new client (use `client.duplicate()` to quickly duplicate an existing client).
-
-#### Subscriber Events
-
-If a client has subscriptions active, it may emit these events:
+To learn more about isolated execution, check out the [guide](./docs/isolated-execution.md).
-**"message" (channel, message)**:
+### Pub/Sub
-Client will emit `message` for every message received that matches an active subscription.
-Listeners are passed the channel name as `channel` and the message as `message`.
+Subscribing to a channel requires a dedicated stand-alone connection. You can easily get one by `.duplicate()`ing an existing Redis connection.
-**"pmessage" (pattern, channel, message)**:
+```typescript
+const subscriber = client.duplicate();
-Client will emit `pmessage` for every message received that matches an active
-subscription pattern. Listeners are passed the original pattern used with
-`PSUBSCRIBE` as `pattern`, the sending channel name as `channel`, and the
-message as `message`.
-
-**"message_buffer" (channel, message)**:
-
-This is the same as the `message` event with the exception, that it is always
-going to emit a buffer. If you listen to the `message` event at the same time as
-the `message_buffer`, it is always going to emit a string.
-
-**"pmessage_buffer" (pattern, channel, message)**:
-
-This is the same as the `pmessage` event with the exception, that it is always
-going to emit a buffer. If you listen to the `pmessage` event at the same time
-as the `pmessage_buffer`, it is always going to emit a string.
-
-**"subscribe" (channel, count)**:
-
-Client will emit `subscribe` in response to a `SUBSCRIBE` command. Listeners are
-passed the channel name as `channel` and the new count of subscriptions for this
-client as `count`.
-
-**"psubscribe" (pattern, count)**:
-
-Client will emit `psubscribe` in response to a `PSUBSCRIBE` command. Listeners
-are passed the original pattern as `pattern`, and the new count of subscriptions
-for this client as `count`.
-
-**"unsubscribe" (channel, count)**:
-
-Client will emit `unsubscribe` in response to a `UNSUBSCRIBE` command. Listeners
-are passed the channel name as `channel` and the new count of subscriptions for
-this client as `count`. When `count` is 0, this client has left subscriber mode
-and no more subscriber events will be emitted.
-
-**"punsubscribe" (pattern, count)**:
-
-Client will emit `punsubscribe` in response to a `PUNSUBSCRIBE` command.
-Listeners are passed the channel name as `channel` and the new count of
-subscriptions for this client as `count`. When `count` is 0, this client has
-left subscriber mode and no more subscriber events will be emitted.
-
-### client.multi([commands])
-
-`MULTI` commands are queued up until an `EXEC` is issued, and then all commands
-are run atomically by Redis. The interface returns an
-individual `Multi` object by calling `client.multi()`. If any command fails to
-queue, all commands are rolled back and none is going to be executed (For
-further information see the [Redis transactions](http://redis.io/topics/transactions) documentation).
-
-```js
-const redis = require("redis");
-const client = redis.createClient();
-
-let setSize = 20;
-
-client.sadd("key", "member1");
-client.sadd("key", "member2");
-
-while (setSize > 0) {
- client.sadd("key", "member" + setSize);
- setSize -= 1;
-}
-
-// chain commands
-client
- .multi()
- .scard("key")
- .smembers("key")
- .keys("*")
- .dbsize()
- .exec(function(err, replies) {
- console.log("MULTI got " + replies.length + " replies");
- replies.forEach(function(reply, index) {
- console.log("REPLY @ index " + index + ": " + reply.toString());
- });
- });
+await subscriber.connect();
```
-#### Multi.exec([callback])
-
-`client.multi()` is a constructor that returns a `Multi` object. `Multi` objects
-share all of the same command methods as `client` objects do. Commands are
-queued up inside the `Multi` object until `Multi.exec()` is invoked.
-
-If your code contains an syntax error an `EXECABORT` error is going to be thrown
-and all commands are going to be aborted. That error contains a `.errors`
-property that contains the concrete errors.
-If all commands were queued successfully and an error is thrown by redis while
-processing the commands that error is going to be returned in the result array!
-No other command is going to be aborted though than the ones failing.
-
-You can either chain together `MULTI` commands as in the above example, or you
-can queue individual commands while still sending regular client command as in
-this example:
-
-```js
-const redis = require("redis");
-const client = redis.createClient();
-
-// start a separate multi command queue
-const multi = client.multi();
-
-// add some commands to the queue
-multi.incr("count_cats", redis.print);
-multi.incr("count_dogs", redis.print);
+Once you have one, simply subscribe and unsubscribe as needed:
-// runs a command immediately outside of the `multi` instance
-client.mset("count_cats", 100, "count_dogs", 50, redis.print);
-
-// drains the multi queue and runs each command atomically
-multi.exec(function(err, replies) {
- console.log(replies); // 101, 51
+```typescript
+await subscriber.subscribe('channel', message => {
+ console.log(message); // 'message'
});
-```
-
-In addition to adding commands to the `MULTI` queue individually, you can also
-pass an array of commands and arguments to the constructor:
-
-```js
-const redis = require("redis");
-
-const client = redis.createClient();
-
-client
- .multi([
- ["mget", "foo", "bar", redis.print],
- ["incr", "hello"],
- ])
- .exec(function(err, replies) {
- console.log(replies);
- });
-```
-
-#### Multi.exec_atomic([callback])
-
-Identical to Multi.exec but with the difference that executing a single command
-will not use transactions.
-
-#### Optimistic Locks
-
-Using `multi` you can make sure your modifications run as a transaction, but you
-can't be sure you got there first. What if another client modified a key while
-you were working with it's data?
-
-To solve this, Redis supports the [WATCH](https://redis.io/topics/transactions)
-command, which is meant to be used with MULTI:
-
-```js
-const redis = require("redis");
-
-const client = redis.createClient();
-client.watch("foo", function(watchError) {
- if (watchError) throw watchError;
-
- client.get("foo", function(getError, result) {
- if (getError) throw getError;
-
- // Process result
- // Heavy and time consuming operation here to generate "bar"
-
- client
- .multi()
- .set("foo", "bar")
- .exec(function(execError, results) {
- /**
- * If err is null, it means Redis successfully attempted
- * the operation.
- */
- if (execError) throw execError;
-
- /**
- * If results === null, it means that a concurrent client
- * changed the key while we were processing it and thus
- * the execution of the MULTI command was not performed.
- *
- * NOTICE: Failing an execution of MULTI is not considered
- * an error. So you will have err === null and results === null
- */
- });
- });
+await subscriber.pSubscribe('channe*', (message, channel) => {
+ console.log(message, channel); // 'message', 'channel'
});
-```
-The above snippet shows the correct usage of `watch` with `multi`. Every time a
-watched key is changed before the execution of a `multi` command, the execution
-will return `null`. On a normal situation, the execution will return an array of
-values with the results of the operations.
-
-As stated in the snippet, failing the execution of a `multi` command being watched
-is not considered an error. The execution may return an error if, for example, the
-client cannot connect to Redis.
-
-An example where we can see the execution of a `multi` command fail is as follows:
-
-```js
-const clients = {
- watcher: redis.createClient(),
- modifier: redis.createClient(),
-};
-
-clients.watcher.watch("foo", function(watchError) {
- if (watchError) throw watchError;
-
- // if you comment out the next line, the transaction will work
- clients.modifier.set("foo", Math.random(), setError => {
- if (setError) throw setError;
- });
-
- // using a setTimeout here to ensure that the MULTI/EXEC will come after the SET.
- // Normally, you would use a callback to ensure order, but I want the above SET command
- // to be easily comment-out-able.
- setTimeout(function() {
- clients.watcher
- .multi()
- .set("foo", "bar")
- .set("hello", "world")
- .exec((multiExecError, results) => {
- if (multiExecError) throw multiExecError;
-
- if (results === null) {
- console.log("transaction aborted because results were null");
- } else {
- console.log("transaction worked and returned", results);
- }
+await subscriber.unsubscribe('channel');
- clients.watcher.quit();
- clients.modifier.quit();
- });
- }, 1000);
-});
+await subscriber.pUnsubscribe('channe*');
```
-#### `WATCH` limitations
-
-Redis WATCH works only on _whole_ key values. For example, with WATCH you can
-watch a hash for modifications, but you cannot watch a specific field of a hash.
-
-The following example would watch the keys `foo` and `hello`, not the field `hello`
-of hash `foo`:
-
-```js
-const redis = require("redis");
-
-const client = redis.createClient();
-
-client.hget("foo", "hello", function(hashGetError, result) {
- if (hashGetError) throw hashGetError;
-
- //Do some processing with the value from this field and watch it after
+Publish a message on a channel:
- client.watch("foo", "hello", function(watchError) {
- if (watchError) throw watchError;
-
- /**
- * This is now watching the keys 'foo' and 'hello'. It is not
- * watching the field 'hello' of hash 'foo'. Because the key 'foo'
- * refers to a hash, this command is now watching the entire hash
- * for modifications.
- */
- });
-});
+```typescript
+await publisher.publish('channel', 'message');
```
-This limitation also applies to sets (you can not watch individual set members)
-and any other collections.
-
-### client.batch([commands])
-
-Identical to `.multi()` without transactions. This is recommended if you want to
-execute many commands at once but don't need to rely on transactions.
-
-`BATCH` commands are queued up until an `EXEC` is issued, and then all commands
-are run atomically by Redis. The interface returns an
-individual `Batch` object by calling `client.batch()`. The only difference
-between .batch and .multi is that no transaction is going to be used.
-Be aware that the errors are - just like in multi statements - in the result.
-Otherwise both, errors and results could be returned at the same time.
-
-If you fire many commands at once this is going to boost the execution speed
-significantly compared to firing the same commands in a loop without waiting for
-the result! See the benchmarks for further comparison. Please remember that all
-commands are kept in memory until they are fired.
-
-### Monitor mode
+### Scan Iterator
-Redis supports the `MONITOR` command, which lets you see all commands received
-by the Redis server across all client connections, including from other client
-libraries and other computers.
+[`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):
-A `monitor` event is going to be emitted for every command fired from any client
-connected to the server including the monitoring client itself. The callback for
-the `monitor` event takes a timestamp from the Redis server, an array of command
-arguments and the raw monitoring string.
-
-#### Example:
-
-```js
-const redis = require("redis");
-const client = redis.createClient();
-
-client.monitor(function(err, res) {
- console.log("Entering monitoring mode.");
-});
-
-client.set("foo", "bar");
-
-client.on("monitor", function(time, args, rawReply) {
- console.log(time + ": " + args); // 1458910076.446514:['set', 'foo', 'bar']
-});
+```typescript
+for await (const key of client.scanIterator()) {
+ // use the key!
+ await client.get(key);
+}
```
-## Extras
-
-Some other things you might find useful.
-
-### `client.server_info`
-
-After the ready probe completes, the results from the INFO command are saved in
-the `client.server_info` object.
+This works with `HSCAN`, `SSCAN`, and `ZSCAN` too:
-The `versions` key contains an array of the elements of the version string for
-easy comparison.
-
-```
-> client.server_info.redis_version
-'2.3.0'
-> client.server_info.versions
-[ 2, 3, 0 ]
+```typescript
+for await (const member of client.hScanIterator('hash')) {}
+for await (const { field, value } of client.sScanIterator('set')) {}
+for await (const { member, score } of client.zScanIterator('sorted-set')) {}
```
-### `redis.print()`
-
-A handy callback function for displaying return values when testing. Example:
+You can override the default options by providing a configuration object:
-```js
-const redis = require("redis");
-const client = redis.createClient();
-
-client.on("connect", function() {
- client.set("foo", "bar", redis.print); // => "Reply: OK"
- client.get("foo", redis.print); // => "Reply: bar"
- client.quit();
+```typescript
+client.scanIterator({
+ TYPE: 'string', // `SCAN` only
+ MATCH: 'patter*',
+ COUNT: 100
});
```
-### Multi-word commands
-
-To execute redis multi-word commands like `SCRIPT LOAD` or `CLIENT LIST` pass
-the second word as first parameter:
-
-```js
-client.script("load", "return 1");
+### 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 {
+ return [key, number.toString()];
+ },
+ transformReply(reply: number): number {
+ return reply;
+ }
+ })
+ }
+ });
-client
- .multi()
- .script("load", "return 1")
- .exec();
+ await client.connect();
-client.multi([["script", "load", "return 1"]]).exec();
+ await client.set('key', '1');
+ await client.add('key', 2); // 3
+})();
```
-### `client.duplicate([options][, callback])`
-
-Duplicate all current options and return a new redisClient instance. All options
-passed to the duplicate function are going to replace the original option. If
-you pass a callback, duplicate is going to wait until the client is ready and
-returns it in the callback. If an error occurs in the meanwhile, that is going
-to return an error instead in the callback.
+### Cluster
-One example of when to use duplicate() would be to accommodate the connection-
-blocking redis commands `BRPOP`, `BLPOP`, and `BRPOPLPUSH`. If these commands
-are used on the same Redis client instance as non-blocking commands, the
-non-blocking ones may be queued up until after the blocking ones finish.
+Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a non-clustered client:
-Another reason to use duplicate() is when multiple DBs on the same server are
-accessed via the redis SELECT command. Each DB could use its own connection.
+```typescript
+import { createCluster } from 'redis';
-### `client.sendCommand(command_name[, [args][, callback]])`
-
-All Redis commands have been added to the `client` object. However, if new
-commands are introduced before this library is updated or if you want to add
-individual commands you can use `sendCommand()` to send arbitrary commands to
-Redis.
-
-All commands are sent as multi-bulk commands. `args` can either be an Array of
-arguments, or omitted / set to undefined.
-
-### `redis.addCommand(command_name)`
-
-Calling addCommand will add a new command to the prototype. The exact command
-name will be used when calling using this new command. Using arbitrary arguments
-is possible as with any other command.
-
-### `client.connected`
-
-Boolean tracking the state of the connection to the Redis server.
-
-### `client.command_queue_length`
-
-The number of commands that have been sent to the Redis server but not yet
-replied to. You can use this to enforce some kind of maximum queue depth for
-commands while connected.
-
-### `client.offline_queue_length`
-
-The number of commands that have been queued up for a future connection. You can
-use this to enforce some kind of maximum queue depth for pre-connection
-commands.
-
-### Commands with Optional and Keyword arguments
-
-This applies to anything that uses an optional `[WITHSCORES]` or `[LIMIT offset count]` in the [redis.io/commands](http://redis.io/commands) documentation.
-
-#### Example
-
-```js
-const args = ["myzset", 1, "one", 2, "two", 3, "three", 99, "ninety-nine"];
+(async () => {
+ const cluster = createCluster({
+ rootNodes: [{
+ host: '10.0.0.1',
+ port: 30001
+ }, {
+ host: '10.0.0.2',
+ port: 30002
+ }]
+ });
-client.zadd(args, function(addError, addResponse) {
- if (addError) throw addError;
- console.log("added " + addResponse + " items.");
+ cluster.on('error', (err) => console.log('Redis Cluster Error', err));
- // -Infinity and +Infinity also work
- const args1 = ["myzset", "+inf", "-inf"];
- client.zrevrangebyscore(args1, function(rangeError, rangeResponse) {
- if (rangeError) throw rangeError;
- console.log("response1", rangeResponse);
- // ...
- });
+ await cluster.connect();
- const max = 3;
- const min = 1;
- const offset = 1;
- const count = 2;
- const args2 = ["myzset", max, min, "WITHSCORES", "LIMIT", offset, count];
- client.zrevrangebyscore(args2, function(rangeError, rangeResponse) {
- if (rangeError) throw rangeError;
- console.log("response2", rangeResponse);
- // ...
- });
-});
+ await cluster.set('key', 'value');
+ const value = await cluster.get('key');
+})();
```
-## Performance
+### Auto-Pipelining
-Much effort has been spent to make Node Redis as fast as possible for common operations.
+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==');
```
-Mac mini (2018), i7-3.2GHz and 32gb memory
-clients: 1, NodeJS: 12.15.0, Redis: 5.0.6, parser: javascript, connected by: tcp
- PING, 1/1 avg/max: 0.03/ 3.28 2501ms total, 31926 ops/sec
- PING, batch 50/1 avg/max: 0.08/ 3.35 2501ms total, 599460 ops/sec
- SET 4B str, 1/1 avg/max: 0.03/ 3.54 2501ms total, 29483 ops/sec
- SET 4B str, batch 50/1 avg/max: 0.10/ 1.39 2501ms total, 477689 ops/sec
- SET 4B buf, 1/1 avg/max: 0.04/ 1.52 2501ms total, 23449 ops/sec
- SET 4B buf, batch 50/1 avg/max: 0.20/ 2.09 2501ms total, 244382 ops/sec
- GET 4B str, 1/1 avg/max: 0.03/ 1.35 2501ms total, 32205 ops/sec
- GET 4B str, batch 50/1 avg/max: 0.09/ 2.02 2501ms total, 568992 ops/sec
- GET 4B buf, 1/1 avg/max: 0.03/ 2.93 2501ms total, 32802 ops/sec
- GET 4B buf, batch 50/1 avg/max: 0.08/ 1.03 2501ms total, 592863 ops/sec
- SET 4KiB str, 1/1 avg/max: 0.03/ 0.76 2501ms total, 29287 ops/sec
- SET 4KiB str, batch 50/1 avg/max: 0.35/ 2.97 2501ms total, 143163 ops/sec
- SET 4KiB buf, 1/1 avg/max: 0.04/ 1.21 2501ms total, 23070 ops/sec
- SET 4KiB buf, batch 50/1 avg/max: 0.28/ 2.34 2501ms total, 176809 ops/sec
- GET 4KiB str, 1/1 avg/max: 0.03/ 1.54 2501ms total, 29555 ops/sec
- GET 4KiB str, batch 50/1 avg/max: 0.18/ 1.59 2501ms total, 279188 ops/sec
- GET 4KiB buf, 1/1 avg/max: 0.03/ 1.80 2501ms total, 30681 ops/sec
- GET 4KiB buf, batch 50/1 avg/max: 0.17/ 5.00 2501ms total, 285886 ops/sec
- INCR, 1/1 avg/max: 0.03/ 1.99 2501ms total, 32757 ops/sec
- INCR, batch 50/1 avg/max: 0.09/ 2.54 2501ms total, 538964 ops/sec
- LPUSH, 1/1 avg/max: 0.05/ 4.85 2501ms total, 19482 ops/sec
- LPUSH, batch 50/1 avg/max: 0.12/ 9.52 2501ms total, 395562 ops/sec
- LRANGE 10, 1/1 avg/max: 0.06/ 9.21 2501ms total, 17062 ops/sec
- LRANGE 10, batch 50/1 avg/max: 0.22/ 1.03 2501ms total, 228269 ops/sec
- LRANGE 100, 1/1 avg/max: 0.05/ 1.44 2501ms total, 19051 ops/sec
- LRANGE 100, batch 50/1 avg/max: 0.99/ 3.46 2501ms total, 50480 ops/sec
- SET 4MiB str, 1/1 avg/max: 4.11/ 13.96 2501ms total, 243 ops/sec
- SET 4MiB str, batch 20/1 avg/max: 91.16/145.01 2553ms total, 219 ops/sec
- SET 4MiB buf, 1/1 avg/max: 2.81/ 11.90 2502ms total, 354 ops/sec
- SET 4MiB buf, batch 20/1 avg/max: 36.21/ 70.96 2535ms total, 552 ops/sec
- GET 4MiB str, 1/1 avg/max: 2.82/ 19.10 2503ms total, 354 ops/sec
- GET 4MiB str, batch 20/1 avg/max: 128.57/207.86 2572ms total, 156 ops/sec
- GET 4MiB buf, 1/1 avg/max: 3.13/ 23.88 2501ms total, 318 ops/sec
- GET 4MiB buf, batch 20/1 avg/max: 65.91/ 87.59 2572ms total, 303 ops/sec
-```
-
-## Debugging
-
-To get debug output run your Node Redis application with `NODE_DEBUG=redis`.
-
-This is also going to result in good stack traces opposed to useless ones
-otherwise for any async operation.
-If you only want to have good stack traces but not the debug output run your
-application in development mode instead (`NODE_ENV=development`).
-
-Good stack traces are only activated in development and debug mode as this
-results in a significant performance penalty.
-**_Comparison_**:
+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()`.
-Standard stack trace:
-
-```
-ReplyError: ERR wrong number of arguments for 'set' command
- at parseError (/home/ruben/repos/redis/node_modules/redis-parser/lib/parser.js:158:12)
- at parseType (/home/ruben/repos/redis/node_modules/redis-parser/lib/parser.js:219:14)
+```typescript
+await Promise.all([
+ client.set('Tm9kZSBSZWRpcw==', 'users:1'),
+ client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==')
+]);
```
-Debug stack trace:
+## Contributing
-```
-ReplyError: ERR wrong number of arguments for 'set' command
- at new Command (/home/ruben/repos/redis/lib/command.js:9:902)
- at RedisClient.set (/home/ruben/repos/redis/lib/commands.js:9:3238)
- at Context. (/home/ruben/repos/redis/test/good_stacks.spec.js:20:20)
- at callFnAsync (/home/ruben/repos/redis/node_modules/mocha/lib/runnable.js:349:8)
- at Test.Runnable.run (/home/ruben/repos/redis/node_modules/mocha/lib/runnable.js:301:7)
- at Runner.runTest (/home/ruben/repos/redis/node_modules/mocha/lib/runner.js:422:10)
- at /home/ruben/repos/redis/node_modules/mocha/lib/runner.js:528:12
- at next (/home/ruben/repos/redis/node_modules/mocha/lib/runner.js:342:14)
- at /home/ruben/repos/redis/node_modules/mocha/lib/runner.js:352:7
- at next (/home/ruben/repos/redis/node_modules/mocha/lib/runner.js:284:14)
- at Immediate._onImmediate (/home/ruben/repos/redis/node_modules/mocha/lib/runner.js:320:5)
- at processImmediate [as _immediateCallback] (timers.js:383:17)
-```
+If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md).
-## Contributing
+Thank you to all the people who already contributed to Node Redis!
-Please see the [contributing guide](CONTRIBUTING.md).
+
## License
diff --git a/benchmark/.gitignore b/benchmark/.gitignore
new file mode 100644
index 00000000000..3c3629e647f
--- /dev/null
+++ b/benchmark/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/benchmark/index.js b/benchmark/index.js
new file mode 100644
index 00000000000..37f88176653
--- /dev/null
+++ b/benchmark/index.js
@@ -0,0 +1,81 @@
+import { add, suite, cycle, complete } from 'benny';
+import v4 from 'v4';
+import v3 from 'v3';
+import { once } from 'events';
+
+const v4Client = v4.createClient(),
+ v4LegacyClient = v4.createClient({
+ legacyMode: true
+ }),
+ v3Client = v3.createClient();
+
+await Promise.all([
+ v4Client.connect(),
+ v4LegacyClient.connect(),
+ once(v3Client, 'connect')
+]);
+
+const key = random(100),
+ value = random(100);
+
+function random(size) {
+ const result = [];
+
+ for (let i = 0; i < size; i++) {
+ result.push(Math.floor(Math.random() * 10));
+ }
+
+ return result.join('');
+}
+
+suite(
+ 'SET GET',
+ add('v4', async () => {
+ await Promise.all([
+ v4Client.set(key, value),
+ v4Client.get(key)
+ ]);
+ }),
+ add('v4 - legacy mode', () => {
+ return new Promise((resolve, reject) => {
+ v4LegacyClient.set(key, value);
+ v4LegacyClient.get(key, (err, reply) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(reply);
+ }
+ });
+ });
+ }),
+ add('v3', () => {
+ return new Promise((resolve, reject) => {
+ v3Client.set(key, value);
+ v3Client.get(key, (err, reply) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(reply);
+ }
+ });
+ });
+ }),
+ cycle(),
+ complete(),
+ complete(() => {
+ return Promise.all([
+ v4Client.disconnect(),
+ v4LegacyClient.disconnect(),
+ new Promise((resolve, reject) => {
+ v3Client.quit((err) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(err);
+ }
+ });
+ })
+ ]);
+ })
+);
+
diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json
new file mode 100644
index 00000000000..a16a420f8cb
--- /dev/null
+++ b/benchmark/package-lock.json
@@ -0,0 +1,926 @@
+{
+ "name": "benchmark",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "license": "ISC",
+ "dependencies": {
+ "@probe.gl/bench": "^3.4.0",
+ "benny": "^3.6.15",
+ "v3": "npm:redis@3.1.2",
+ "v4": "file:../"
+ }
+ },
+ "..": {
+ "name": "redis",
+ "version": "4.0.0-next.5",
+ "license": "MIT",
+ "dependencies": {
+ "cluster-key-slot": "1.1.0",
+ "redis-parser": "3.0.0",
+ "yallist": "4.0.0"
+ },
+ "devDependencies": {
+ "@istanbuljs/nyc-config-typescript": "^1.0.1",
+ "@types/mocha": "^9.0.0",
+ "@types/node": "^16.4.5",
+ "@types/sinon": "^10.0.2",
+ "@types/which": "^2.0.1",
+ "@types/yallist": "^4.0.1",
+ "mocha": "^9.0.3",
+ "nyc": "^15.1.0",
+ "release-it": "^14.10.1",
+ "sinon": "^11.1.2",
+ "source-map-support": "^0.5.19",
+ "ts-node": "^10.1.0",
+ "typedoc": "^0.21.4",
+ "typedoc-github-wiki-theme": "^0.5.1",
+ "typedoc-plugin-markdown": "^3.10.4",
+ "typescript": "^4.3.5",
+ "which": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@arrows/array": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
+ "integrity": "sha512-MGYS8xi3c4tTy1ivhrVntFvufoNzje0PchjEz6G/SsWRgUKxL4tKwS6iPdO8vsaJYldagAeWMd5KRD0aX3Q39g==",
+ "dependencies": {
+ "@arrows/composition": "^1.2.2"
+ }
+ },
+ "node_modules/@arrows/composition": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@arrows/composition/-/composition-1.2.2.tgz",
+ "integrity": "sha512-9fh1yHwrx32lundiB3SlZ/VwuStPB4QakPsSLrGJFH6rCXvdrd060ivAZ7/2vlqPnEjBkPRRXOcG1YOu19p2GQ=="
+ },
+ "node_modules/@arrows/dispatch": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@arrows/dispatch/-/dispatch-1.0.3.tgz",
+ "integrity": "sha512-v/HwvrFonitYZM2PmBlAlCqVqxrkIIoiEuy5bQgn0BdfvlL0ooSBzcPzTMrtzY8eYktPyYcHg8fLbSgyybXEqw==",
+ "dependencies": {
+ "@arrows/composition": "^1.2.2"
+ }
+ },
+ "node_modules/@arrows/error": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@arrows/error/-/error-1.0.2.tgz",
+ "integrity": "sha512-yvkiv1ay4Z3+Z6oQsUkedsQm5aFdyPpkBUQs8vejazU/RmANABx6bMMcBPPHI4aW43VPQmXFfBzr/4FExwWTEA=="
+ },
+ "node_modules/@arrows/multimethod": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@arrows/multimethod/-/multimethod-1.1.7.tgz",
+ "integrity": "sha512-EjHD3XuGAV4G28rm7mu8k7zQJh/EOizh104/p9i2ofGcnL5mgKONFH/Bq6H3SJjM+WDAlKcR9WBpNhaAKCnH2g==",
+ "dependencies": {
+ "@arrows/array": "^1.4.0",
+ "@arrows/composition": "^1.2.2",
+ "@arrows/error": "^1.0.2",
+ "fast-deep-equal": "^3.1.1"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.14.8",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
+ "integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
+ "dependencies": {
+ "regenerator-runtime": "^0.13.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@probe.gl/bench": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@probe.gl/bench/-/bench-3.4.0.tgz",
+ "integrity": "sha512-S7iNPz5G3zEfEP0S4SAMvtj+dwP7EWfVBaA8Cy5CVIgM1lnpUbXvqoAJxlVEedNC32Icxwq65XQheufy1Zzmug==",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0",
+ "probe.gl": "3.4.0"
+ }
+ },
+ "node_modules/@probe.gl/stats": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-3.4.0.tgz",
+ "integrity": "sha512-Gl37r9qGuiKadIvTZdSZvzCNOttJYw6RcY1oT0oDuB8r2uhuZAdSMQRQTy9FTinp6MY6O9wngGnV6EpQ8wSBAw==",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/benchmark": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
+ "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
+ "dependencies": {
+ "lodash": "^4.17.4",
+ "platform": "^1.3.3"
+ }
+ },
+ "node_modules/benny": {
+ "version": "3.6.15",
+ "resolved": "https://registry.npmjs.org/benny/-/benny-3.6.15.tgz",
+ "integrity": "sha512-kq6XVGGYVou3Y8KNPs3SEF881vi5fJ8sIf9w69D2rreiNfRicWVWK6u6/mObMw6BiexoHHumtipn5gcu0Tngng==",
+ "dependencies": {
+ "@arrows/composition": "^1.0.0",
+ "@arrows/dispatch": "^1.0.2",
+ "@arrows/multimethod": "^1.1.6",
+ "benchmark": "^2.1.4",
+ "fs-extra": "^9.0.1",
+ "json2csv": "^5.0.4",
+ "kleur": "^4.1.3",
+ "log-update": "^4.0.0",
+ "prettier": "^2.1.2",
+ "stats-median": "^1.0.1"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/denque": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
+ "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/json2csv": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.6.tgz",
+ "integrity": "sha512-0/4Lv6IenJV0qj2oBdgPIAmFiKKnh8qh7bmLFJ+/ZZHLjSeiL3fKKGX3UryvKPbxFbhV+JcYo9KUC19GJ/Z/4A==",
+ "dependencies": {
+ "commander": "^6.1.0",
+ "jsonparse": "^1.3.1",
+ "lodash.get": "^4.4.2"
+ },
+ "bin": {
+ "json2csv": "bin/json2csv.js"
+ },
+ "engines": {
+ "node": ">= 10",
+ "npm": ">= 6.13.0"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
+ "engines": [
+ "node >= 0.2.0"
+ ]
+ },
+ "node_modules/kleur": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
+ "integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
+ },
+ "node_modules/log-update": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+ "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+ "dependencies": {
+ "ansi-escapes": "^4.3.0",
+ "cli-cursor": "^3.1.0",
+ "slice-ansi": "^4.0.0",
+ "wrap-ansi": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/platform": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
+ },
+ "node_modules/prettier": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
+ "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/probe.gl": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/probe.gl/-/probe.gl-3.4.0.tgz",
+ "integrity": "sha512-9CLByZATuhuG/Viq3ckfWU+dAhb7dMmjzsyCy4s7ds9ueTejcVRENxL197/XacOK/AN61YrEERB0QnouB0Qc0Q==",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0",
+ "@probe.gl/stats": "3.4.0"
+ }
+ },
+ "node_modules/redis-commands": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
+ "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
+ },
+ "node_modules/redis-errors": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
+ "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/redis-parser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
+ "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
+ "dependencies": {
+ "redis-errors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.9",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
+ "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/stats-median": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/stats-median/-/stats-median-1.0.1.tgz",
+ "integrity": "sha512-IYsheLg6dasD3zT/w9+8Iq9tcIQqqu91ZIpJOnIEM25C3X/g4Tl8mhXwW2ZQpbrsJISr9+wizEYgsibN5/b32Q=="
+ },
+ "node_modules/string-width": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
+ "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dependencies": {
+ "ansi-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/v3": {
+ "name": "redis",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
+ "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
+ "dependencies": {
+ "denque": "^1.5.0",
+ "redis-commands": "^1.7.0",
+ "redis-errors": "^1.2.0",
+ "redis-parser": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-redis"
+ }
+ },
+ "node_modules/v4": {
+ "resolved": "..",
+ "link": true
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ }
+ },
+ "dependencies": {
+ "@arrows/array": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
+ "integrity": "sha512-MGYS8xi3c4tTy1ivhrVntFvufoNzje0PchjEz6G/SsWRgUKxL4tKwS6iPdO8vsaJYldagAeWMd5KRD0aX3Q39g==",
+ "requires": {
+ "@arrows/composition": "^1.2.2"
+ }
+ },
+ "@arrows/composition": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@arrows/composition/-/composition-1.2.2.tgz",
+ "integrity": "sha512-9fh1yHwrx32lundiB3SlZ/VwuStPB4QakPsSLrGJFH6rCXvdrd060ivAZ7/2vlqPnEjBkPRRXOcG1YOu19p2GQ=="
+ },
+ "@arrows/dispatch": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@arrows/dispatch/-/dispatch-1.0.3.tgz",
+ "integrity": "sha512-v/HwvrFonitYZM2PmBlAlCqVqxrkIIoiEuy5bQgn0BdfvlL0ooSBzcPzTMrtzY8eYktPyYcHg8fLbSgyybXEqw==",
+ "requires": {
+ "@arrows/composition": "^1.2.2"
+ }
+ },
+ "@arrows/error": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@arrows/error/-/error-1.0.2.tgz",
+ "integrity": "sha512-yvkiv1ay4Z3+Z6oQsUkedsQm5aFdyPpkBUQs8vejazU/RmANABx6bMMcBPPHI4aW43VPQmXFfBzr/4FExwWTEA=="
+ },
+ "@arrows/multimethod": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@arrows/multimethod/-/multimethod-1.1.7.tgz",
+ "integrity": "sha512-EjHD3XuGAV4G28rm7mu8k7zQJh/EOizh104/p9i2ofGcnL5mgKONFH/Bq6H3SJjM+WDAlKcR9WBpNhaAKCnH2g==",
+ "requires": {
+ "@arrows/array": "^1.4.0",
+ "@arrows/composition": "^1.2.2",
+ "@arrows/error": "^1.0.2",
+ "fast-deep-equal": "^3.1.1"
+ }
+ },
+ "@babel/runtime": {
+ "version": "7.14.8",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
+ "integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@probe.gl/bench": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@probe.gl/bench/-/bench-3.4.0.tgz",
+ "integrity": "sha512-S7iNPz5G3zEfEP0S4SAMvtj+dwP7EWfVBaA8Cy5CVIgM1lnpUbXvqoAJxlVEedNC32Icxwq65XQheufy1Zzmug==",
+ "requires": {
+ "@babel/runtime": "^7.0.0",
+ "probe.gl": "3.4.0"
+ }
+ },
+ "@probe.gl/stats": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-3.4.0.tgz",
+ "integrity": "sha512-Gl37r9qGuiKadIvTZdSZvzCNOttJYw6RcY1oT0oDuB8r2uhuZAdSMQRQTy9FTinp6MY6O9wngGnV6EpQ8wSBAw==",
+ "requires": {
+ "@babel/runtime": "^7.0.0"
+ }
+ },
+ "ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "requires": {
+ "type-fest": "^0.21.3"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="
+ },
+ "at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="
+ },
+ "benchmark": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
+ "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
+ "requires": {
+ "lodash": "^4.17.4",
+ "platform": "^1.3.3"
+ }
+ },
+ "benny": {
+ "version": "3.6.15",
+ "resolved": "https://registry.npmjs.org/benny/-/benny-3.6.15.tgz",
+ "integrity": "sha512-kq6XVGGYVou3Y8KNPs3SEF881vi5fJ8sIf9w69D2rreiNfRicWVWK6u6/mObMw6BiexoHHumtipn5gcu0Tngng==",
+ "requires": {
+ "@arrows/composition": "^1.0.0",
+ "@arrows/dispatch": "^1.0.2",
+ "@arrows/multimethod": "^1.1.6",
+ "benchmark": "^2.1.4",
+ "fs-extra": "^9.0.1",
+ "json2csv": "^5.0.4",
+ "kleur": "^4.1.3",
+ "log-update": "^4.0.0",
+ "prettier": "^2.1.2",
+ "stats-median": "^1.0.1"
+ }
+ },
+ "cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "requires": {
+ "restore-cursor": "^3.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="
+ },
+ "denque": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
+ "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ=="
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "requires": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+ },
+ "json2csv": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.6.tgz",
+ "integrity": "sha512-0/4Lv6IenJV0qj2oBdgPIAmFiKKnh8qh7bmLFJ+/ZZHLjSeiL3fKKGX3UryvKPbxFbhV+JcYo9KUC19GJ/Z/4A==",
+ "requires": {
+ "commander": "^6.1.0",
+ "jsonparse": "^1.3.1",
+ "lodash.get": "^4.4.2"
+ }
+ },
+ "jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "requires": {
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
+ }
+ },
+ "jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
+ },
+ "kleur": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
+ "integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA=="
+ },
+ "lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
+ },
+ "log-update": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+ "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+ "requires": {
+ "ansi-escapes": "^4.3.0",
+ "cli-cursor": "^3.1.0",
+ "slice-ansi": "^4.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
+ },
+ "onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "platform": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
+ },
+ "prettier": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
+ "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ=="
+ },
+ "probe.gl": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/probe.gl/-/probe.gl-3.4.0.tgz",
+ "integrity": "sha512-9CLByZATuhuG/Viq3ckfWU+dAhb7dMmjzsyCy4s7ds9ueTejcVRENxL197/XacOK/AN61YrEERB0QnouB0Qc0Q==",
+ "requires": {
+ "@babel/runtime": "^7.0.0",
+ "@probe.gl/stats": "3.4.0"
+ }
+ },
+ "redis-commands": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
+ "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
+ },
+ "redis-errors": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
+ "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
+ },
+ "redis-parser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
+ "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
+ "requires": {
+ "redis-errors": "^1.0.0"
+ }
+ },
+ "regenerator-runtime": {
+ "version": "0.13.9",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
+ "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
+ },
+ "restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "requires": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "signal-exit": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
+ },
+ "slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ }
+ },
+ "stats-median": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/stats-median/-/stats-median-1.0.1.tgz",
+ "integrity": "sha512-IYsheLg6dasD3zT/w9+8Iq9tcIQqqu91ZIpJOnIEM25C3X/g4Tl8mhXwW2ZQpbrsJISr9+wizEYgsibN5/b32Q=="
+ },
+ "string-width": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
+ "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ },
+ "type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="
+ },
+ "universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
+ },
+ "v3": {
+ "version": "npm:redis@3.1.2",
+ "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
+ "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
+ "requires": {
+ "denque": "^1.5.0",
+ "redis-commands": "^1.7.0",
+ "redis-errors": "^1.2.0",
+ "redis-parser": "^3.0.0"
+ }
+ },
+ "v4": {
+ "version": "file:..",
+ "requires": {
+ "@istanbuljs/nyc-config-typescript": "^1.0.1",
+ "@types/mocha": "^9.0.0",
+ "@types/node": "^16.4.5",
+ "@types/sinon": "^10.0.2",
+ "@types/which": "^2.0.1",
+ "@types/yallist": "^4.0.1",
+ "cluster-key-slot": "1.1.0",
+ "mocha": "^9.0.3",
+ "nyc": "^15.1.0",
+ "redis-parser": "3.0.0",
+ "release-it": "^14.10.1",
+ "sinon": "^11.1.2",
+ "source-map-support": "^0.5.19",
+ "ts-node": "^10.1.0",
+ "typedoc": "^0.21.4",
+ "typedoc-github-wiki-theme": "^0.5.1",
+ "typedoc-plugin-markdown": "^3.10.4",
+ "typescript": "^4.3.5",
+ "which": "^2.0.2",
+ "yallist": "4.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ }
+ }
+}
diff --git a/benchmark/package.json b/benchmark/package.json
new file mode 100644
index 00000000000..5226a5b0c89
--- /dev/null
+++ b/benchmark/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "benchmark",
+ "private": true,
+ "description": "",
+ "main": "index.js",
+ "type": "module",
+ "scripts": {
+ "start": "node ./"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "benny": "^3.6.15",
+ "v3": "npm:redis@3.1.2",
+ "v4": "file:../"
+ }
+}
diff --git a/benchmarks/diff_multi_bench_output.js b/benchmarks/diff_multi_bench_output.js
deleted file mode 100755
index c3ed47a5fb5..00000000000
--- a/benchmarks/diff_multi_bench_output.js
+++ /dev/null
@@ -1,95 +0,0 @@
-'use strict';
-
-var fs = require('fs');
-var metrics = require('metrics');
-// `node diff_multi_bench_output.js beforeBench.txt afterBench.txt`
-var file1 = process.argv[2];
-var file2 = process.argv[3];
-
-if (!file1 || !file2) {
- console.log('Please supply two file arguments:');
- var n = __filename;
- n = n.substring(n.lastIndexOf('/', n.length));
- console.log(' node .' + n + ' benchBefore.txt benchAfter.txt\n');
- console.log('To generate the benchmark files, run');
- console.log(' npm run benchmark > benchBefore.txt\n');
- console.log('Thank you for benchmarking responsibly.');
- return;
-}
-
-var before_lines = fs.readFileSync(file1, 'utf8').split('\n');
-var after_lines = fs.readFileSync(file2, 'utf8').split('\n');
-var total_ops = new metrics.Histogram.createUniformHistogram();
-
-console.log('Comparing before,', file1, '(', before_lines.length, 'lines)', 'to after,', file2, '(', after_lines.length, 'lines)');
-
-function is_whitespace (s) {
- return !!s.trim();
-}
-
-function pad (input, len, chr, right) {
- var str = input.toString();
- chr = chr || ' ';
-
- if (right) {
- while (str.length < len) {
- str += chr;
- }
- } else {
- while (str.length < len) {
- str = chr + str;
- }
- }
- return str;
-}
-
-// green if greater than 0, red otherwise
-function humanize_diff (num, unit, toFixed) {
- unit = unit || '';
- if (num > 0) {
- return ' +' + pad(num.toFixed(toFixed || 0) + unit, 7);
- }
- return ' -' + pad(Math.abs(num).toFixed(toFixed || 0) + unit, 7);
-}
-
-function command_name (words) {
- var line = words.join(' ');
- return line.substr(0, line.indexOf(','));
-}
-
-before_lines.forEach(function (b, i) {
- var a = after_lines[i];
- if (!a || !b || !b.trim() || !a.trim()) {
- // console.log('#ignored#', '>'+a+'<', '>'+b+'<');
- return;
- }
- var b_words = b.split(' ').filter(is_whitespace);
- var a_words = a.split(' ').filter(is_whitespace);
-
- var ops = [b_words, a_words].map(function (words) {
- // console.log(words);
- return words.slice(-2, -1) | 0;
- }).filter(function (num) {
- var isNaN = !num && num !== 0;
- return !isNaN;
- });
- if (ops.length !== 2) {
- return;
- }
- var delta = ops[1] - ops[0];
- var pct = +((delta / ops[0]) * 100);
- ops[0] = pad(ops[0], 6);
- ops[1] = pad(ops[1], 6);
- total_ops.update(delta);
- delta = humanize_diff(delta);
- var small_delta = pct < 3 && pct > -3;
- // Let's mark differences above 20% bold
- var big_delta = pct > 20 || pct < -20 ? ';1' : '';
- pct = humanize_diff(pct, '', 2) + '%';
- var str = pad((command_name(a_words) === command_name(b_words) ? command_name(a_words) + ':' : '404:'), 14, false, true) +
- (pad(ops.join(' -> '), 15) + ' ops/sec (∆' + delta + pct + ')');
- str = (small_delta ? '' : (/-[^>]/.test(str) ? '\x1b[31' : '\x1b[32') + big_delta + 'm') + str + '\x1b[0m';
- console.log(str);
-});
-
-console.log('Mean difference in ops/sec:', humanize_diff(total_ops.mean(), '', 1));
diff --git a/benchmarks/multi_bench.js b/benchmarks/multi_bench.js
deleted file mode 100644
index 86cf9329ce9..00000000000
--- a/benchmarks/multi_bench.js
+++ /dev/null
@@ -1,291 +0,0 @@
-'use strict';
-
-var path = require('path');
-var RedisProcess = require('../test/lib/redis-process');
-var rp;
-var client_nr = 0;
-var redis = require('../index');
-var totalTime = 0;
-var metrics = require('metrics');
-var tests = [];
-// var bluebird = require('bluebird');
-// bluebird.promisifyAll(redis.RedisClient.prototype);
-// bluebird.promisifyAll(redis.Multi.prototype);
-
-function returnArg (name, def) {
- var matches = process.argv.filter(function (entry) {
- return entry.indexOf(name + '=') === 0;
- });
- if (matches.length) {
- return matches[0].substr(name.length + 1);
- }
- return def;
-}
-var num_clients = returnArg('clients', 1);
-var run_time = returnArg('time', 2500); // ms
-var pipeline = returnArg('pipeline', 1); // number of concurrent commands
-var versions_logged = false;
-var client_options = {
- parser: returnArg('parser', 'javascript'),
- path: returnArg('socket') // '/tmp/redis.sock'
-};
-var small_str, large_str, small_buf, large_buf, very_large_str, very_large_buf;
-
-function lpad (input, len, chr) {
- var str = input.toString();
- chr = chr || ' ';
- while (str.length < len) {
- str = chr + str;
- }
- return str;
-}
-
-metrics.Histogram.prototype.print_line = function () {
- var obj = this.printObj();
- return lpad((obj.mean / 1e6).toFixed(2), 6) + '/' + lpad((obj.max / 1e6).toFixed(2), 6);
-};
-
-function Test (args) {
- this.args = args;
- this.args.pipeline = +pipeline;
- this.callback = null;
- this.clients = [];
- this.clients_ready = 0;
- this.commands_sent = 0;
- this.commands_completed = 0;
- this.max_pipeline = +pipeline;
- this.batch_pipeline = this.args.batch || 0;
- this.client_options = args.client_options || {};
- this.client_options.parser = client_options.parser;
- this.client_options.connect_timeout = 1000;
- if (client_options.path) {
- this.client_options.path = client_options.path;
- }
- this.connect_latency = new metrics.Histogram();
- this.ready_latency = new metrics.Histogram();
- this.command_latency = new metrics.Histogram();
-}
-
-Test.prototype.run = function (callback) {
- var i;
- this.callback = callback;
- for (i = 0; i < num_clients ; i++) {
- this.new_client(i);
- }
-};
-
-Test.prototype.new_client = function (id) {
- var self = this, new_client;
-
- new_client = redis.createClient(this.client_options);
- new_client.create_time = Date.now();
-
- new_client.on('connect', function () {
- self.connect_latency.update(Date.now() - new_client.create_time);
- });
-
- new_client.on('ready', function () {
- if (!versions_logged) {
- console.log(
- 'clients: ' + num_clients +
- ', NodeJS: ' + process.versions.node +
- ', Redis: ' + new_client.server_info.redis_version +
- ', parser: ' + client_options.parser +
- ', connected by: ' + (client_options.path ? 'socket' : 'tcp')
- );
- versions_logged = true;
- }
- self.ready_latency.update(Date.now() - new_client.create_time);
- self.clients_ready++;
- if (self.clients_ready === self.clients.length) {
- self.on_clients_ready();
- }
- });
-
- // If no redis server is running, start one
- new_client.on('error', function (err) {
- if (err.code === 'CONNECTION_BROKEN') {
- throw err;
- }
- if (rp) {
- return;
- }
- rp = true;
- var conf = '../test/conf/redis.conf';
- RedisProcess.start(function (err, _rp) {
- if (err) {
- throw err;
- }
- rp = _rp;
- }, path.resolve(__dirname, conf));
- });
-
- self.clients[id] = new_client;
-};
-
-Test.prototype.on_clients_ready = function () {
- process.stdout.write(lpad(this.args.descr, 13) + ', ' + (this.args.batch ? lpad('batch ' + this.args.batch, 9) : lpad(this.args.pipeline, 9)) + '/' + this.clients_ready + ' ');
- this.test_start = Date.now();
- this.fill_pipeline();
-};
-
-Test.prototype.fill_pipeline = function () {
- var pipeline = this.commands_sent - this.commands_completed;
-
- if (this.test_start < Date.now() - run_time) {
- if (this.ended) {
- return;
- }
- this.ended = true;
- this.print_stats();
- this.stop_clients();
- return;
- }
-
- if (this.batch_pipeline) {
- this.batch();
- } else {
- while (pipeline < this.max_pipeline) {
- this.commands_sent++;
- pipeline++;
- this.send_next();
- }
- }
-};
-
-Test.prototype.batch = function () {
- var self = this,
- cur_client = client_nr++ % this.clients.length,
- start = process.hrtime(),
- i = 0,
- batch = this.clients[cur_client].batch();
-
- while (i++ < this.batch_pipeline) {
- this.commands_sent++;
- batch[this.args.command](this.args.args);
- }
-
- batch.exec(function (err, res) {
- if (err) {
- throw err;
- }
- self.commands_completed += res.length;
- self.command_latency.update(process.hrtime(start)[1]);
- self.fill_pipeline();
- });
-};
-
-Test.prototype.stop_clients = function () {
- var self = this;
-
- this.clients.forEach(function (client, pos) {
- if (pos === self.clients.length - 1) {
- client.quit(function (err, res) {
- self.callback();
- });
- } else {
- client.quit();
- }
- });
-};
-
-Test.prototype.send_next = function () {
- var self = this,
- cur_client = this.commands_sent % this.clients.length,
- start = process.hrtime();
-
- this.clients[cur_client][this.args.command](this.args.args, function (err, res) {
- if (err) {
- throw err;
- }
- self.commands_completed++;
- self.command_latency.update(process.hrtime(start)[1]);
- self.fill_pipeline();
- });
-};
-
-Test.prototype.print_stats = function () {
- var duration = Date.now() - this.test_start;
- totalTime += duration;
-
- console.log('avg/max: ' + this.command_latency.print_line() + lpad(duration, 5) + 'ms total, ' +
- lpad(Math.round(this.commands_completed / (duration / 1000)), 7) + ' ops/sec');
-};
-
-small_str = '1234';
-small_buf = Buffer.from(small_str);
-large_str = (new Array(4096 + 1).join('-'));
-large_buf = Buffer.from(large_str);
-very_large_str = (new Array((4 * 1024 * 1024) + 1).join('-'));
-very_large_buf = Buffer.from(very_large_str);
-
-tests.push(new Test({descr: 'PING', command: 'ping', args: []}));
-tests.push(new Test({descr: 'PING', command: 'ping', args: [], batch: 50}));
-
-tests.push(new Test({descr: 'SET 4B str', command: 'set', args: ['foo_rand000000000000', small_str]}));
-tests.push(new Test({descr: 'SET 4B str', command: 'set', args: ['foo_rand000000000000', small_str], batch: 50}));
-
-tests.push(new Test({descr: 'SET 4B buf', command: 'set', args: ['foo_rand000000000000', small_buf]}));
-tests.push(new Test({descr: 'SET 4B buf', command: 'set', args: ['foo_rand000000000000', small_buf], batch: 50}));
-
-tests.push(new Test({descr: 'GET 4B str', command: 'get', args: ['foo_rand000000000000']}));
-tests.push(new Test({descr: 'GET 4B str', command: 'get', args: ['foo_rand000000000000'], batch: 50}));
-
-tests.push(new Test({descr: 'GET 4B buf', command: 'get', args: ['foo_rand000000000000'], client_options: { return_buffers: true} }));
-tests.push(new Test({descr: 'GET 4B buf', command: 'get', args: ['foo_rand000000000000'], batch: 50, client_options: { return_buffers: true} }));
-
-tests.push(new Test({descr: 'SET 4KiB str', command: 'set', args: ['foo_rand000000000001', large_str]}));
-tests.push(new Test({descr: 'SET 4KiB str', command: 'set', args: ['foo_rand000000000001', large_str], batch: 50}));
-
-tests.push(new Test({descr: 'SET 4KiB buf', command: 'set', args: ['foo_rand000000000001', large_buf]}));
-tests.push(new Test({descr: 'SET 4KiB buf', command: 'set', args: ['foo_rand000000000001', large_buf], batch: 50}));
-
-tests.push(new Test({descr: 'GET 4KiB str', command: 'get', args: ['foo_rand000000000001']}));
-tests.push(new Test({descr: 'GET 4KiB str', command: 'get', args: ['foo_rand000000000001'], batch: 50}));
-
-tests.push(new Test({descr: 'GET 4KiB buf', command: 'get', args: ['foo_rand000000000001'], client_options: { return_buffers: true} }));
-tests.push(new Test({descr: 'GET 4KiB buf', command: 'get', args: ['foo_rand000000000001'], batch: 50, client_options: { return_buffers: true} }));
-
-tests.push(new Test({descr: 'INCR', command: 'incr', args: ['counter_rand000000000000']}));
-tests.push(new Test({descr: 'INCR', command: 'incr', args: ['counter_rand000000000000'], batch: 50}));
-
-tests.push(new Test({descr: 'LPUSH', command: 'lpush', args: ['mylist', small_str]}));
-tests.push(new Test({descr: 'LPUSH', command: 'lpush', args: ['mylist', small_str], batch: 50}));
-
-tests.push(new Test({descr: 'LRANGE 10', command: 'lrange', args: ['mylist', '0', '9']}));
-tests.push(new Test({descr: 'LRANGE 10', command: 'lrange', args: ['mylist', '0', '9'], batch: 50}));
-
-tests.push(new Test({descr: 'LRANGE 100', command: 'lrange', args: ['mylist', '0', '99']}));
-tests.push(new Test({descr: 'LRANGE 100', command: 'lrange', args: ['mylist', '0', '99'], batch: 50}));
-
-tests.push(new Test({descr: 'SET 4MiB str', command: 'set', args: ['foo_rand000000000002', very_large_str]}));
-tests.push(new Test({descr: 'SET 4MiB str', command: 'set', args: ['foo_rand000000000002', very_large_str], batch: 20}));
-
-tests.push(new Test({descr: 'SET 4MiB buf', command: 'set', args: ['foo_rand000000000002', very_large_buf]}));
-tests.push(new Test({descr: 'SET 4MiB buf', command: 'set', args: ['foo_rand000000000002', very_large_buf], batch: 20}));
-
-tests.push(new Test({descr: 'GET 4MiB str', command: 'get', args: ['foo_rand000000000002']}));
-tests.push(new Test({descr: 'GET 4MiB str', command: 'get', args: ['foo_rand000000000002'], batch: 20}));
-
-tests.push(new Test({descr: 'GET 4MiB buf', command: 'get', args: ['foo_rand000000000002'], client_options: { return_buffers: true} }));
-tests.push(new Test({descr: 'GET 4MiB buf', command: 'get', args: ['foo_rand000000000002'], batch: 20, client_options: { return_buffers: true} }));
-
-function next () {
- var test = tests.shift();
- if (test) {
- test.run(function () {
- next();
- });
- } else if (rp) {
- // Stop the redis process if started by the benchmark
- rp.stop(function () {
- rp = undefined;
- next();
- });
- } else {
- console.log('End of tests. Total time elapsed:', totalTime, 'ms');
- process.exit(0);
- }
-}
-
-next();
diff --git a/docs/FAQ.md b/docs/FAQ.md
new file mode 100644
index 00000000000..b5074e73025
--- /dev/null
+++ b/docs/FAQ.md
@@ -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). Commands from the same "tick" will be sent in batches and respect the [`writableHighWaterMark`](https://nodejs.org/api/stream.html#stream_new_stream_writable_options).
+
+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.
diff --git a/docs/client-configuration.md b/docs/client-configuration.md
new file mode 100644
index 00000000000..4b93340ad8f
--- /dev/null
+++ b/docs/client-configuration.md
@@ -0,0 +1,30 @@
+# `createClient` configuration
+
+| Property | Default | Description |
+|--------------------------|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
+| socket | | Object defining socket connection properties |
+| socket.url | | `[redis[s]:]//[[username][:password]@][host][:port]` |
+| socket.host | `'localhost'` | Hostname to connect to |
+| socket.port | `6379` | Port to connect to |
+| socket.username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
+| socket.password | | ACL password or the old "--requirepass" password |
+| 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 |
+| modules | | Object defining which [Redis Modules](https://redis.io/modules) to include (TODO - document) |
+| 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.
diff --git a/docs/isolated-execution.md b/docs/isolated-execution.md
new file mode 100644
index 00000000000..78b34252a0f
--- /dev/null
+++ b/docs/isolated-execution.md
@@ -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'
+);
+```
diff --git a/docs/v3-to-v4.md b/docs/v3-to-v4.md
new file mode 100644
index 00000000000..7c3e9880431
--- /dev/null
+++ b/docs/v3-to-v4.md
@@ -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.
diff --git a/examples/auth.js b/examples/auth.js
deleted file mode 100644
index e36b7006562..00000000000
--- a/examples/auth.js
+++ /dev/null
@@ -1,7 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-// The client stashes the password and will reauthenticate on every connect.
-redis.createClient({
- password: 'somepass'
-});
diff --git a/examples/backpressure_drain.js b/examples/backpressure_drain.js
deleted file mode 100644
index 0e9140c6b29..00000000000
--- a/examples/backpressure_drain.js
+++ /dev/null
@@ -1,34 +0,0 @@
-'use strict';
-
-var redis = require('../index');
-var client = redis.createClient();
-var remaining_ops = 100000;
-var paused = false;
-
-function op () {
- if (remaining_ops <= 0) {
- console.error('Finished.');
- process.exit(0);
- }
-
- remaining_ops--;
- client.hset('test hash', 'val ' + remaining_ops, remaining_ops);
- if (client.should_buffer === true) {
- console.log('Pausing at ' + remaining_ops);
- paused = true;
- } else {
- setTimeout(op, 1);
- }
-}
-
-client.on('drain', function () {
- if (paused) {
- console.log('Resuming at ' + remaining_ops);
- paused = false;
- process.nextTick(op);
- } else {
- console.log('Got drain while not paused at ' + remaining_ops);
- }
-});
-
-op();
diff --git a/examples/eval.js b/examples/eval.js
deleted file mode 100644
index cffdb548a16..00000000000
--- a/examples/eval.js
+++ /dev/null
@@ -1,14 +0,0 @@
-'use strict';
-
-var redis = require('../index');
-var client = redis.createClient();
-
-client.eval('return 100.5', 0, function (err, res) {
- console.dir(err);
- console.dir(res);
-});
-
-client.eval([ 'return 100.5', 0 ], function (err, res) {
- console.dir(err);
- console.dir(res);
-});
diff --git a/examples/extend.js b/examples/extend.js
deleted file mode 100644
index 8128b41bdca..00000000000
--- a/examples/extend.js
+++ /dev/null
@@ -1,26 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-var client = redis.createClient();
-
-// Extend the RedisClient prototype to add a custom method
-// This one converts the results from 'INFO' into a JavaScript Object
-
-redis.RedisClient.prototype.parse_info = function (callback) {
- this.info(function (err, res) {
- var lines = res.toString().split('\r\n').sort();
- var obj = {};
- lines.forEach(function (line) {
- var parts = line.split(':');
- if (parts[1]) {
- obj[parts[0]] = parts[1];
- }
- });
- callback(obj);
- });
-};
-
-client.parse_info(function (info) {
- console.dir(info);
- client.quit();
-});
diff --git a/examples/file.js b/examples/file.js
deleted file mode 100644
index 0bacd723b32..00000000000
--- a/examples/file.js
+++ /dev/null
@@ -1,38 +0,0 @@
-'use strict';
-
-// Read a file from disk, store it in Redis, then read it back from Redis.
-
-var redis = require('redis');
-var client = redis.createClient({
- return_buffers: true
-});
-var fs = require('fs');
-var assert = require('assert');
-var filename = 'grumpyCat.jpg';
-
-// Get the file I use for testing like this:
-// curl http://media4.popsugar-assets.com/files/2014/08/08/878/n/1922507/caef16ec354ca23b_thumb_temp_cover_file32304521407524949.xxxlarge/i/Funny-Cat-GIFs.jpg -o grumpyCat.jpg
-// or just use your own file.
-
-// Read a file from fs, store it in Redis, get it back from Redis, write it back to fs.
-fs.readFile(filename, function (err, data) {
- if (err) throw err;
- console.log('Read ' + data.length + ' bytes from filesystem.');
-
- client.set(filename, data, redis.print); // set entire file
- client.get(filename, function (err, reply) { // get entire file
- if (err) {
- console.log('Get error: ' + err);
- } else {
- assert.strictEqual(data.inspect(), reply.inspect());
- fs.writeFile('duplicate_' + filename, reply, function (err) {
- if (err) {
- console.log('Error on write: ' + err);
- } else {
- console.log('File written.');
- }
- client.end();
- });
- }
- });
-});
diff --git a/examples/mget.js b/examples/mget.js
deleted file mode 100644
index e774ac44e0e..00000000000
--- a/examples/mget.js
+++ /dev/null
@@ -1,7 +0,0 @@
-'use strict';
-
-var client = require('redis').createClient();
-
-client.mget(['sessions started', 'sessions started', 'foo'], function (err, res) {
- console.dir(res);
-});
diff --git a/examples/monitor.js b/examples/monitor.js
deleted file mode 100644
index 93d5d100f5a..00000000000
--- a/examples/monitor.js
+++ /dev/null
@@ -1,12 +0,0 @@
-'use strict';
-
-var client = require('../index').createClient();
-var util = require('util');
-
-client.monitor(function (err, res) {
- console.log('Entering monitoring mode.');
-});
-
-client.on('monitor', function (time, args) {
- console.log(time + ': ' + util.inspect(args));
-});
diff --git a/examples/multi.js b/examples/multi.js
deleted file mode 100644
index e0e3bf13fcb..00000000000
--- a/examples/multi.js
+++ /dev/null
@@ -1,49 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-var client = redis.createClient();
-var set_size = 20;
-
-client.sadd('bigset', 'a member');
-client.sadd('bigset', 'another member');
-
-while (set_size > 0) {
- client.sadd('bigset', 'member ' + set_size);
- set_size -= 1;
-}
-
-// multi chain with an individual callback
-client.multi()
- .scard('bigset')
- .smembers('bigset')
- .keys('*', function (err, replies) {
- client.mget(replies, redis.print);
- })
- .dbsize()
- .exec(function (err, replies) {
- console.log('MULTI got ' + replies.length + ' replies');
- replies.forEach(function (reply, index) {
- console.log('Reply ' + index + ': ' + reply.toString());
- });
- });
-
-client.mset('incr thing', 100, 'incr other thing', 1, redis.print);
-
-// start a separate multi command queue
-var multi = client.multi();
-multi.incr('incr thing', redis.print);
-multi.incr('incr other thing', redis.print);
-
-// runs immediately
-client.get('incr thing', redis.print); // 100
-
-// drains multi queue and runs atomically
-multi.exec(function (err, replies) {
- console.log(replies); // 101, 2
-});
-
-// you can re-run the same transaction if you like
-multi.exec(function (err, replies) {
- console.log(replies); // 102, 3
- client.quit();
-});
diff --git a/examples/multi2.js b/examples/multi2.js
deleted file mode 100644
index 3a0ceaec6a3..00000000000
--- a/examples/multi2.js
+++ /dev/null
@@ -1,31 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-var client = redis.createClient();
-
-// start a separate command queue for multi
-var multi = client.multi();
-multi.incr('incr thing', redis.print);
-multi.incr('incr other thing', redis.print);
-
-// runs immediately
-client.mset('incr thing', 100, 'incr other thing', 1, redis.print);
-
-// drains multi queue and runs atomically
-multi.exec(function (err, replies) {
- console.log(replies); // 101, 2
-});
-
-// you can re-run the same transaction if you like
-multi.exec(function (err, replies) {
- console.log(replies); // 102, 3
- client.quit();
-});
-
-client.multi([
- ['mget', 'multifoo', 'multibar', redis.print],
- ['incr', 'multifoo'],
- ['incr', 'multibar']
-]).exec(function (err, replies) {
- console.log(replies.toString());
-});
diff --git a/examples/psubscribe.js b/examples/psubscribe.js
deleted file mode 100644
index ecd24274047..00000000000
--- a/examples/psubscribe.js
+++ /dev/null
@@ -1,33 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-var client1 = redis.createClient();
-var client2 = redis.createClient();
-var client3 = redis.createClient();
-var client4 = redis.createClient();
-var msg_count = 0;
-
-client1.on('psubscribe', function (pattern, count) {
- console.log('client1 psubscribed to ' + pattern + ', ' + count + ' total subscriptions');
- client2.publish('channeltwo', 'Me!');
- client3.publish('channelthree', 'Me too!');
- client4.publish('channelfour', 'And me too!');
-});
-
-client1.on('punsubscribe', function (pattern, count) {
- console.log('client1 punsubscribed from ' + pattern + ', ' + count + ' total subscriptions');
- client4.end();
- client3.end();
- client2.end();
- client1.end();
-});
-
-client1.on('pmessage', function (pattern, channel, message) {
- console.log('(' + pattern + ') client1 received message on ' + channel + ': ' + message);
- msg_count += 1;
- if (msg_count === 3) {
- client1.punsubscribe();
- }
-});
-
-client1.psubscribe('channel*');
diff --git a/examples/pub_sub.js b/examples/pub_sub.js
deleted file mode 100644
index d0970eb502f..00000000000
--- a/examples/pub_sub.js
+++ /dev/null
@@ -1,42 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-var client1 = redis.createClient();
-var msg_count = 0;
-var client2 = redis.createClient();
-
-// Most clients probably don't do much on 'subscribe'. This example uses it to coordinate things within one program.
-client1.on('subscribe', function (channel, count) {
- console.log('client1 subscribed to ' + channel + ', ' + count + ' total subscriptions');
- if (count === 2) {
- client2.publish('a nice channel', 'I am sending a message.');
- client2.publish('another one', 'I am sending a second message.');
- client2.publish('a nice channel', 'I am sending my last message.');
- }
-});
-
-client1.on('unsubscribe', function (channel, count) {
- console.log('client1 unsubscribed from ' + channel + ', ' + count + ' total subscriptions');
- if (count === 0) {
- client2.end();
- client1.end();
- }
-});
-
-client1.on('message', function (channel, message) {
- console.log('client1 channel ' + channel + ': ' + message);
- msg_count += 1;
- if (msg_count === 3) {
- client1.unsubscribe();
- }
-});
-
-client1.on('ready', function () {
- // if you need auth, do it here
- client1.incr('did a thing');
- client1.subscribe('a nice channel', 'another one');
-});
-
-client2.on('ready', function () {
- // if you need auth, do it here
-});
diff --git a/examples/scan.js b/examples/scan.js
deleted file mode 100644
index e6b67ea6ae0..00000000000
--- a/examples/scan.js
+++ /dev/null
@@ -1,51 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-var client = redis.createClient();
-
-var cursor = '0';
-
-function scan () {
- client.scan(
- cursor,
- 'MATCH', 'q:job:*',
- 'COUNT', '10',
- function (err, res) {
- if (err) throw err;
-
- // Update the cursor position for the next scan
- cursor = res[0];
- // get the SCAN result for this iteration
- var keys = res[1];
-
- // Remember: more or less than COUNT or no keys may be returned
- // See http://redis.io/commands/scan#the-count-option
- // Also, SCAN may return the same key multiple times
- // See http://redis.io/commands/scan#scan-guarantees
- // Additionally, you should always have the code that uses the keys
- // before the code checking the cursor.
- if (keys.length > 0) {
- console.log('Array of matching keys', keys);
- }
-
- // It's important to note that the cursor and returned keys
- // vary independently. The scan is never complete until redis
- // returns a non-zero cursor. However, with MATCH and large
- // collections, most iterations will return an empty keys array.
-
- // Still, a cursor of zero DOES NOT mean that there are no keys.
- // A zero cursor just means that the SCAN is complete, but there
- // might be one last batch of results to process.
-
- // From :
- // 'An iteration starts when the cursor is set to 0,
- // and terminates when the cursor returned by the server is 0.'
- if (cursor === '0') {
- return console.log('Iteration complete');
- }
-
- return scan();
- }
- );
-}
-scan();
diff --git a/examples/simple.js b/examples/simple.js
deleted file mode 100644
index 2fc2c3aefbd..00000000000
--- a/examples/simple.js
+++ /dev/null
@@ -1,26 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-var client = redis.createClient();
-
-client.on('error', function (err) {
- console.log('error event - ' + client.host + ':' + client.port + ' - ' + err);
-});
-
-client.set('string key', 'string val', redis.print);
-client.hset('hash key', 'hashtest 1', 'some value', redis.print);
-client.hset(['hash key', 'hashtest 2', 'some other value'], redis.print);
-client.hkeys('hash key', function (err, replies) {
- if (err) {
- return console.error('error response - ' + err);
- }
-
- console.log(replies.length + ' replies:');
- replies.forEach(function (reply, i) {
- console.log(' ' + i + ': ' + reply);
- });
-});
-
-client.quit(function (err, res) {
- console.log('Exiting from quit command.');
-});
diff --git a/examples/sort.js b/examples/sort.js
deleted file mode 100644
index b09b06fbbf0..00000000000
--- a/examples/sort.js
+++ /dev/null
@@ -1,19 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-var client = redis.createClient();
-
-client.sadd('mylist', 1);
-client.sadd('mylist', 2);
-client.sadd('mylist', 3);
-
-client.set('weight_1', 5);
-client.set('weight_2', 500);
-client.set('weight_3', 1);
-
-client.set('object_1', 'foo');
-client.set('object_2', 'bar');
-client.set('object_3', 'qux');
-
-client.sort('mylist', 'by', 'weight_*', 'get', 'object_*', redis.print);
-// Prints Reply: qux,foo,bar
diff --git a/examples/streams.js b/examples/streams.js
deleted file mode 100644
index 726e4adf920..00000000000
--- a/examples/streams.js
+++ /dev/null
@@ -1,47 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-var client1 = redis.createClient();
-var client2 = redis.createClient();
-var client3 = redis.createClient();
-
-client1.xadd('mystream', '*', 'field1', 'm1', function (err) {
- if (err) {
- return console.error(err);
- }
- client1.xgroup('CREATE', 'mystream', 'mygroup', '$', function (err) {
- if (err) {
- return console.error(err);
- }
- });
-
- client2.xreadgroup('GROUP', 'mygroup', 'consumer', 'Block', 1000, 'NOACK',
- 'STREAMS', 'mystream', '>', function (err, stream) {
- if (err) {
- return console.error(err);
- }
- console.log('client2 ' + stream);
- });
-
- client3.xreadgroup('GROUP', 'mygroup', 'consumer', 'Block', 1000, 'NOACK',
- 'STREAMS', 'mystream', '>', function (err, stream) {
- if (err) {
- return console.error(err);
- }
- console.log('client3 ' + stream);
- });
-
-
- client1.xadd('mystream', '*', 'field1', 'm2', function (err) {
- if (err) {
- return console.error(err);
- }
- });
-
- client1.xadd('mystream', '*', 'field1', 'm3', function (err) {
- if (err) {
- return console.error(err);
- }
- });
-
-});
diff --git a/examples/subqueries.js b/examples/subqueries.js
deleted file mode 100644
index 5677c129195..00000000000
--- a/examples/subqueries.js
+++ /dev/null
@@ -1,17 +0,0 @@
-'use strict';
-
-// Sending commands in response to other commands.
-// This example runs 'type' against every key in the database
-//
-var client = require('redis').createClient();
-
-client.keys('*', function (err, keys) {
- keys.forEach(function (key, pos) {
- client.type(key, function (err, keytype) {
- console.log(key + ' is ' + keytype);
- if (pos === (keys.length - 1)) {
- client.quit();
- }
- });
- });
-});
diff --git a/examples/subquery.js b/examples/subquery.js
deleted file mode 100644
index 355dd94abca..00000000000
--- a/examples/subquery.js
+++ /dev/null
@@ -1,17 +0,0 @@
-'use strict';
-
-var client = require('redis').createClient();
-
-// build a map of all keys and their types
-client.keys('*', function (err, all_keys) {
- var key_types = {};
-
- all_keys.forEach(function (key, pos) { // use second arg of forEach to get pos
- client.type(key, function (err, type) {
- key_types[key] = type;
- if (pos === all_keys.length - 1) { // callbacks all run in order
- console.dir(key_types);
- }
- });
- });
-});
diff --git a/examples/unix_socket.js b/examples/unix_socket.js
deleted file mode 100644
index b51aef2d11a..00000000000
--- a/examples/unix_socket.js
+++ /dev/null
@@ -1,32 +0,0 @@
-'use strict';
-
-var redis = require('redis');
-var client = redis.createClient('/tmp/redis.sock');
-var profiler = require('v8-profiler');
-
-client.on('connect', function () {
- console.log('Got Unix socket connection.');
-});
-
-client.on('error', function (err) {
- console.log(err.message);
-});
-
-client.set('space chars', 'space value');
-
-setInterval(function () {
- client.get('space chars');
-}, 100);
-
-function done () {
- client.info(function (err, reply) {
- console.log(reply.toString());
- client.quit();
- });
-}
-
-setTimeout(function () {
- console.log('Taking snapshot.');
- profiler.takeSnapshot();
- done();
-}, 5000);
diff --git a/examples/web_server.js b/examples/web_server.js
deleted file mode 100644
index ba5950d0384..00000000000
--- a/examples/web_server.js
+++ /dev/null
@@ -1,33 +0,0 @@
-'use strict';
-
-// A simple web server that generates dyanmic content based on responses from Redis
-
-var http = require('http');
-var redis_client = require('redis').createClient();
-
-http.createServer(function (request, response) { // The server
- response.writeHead(200, {
- 'Content-Type': 'text/plain'
- });
-
- var redis_info, total_requests;
-
- redis_client.info(function (err, reply) {
- redis_info = reply; // stash response in outer scope
- });
- redis_client.incr('requests', function (err, reply) {
- total_requests = reply; // stash response in outer scope
- });
- redis_client.hincrby('ip', request.connection.remoteAddress, 1);
- redis_client.hgetall('ip', function (err, reply) {
- // This is the last reply, so all of the previous replies must have completed already
- response.write('This page was generated after talking to redis.\n\n' +
- 'Redis info:\n' + redis_info + '\n' +
- 'Total requests: ' + total_requests + '\n\n' +
- 'IP count: \n');
- Object.keys(reply).forEach(function (ip) {
- response.write(' ' + ip + ': ' + reply[ip] + '\n');
- });
- response.end();
- });
-}).listen(80);
diff --git a/index.js b/index.js
deleted file mode 100644
index fe79c5f3935..00000000000
--- a/index.js
+++ /dev/null
@@ -1,1039 +0,0 @@
-'use strict';
-
-var net = require('net');
-var tls = require('tls');
-var util = require('util');
-var utils = require('./lib/utils');
-var Command = require('./lib/command');
-var Queue = require('denque');
-var errorClasses = require('./lib/customErrors');
-var EventEmitter = require('events');
-var Parser = require('redis-parser');
-var RedisErrors = require('redis-errors');
-var commands = require('redis-commands');
-var debug = require('./lib/debug');
-var unifyOptions = require('./lib/createClient');
-var SUBSCRIBE_COMMANDS = {
- subscribe: true,
- unsubscribe: true,
- psubscribe: true,
- punsubscribe: true
-};
-
-function noop () {}
-
-function handle_detect_buffers_reply (reply, command, buffer_args) {
- if (buffer_args === false || this.message_buffers) {
- // If detect_buffers option was specified, then the reply from the parser will be a buffer.
- // If this command did not use Buffer arguments, then convert the reply to Strings here.
- reply = utils.reply_to_strings(reply);
- }
-
- if (command === 'hgetall') {
- reply = utils.reply_to_object(reply);
- }
- return reply;
-}
-
-exports.debug_mode = /\bredis\b/i.test(process.env.NODE_DEBUG);
-
-// Attention: The second parameter might be removed at will and is not officially supported.
-// Do not rely on this
-function RedisClient (options, stream) {
- // Copy the options so they are not mutated
- options = utils.clone(options);
- EventEmitter.call(this);
- var cnx_options = {};
- var self = this;
- /* istanbul ignore next: travis does not work with stunnel atm. Therefore the tls tests are skipped on travis */
- for (var tls_option in options.tls) {
- cnx_options[tls_option] = options.tls[tls_option];
- // Copy the tls options into the general options to make sure the address is set right
- if (tls_option === 'port' || tls_option === 'host' || tls_option === 'path' || tls_option === 'family') {
- options[tls_option] = options.tls[tls_option];
- }
- }
- if (stream) {
- // The stream from the outside is used so no connection from this side is triggered but from the server this client should talk to
- // Reconnect etc won't work with this. This requires monkey patching to work, so it is not officially supported
- options.stream = stream;
- this.address = '"Private stream"';
- } else if (options.path) {
- cnx_options.path = options.path;
- this.address = options.path;
- } else {
- cnx_options.port = +options.port || 6379;
- cnx_options.host = options.host || '127.0.0.1';
- cnx_options.family = (!options.family && net.isIP(cnx_options.host)) || (options.family === 'IPv6' ? 6 : 4);
- this.address = cnx_options.host + ':' + cnx_options.port;
- }
-
- this.connection_options = cnx_options;
- this.connection_id = RedisClient.connection_id++;
- this.connected = false;
- this.ready = false;
- if (options.socket_keepalive === undefined) {
- options.socket_keepalive = true;
- }
- if (options.socket_initial_delay === undefined) {
- options.socket_initial_delay = 0;
- // set default to 0, which is aligned to https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay
- }
- for (var command in options.rename_commands) {
- options.rename_commands[command.toLowerCase()] = options.rename_commands[command];
- }
- options.return_buffers = !!options.return_buffers;
- options.detect_buffers = !!options.detect_buffers;
- // Override the detect_buffers setting if return_buffers is active and print a warning
- if (options.return_buffers && options.detect_buffers) {
- self.warn('WARNING: You activated return_buffers and detect_buffers at the same time. The return value is always going to be a buffer.');
- options.detect_buffers = false;
- }
- if (options.detect_buffers) {
- // We only need to look at the arguments if we do not know what we have to return
- this.handle_reply = handle_detect_buffers_reply;
- }
- this.should_buffer = false;
- this.command_queue = new Queue(); // Holds sent commands to de-pipeline them
- this.offline_queue = new Queue(); // Holds commands issued but not able to be sent
- this.pipeline_queue = new Queue(); // Holds all pipelined commands
- // ATTENTION: connect_timeout should change in v.3.0 so it does not count towards ending reconnection attempts after x seconds
- // This should be done by the retry_strategy. Instead it should only be the timeout for connecting to redis
- this.connect_timeout = +options.connect_timeout || 3600000; // 60 * 60 * 1000 ms
- this.enable_offline_queue = options.enable_offline_queue === false ? false : true;
- this.initialize_retry_vars();
- this.pub_sub_mode = 0;
- this.subscription_set = {};
- this.monitoring = false;
- this.message_buffers = false;
- this.closing = false;
- this.server_info = {};
- this.auth_pass = options.auth_pass || options.password;
- this.auth_user = options.auth_user || options.user;
- this.selected_db = options.db; // Save the selected db here, used when reconnecting
- this.fire_strings = true; // Determine if strings or buffers should be written to the stream
- this.pipeline = false;
- this.sub_commands_left = 0;
- this.times_connected = 0;
- this.buffers = options.return_buffers || options.detect_buffers;
- this.options = options;
- this.reply = 'ON'; // Returning replies is the default
- this.create_stream();
- // The listeners will not be attached right away, so let's print the deprecation message while the listener is attached
- this.on('newListener', function (event) {
- if ((event === 'message_buffer' || event === 'pmessage_buffer' || event === 'messageBuffer' || event === 'pmessageBuffer') && !this.buffers && !this.message_buffers) {
- this.reply_parser.optionReturnBuffers = true;
- this.message_buffers = true;
- this.handle_reply = handle_detect_buffers_reply;
- }
- });
-}
-util.inherits(RedisClient, EventEmitter);
-
-RedisClient.connection_id = 0;
-
-function create_parser (self) {
- return new Parser({
- returnReply: function (data) {
- self.return_reply(data);
- },
- returnError: function (err) {
- // Return a ReplyError to indicate Redis returned an error
- self.return_error(err);
- },
- returnFatalError: function (err) {
- // Error out all fired commands. Otherwise they might rely on faulty data. We have to reconnect to get in a working state again
- // Note: the execution order is important. First flush and emit, then create the stream
- err.message += '. Please report this.';
- self.ready = false;
- self.flush_and_error({
- message: 'Fatal error encountered. Command aborted.',
- code: 'NR_FATAL'
- }, {
- error: err,
- queues: ['command_queue']
- });
- self.emit('error', err);
- self.create_stream();
- },
- returnBuffers: self.buffers || self.message_buffers,
- stringNumbers: self.options.string_numbers || false
- });
-}
-
-/******************************************************************************
-
- All functions in here are internal besides the RedisClient constructor
- and the exported functions. Don't rely on them as they will be private
- functions in node_redis v.3
-
-******************************************************************************/
-
-// Attention: the function name "create_stream" should not be changed, as other libraries need this to mock the stream (e.g. fakeredis)
-RedisClient.prototype.create_stream = function () {
- var self = this;
-
- // Init parser
- this.reply_parser = create_parser(this);
-
- if (this.options.stream) {
- // Only add the listeners once in case of a reconnect try (that won't work)
- if (this.stream) {
- return;
- }
- this.stream = this.options.stream;
- } else {
- // On a reconnect destroy the former stream and retry
- if (this.stream) {
- this.stream.removeAllListeners();
- this.stream.destroy();
- }
-
- /* istanbul ignore if: travis does not work with stunnel atm. Therefore the tls tests are skipped on travis */
- if (this.options.tls) {
- this.stream = tls.connect(this.connection_options);
- } else {
- this.stream = net.createConnection(this.connection_options);
- }
- }
-
- if (this.options.connect_timeout) {
- this.stream.setTimeout(this.connect_timeout, function () {
- // Note: This is only tested if a internet connection is established
- self.retry_totaltime = self.connect_timeout;
- self.connection_gone('timeout');
- });
- }
-
- /* istanbul ignore next: travis does not work with stunnel atm. Therefore the tls tests are skipped on travis */
- var connect_event = this.options.tls ? 'secureConnect' : 'connect';
- this.stream.once(connect_event, function () {
- this.removeAllListeners('timeout');
- self.times_connected++;
- self.on_connect();
- });
-
- this.stream.on('data', function (buffer_from_socket) {
- // The buffer_from_socket.toString() has a significant impact on big chunks and therefore this should only be used if necessary
- debug('Net read ' + self.address + ' id ' + self.connection_id); // + ': ' + buffer_from_socket.toString());
- self.reply_parser.execute(buffer_from_socket);
- });
-
- this.stream.on('error', function (err) {
- self.on_error(err);
- });
-
- this.stream.once('close', function (hadError) {
- self.connection_gone('close');
- });
-
- this.stream.once('end', function () {
- self.connection_gone('end');
- });
-
- this.stream.on('drain', function () {
- self.drain();
- });
-
- this.stream.setNoDelay();
-
- // Fire the command before redis is connected to be sure it's the first fired command
- if (this.auth_pass !== undefined) {
- this.ready = true;
- // Fail silently as we might not be able to connect
- this.auth(this.auth_pass, this.auth_user, function (err) {
- if (err && err.code !== 'UNCERTAIN_STATE') {
- self.emit('error', err);
- }
- });
- this.ready = false;
- }
-};
-
-RedisClient.prototype.handle_reply = function (reply, command) {
- if (command === 'hgetall') {
- reply = utils.reply_to_object(reply);
- }
- return reply;
-};
-
-RedisClient.prototype.cork = noop;
-RedisClient.prototype.uncork = noop;
-
-RedisClient.prototype.initialize_retry_vars = function () {
- this.retry_timer = null;
- this.retry_totaltime = 0;
- this.retry_delay = 200;
- this.retry_backoff = 1.7;
- this.attempts = 1;
-};
-
-RedisClient.prototype.warn = function (msg) {
- var self = this;
- // Warn on the next tick. Otherwise no event listener can be added
- // for warnings that are emitted in the redis client constructor
- process.nextTick(function () {
- if (self.listeners('warning').length !== 0) {
- self.emit('warning', msg);
- } else {
- console.warn('node_redis:', msg);
- }
- });
-};
-
-// Flush provided queues, erroring any items with a callback first
-RedisClient.prototype.flush_and_error = function (error_attributes, options) {
- options = options || {};
- var aggregated_errors = [];
- var queue_names = options.queues || ['command_queue', 'offline_queue']; // Flush the command_queue first to keep the order intakt
- for (var i = 0; i < queue_names.length; i++) {
- // If the command was fired it might have been processed so far
- if (queue_names[i] === 'command_queue') {
- error_attributes.message += ' It might have been processed.';
- } else { // As the command_queue is flushed first, remove this for the offline queue
- error_attributes.message = error_attributes.message.replace(' It might have been processed.', '');
- }
- // Don't flush everything from the queue
- for (var command_obj = this[queue_names[i]].shift(); command_obj; command_obj = this[queue_names[i]].shift()) {
- var err = new errorClasses.AbortError(error_attributes);
- if (command_obj.error) {
- err.stack = err.stack + command_obj.error.stack.replace(/^Error.*?\n/, '\n');
- }
- err.command = command_obj.command.toUpperCase();
- if (command_obj.args && command_obj.args.length) {
- err.args = command_obj.args;
- }
- if (options.error) {
- err.origin = options.error;
- }
- if (typeof command_obj.callback === 'function') {
- command_obj.callback(err);
- } else {
- aggregated_errors.push(err);
- }
- }
- }
- // Currently this would be a breaking change, therefore it's only emitted in debug_mode
- if (exports.debug_mode && aggregated_errors.length) {
- var error;
- if (aggregated_errors.length === 1) {
- error = aggregated_errors[0];
- } else {
- error_attributes.message = error_attributes.message.replace('It', 'They').replace(/command/i, '$&s');
- error = new errorClasses.AggregateError(error_attributes);
- error.errors = aggregated_errors;
- }
- this.emit('error', error);
- }
-};
-
-RedisClient.prototype.on_error = function (err) {
- if (this.closing) {
- return;
- }
-
- err.message = 'Redis connection to ' + this.address + ' failed - ' + err.message;
- debug(err.message);
- this.connected = false;
- this.ready = false;
-
- // Only emit the error if the retry_strategy option is not set
- if (!this.options.retry_strategy) {
- this.emit('error', err);
- }
- // 'error' events get turned into exceptions if they aren't listened for. If the user handled this error
- // then we should try to reconnect.
- this.connection_gone('error', err);
-};
-
-RedisClient.prototype.on_connect = function () {
- debug('Stream connected ' + this.address + ' id ' + this.connection_id);
-
- this.connected = true;
- this.ready = false;
- this.emitted_end = false;
- this.stream.setKeepAlive(this.options.socket_keepalive, this.options.socket_initial_delay);
- this.stream.setTimeout(0);
-
- this.emit('connect');
- this.initialize_retry_vars();
-
- if (this.options.no_ready_check) {
- this.on_ready();
- } else {
- this.ready_check();
- }
-};
-
-RedisClient.prototype.on_ready = function () {
- var self = this;
-
- debug('on_ready called ' + this.address + ' id ' + this.connection_id);
- this.ready = true;
-
- this.cork = function () {
- self.pipeline = true;
- if (self.stream.cork) {
- self.stream.cork();
- }
- };
- this.uncork = function () {
- if (self.fire_strings) {
- self.write_strings();
- } else {
- self.write_buffers();
- }
- self.pipeline = false;
- self.fire_strings = true;
- if (self.stream.uncork) {
- // TODO: Consider using next tick here. See https://github.com/NodeRedis/node_redis/issues/1033
- self.stream.uncork();
- }
- };
-
- // Restore modal commands from previous connection. The order of the commands is important
- if (this.selected_db !== undefined) {
- this.internal_send_command(new Command('select', [this.selected_db]));
- }
- if (this.monitoring) { // Monitor has to be fired before pub sub commands
- this.internal_send_command(new Command('monitor', []));
- }
- var callback_count = Object.keys(this.subscription_set).length;
- if (!this.options.disable_resubscribing && callback_count) {
- // only emit 'ready' when all subscriptions were made again
- // TODO: Remove the countdown for ready here. This is not coherent with all other modes and should therefore not be handled special
- // We know we are ready as soon as all commands were fired
- var callback = function () {
- callback_count--;
- if (callback_count === 0) {
- self.emit('ready');
- }
- };
- debug('Sending pub/sub on_ready commands');
- for (var key in this.subscription_set) {
- var command = key.slice(0, key.indexOf('_'));
- var args = this.subscription_set[key];
- this[command]([args], callback);
- }
- this.send_offline_queue();
- return;
- }
- this.send_offline_queue();
- this.emit('ready');
-};
-
-RedisClient.prototype.on_info_cmd = function (err, res) {
- if (err) {
- if (err.message === "ERR unknown command 'info'") {
- this.on_ready();
- return;
- }
- err.message = 'Ready check failed: ' + err.message;
- this.emit('error', err);
- return;
- }
-
- /* istanbul ignore if: some servers might not respond with any info data. This is just a safety check that is difficult to test */
- if (!res) {
- debug('The info command returned without any data.');
- this.on_ready();
- return;
- }
-
- if (!this.server_info.loading || this.server_info.loading === '0') {
- // If the master_link_status exists but the link is not up, try again after 50 ms
- if (this.server_info.master_link_status && this.server_info.master_link_status !== 'up') {
- this.server_info.loading_eta_seconds = 0.05;
- } else {
- // Eta loading should change
- debug('Redis server ready.');
- this.on_ready();
- return;
- }
- }
-
- var retry_time = +this.server_info.loading_eta_seconds * 1000;
- if (retry_time > 1000) {
- retry_time = 1000;
- }
- debug('Redis server still loading, trying again in ' + retry_time);
- setTimeout(function (self) {
- self.ready_check();
- }, retry_time, this);
-};
-
-RedisClient.prototype.ready_check = function () {
- var self = this;
- debug('Checking server ready state...');
- // Always fire this info command as first command even if other commands are already queued up
- this.ready = true;
- this.info(function (err, res) {
- self.on_info_cmd(err, res);
- });
- this.ready = false;
-};
-
-RedisClient.prototype.send_offline_queue = function () {
- for (var command_obj = this.offline_queue.shift(); command_obj; command_obj = this.offline_queue.shift()) {
- debug('Sending offline command: ' + command_obj.command);
- this.internal_send_command(command_obj);
- }
- this.drain();
-};
-
-var retry_connection = function (self, error) {
- debug('Retrying connection...');
-
- var reconnect_params = {
- delay: self.retry_delay,
- attempt: self.attempts,
- error: error
- };
- if (self.options.camel_case) {
- reconnect_params.totalRetryTime = self.retry_totaltime;
- reconnect_params.timesConnected = self.times_connected;
- } else {
- reconnect_params.total_retry_time = self.retry_totaltime;
- reconnect_params.times_connected = self.times_connected;
- }
- self.emit('reconnecting', reconnect_params);
-
- self.retry_totaltime += self.retry_delay;
- self.attempts += 1;
- self.retry_delay = Math.round(self.retry_delay * self.retry_backoff);
- self.create_stream();
- self.retry_timer = null;
-};
-
-RedisClient.prototype.connection_gone = function (why, error) {
- // If a retry is already in progress, just let that happen
- if (this.retry_timer) {
- return;
- }
- error = error || null;
-
- debug('Redis connection is gone from ' + why + ' event.');
- this.connected = false;
- this.ready = false;
- // Deactivate cork to work with the offline queue
- this.cork = noop;
- this.uncork = noop;
- this.pipeline = false;
- this.pub_sub_mode = 0;
-
- // since we are collapsing end and close, users don't expect to be called twice
- if (!this.emitted_end) {
- this.emit('end');
- this.emitted_end = true;
- }
-
- // If this is a requested shutdown, then don't retry
- if (this.closing) {
- debug('Connection ended by quit / end command, not retrying.');
- this.flush_and_error({
- message: 'Stream connection ended and command aborted.',
- code: 'NR_CLOSED'
- }, {
- error: error
- });
- return;
- }
-
- if (typeof this.options.retry_strategy === 'function') {
- var retry_params = {
- attempt: this.attempts,
- error: error
- };
- if (this.options.camel_case) {
- retry_params.totalRetryTime = this.retry_totaltime;
- retry_params.timesConnected = this.times_connected;
- } else {
- retry_params.total_retry_time = this.retry_totaltime;
- retry_params.times_connected = this.times_connected;
- }
- this.retry_delay = this.options.retry_strategy(retry_params);
- if (typeof this.retry_delay !== 'number') {
- // Pass individual error through
- if (this.retry_delay instanceof Error) {
- error = this.retry_delay;
- }
-
- var errorMessage = 'Redis connection in broken state: retry aborted.';
-
- this.flush_and_error({
- message: errorMessage,
- code: 'CONNECTION_BROKEN',
- }, {
- error: error
- });
- var retryError = new Error(errorMessage);
- retryError.code = 'CONNECTION_BROKEN';
- if (error) {
- retryError.origin = error;
- }
- this.end(false);
- this.emit('error', retryError);
- return;
- }
- }
-
- if (this.retry_totaltime >= this.connect_timeout) {
- var message = 'Redis connection in broken state: connection timeout exceeded.';
- this.flush_and_error({
- message: message,
- code: 'CONNECTION_BROKEN',
- }, {
- error: error
- });
- var err = new Error(message);
- err.code = 'CONNECTION_BROKEN';
- if (error) {
- err.origin = error;
- }
- this.end(false);
- this.emit('error', err);
- return;
- }
-
- // Retry commands after a reconnect instead of throwing an error. Use this with caution
- if (this.options.retry_unfulfilled_commands) {
- this.offline_queue.unshift.apply(this.offline_queue, this.command_queue.toArray());
- this.command_queue.clear();
- } else if (this.command_queue.length !== 0) {
- this.flush_and_error({
- message: 'Redis connection lost and command aborted.',
- code: 'UNCERTAIN_STATE'
- }, {
- error: error,
- queues: ['command_queue']
- });
- }
-
- if (this.retry_totaltime + this.retry_delay > this.connect_timeout) {
- // Do not exceed the maximum
- this.retry_delay = this.connect_timeout - this.retry_totaltime;
- }
-
- debug('Retry connection in ' + this.retry_delay + ' ms');
- this.retry_timer = setTimeout(retry_connection, this.retry_delay, this, error);
-};
-
-RedisClient.prototype.return_error = function (err) {
- var command_obj = this.command_queue.shift();
- if (command_obj.error) {
- err.stack = command_obj.error.stack.replace(/^Error.*?\n/, 'ReplyError: ' + err.message + '\n');
- }
- err.command = command_obj.command.toUpperCase();
- if (command_obj.args && command_obj.args.length) {
- err.args = command_obj.args;
- }
-
- // Count down pub sub mode if in entering modus
- if (this.pub_sub_mode > 1) {
- this.pub_sub_mode--;
- }
-
- var match = err.message.match(utils.err_code);
- // LUA script could return user errors that don't behave like all other errors!
- if (match) {
- err.code = match[1];
- }
-
- utils.callback_or_emit(this, command_obj.callback, err);
-};
-
-RedisClient.prototype.drain = function () {
- this.should_buffer = false;
-};
-
-function normal_reply (self, reply) {
- var command_obj = self.command_queue.shift();
- if (typeof command_obj.callback === 'function') {
- if (command_obj.command !== 'exec') {
- reply = self.handle_reply(reply, command_obj.command, command_obj.buffer_args);
- }
- command_obj.callback(null, reply);
- } else {
- debug('No callback for reply');
- }
-}
-
-function subscribe_unsubscribe (self, reply, type) {
- // Subscribe commands take an optional callback and also emit an event, but only the _last_ response is included in the callback
- // The pub sub commands return each argument in a separate return value and have to be handled that way
- var command_obj = self.command_queue.get(0);
- var buffer = self.options.return_buffers || self.options.detect_buffers && command_obj.buffer_args;
- var channel = (buffer || reply[1] === null) ? reply[1] : reply[1].toString();
- var count = +reply[2]; // Return the channel counter as number no matter if `string_numbers` is activated or not
- debug(type, channel);
-
- // Emit first, then return the callback
- if (channel !== null) { // Do not emit or "unsubscribe" something if there was no channel to unsubscribe from
- self.emit(type, channel, count);
- if (type === 'subscribe' || type === 'psubscribe') {
- self.subscription_set[type + '_' + channel] = channel;
- } else {
- type = type === 'unsubscribe' ? 'subscribe' : 'psubscribe'; // Make types consistent
- delete self.subscription_set[type + '_' + channel];
- }
- }
-
- if (command_obj.args.length === 1 || self.sub_commands_left === 1 || command_obj.args.length === 0 && (count === 0 || channel === null)) {
- if (count === 0) { // unsubscribed from all channels
- var running_command;
- var i = 1;
- self.pub_sub_mode = 0; // Deactivating pub sub mode
- // This should be a rare case and therefore handling it this way should be good performance wise for the general case
- while (running_command = self.command_queue.get(i)) {
- if (SUBSCRIBE_COMMANDS[running_command.command]) {
- self.pub_sub_mode = i; // Entering pub sub mode again
- break;
- }
- i++;
- }
- }
- self.command_queue.shift();
- if (typeof command_obj.callback === 'function') {
- // TODO: The current return value is pretty useless.
- // Evaluate to change this in v.4 to return all subscribed / unsubscribed channels in an array including the number of channels subscribed too
- command_obj.callback(null, channel);
- }
- self.sub_commands_left = 0;
- } else {
- if (self.sub_commands_left !== 0) {
- self.sub_commands_left--;
- } else {
- self.sub_commands_left = command_obj.args.length ? command_obj.args.length - 1 : count;
- }
- }
-}
-
-function return_pub_sub (self, reply) {
- var type = reply[0].toString();
- if (type === 'message') { // channel, message
- if (!self.options.return_buffers || self.message_buffers) { // backwards compatible. Refactor this in v.4 to always return a string on the normal emitter
- self.emit('message', reply[1].toString(), reply[2].toString());
- self.emit('message_buffer', reply[1], reply[2]);
- self.emit('messageBuffer', reply[1], reply[2]);
- } else {
- self.emit('message', reply[1], reply[2]);
- }
- } else if (type === 'pmessage') { // pattern, channel, message
- if (!self.options.return_buffers || self.message_buffers) { // backwards compatible. Refactor this in v.4 to always return a string on the normal emitter
- self.emit('pmessage', reply[1].toString(), reply[2].toString(), reply[3].toString());
- self.emit('pmessage_buffer', reply[1], reply[2], reply[3]);
- self.emit('pmessageBuffer', reply[1], reply[2], reply[3]);
- } else {
- self.emit('pmessage', reply[1], reply[2], reply[3]);
- }
- } else {
- subscribe_unsubscribe(self, reply, type);
- }
-}
-
-RedisClient.prototype.return_reply = function (reply) {
- if (this.monitoring) {
- var replyStr;
- if (this.buffers && Buffer.isBuffer(reply)) {
- replyStr = reply.toString();
- } else {
- replyStr = reply;
- }
- // If in monitor mode, all normal commands are still working and we only want to emit the streamlined commands
- if (typeof replyStr === 'string' && utils.monitor_regex.test(replyStr)) {
- var timestamp = replyStr.slice(0, replyStr.indexOf(' '));
- var args = replyStr.slice(replyStr.indexOf('"') + 1, -1).split('" "').map(function (elem) {
- return elem.replace(/\\"/g, '"');
- });
- this.emit('monitor', timestamp, args, replyStr);
- return;
- }
- }
- if (this.pub_sub_mode === 0) {
- normal_reply(this, reply);
- } else if (this.pub_sub_mode !== 1) {
- this.pub_sub_mode--;
- normal_reply(this, reply);
- } else if (!(reply instanceof Array) || reply.length <= 2) {
- // Only PING and QUIT are allowed in this context besides the pub sub commands
- // Ping replies with ['pong', null|value] and quit with 'OK'
- normal_reply(this, reply);
- } else {
- return_pub_sub(this, reply);
- }
-};
-
-function handle_offline_command (self, command_obj) {
- var command = command_obj.command;
- var err, msg;
- if (self.closing || !self.enable_offline_queue) {
- command = command.toUpperCase();
- if (!self.closing) {
- if (self.stream.writable) {
- msg = 'The connection is not yet established and the offline queue is deactivated.';
- } else {
- msg = 'Stream not writeable.';
- }
- } else {
- msg = 'The connection is already closed.';
- }
- err = new errorClasses.AbortError({
- message: command + " can't be processed. " + msg,
- code: 'NR_CLOSED',
- command: command
- });
- if (command_obj.args.length) {
- err.args = command_obj.args;
- }
- utils.reply_in_order(self, command_obj.callback, err);
- } else {
- debug('Queueing ' + command + ' for next server connection.');
- self.offline_queue.push(command_obj);
- }
- self.should_buffer = true;
-}
-
-// Do not call internal_send_command directly, if you are not absolutly certain it handles everything properly
-// e.g. monitor / info does not work with internal_send_command only
-RedisClient.prototype.internal_send_command = function (command_obj) {
- var arg, prefix_keys;
- var i = 0;
- var command_str = '';
- var args = command_obj.args;
- var command = command_obj.command;
- var len = args.length;
- var big_data = false;
- var args_copy = new Array(len);
-
- if (process.domain && command_obj.callback) {
- command_obj.callback = process.domain.bind(command_obj.callback);
- }
-
- if (this.ready === false || this.stream.writable === false) {
- // Handle offline commands right away
- handle_offline_command(this, command_obj);
- return false; // Indicate buffering
- }
-
- for (i = 0; i < len; i += 1) {
- if (typeof args[i] === 'string') {
- // 30000 seemed to be a good value to switch to buffers after testing and checking the pros and cons
- if (args[i].length > 30000) {
- big_data = true;
- args_copy[i] = Buffer.from(args[i], 'utf8');
- } else {
- args_copy[i] = args[i];
- }
- } else if (typeof args[i] === 'object') { // Checking for object instead of Buffer.isBuffer helps us finding data types that we can't handle properly
- if (args[i] instanceof Date) { // Accept dates as valid input
- args_copy[i] = args[i].toString();
- } else if (Buffer.isBuffer(args[i])) {
- args_copy[i] = args[i];
- command_obj.buffer_args = true;
- big_data = true;
- } else {
- var invalidArgError = new Error(
- 'node_redis: The ' + command.toUpperCase() + ' command contains a invalid argument type.\n' +
- 'Only strings, dates and buffers are accepted. Please update your code to use valid argument types.'
- );
- invalidArgError.command = command_obj.command.toUpperCase();
- if (command_obj.args && command_obj.args.length) {
- invalidArgError.args = command_obj.args;
- }
- if (command_obj.callback) {
- command_obj.callback(invalidArgError);
- return false;
- }
- throw invalidArgError;
- }
- } else if (typeof args[i] === 'undefined') {
- var undefinedArgError = new Error(
- 'node_redis: The ' + command.toUpperCase() + ' command contains a invalid argument type of "undefined".\n' +
- 'Only strings, dates and buffers are accepted. Please update your code to use valid argument types.'
- );
- undefinedArgError.command = command_obj.command.toUpperCase();
- if (command_obj.args && command_obj.args.length) {
- undefinedArgError.args = command_obj.args;
- }
- // there is always a callback in this scenario
- command_obj.callback(undefinedArgError);
- return false;
- } else {
- // Seems like numbers are converted fast using string concatenation
- args_copy[i] = '' + args[i];
- }
- }
-
- if (this.options.prefix) {
- prefix_keys = commands.getKeyIndexes(command, args_copy);
- for (i = prefix_keys.pop(); i !== undefined; i = prefix_keys.pop()) {
- args_copy[i] = this.options.prefix + args_copy[i];
- }
- }
- if (this.options.rename_commands && this.options.rename_commands[command]) {
- command = this.options.rename_commands[command];
- }
- // Always use 'Multi bulk commands', but if passed any Buffer args, then do multiple writes, one for each arg.
- // This means that using Buffers in commands is going to be slower, so use Strings if you don't already have a Buffer.
- command_str = '*' + (len + 1) + '\r\n$' + command.length + '\r\n' + command + '\r\n';
-
- if (big_data === false) { // Build up a string and send entire command in one write
- for (i = 0; i < len; i += 1) {
- arg = args_copy[i];
- command_str += '$' + Buffer.byteLength(arg) + '\r\n' + arg + '\r\n';
- }
- debug('Send ' + this.address + ' id ' + this.connection_id + ': ' + command_str);
- this.write(command_str);
- } else {
- debug('Send command (' + command_str + ') has Buffer arguments');
- this.fire_strings = false;
- this.write(command_str);
-
- for (i = 0; i < len; i += 1) {
- arg = args_copy[i];
- if (typeof arg === 'string') {
- this.write('$' + Buffer.byteLength(arg) + '\r\n' + arg + '\r\n');
- } else { // buffer
- this.write('$' + arg.length + '\r\n');
- this.write(arg);
- this.write('\r\n');
- }
- debug('send_command: buffer send ' + arg.length + ' bytes');
- }
- }
- if (command_obj.call_on_write) {
- command_obj.call_on_write();
- }
- // Handle `CLIENT REPLY ON|OFF|SKIP`
- // This has to be checked after call_on_write
- /* istanbul ignore else: TODO: Remove this as soon as we test Redis 3.2 on travis */
- if (this.reply === 'ON') {
- this.command_queue.push(command_obj);
- } else {
- // Do not expect a reply
- // Does this work in combination with the pub sub mode?
- if (command_obj.callback) {
- utils.reply_in_order(this, command_obj.callback, null, undefined, this.command_queue);
- }
- if (this.reply === 'SKIP') {
- this.reply = 'SKIP_ONE_MORE';
- } else if (this.reply === 'SKIP_ONE_MORE') {
- this.reply = 'ON';
- }
- }
- return !this.should_buffer;
-};
-
-RedisClient.prototype.write_strings = function () {
- var str = '';
- for (var command = this.pipeline_queue.shift(); command; command = this.pipeline_queue.shift()) {
- // Write to stream if the string is bigger than 4mb. The biggest string may be Math.pow(2, 28) - 15 chars long
- if (str.length + command.length > 4 * 1024 * 1024) {
- this.should_buffer = !this.stream.write(str);
- str = '';
- }
- str += command;
- }
- if (str !== '') {
- this.should_buffer = !this.stream.write(str);
- }
-};
-
-RedisClient.prototype.write_buffers = function () {
- for (var command = this.pipeline_queue.shift(); command; command = this.pipeline_queue.shift()) {
- this.should_buffer = !this.stream.write(command);
- }
-};
-
-RedisClient.prototype.write = function (data) {
- if (this.pipeline === false) {
- this.should_buffer = !this.stream.write(data);
- return;
- }
- this.pipeline_queue.push(data);
-};
-
-Object.defineProperty(exports, 'debugMode', {
- get: function () {
- return this.debug_mode;
- },
- set: function (val) {
- this.debug_mode = val;
- }
-});
-
-// Don't officially expose the command_queue directly but only the length as read only variable
-Object.defineProperty(RedisClient.prototype, 'command_queue_length', {
- get: function () {
- return this.command_queue.length;
- }
-});
-
-Object.defineProperty(RedisClient.prototype, 'offline_queue_length', {
- get: function () {
- return this.offline_queue.length;
- }
-});
-
-// Add support for camelCase by adding read only properties to the client
-// All known exposed snake_case variables are added here
-Object.defineProperty(RedisClient.prototype, 'retryDelay', {
- get: function () {
- return this.retry_delay;
- }
-});
-
-Object.defineProperty(RedisClient.prototype, 'retryBackoff', {
- get: function () {
- return this.retry_backoff;
- }
-});
-
-Object.defineProperty(RedisClient.prototype, 'commandQueueLength', {
- get: function () {
- return this.command_queue.length;
- }
-});
-
-Object.defineProperty(RedisClient.prototype, 'offlineQueueLength', {
- get: function () {
- return this.offline_queue.length;
- }
-});
-
-Object.defineProperty(RedisClient.prototype, 'shouldBuffer', {
- get: function () {
- return this.should_buffer;
- }
-});
-
-Object.defineProperty(RedisClient.prototype, 'connectionId', {
- get: function () {
- return this.connection_id;
- }
-});
-
-Object.defineProperty(RedisClient.prototype, 'serverInfo', {
- get: function () {
- return this.server_info;
- }
-});
-
-exports.createClient = function () {
- return new RedisClient(unifyOptions.apply(null, arguments));
-};
-exports.RedisClient = RedisClient;
-exports.print = utils.print;
-exports.Multi = require('./lib/multi');
-exports.AbortError = errorClasses.AbortError;
-exports.RedisError = RedisErrors.RedisError;
-exports.ParserError = RedisErrors.ParserError;
-exports.ReplyError = RedisErrors.ReplyError;
-exports.AggregateError = errorClasses.AggregateError;
-
-// Add all redis commands / node_redis api to the client
-require('./lib/individualCommands');
-require('./lib/extendedApi');
-
-//enables adding new commands (for modules and new commands)
-exports.addCommand = exports.add_command = require('./lib/commands');
diff --git a/index.ts b/index.ts
new file mode 100644
index 00000000000..408cbe3b996
--- /dev/null
+++ b/index.ts
@@ -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';
diff --git a/lib/client.spec.ts b/lib/client.spec.ts
new file mode 100644
index 00000000000..f73049d2286
--- /dev/null
+++ b/lib/client.spec.ts
@@ -0,0 +1,562 @@
+import { strict as assert, AssertionError } from 'assert';
+import { once } from 'events';
+import { itWithClient, TEST_REDIS_SERVERS, TestRedisServers, waitTillBeenCalled, isRedisVersionGreaterThan } from './test-utils';
+import RedisClient from './client';
+import { AbortError, ClientClosedError, ConnectionTimeoutError, WatchError } from './errors';
+import { defineScript } from './lua-script';
+import { spy } from 'sinon';
+
+export const SQUARE_SCRIPT = defineScript({
+ NUMBER_OF_KEYS: 0,
+ SCRIPT: 'return ARGV[1] * ARGV[1];',
+ transformArguments(number: number): Array {
+ return [number.toString()];
+ },
+ transformReply(reply: number): number {
+ return reply;
+ }
+});
+
+describe('Client', () => {
+ describe('authentication', () => {
+ itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => {
+ assert.equal(
+ await client.ping(),
+ 'PONG'
+ );
+ });
+
+ it('should not retry connecting if failed due to wrong auth', async () => {
+ const client = RedisClient.create({
+ socket: {
+ ...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD],
+ password: 'wrongpassword'
+ }
+ });
+
+ await assert.rejects(
+ client.connect(),
+ {
+ message: isRedisVersionGreaterThan([6]) ?
+ 'WRONGPASS invalid username-password pair or user is disabled.' :
+ 'ERR invalid password'
+ }
+ );
+
+ assert.equal(client.isOpen, false);
+ });
+ });
+
+ describe('legacyMode', () => {
+ const client = RedisClient.create({
+ socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN],
+ scripts: {
+ square: SQUARE_SCRIPT
+ },
+ legacyMode: true
+ });
+
+ before(() => client.connect());
+ afterEach(() => client.v4.flushAll());
+ after(() => client.disconnect());
+
+ it('client.sendCommand should call the callback', done => {
+ (client as any).sendCommand('PING', (err?: Error, reply?: string) => {
+ if (err) {
+ return done(err);
+ }
+
+ try {
+ assert.equal(reply, 'PONG');
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+ });
+
+ it('client.sendCommand should work without callback', async () => {
+ (client as any).sendCommand('PING');
+ await client.v4.ping(); // make sure the first command was replied
+ });
+
+ it('client.v4.sendCommand should return a promise', async () => {
+ assert.equal(
+ await client.v4.sendCommand(['PING']),
+ 'PONG'
+ );
+ });
+
+ it('client.{command} should accept vardict arguments', done => {
+ (client as any).set('a', 'b', (err?: Error, reply?: string) => {
+ if (err) {
+ return done(err);
+ }
+
+ try {
+ assert.equal(reply, 'OK');
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+ });
+
+ it('client.{command} should accept arguments array', done => {
+ (client as any).set(['a', 'b'], (err?: Error, reply?: string) => {
+ if (err) {
+ return done(err);
+ }
+
+ try {
+ assert.equal(reply, 'OK');
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+ });
+
+ it('client.{command} should accept mix of strings and array of strings', done => {
+ (client as any).set(['a'], 'b', ['XX'], (err?: Error, reply?: string) => {
+ if (err) {
+ return done(err);
+ }
+
+ try {
+ assert.equal(reply, null);
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+ });
+
+ it('client.multi.ping.exec should call the callback', done => {
+ (client as any).multi()
+ .ping()
+ .exec((err?: Error, reply?: string) => {
+ if (err) {
+ return done(err);
+ }
+
+ try {
+ assert.deepEqual(reply, ['PONG']);
+ done();
+ } catch (err) {
+ done(err);
+ }
+ });
+ });
+
+ it('client.multi.ping.exec should work without callback', async () => {
+ (client as any).multi()
+ .ping()
+ .exec();
+ await client.v4.ping(); // make sure the first command was replied
+ });
+
+ it('client.multi.ping.v4.ping.v4.exec should return a promise', async () => {
+ assert.deepEqual(
+ await ((client as any).multi()
+ .ping()
+ .v4.ping()
+ .v4.exec()),
+ ['PONG', 'PONG']
+ );
+ });
+
+ it('client.{script} should return a promise', async () => {
+ assert.equal(await client.square(2), 4);
+ });
+ });
+
+ describe('events', () => {
+ it('connect, ready, end', async () => {
+ const client = RedisClient.create({
+ socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
+ });
+
+ await Promise.all([
+ client.connect(),
+ once(client, 'connect'),
+ once(client, 'ready')
+ ]);
+
+ await Promise.all([
+ client.disconnect(),
+ once(client, 'end')
+ ]);
+ });
+ });
+
+ describe('sendCommand', () => {
+ itWithClient(TestRedisServers.OPEN, 'PING', async client => {
+ assert.equal(await client.sendCommand(['PING']), 'PONG');
+ });
+
+ describe('AbortController', () => {
+ before(function () {
+ if (!global.AbortController) {
+ this.skip();
+ }
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'success', async client => {
+ await client.sendCommand(['PING'], {
+ signal: new AbortController().signal
+ });
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'AbortError', client => {
+ const controller = new AbortController();
+ controller.abort();
+
+ return assert.rejects(
+ client.sendCommand(['PING'], {
+ signal: controller.signal
+ }),
+ AbortError
+ );
+ });
+ });
+ });
+
+ describe('multi', () => {
+ itWithClient(TestRedisServers.OPEN, 'simple', async client => {
+ assert.deepEqual(
+ await client.multi()
+ .ping()
+ .set('key', 'value')
+ .get('key')
+ .exec(),
+ ['PONG', 'OK', 'value']
+ );
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'should reject the whole chain on error', client => {
+ client.on('error', () => {
+ // ignore errors
+ });
+
+ return assert.rejects(
+ client.multi()
+ .ping()
+ .addCommand(['DEBUG', 'RESTART'])
+ .ping()
+ .exec()
+ );
+ });
+
+ it('with script', async () => {
+ const client = RedisClient.create({
+ scripts: {
+ square: SQUARE_SCRIPT
+ }
+ });
+
+ await client.connect();
+
+ try {
+ assert.deepEqual(
+ await client.multi()
+ .square(2)
+ .exec(),
+ [4]
+ );
+ } finally {
+ await client.disconnect();
+ }
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'WatchError', async client => {
+ await client.watch('key');
+
+ await client.set(
+ RedisClient.commandOptions({
+ isolated: true
+ }),
+ 'key',
+ '1'
+ );
+
+ await assert.rejects(
+ client.multi()
+ .decr('key')
+ .exec(),
+ WatchError
+ );
+ });
+ });
+
+ it('scripts', async () => {
+ const client = RedisClient.create({
+ scripts: {
+ square: SQUARE_SCRIPT
+ }
+ });
+
+ await client.connect();
+
+ try {
+ assert.equal(
+ await client.square(2),
+ 4
+ );
+ } finally {
+ await client.disconnect();
+ }
+ });
+
+ it('modules', async () => {
+ const client = RedisClient.create({
+ modules: {
+ module: {
+ echo: {
+ transformArguments(message: string): Array {
+ return ['ECHO', message];
+ },
+ transformReply(reply: string): string {
+ return reply;
+ }
+ }
+ }
+ }
+ });
+
+ await client.connect();
+
+ try {
+ assert.equal(
+ await client.module.echo('message'),
+ 'message'
+ );
+ } finally {
+ await client.disconnect();
+ }
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'executeIsolated', async client => {
+ await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
+
+ assert.equal(
+ await client.executeIsolated(isolatedClient =>
+ isolatedClient.sendCommand(['CLIENT', 'GETNAME'])
+ ),
+ null
+ );
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'should reconnect after DEBUG RESTART', async client => {
+ client.on('error', () => {
+ // ignore errors
+ });
+
+ await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
+ await assert.rejects(client.sendCommand(['DEBUG', 'RESTART']));
+ assert.ok(await client.sendCommand(['CLIENT', 'GETNAME']) === null);
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'should SELECT db after reconnection', async client => {
+ client.on('error', () => {
+ // ignore errors
+ });
+
+ await client.select(1);
+ await assert.rejects(client.sendCommand(['DEBUG', 'RESTART']));
+ assert.equal(
+ (await client.clientInfo()).db,
+ 1
+ );
+ }, {
+ // because of CLIENT INFO
+ minimumRedisVersion: [6, 2]
+ });
+
+ itWithClient(TestRedisServers.OPEN, '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);
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'hScanIterator', async client => {
+ const hash: Record = {};
+ for (let i = 0; i < 100; i++) {
+ hash[i.toString()] = i.toString();
+ }
+
+ await client.hSet('key', hash);
+
+ const results: Record = {};
+ for await (const { field, value } of client.hScanIterator('key')) {
+ results[field] = value;
+ }
+
+ assert.deepEqual(hash, results);
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'sScanIterator', async client => {
+ const members = new Set();
+ for (let i = 0; i < 100; i++) {
+ members.add(i.toString());
+ }
+
+ await client.sAdd('key', Array.from(members));
+
+ const results = new Set();
+ for await (const key of client.sScanIterator('key')) {
+ results.add(key);
+ }
+
+ assert.deepEqual(members, results);
+ });
+
+ itWithClient(TestRedisServers.OPEN, '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(member => [member.value, member.score]).sort(sort)
+ );
+ });
+
+ itWithClient(TestRedisServers.OPEN, '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);
+ } finally {
+ await subscriber.disconnect();
+ }
+ });
+
+ it('ConnectionTimeoutError', async () => {
+ const client = RedisClient.create({
+ socket: {
+ ...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
+ connectTimeout: 1
+ }
+ });
+
+ try {
+ const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
+ start = process.hrtime.bigint();
+
+ // block the event loop for 1ms, to make sure the connection will timeout
+ while (process.hrtime.bigint() - start < 1_000_000) {}
+
+ await promise;
+ } catch (err) {
+ if (err instanceof AssertionError) {
+ await client.disconnect();
+ }
+
+ throw err;
+ }
+ });
+
+ it('client.quit', async () => {
+ const client = RedisClient.create({
+ socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
+ });
+
+ await client.connect();
+
+ try {
+ const quitPromise = client.quit();
+ assert.equal(client.isOpen, false);
+ await Promise.all([
+ quitPromise,
+ assert.rejects(client.ping(), ClientClosedError)
+ ]);
+ } finally {
+ if (client.isOpen) {
+ await client.disconnect();
+ }
+ }
+ });
+});
diff --git a/lib/client.ts b/lib/client.ts
new file mode 100644
index 00000000000..a8da7f5ddd5
--- /dev/null
+++ b/lib/client.ts
@@ -0,0 +1,468 @@
+import RedisSocket, { RedisSocketOptions } from './socket';
+import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
+import COMMANDS from './commands';
+import { RedisCommand, RedisModules, RedisReply } from './commands';
+import RedisMultiCommand, { MultiQueuedCommand, RedisMultiCommandType } from './multi-command';
+import EventEmitter from 'events';
+import { CommandOptions, commandOptions, isCommandOptions } from './command-options';
+import { RedisLuaScript, RedisLuaScripts } from './lua-script';
+import { ScanOptions, ZMember } from './commands/generic-transformers';
+import { ScanCommandOptions } from './commands/SCAN';
+import { HScanTuple } from './commands/HSCAN';
+import { encodeCommand, extendWithDefaultCommands, extendWithModulesAndScripts, transformCommandArguments } from './commander';
+import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
+import { ClientClosedError } from './errors';
+
+export interface RedisClientOptions {
+ socket?: RedisSocketOptions;
+ modules?: M;
+ scripts?: S;
+ commandsQueueMaxLength?: number;
+ readonly?: boolean;
+ legacyMode?: boolean;
+ isolationPoolOptions?: PoolOptions;
+}
+
+export type RedisCommandSignature =
+ (...args: Parameters | [options: CommandOptions, ...rest: Parameters]) => Promise>;
+
+type WithCommands = {
+ [P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>;
+};
+
+type WithModules = {
+ [P in keyof M]: {
+ [C in keyof M[P]]: RedisCommandSignature;
+ };
+};
+
+type WithScripts = {
+ [P in keyof S]: RedisCommandSignature;
+};
+
+export type WithPlugins =
+ WithCommands & WithModules & WithScripts;
+
+export type RedisClientType =
+ WithPlugins & RedisClient;
+
+export interface ClientCommandOptions extends QueueCommandOptions {
+ isolated?: boolean;
+}
+
+export default class RedisClient extends EventEmitter {
+ static commandOptions(options: ClientCommandOptions): CommandOptions {
+ return commandOptions(options);
+ }
+
+ static async commandsExecutor(
+ this: RedisClient,
+ command: RedisCommand,
+ args: Array
+ ): Promise> {
+ const { args: redisArgs, options } = transformCommandArguments(command, args);
+
+ const reply = command.transformReply(
+ await this.#sendCommand(redisArgs, options),
+ redisArgs.preserve
+ );
+
+ return reply;
+ }
+
+ static async #scriptsExecutor(
+ this: RedisClient,
+ script: RedisLuaScript,
+ args: Array
+ ): Promise {
+ const { args: redisArgs, options } = transformCommandArguments(script, args);
+
+ const reply = script.transformReply(
+ await this.executeScript(script, redisArgs, options),
+ redisArgs.preserve
+ );
+
+ return reply;
+ }
+
+ static create(options?: RedisClientOptions): RedisClientType {
+ const Client = (extendWithModulesAndScripts({
+ BaseClass: RedisClient,
+ modules: options?.modules,
+ modulesCommandsExecutor: RedisClient.commandsExecutor,
+ scripts: options?.scripts,
+ scriptsExecutor: RedisClient.#scriptsExecutor
+ }));
+
+ if (Client !== RedisClient) {
+ Client.prototype.Multi = RedisMultiCommand.extend(options);
+ }
+
+ return new Client(options);
+ }
+
+ readonly #options?: RedisClientOptions;
+ readonly #socket: RedisSocket;
+ readonly #queue: RedisCommandsQueue;
+ readonly #isolationPool: Pool>;
+ readonly #v4: Record = {};
+ #selectedDB = 0;
+
+ get options(): RedisClientOptions | null | undefined {
+ return this.#options;
+ }
+
+ get isOpen(): boolean {
+ return this.#socket.isOpen;
+ }
+
+ get v4(): Record {
+ if (!this.#options?.legacyMode) {
+ throw new Error('the client is not in "legacy mode"');
+ }
+
+ return this.#v4;
+ }
+
+ constructor(options?: RedisClientOptions) {
+ super();
+ this.#options = options;
+ this.#socket = this.#initiateSocket();
+ this.#queue = this.#initiateQueue();
+ this.#isolationPool = createPool({
+ create: async () => {
+ const duplicate = this.duplicate();
+ await duplicate.connect();
+ return duplicate;
+ },
+ destroy: client => client.disconnect()
+ }, options?.isolationPoolOptions);
+ this.#legacyMode();
+ }
+
+ #initiateSocket(): RedisSocket {
+ const socketInitiator = async (): Promise => {
+ const v4Commands = this.#options?.legacyMode ? this.#v4 : this,
+ promises = [];
+
+ if (this.#selectedDB !== 0) {
+ promises.push(v4Commands.select(RedisClient.commandOptions({ asap: true }), this.#selectedDB));
+ }
+
+ if (this.#options?.readonly) {
+ promises.push(v4Commands.readonly(RedisClient.commandOptions({ asap: true })));
+ }
+
+ if (this.#options?.socket?.username || this.#options?.socket?.password) {
+ promises.push(v4Commands.auth(RedisClient.commandOptions({ asap: true }), this.#options.socket));
+ }
+
+ const resubscribePromise = this.#queue.resubscribe();
+ if (resubscribePromise) {
+ promises.push(resubscribePromise);
+ this.#tick();
+ }
+
+ 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('end', () => this.emit('end'));
+ }
+
+ #initiateQueue(): RedisCommandsQueue {
+ return new RedisCommandsQueue(
+ this.#options?.commandsQueueMaxLength,
+ (encodedCommands: string) => this.#socket.write(encodedCommands)
+ );
+ }
+
+ #legacyMode(): void {
+ if (!this.#options?.legacyMode) return;
+
+ (this as any).#v4.sendCommand = this.#sendCommand.bind(this);
+ (this as any).sendCommand = (...args: Array): void => {
+ const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] as Function : undefined,
+ actualArgs = !callback ? args : args.slice(0, -1);
+ this.#sendCommand(actualArgs.flat() as Array)
+ .then((reply: unknown) => {
+ 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): void => {
+ (this as any).sendCommand(name, ...args);
+ };
+ }
+
+ duplicate(): RedisClientType {
+ return new (Object.getPrototypeOf(this).constructor)(this.#options);
+ }
+
+ async connect(): Promise {
+ await this.#socket.connect();
+ }
+
+ async SELECT(db: number): Promise;
+ async SELECT(options: CommandOptions, db: number): Promise;
+ async SELECT(options?: any, db?: any): Promise {
+ if (!isCommandOptions(options)) {
+ db = options;
+ options = null;
+ }
+
+ await this.#sendCommand(['SELECT', db.toString()], options);
+ this.#selectedDB = db;
+ }
+
+ select = this.SELECT;
+
+ SUBSCRIBE(channels: string | Array, listener: PubSubListener): Promise {
+ return this.#subscribe(PubSubSubscribeCommands.SUBSCRIBE, channels, listener);
+ }
+
+ subscribe = this.SUBSCRIBE;
+
+ PSUBSCRIBE(patterns: string | Array, listener: PubSubListener): Promise {
+ return this.#subscribe(PubSubSubscribeCommands.PSUBSCRIBE, patterns, listener);
+ }
+
+ pSubscribe = this.PSUBSCRIBE;
+
+ #subscribe(command: PubSubSubscribeCommands, channels: string | Array, listener: PubSubListener): Promise {
+ const promise = this.#queue.subscribe(command, channels, listener);
+ this.#tick();
+ return promise;
+ }
+
+ UNSUBSCRIBE(channels?: string | Array, listener?: PubSubListener): Promise {
+ return this.#unsubscribe(PubSubUnsubscribeCommands.UNSUBSCRIBE, channels, listener);
+ }
+
+ unsubscribe = this.UNSUBSCRIBE;
+
+ PUNSUBSCRIBE(patterns?: string | Array, listener?: PubSubListener): Promise {
+ return this.#unsubscribe(PubSubUnsubscribeCommands.PUNSUBSCRIBE, patterns, listener);
+ }
+
+ pUnsubscribe = this.PUNSUBSCRIBE;
+
+ #unsubscribe(command: PubSubUnsubscribeCommands, channels?: string | Array, listener?: PubSubListener): Promise {
+ const promise = this.#queue.unsubscribe(command, channels, listener);
+ this.#tick();
+ return promise;
+ }
+
+ QUIT(): Promise {
+ return this.#socket.quit(async () => {
+ this.#queue.addEncodedCommand(encodeCommand(['QUIT']));
+ this.#tick();
+ });
+ }
+
+ quit = this.QUIT;
+
+ sendCommand(args: Array, options?: ClientCommandOptions): Promise {
+ return this.#sendCommand(args, options);
+ }
+
+ // using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
+ #sendCommand(args: Array, options?: ClientCommandOptions): Promise {
+ return this.sendEncodedCommand(encodeCommand(args), options);
+ }
+
+ async sendEncodedCommand(encodedCommand: string, options?: ClientCommandOptions): Promise {
+ if (!this.#socket.isOpen) {
+ throw new ClientClosedError();
+ }
+
+ if (options?.isolated) {
+ return this.executeIsolated(isolatedClient =>
+ isolatedClient.sendEncodedCommand(encodedCommand, {
+ ...options,
+ isolated: false
+ })
+ );
+ }
+
+ const promise = this.#queue.addEncodedCommand(encodedCommand, options);
+ this.#tick();
+ return await promise;
+ }
+
+ executeIsolated(fn: (client: RedisClientType) => T | Promise): Promise {
+ return this.#isolationPool.use(fn);
+ }
+
+ async executeScript(script: RedisLuaScript, args: Array, options?: ClientCommandOptions): Promise> {
+ try {
+ return await this.#sendCommand([
+ 'EVALSHA',
+ script.SHA1,
+ script.NUMBER_OF_KEYS.toString(),
+ ...args
+ ], options);
+ } catch (err: any) {
+ if (!err?.message?.startsWith?.('NOSCRIPT')) {
+ throw err;
+ }
+
+ return await this.#sendCommand([
+ 'EVAL',
+ script.SCRIPT,
+ script.NUMBER_OF_KEYS.toString(),
+ ...args
+ ], options);
+ }
+ }
+
+ #multiExecutor(commands: Array, chainId?: symbol): Promise> {
+ const promise = Promise.all(
+ commands.map(({encodedCommand}) => {
+ return this.#queue.addEncodedCommand(encodedCommand, RedisClient.commandOptions({
+ chainId
+ }));
+ })
+ );
+
+ this.#tick();
+
+ return promise;
+ }
+
+ multi(): RedisMultiCommandType {
+ return new (this as any).Multi(
+ this.#multiExecutor.bind(this),
+ this.#options
+ );
+ }
+
+ async* scanIterator(options?: ScanCommandOptions): AsyncIterable {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ this.#queue.flushAll(new Error('Disconnecting'));
+ await Promise.all([
+ this.#socket.disconnect(),
+ this.#destroyIsolationPool()
+ ]);
+ }
+
+ async #destroyIsolationPool(): Promise {
+ await this.#isolationPool.drain();
+ await this.#isolationPool.clear();
+ }
+
+ #isTickQueued = false;
+
+ #tick(): void {
+ const {chunkRecommendedSize} = this.#socket;
+ if (!chunkRecommendedSize) {
+ return;
+ }
+
+ if (!this.#isTickQueued && this.#queue.waitingToBeSentCommandsLength < chunkRecommendedSize) {
+ queueMicrotask(() => this.#tick());
+ this.#isTickQueued = true;
+ return;
+ }
+
+ const isBuffering = this.#queue.executeChunk(chunkRecommendedSize);
+ if (isBuffering === true) {
+ this.#socket.once('drain', () => this.#tick());
+ } else if (isBuffering === false) {
+ this.#tick();
+ return;
+ }
+
+ this.#isTickQueued = false;
+ }
+}
+
+extendWithDefaultCommands(RedisClient, RedisClient.commandsExecutor);
+(RedisClient.prototype as any).Multi = RedisMultiCommand.extend();
diff --git a/lib/cluster-slots.ts b/lib/cluster-slots.ts
new file mode 100644
index 00000000000..3e255fc2a66
--- /dev/null
+++ b/lib/cluster-slots.ts
@@ -0,0 +1,221 @@
+import calculateSlot from 'cluster-key-slot';
+import RedisClient, { RedisClientType } from './client';
+import { RedisSocketOptions } from './socket';
+import { RedisClusterMasterNode, RedisClusterReplicaNode } from './commands/CLUSTER_NODES';
+import { RedisClusterOptions } from './cluster';
+import { RedisModules } from './commands';
+import { RedisLuaScripts } from './lua-script';
+
+export interface ClusterNode {
+ id: string;
+ client: RedisClientType;
+}
+
+interface SlotNodes {
+ master: ClusterNode;
+ replicas: Array>;
+ clientIterator: IterableIterator> | undefined;
+}
+
+export default class RedisClusterSlots {
+ readonly #options: RedisClusterOptions;
+ readonly #nodeByUrl = new Map>();
+ readonly #slots: Array> = [];
+
+ constructor(options: RedisClusterOptions) {
+ this.#options = options;
+ }
+
+ async connect(): Promise {
+ for (const rootNode of this.#options.rootNodes) {
+ try {
+ await this.#discoverNodes(rootNode);
+ return;
+ } catch (err) {
+ console.error(err);
+ // this.emit('error', err);
+ }
+ }
+
+ throw new Error('None of the root nodes is available');
+ }
+
+ async discover(startWith: RedisClientType): Promise {
+ try {
+ await this.#discoverNodes(startWith.options?.socket);
+ return;
+ } catch (err) {
+ console.error(err);
+ // this.emit('error', err);
+ }
+
+ for (const { client } of this.#nodeByUrl.values()) {
+ if (client === startWith) continue;
+
+ try {
+ await this.#discoverNodes(client.options?.socket);
+ return;
+ } catch (err) {
+ console.error(err);
+ // this.emit('error', err);
+ }
+ }
+
+ throw new Error('None of the cluster nodes is available');
+ }
+
+ async #discoverNodes(socketOptions?: RedisSocketOptions): Promise {
+ const client = RedisClient.create({
+ socket: socketOptions
+ });
+
+ await client.connect();
+
+ try {
+ await this.#reset(await client.clusterNodes());
+ } finally {
+ await client.disconnect(); // TODO: catch error from disconnect?
+ }
+ }
+
+ async #reset(masters: Array): Promise {
+ // Override this.#slots and add not existing clients to this.#clientByKey
+ const promises: Array> = [],
+ clientsInUse = new Set();
+ 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.#clientBykey using clientsInUse
+ for (const [url, { client }] of this.#nodeByUrl.entries()) {
+ if (clientsInUse.has(url)) continue;
+
+ // TODO: ignore error from `.disconnect`?
+ promises.push(client.disconnect());
+ this.#nodeByUrl.delete(url);
+ }
+
+ await Promise.all(promises);
+ }
+
+ #initiateClientForNode(nodeData: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set, promises: Array>): ClusterNode {
+ const url = `${nodeData.host}:${nodeData.port}`;
+ clientsInUse.add(url);
+
+ let node = this.#nodeByUrl.get(url);
+ if (!node) {
+ node = {
+ id: nodeData.id,
+ client: RedisClient.create({
+ socket: {
+ host: nodeData.host,
+ port: nodeData.port
+ },
+ readonly
+ })
+ };
+ promises.push(node.client.connect());
+ this.#nodeByUrl.set(url, node);
+ }
+
+ return node;
+ }
+
+ getSlotMaster(slot: number): ClusterNode {
+ return this.#slots[slot].master;
+ }
+
+ *#slotClientIterator(slotNumber: number): IterableIterator> {
+ const slot = this.#slots[slotNumber];
+ yield slot.master.client;
+
+ for (const replica of slot.replicas) {
+ yield replica.client;
+ }
+ }
+
+ #getSlotClient(slotNumber: number): RedisClientType {
+ 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>;
+
+ #getRandomClient(): RedisClientType {
+ 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, isReadonly?: boolean): RedisClientType {
+ 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> {
+ const masters = [];
+
+ for (const node of this.#nodeByUrl.values()) {
+ if (node.client.options?.readonly) continue;
+
+ masters.push(node);
+ }
+
+ return masters;
+ }
+
+ getNodeByUrl(url: string): ClusterNode | undefined {
+ return this.#nodeByUrl.get(url);
+ }
+
+ async disconnect(): Promise {
+ await Promise.all(
+ [...this.#nodeByUrl.values()].map(({ client }) => client.disconnect())
+ );
+
+ this.#nodeByUrl.clear();
+ this.#slots.splice(0);
+ }
+}
diff --git a/lib/cluster.spec.ts b/lib/cluster.spec.ts
new file mode 100644
index 00000000000..b7dbe50c908
--- /dev/null
+++ b/lib/cluster.spec.ts
@@ -0,0 +1,115 @@
+import { strict as assert } from 'assert';
+import RedisCluster from './cluster';
+import { defineScript } from './lua-script';
+import { itWithCluster, itWithDedicatedCluster, TestRedisClusters, TEST_REDIS_CLUSTERES } from './test-utils';
+import calculateSlot from 'cluster-key-slot';
+import { ClusterSlotStates } from './commands/CLUSTER_SETSLOT';
+
+describe('Cluster', () => {
+ it('sendCommand', async () => {
+ const cluster = RedisCluster.create({
+ rootNodes: TEST_REDIS_CLUSTERES[TestRedisClusters.OPEN],
+ useReplicas: true
+ });
+
+ await cluster.connect();
+
+ try {
+ await cluster.ping();
+ await cluster.set('a', 'b');
+ await cluster.set('a{a}', 'bb');
+ await cluster.set('aa', 'bb');
+ await cluster.get('aa');
+ await cluster.get('aa');
+ await cluster.get('aa');
+ await cluster.get('aa');
+ } finally {
+ await cluster.disconnect();
+ }
+ });
+
+ itWithCluster(TestRedisClusters.OPEN, 'multi', async cluster => {
+ const key = 'key';
+ assert.deepEqual(
+ await cluster.multi(key)
+ .ping()
+ .set(key, 'value')
+ .get(key)
+ .exec(),
+ ['PONG', 'OK', 'value']
+ );
+ });
+
+ it('scripts', async () => {
+ const cluster = RedisCluster.create({
+ rootNodes: TEST_REDIS_CLUSTERES[TestRedisClusters.OPEN],
+ scripts: {
+ add: defineScript({
+ NUMBER_OF_KEYS: 0,
+ SCRIPT: 'return ARGV[1] + 1;',
+ transformArguments(number: number): Array {
+ assert.equal(number, 1);
+ return [number.toString()];
+ },
+ transformReply(reply: number): number {
+ assert.equal(reply, 2);
+ return reply;
+ }
+ })
+ }
+ });
+
+ await cluster.connect();
+
+ try {
+ assert.equal(
+ await cluster.add(1),
+ 2
+ );
+ } finally {
+ await cluster.disconnect();
+ }
+ });
+
+ itWithDedicatedCluster('should handle live resharding', async cluster => {
+ const key = 'key',
+ value = 'value';
+ await cluster.set(key, value);
+
+ const slot = calculateSlot(key),
+ from = cluster.getSlotMaster(slot),
+ to = cluster.getMasters().find(node => node.id !== from.id);
+
+ await to!.client.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, from.id);
+
+ // should be able to get the key from the original node before it was migrated
+ assert.equal(
+ await cluster.get(key),
+ value
+ );
+
+ await from.client.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, to!.id);
+
+ // should be able to get the key from the original node using the "ASKING" command
+ assert.equal(
+ await cluster.get(key),
+ value
+ );
+
+ const { port: toPort } = 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
+ );
+ });
+});
diff --git a/lib/cluster.ts b/lib/cluster.ts
new file mode 100644
index 00000000000..6da6dc55f46
--- /dev/null
+++ b/lib/cluster.ts
@@ -0,0 +1,202 @@
+import { RedisCommand, RedisModules } from './commands';
+import RedisClient, { ClientCommandOptions, RedisClientType, WithPlugins } from './client';
+import { RedisSocketOptions } from './socket';
+import RedisClusterSlots, { ClusterNode } from './cluster-slots';
+import { RedisLuaScript, RedisLuaScripts } from './lua-script';
+import { extendWithModulesAndScripts, extendWithDefaultCommands, transformCommandArguments } from './commander';
+import RedisMultiCommand, { MultiQueuedCommand, RedisMultiCommandType } from './multi-command';
+
+export interface RedisClusterOptions {
+ rootNodes: Array;
+ modules?: M;
+ scripts?: S;
+ useReplicas?: boolean;
+ maxCommandRedirections?: number;
+}
+
+export type RedisClusterType =
+ WithPlugins & RedisCluster;
+
+export default class RedisCluster {
+ static #extractFirstKey(command: RedisCommand, originalArgs: Array, redisArgs: Array): string | 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 async commandsExecutor(
+ this: RedisCluster,
+ command: RedisCommand,
+ args: Array
+ ): Promise> {
+ const { args: redisArgs, options } = transformCommandArguments(command, args);
+
+ const reply = command.transformReply(
+ await this.sendCommand(
+ RedisCluster.#extractFirstKey(command, args, redisArgs),
+ command.IS_READ_ONLY,
+ redisArgs,
+ options
+ ),
+ redisArgs.preserve
+ );
+
+ return reply;
+ }
+
+ static async #scriptsExecutor(
+ this: RedisCluster,
+ script: RedisLuaScript,
+ args: Array
+ ): Promise {
+ const { args: redisArgs, options } = transformCommandArguments(script, args);
+
+ const reply = script.transformReply(
+ await this.executeScript(
+ script,
+ args,
+ redisArgs,
+ options
+ ),
+ redisArgs.preserve
+ );
+
+ return reply;
+ }
+
+ static create(options?: RedisClusterOptions): RedisClusterType {
+ return new (extendWithModulesAndScripts({
+ BaseClass: RedisCluster,
+ modules: options?.modules,
+ modulesCommandsExecutor: RedisCluster.commandsExecutor,
+ scripts: options?.scripts,
+ scriptsExecutor: RedisCluster.#scriptsExecutor
+ }))(options);
+ }
+
+ readonly #options: RedisClusterOptions;
+ readonly #slots: RedisClusterSlots;
+ readonly #Multi: new (...args: ConstructorParameters) => RedisMultiCommandType;
+
+ constructor(options: RedisClusterOptions) {
+ this.#options = options;
+ this.#slots = new RedisClusterSlots(options);
+ this.#Multi = RedisMultiCommand.extend(options);
+ }
+
+ async connect(): Promise {
+ return this.#slots.connect();
+ }
+
+ async sendCommand(
+ firstKey: string | undefined,
+ isReadonly: boolean | undefined,
+ args: Array,
+ options?: ClientCommandOptions,
+ redirections = 0
+ ): Promise> {
+ const client = this.#slots.getClient(firstKey, isReadonly);
+
+ try {
+ return await client.sendCommand(args, options);
+ } catch (err: any) {
+ const shouldRetry = await this.#handleCommandError(err, client, redirections);
+ if (shouldRetry === true) {
+ return this.sendCommand(firstKey, isReadonly, args, options, redirections + 1);
+ } else if (shouldRetry) {
+ return shouldRetry.sendCommand(args, options);
+ }
+
+ throw err;
+ }
+ }
+
+ async executeScript(
+ script: RedisLuaScript,
+ originalArgs: Array,
+ redisArgs: Array,
+ options?: ClientCommandOptions,
+ redirections = 0
+ ): Promise> {
+ const client = this.#slots.getClient(
+ RedisCluster.#extractFirstKey(script, originalArgs, redisArgs),
+ script.IS_READ_ONLY
+ );
+
+ try {
+ return await client.executeScript(script, redisArgs, options);
+ } 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);
+ }
+
+ throw err;
+ }
+ }
+
+ async #handleCommandError(err: Error, client: RedisClientType, redirections: number): Promise> {
+ 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): RedisMultiCommandType {
+ return new this.#Multi(
+ async (commands: Array, chainId?: symbol) => {
+ const client = this.#slots.getClient(routing);
+
+ return Promise.all(
+ commands.map(({encodedCommand}) => {
+ return client.sendEncodedCommand(encodedCommand, RedisClient.commandOptions({
+ chainId
+ }));
+ })
+ );
+ },
+ this.#options
+ );
+ }
+
+ getMasters(): Array> {
+ return this.#slots.getMasters();
+ }
+
+ getSlotMaster(slot: number): ClusterNode {
+ return this.#slots.getSlotMaster(slot);
+ }
+
+ disconnect(): Promise {
+ return this.#slots.disconnect();
+ }
+}
+
+extendWithDefaultCommands(RedisCluster, RedisCluster.commandsExecutor);
+
diff --git a/lib/command-options.ts b/lib/command-options.ts
new file mode 100644
index 00000000000..2096258046f
--- /dev/null
+++ b/lib/command-options.ts
@@ -0,0 +1,14 @@
+const symbol = Symbol('Command Options');
+
+export type CommandOptions = T & {
+ readonly [symbol]: true;
+};
+
+export function commandOptions(options: T): CommandOptions {
+ (options as any)[symbol] = true;
+ return options as CommandOptions;
+}
+
+export function isCommandOptions(options: any): options is CommandOptions {
+ return options && options[symbol] === true;
+}
diff --git a/lib/command.js b/lib/command.js
deleted file mode 100644
index 717115c82eb..00000000000
--- a/lib/command.js
+++ /dev/null
@@ -1,16 +0,0 @@
-'use strict';
-
-var betterStackTraces = /development/i.test(process.env.NODE_ENV) || /\bredis\b/i.test(process.env.NODE_DEBUG);
-
-function Command (command, args, callback, call_on_write) {
- this.command = command;
- this.args = args;
- this.buffer_args = false;
- this.callback = callback;
- this.call_on_write = call_on_write;
- if (betterStackTraces) {
- this.error = new Error();
- }
-}
-
-module.exports = Command;
diff --git a/lib/commander.spec.ts b/lib/commander.spec.ts
new file mode 100644
index 00000000000..a38330abada
--- /dev/null
+++ b/lib/commander.spec.ts
@@ -0,0 +1,28 @@
+import { strict as assert } from 'assert';
+import { describe } from 'mocha';
+import { encodeCommand } from './commander';
+
+describe('Commander', () => {
+ describe('encodeCommand (see #1628)', () => {
+ it('1 byte', () => {
+ assert.equal(
+ encodeCommand(['a', 'z']),
+ '*2\r\n$1\r\na\r\n$1\r\nz\r\n'
+ );
+ });
+
+ it('2 bytes', () => {
+ assert.equal(
+ encodeCommand(['א', 'ת']),
+ '*2\r\n$2\r\nא\r\n$2\r\nת\r\n'
+ );
+ });
+
+ it('4 bytes', () => {
+ assert.equal(
+ encodeCommand(['🐣', '🐤']),
+ '*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n'
+ );
+ });
+ });
+});
diff --git a/lib/commander.ts b/lib/commander.ts
new file mode 100644
index 00000000000..51adc417ba9
--- /dev/null
+++ b/lib/commander.ts
@@ -0,0 +1,109 @@
+
+import COMMANDS, { RedisCommand, RedisModules, TransformArgumentsReply } from './commands';
+import { RedisLuaScript, RedisLuaScripts } from './lua-script';
+import { CommandOptions, isCommandOptions } from './command-options';
+
+type Instantiable = new(...args: Array) => T;
+
+type CommandExecutor = (this: InstanceType, command: RedisCommand, args: Array) => unknown;
+
+export function extendWithDefaultCommands(BaseClass: T, executor: CommandExecutor): void {
+ for (const [name, command] of Object.entries(COMMANDS)) {
+ BaseClass.prototype[name] = function (...args: Array): unknown {
+ return executor.call(this, command, args);
+ };
+ }
+}
+
+interface ExtendWithModulesAndScriptsConfig<
+ T extends Instantiable,
+ M extends RedisModules,
+ S extends RedisLuaScripts
+> {
+ BaseClass: T;
+ modules: M | undefined;
+ modulesCommandsExecutor: CommandExecutor;
+ scripts: S | undefined;
+ scriptsExecutor(this: InstanceType, script: RedisLuaScript, args: Array): unknown;
+}
+
+export function extendWithModulesAndScripts<
+ T extends Instantiable,
+ M extends RedisModules,
+ S extends RedisLuaScripts,
+>(config: ExtendWithModulesAndScriptsConfig): T {
+ let Commander: T | undefined;
+
+ if (config.modules) {
+ Commander = class extends config.BaseClass {
+ constructor(...args: Array) {
+ super(...args);
+
+ for (const module of Object.keys(config.modules as RedisModules)) {
+ 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) {
+ this.self = self;
+ }
+ };
+
+ for (const [commandName, command] of Object.entries(module)) {
+ Commander.prototype[moduleName].prototype[commandName] = function (...args: Array): 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 {
+ return config.scriptsExecutor.call(this, script, args);
+ };
+ }
+ }
+
+ return (Commander ?? config.BaseClass) as any;
+}
+
+export function transformCommandArguments(
+ command: RedisCommand,
+ args: Array
+): {
+ args: TransformArgumentsReply;
+ options: CommandOptions | undefined;
+} {
+ let options;
+ if (isCommandOptions(args[0])) {
+ options = args[0];
+ args = args.slice(1);
+ }
+
+ return {
+ args: command.transformArguments(...args),
+ options
+ };
+}
+
+export function encodeCommand(args: Array): string {
+ const encoded = [
+ `*${args.length}`,
+ `$${Buffer.byteLength(args[0])}`,
+ args[0]
+ ];
+
+ for (let i = 1; i < args.length; i++) {
+ encoded.push(`$${Buffer.byteLength(args[i])}`, args[i]);
+ }
+
+ return encoded.join('\r\n') + '\r\n';
+}
diff --git a/lib/commands-queue.ts b/lib/commands-queue.ts
new file mode 100644
index 00000000000..1890e0a00a9
--- /dev/null
+++ b/lib/commands-queue.ts
@@ -0,0 +1,333 @@
+import LinkedList from 'yallist';
+import RedisParser from 'redis-parser';
+import { AbortError } from './errors';
+import { RedisReply } from './commands';
+import { encodeCommand } from './commander';
+
+export interface QueueCommandOptions {
+ asap?: boolean;
+ signal?: any; // TODO: `AbortSignal` type is incorrect
+ chainId?: symbol;
+}
+
+interface CommandWaitingToBeSent extends CommandWaitingForReply {
+ encodedCommand: string;
+ chainId?: symbol;
+ abort?: {
+ signal: any; // TODO: `AbortSignal` type is incorrect
+ listener(): void;
+ };
+}
+
+interface CommandWaitingForReply {
+ resolve(reply?: any): void;
+ reject(err: Error): void;
+ channelsCounter?: number;
+}
+
+export type CommandsQueueExecutor = (encodedCommands: string) => boolean | undefined;
+
+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>;
+
+export default class RedisCommandsQueue {
+ static #flushQueue(queue: LinkedList, err: Error): void {
+ while (queue.length) {
+ queue.shift()!.reject(err);
+ }
+ }
+
+ static #emitPubSubMessage(listeners: Set, message: string, channel: string): void {
+ for (const listener of listeners) {
+ listener(message, channel);
+ }
+ }
+
+ readonly #maxLength: number | null | undefined;
+
+ readonly #executor: CommandsQueueExecutor;
+
+ readonly #waitingToBeSent = new LinkedList();
+
+ #waitingToBeSentCommandsLength = 0;
+
+ get waitingToBeSentCommandsLength() {
+ return this.#waitingToBeSentCommandsLength;
+ }
+
+ readonly #waitingForReply = new LinkedList();
+
+ readonly #pubSubState = {
+ subscribing: 0,
+ subscribed: 0,
+ unsubscribing: 0
+ };
+
+ readonly #pubSubListeners = {
+ channels: new Map(),
+ patterns: 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, executor: CommandsQueueExecutor) {
+ this.#maxLength = maxLength;
+ this.#executor = executor;
+ }
+
+ addEncodedCommand(encodedCommand: string, options?: QueueCommandOptions): Promise {
+ 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({
+ encodedCommand,
+ chainId: options?.chainId,
+ resolve,
+ reject
+ });
+
+ if (options?.signal) {
+ const listener = () => {
+ this.#waitingToBeSent.removeNode(node);
+ node.value.reject(new AbortError());
+ };
+
+ node.value.abort = {
+ signal: options.signal,
+ listener
+ };
+ options.signal.addEventListener('abort', listener, {
+ once: true
+ });
+ }
+
+ if (options?.asap) {
+ this.#waitingToBeSent.unshiftNode(node);
+ } else {
+ this.#waitingToBeSent.pushNode(node);
+ }
+
+ this.#waitingToBeSentCommandsLength += encodedCommand.length;
+ });
+ }
+
+ subscribe(command: PubSubSubscribeCommands, channels: string | Array, listener: PubSubListener): Promise {
+ const channelsToSubscribe: Array = [],
+ 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, listener?: PubSubListener): Promise {
+ const listeners = command === PubSubUnsubscribeCommands.UNSUBSCRIBE ? this.#pubSubListeners.channels : this.#pubSubListeners.patterns;
+ if (!channels) {
+ listeners.clear();
+ return this.#pushPubSubCommand(command);
+ }
+
+ 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?: Array): Promise {
+ return new Promise((resolve, reject) => {
+ const isSubscribe = command === PubSubSubscribeCommands.SUBSCRIBE || command === PubSubSubscribeCommands.PSUBSCRIBE,
+ inProgressKey = isSubscribe ? 'subscribing' : 'unsubscribing',
+ commandArgs: Array = [command];
+ let channelsCounter: number;
+ if (channels?.length) {
+ commandArgs.push(...channels);
+ channelsCounter = channels.length;
+ } else {
+ // unsubscribe only
+ channelsCounter = (
+ command[0] === 'P' ?
+ this.#pubSubListeners.patterns :
+ this.#pubSubListeners.channels
+ ).size;
+ }
+
+ this.#pubSubState[inProgressKey] += channelsCounter;
+ this.#waitingToBeSent.push({
+ encodedCommand: encodeCommand(commandArgs),
+ channelsCounter,
+ resolve: () => {
+ this.#pubSubState[inProgressKey] -= channelsCounter;
+ this.#pubSubState.subscribed += channelsCounter * (isSubscribe ? 1 : -1);
+ resolve();
+ },
+ reject: () => {
+ this.#pubSubState[inProgressKey] -= channelsCounter;
+ reject();
+ }
+ });
+ });
+ }
+
+ resubscribe(): Promise | 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()])
+ ]);
+ }
+
+ executeChunk(recommendedSize: number): boolean | undefined {
+ if (!this.#waitingToBeSent.length) return;
+
+ const encoded: Array = [];
+ let size = 0,
+ lastCommandChainId: symbol | undefined;
+ for (const command of this.#waitingToBeSent) {
+ encoded.push(command.encodedCommand);
+ size += command.encodedCommand.length;
+ if (size > recommendedSize) {
+ lastCommandChainId = command.chainId;
+ break;
+ }
+ }
+
+ if (!lastCommandChainId && encoded.length === this.#waitingToBeSent.length) {
+ lastCommandChainId = this.#waitingToBeSent.tail!.value.chainId;
+ }
+
+ lastCommandChainId ??= this.#waitingToBeSent.tail?.value.chainId;
+
+ this.#executor(encoded.join(''));
+
+ for (let i = 0; i < encoded.length; i++) {
+ const waitingToBeSent = this.#waitingToBeSent.shift()!;
+ if (waitingToBeSent.abort) {
+ waitingToBeSent.abort.signal.removeEventListener('abort', waitingToBeSent.abort.listener);
+ }
+
+ this.#waitingForReply.push({
+ resolve: waitingToBeSent.resolve,
+ reject: waitingToBeSent.reject,
+ channelsCounter: waitingToBeSent.channelsCounter
+ });
+ }
+
+ this.#chainInExecution = lastCommandChainId;
+ this.#waitingToBeSentCommandsLength -= size;
+ }
+
+ parseResponse(data: Buffer): void {
+ 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);
+ }
+}
diff --git a/lib/commands.js b/lib/commands.js
deleted file mode 100644
index a3b5189698b..00000000000
--- a/lib/commands.js
+++ /dev/null
@@ -1,105 +0,0 @@
-'use strict';
-
-var commands = require('redis-commands');
-var Multi = require('./multi');
-var RedisClient = require('../').RedisClient;
-var Command = require('./command');
-
-var addCommand = function (command) {
- // Some rare Redis commands use special characters in their command name
- // Convert those to a underscore to prevent using invalid function names
- var commandName = command.replace(/(?:^([0-9])|[^a-zA-Z0-9_$])/g, '_$1');
-
- // Do not override existing functions
- if (!RedisClient.prototype[command]) {
- RedisClient.prototype[command.toUpperCase()] = RedisClient.prototype[command] = function () {
- var arr;
- var len = arguments.length;
- var callback;
- var i = 0;
- if (Array.isArray(arguments[0])) {
- arr = arguments[0];
- if (len === 2) {
- callback = arguments[1];
- }
- } else if (len > 1 && Array.isArray(arguments[1])) {
- if (len === 3) {
- callback = arguments[2];
- }
- len = arguments[1].length;
- arr = new Array(len + 1);
- arr[0] = arguments[0];
- for (; i < len; i += 1) {
- arr[i + 1] = arguments[1][i];
- }
- } else {
- // The later should not be the average use case
- if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
- len--;
- callback = arguments[len];
- }
- arr = new Array(len);
- for (; i < len; i += 1) {
- arr[i] = arguments[i];
- }
- }
- return this.internal_send_command(new Command(command, arr, callback));
- };
- // Alias special function names (e.g. NR.RUN becomes NR_RUN and nr_run)
- if (commandName !== command) {
- RedisClient.prototype[commandName.toUpperCase()] = RedisClient.prototype[commandName] = RedisClient.prototype[command];
- }
- Object.defineProperty(RedisClient.prototype[command], 'name', {
- value: commandName
- });
- }
-
- // Do not override existing functions
- if (!Multi.prototype[command]) {
- Multi.prototype[command.toUpperCase()] = Multi.prototype[command] = function () {
- var arr;
- var len = arguments.length;
- var callback;
- var i = 0;
- if (Array.isArray(arguments[0])) {
- arr = arguments[0];
- if (len === 2) {
- callback = arguments[1];
- }
- } else if (len > 1 && Array.isArray(arguments[1])) {
- if (len === 3) {
- callback = arguments[2];
- }
- len = arguments[1].length;
- arr = new Array(len + 1);
- arr[0] = arguments[0];
- for (; i < len; i += 1) {
- arr[i + 1] = arguments[1][i];
- }
- } else {
- // The later should not be the average use case
- if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
- len--;
- callback = arguments[len];
- }
- arr = new Array(len);
- for (; i < len; i += 1) {
- arr[i] = arguments[i];
- }
- }
- this.queue.push(new Command(command, arr, callback));
- return this;
- };
- // Alias special function names (e.g. NR.RUN becomes NR_RUN and nr_run)
- if (commandName !== command) {
- Multi.prototype[commandName.toUpperCase()] = Multi.prototype[commandName] = Multi.prototype[command];
- }
- Object.defineProperty(Multi.prototype[command], 'name', {
- value: commandName
- });
- }
-};
-
-commands.list.forEach(addCommand);
-
-module.exports = addCommand;
diff --git a/lib/commands/ACL_CAT.spec.ts b/lib/commands/ACL_CAT.spec.ts
new file mode 100644
index 00000000000..77ed1cb7a07
--- /dev/null
+++ b/lib/commands/ACL_CAT.spec.ts
@@ -0,0 +1,23 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments } from './ACL_CAT';
+
+describe('ACL CAT', () => {
+ describeHandleMinimumRedisVersion([6]);
+
+ describe('transformArguments', () => {
+ it('simple', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['ACL', 'CAT']
+ );
+ });
+
+ it('with categoryName', () => {
+ assert.deepEqual(
+ transformArguments('dangerous'),
+ ['ACL', 'CAT', 'dangerous']
+ );
+ });
+ });
+});
diff --git a/lib/commands/ACL_CAT.ts b/lib/commands/ACL_CAT.ts
new file mode 100644
index 00000000000..d1620ef1584
--- /dev/null
+++ b/lib/commands/ACL_CAT.ts
@@ -0,0 +1,13 @@
+import { transformReplyStringArray } from './generic-transformers';
+
+export function transformArguments(categoryName?: string): Array {
+ const args = ['ACL', 'CAT'];
+
+ if (categoryName) {
+ args.push(categoryName);
+ }
+
+ return args;
+}
+
+export const transformReply = transformReplyStringArray;
diff --git a/lib/commands/ACL_DELUSER.spec.ts b/lib/commands/ACL_DELUSER.spec.ts
new file mode 100644
index 00000000000..c64e8db1965
--- /dev/null
+++ b/lib/commands/ACL_DELUSER.spec.ts
@@ -0,0 +1,30 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion, itWithClient, TestRedisServers } from '../test-utils';
+import { transformArguments } from './ACL_DELUSER';
+
+describe('ACL DELUSER', () => {
+ describeHandleMinimumRedisVersion([6]);
+
+ describe('transformArguments', () => {
+ it('string', () => {
+ assert.deepEqual(
+ transformArguments('username'),
+ ['ACL', 'DELUSER', 'username']
+ );
+ });
+
+ it('array', () => {
+ assert.deepEqual(
+ transformArguments(['1', '2']),
+ ['ACL', 'DELUSER', '1', '2']
+ );
+ });
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'client.aclDelUser', async client => {
+ assert.equal(
+ await client.aclDelUser('dosenotexists'),
+ 0
+ );
+ });
+});
diff --git a/lib/commands/ACL_DELUSER.ts b/lib/commands/ACL_DELUSER.ts
new file mode 100644
index 00000000000..7fb4904be41
--- /dev/null
+++ b/lib/commands/ACL_DELUSER.ts
@@ -0,0 +1,7 @@
+import { pushVerdictArguments, transformReplyNumber } from './generic-transformers';
+
+export function transformArguments(username: string | Array): Array {
+ return pushVerdictArguments(['ACL', 'DELUSER'], username);
+}
+
+export const transformReply = transformReplyNumber;
diff --git a/lib/commands/ACL_GENPASS.spec.ts b/lib/commands/ACL_GENPASS.spec.ts
new file mode 100644
index 00000000000..a288a4f7142
--- /dev/null
+++ b/lib/commands/ACL_GENPASS.spec.ts
@@ -0,0 +1,23 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments } from './ACL_GENPASS';
+
+describe('ACL GENPASS', () => {
+ describeHandleMinimumRedisVersion([6]);
+
+ describe('transformArguments', () => {
+ it('simple', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['ACL', 'GENPASS']
+ );
+ });
+
+ it('with bits', () => {
+ assert.deepEqual(
+ transformArguments(128),
+ ['ACL', 'GENPASS', '128']
+ );
+ });
+ });
+});
diff --git a/lib/commands/ACL_GENPASS.ts b/lib/commands/ACL_GENPASS.ts
new file mode 100644
index 00000000000..ec55aebdb07
--- /dev/null
+++ b/lib/commands/ACL_GENPASS.ts
@@ -0,0 +1,13 @@
+import { transformReplyString } from './generic-transformers';
+
+export function transformArguments(bits?: number): Array {
+ const args = ['ACL', 'GENPASS'];
+
+ if (bits) {
+ args.push(bits.toString());
+ }
+
+ return args;
+}
+
+export const transformReply = transformReplyString;
diff --git a/lib/commands/ACL_GETUSER.spec.ts b/lib/commands/ACL_GETUSER.spec.ts
new file mode 100644
index 00000000000..c43cdc364ae
--- /dev/null
+++ b/lib/commands/ACL_GETUSER.spec.ts
@@ -0,0 +1,27 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion, itWithClient, TestRedisServers } from '../test-utils';
+import { transformArguments } from './ACL_GETUSER';
+
+describe('ACL GETUSER', () => {
+ describeHandleMinimumRedisVersion([6]);
+
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments('username'),
+ ['ACL', 'GETUSER', 'username']
+ );
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'client.aclGetUser', async client => {
+ assert.deepEqual(
+ await client.aclGetUser('default'),
+ {
+ flags: ['on', 'allkeys', 'allchannels', 'allcommands', 'nopass'],
+ passwords: [],
+ commands: '+@all',
+ keys: ['*'],
+ channels: ['*']
+ }
+ );
+ });
+});
diff --git a/lib/commands/ACL_GETUSER.ts b/lib/commands/ACL_GETUSER.ts
new file mode 100644
index 00000000000..876a723c390
--- /dev/null
+++ b/lib/commands/ACL_GETUSER.ts
@@ -0,0 +1,34 @@
+export function transformArguments(username: string): Array {
+ return ['ACL', 'GETUSER', username];
+}
+
+type AclGetUserRawReply = [
+ _: string,
+ flags: Array,
+ _: string,
+ passwords: Array,
+ _: string,
+ commands: string,
+ _: string,
+ keys: Array,
+ _: string,
+ channels: Array
+];
+
+interface AclUser {
+ flags: Array;
+ passwords: Array;
+ commands: string;
+ keys: Array;
+ channels: Array
+}
+
+export function transformReply(reply: AclGetUserRawReply): AclUser {
+ return {
+ flags: reply[1],
+ passwords: reply[3],
+ commands: reply[5],
+ keys: reply[7],
+ channels: reply[9]
+ };
+}
diff --git a/lib/commands/ACL_LIST.spec.ts b/lib/commands/ACL_LIST.spec.ts
new file mode 100644
index 00000000000..ab6bae762f1
--- /dev/null
+++ b/lib/commands/ACL_LIST.spec.ts
@@ -0,0 +1,14 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments } from './ACL_LIST';
+
+describe('ACL LIST', () => {
+ describeHandleMinimumRedisVersion([6]);
+
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['ACL', 'LIST']
+ );
+ });
+});
diff --git a/lib/commands/ACL_LIST.ts b/lib/commands/ACL_LIST.ts
new file mode 100644
index 00000000000..3f2845b907a
--- /dev/null
+++ b/lib/commands/ACL_LIST.ts
@@ -0,0 +1,7 @@
+import { transformReplyStringArray } from './generic-transformers';
+
+export function transformArguments(): Array {
+ return ['ACL', 'LIST'];
+}
+
+export const transformReply = transformReplyStringArray;
diff --git a/lib/commands/ACL_LOAD.spec.ts b/lib/commands/ACL_LOAD.spec.ts
new file mode 100644
index 00000000000..d173d7f1355
--- /dev/null
+++ b/lib/commands/ACL_LOAD.spec.ts
@@ -0,0 +1,14 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments } from './ACL_SAVE';
+
+describe('ACL SAVE', () => {
+ describeHandleMinimumRedisVersion([6]);
+
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['ACL', 'SAVE']
+ );
+ });
+});
diff --git a/lib/commands/ACL_LOAD.ts b/lib/commands/ACL_LOAD.ts
new file mode 100644
index 00000000000..59418614ed3
--- /dev/null
+++ b/lib/commands/ACL_LOAD.ts
@@ -0,0 +1,7 @@
+import { transformReplyString } from './generic-transformers';
+
+export function transformArguments(): Array {
+ return ['ACL', 'LOAD'];
+}
+
+export const transformReply = transformReplyString;
diff --git a/lib/commands/ACL_LOG.spec.ts b/lib/commands/ACL_LOG.spec.ts
new file mode 100644
index 00000000000..3ce76ce4563
--- /dev/null
+++ b/lib/commands/ACL_LOG.spec.ts
@@ -0,0 +1,53 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments, transformReply } from './ACL_LOG';
+
+describe('ACL LOG', () => {
+ describeHandleMinimumRedisVersion([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'
+ }]
+ );
+ });
+});
diff --git a/lib/commands/ACL_LOG.ts b/lib/commands/ACL_LOG.ts
new file mode 100644
index 00000000000..ed0590b5783
--- /dev/null
+++ b/lib/commands/ACL_LOG.ts
@@ -0,0 +1,48 @@
+export function transformArguments(count?: number): Array {
+ 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): Array {
+ 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]
+ }));
+}
diff --git a/lib/commands/ACL_LOG_RESET.spec.ts b/lib/commands/ACL_LOG_RESET.spec.ts
new file mode 100644
index 00000000000..3f0e628d9f0
--- /dev/null
+++ b/lib/commands/ACL_LOG_RESET.spec.ts
@@ -0,0 +1,14 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments } from './ACL_LOG_RESET';
+
+describe('ACL LOG RESET', () => {
+ describeHandleMinimumRedisVersion([6]);
+
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['ACL', 'LOG', 'RESET']
+ );
+ });
+});
diff --git a/lib/commands/ACL_LOG_RESET.ts b/lib/commands/ACL_LOG_RESET.ts
new file mode 100644
index 00000000000..30b8ccb20c7
--- /dev/null
+++ b/lib/commands/ACL_LOG_RESET.ts
@@ -0,0 +1,7 @@
+import { transformReplyString } from './generic-transformers';
+
+export function transformArguments(): Array {
+ return ['ACL', 'LOG', 'RESET'];
+}
+
+export const transformReply = transformReplyString;
diff --git a/lib/commands/ACL_SAVE.spec.ts b/lib/commands/ACL_SAVE.spec.ts
new file mode 100644
index 00000000000..b34c7bb0e6f
--- /dev/null
+++ b/lib/commands/ACL_SAVE.spec.ts
@@ -0,0 +1,14 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments } from './ACL_LOAD';
+
+describe('ACL LOAD', () => {
+ describeHandleMinimumRedisVersion([6]);
+
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['ACL', 'LOAD']
+ );
+ });
+});
diff --git a/lib/commands/ACL_SAVE.ts b/lib/commands/ACL_SAVE.ts
new file mode 100644
index 00000000000..5b9c7b84cc3
--- /dev/null
+++ b/lib/commands/ACL_SAVE.ts
@@ -0,0 +1,7 @@
+import { transformReplyString } from './generic-transformers';
+
+export function transformArguments(): Array {
+ return ['ACL', 'SAVE'];
+}
+
+export const transformReply = transformReplyString;
diff --git a/lib/commands/ACL_SETUSER.spec.ts b/lib/commands/ACL_SETUSER.spec.ts
new file mode 100644
index 00000000000..f3badfcdca8
--- /dev/null
+++ b/lib/commands/ACL_SETUSER.spec.ts
@@ -0,0 +1,23 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments } from './ACL_SETUSER';
+
+describe('ACL SETUSER', () => {
+ describeHandleMinimumRedisVersion([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']
+ );
+ });
+ });
+});
diff --git a/lib/commands/ACL_SETUSER.ts b/lib/commands/ACL_SETUSER.ts
new file mode 100644
index 00000000000..b2829ca964f
--- /dev/null
+++ b/lib/commands/ACL_SETUSER.ts
@@ -0,0 +1,7 @@
+import { pushVerdictArguments, transformReplyString } from './generic-transformers';
+
+export function transformArguments(username: string, rule: string | Array): Array {
+ return pushVerdictArguments(['ACL', 'SETUSER', username], rule);
+}
+
+export const transformReply = transformReplyString;
diff --git a/lib/commands/ACL_USERS.spec.ts b/lib/commands/ACL_USERS.spec.ts
new file mode 100644
index 00000000000..14b76725fcd
--- /dev/null
+++ b/lib/commands/ACL_USERS.spec.ts
@@ -0,0 +1,14 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments } from './ACL_USERS';
+
+describe('ACL USERS', () => {
+ describeHandleMinimumRedisVersion([6]);
+
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['ACL', 'USERS']
+ );
+ });
+});
diff --git a/lib/commands/ACL_USERS.ts b/lib/commands/ACL_USERS.ts
new file mode 100644
index 00000000000..f9e837a4347
--- /dev/null
+++ b/lib/commands/ACL_USERS.ts
@@ -0,0 +1,7 @@
+import { transformReplyStringArray } from './generic-transformers';
+
+export function transformArguments(): Array {
+ return ['ACL', 'USERS'];
+}
+
+export const transformReply = transformReplyStringArray;
diff --git a/lib/commands/ACL_WHOAMI.spec.ts b/lib/commands/ACL_WHOAMI.spec.ts
new file mode 100644
index 00000000000..a933057ea9d
--- /dev/null
+++ b/lib/commands/ACL_WHOAMI.spec.ts
@@ -0,0 +1,14 @@
+import { strict as assert } from 'assert';
+import { describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments } from './ACL_WHOAMI';
+
+describe('ACL WHOAMI', () => {
+ describeHandleMinimumRedisVersion([6]);
+
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['ACL', 'WHOAMI']
+ );
+ });
+});
diff --git a/lib/commands/ACL_WHOAMI.ts b/lib/commands/ACL_WHOAMI.ts
new file mode 100644
index 00000000000..3fc70649f87
--- /dev/null
+++ b/lib/commands/ACL_WHOAMI.ts
@@ -0,0 +1,7 @@
+import { transformReplyString } from './generic-transformers';
+
+export function transformArguments(): Array {
+ return ['ACL', 'WHOAMI'];
+}
+
+export const transformReply = transformReplyString;
diff --git a/lib/commands/APPEND.spec.ts b/lib/commands/APPEND.spec.ts
new file mode 100644
index 00000000000..283ab807956
--- /dev/null
+++ b/lib/commands/APPEND.spec.ts
@@ -0,0 +1,11 @@
+import { strict as assert } from 'assert';
+import { transformArguments } from './APPEND';
+
+describe('AUTH', () => {
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments('key', 'value'),
+ ['APPEND', 'key', 'value']
+ );
+ });
+});
diff --git a/lib/commands/APPEND.ts b/lib/commands/APPEND.ts
new file mode 100644
index 00000000000..f64e835113c
--- /dev/null
+++ b/lib/commands/APPEND.ts
@@ -0,0 +1,9 @@
+import { transformReplyString } from './generic-transformers';
+
+export const FIRST_KEY_INDEX = 1;
+
+export function transformArguments(key: string, value: string): Array {
+ return ['APPEND', key, value];
+}
+
+export const transformReply = transformReplyString;
diff --git a/lib/commands/ASKING.spec.ts b/lib/commands/ASKING.spec.ts
new file mode 100644
index 00000000000..3da2015199e
--- /dev/null
+++ b/lib/commands/ASKING.spec.ts
@@ -0,0 +1,11 @@
+import { strict as assert } from 'assert';
+import { transformArguments } from './ASKING';
+
+describe('ASKING', () => {
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['ASKING']
+ );
+ });
+});
diff --git a/lib/commands/ASKING.ts b/lib/commands/ASKING.ts
new file mode 100644
index 00000000000..3f836131ac1
--- /dev/null
+++ b/lib/commands/ASKING.ts
@@ -0,0 +1,7 @@
+import { transformReplyString } from './generic-transformers';
+
+export function transformArguments(): Array {
+ return ['ASKING'];
+}
+
+export const transformReply = transformReplyString;
diff --git a/lib/commands/AUTH.spec.ts b/lib/commands/AUTH.spec.ts
new file mode 100644
index 00000000000..1907488346e
--- /dev/null
+++ b/lib/commands/AUTH.spec.ts
@@ -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']
+ );
+ });
+ });
+});
diff --git a/lib/commands/AUTH.ts b/lib/commands/AUTH.ts
new file mode 100644
index 00000000000..750f0f54354
--- /dev/null
+++ b/lib/commands/AUTH.ts
@@ -0,0 +1,16 @@
+import { transformReplyString } from './generic-transformers';
+
+export interface AuthOptions {
+ username?: string;
+ password: string;
+}
+
+export function transformArguments({username, password}: AuthOptions): Array {
+ if (!username) {
+ return ['AUTH', password];
+ }
+
+ return ['AUTH', username, password];
+}
+
+export const transformReply = transformReplyString;
diff --git a/lib/commands/BGREWRITEAOF.spec.ts b/lib/commands/BGREWRITEAOF.spec.ts
new file mode 100644
index 00000000000..d0e150e155b
--- /dev/null
+++ b/lib/commands/BGREWRITEAOF.spec.ts
@@ -0,0 +1,11 @@
+import { strict as assert } from 'assert';
+import { transformArguments } from './BGREWRITEAOF';
+
+describe('BGREWRITEAOF', () => {
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['BGREWRITEAOF']
+ );
+ });
+});
diff --git a/lib/commands/BGREWRITEAOF.ts b/lib/commands/BGREWRITEAOF.ts
new file mode 100644
index 00000000000..52d3f4df9da
--- /dev/null
+++ b/lib/commands/BGREWRITEAOF.ts
@@ -0,0 +1,7 @@
+import { transformReplyString } from './generic-transformers';
+
+export function transformArguments(): Array {
+ return ['BGREWRITEAOF'];
+}
+
+export const transformReply = transformReplyString;
diff --git a/lib/commands/BGSAVE.spec.ts b/lib/commands/BGSAVE.spec.ts
new file mode 100644
index 00000000000..8e4de5eef5b
--- /dev/null
+++ b/lib/commands/BGSAVE.spec.ts
@@ -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']
+ );
+ });
+ });
+});
diff --git a/lib/commands/BGSAVE.ts b/lib/commands/BGSAVE.ts
new file mode 100644
index 00000000000..f09f906ade7
--- /dev/null
+++ b/lib/commands/BGSAVE.ts
@@ -0,0 +1,17 @@
+import { transformReplyString } from './generic-transformers';
+
+interface BgSaveOptions {
+ SCHEDULE?: true;
+}
+
+export function transformArguments(options?: BgSaveOptions): Array {
+ const args = ['BGSAVE'];
+
+ if (options?.SCHEDULE) {
+ args.push('SCHEDULE');
+ }
+
+ return args;
+}
+
+export const transformReply = transformReplyString;
\ No newline at end of file
diff --git a/lib/commands/BITCOUNT.spec.ts b/lib/commands/BITCOUNT.spec.ts
new file mode 100644
index 00000000000..bf4cf39cab6
--- /dev/null
+++ b/lib/commands/BITCOUNT.spec.ts
@@ -0,0 +1,31 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient } 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']
+ );
+ });
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'client.bitCount', async client => {
+ assert.equal(
+ await client.bitCount('key'),
+ 0
+ );
+ });
+});
diff --git a/lib/commands/BITCOUNT.ts b/lib/commands/BITCOUNT.ts
new file mode 100644
index 00000000000..1aececc377e
--- /dev/null
+++ b/lib/commands/BITCOUNT.ts
@@ -0,0 +1,25 @@
+import { transformReplyNumber } from './generic-transformers';
+
+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 {
+ const args = ['BITCOUNT', key];
+
+ if (range) {
+ args.push(
+ range.start.toString(),
+ range.end.toString()
+ );
+ }
+
+ return args;
+}
+
+export const transformReply = transformReplyNumber;
\ No newline at end of file
diff --git a/lib/commands/BITFIELD.spec.ts b/lib/commands/BITFIELD.spec.ts
new file mode 100644
index 00000000000..4d6d9d11c1a
--- /dev/null
+++ b/lib/commands/BITFIELD.spec.ts
@@ -0,0 +1,42 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient } 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']
+ );
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'client.bitField', async client => {
+ assert.deepEqual(
+ await client.bitField('key', []),
+ []
+ );
+ });
+});
diff --git a/lib/commands/BITFIELD.ts b/lib/commands/BITFIELD.ts
new file mode 100644
index 00000000000..445c26e28a3
--- /dev/null
+++ b/lib/commands/BITFIELD.ts
@@ -0,0 +1,84 @@
+import { transformReplyNumberNullArray } from './generic-transformers';
+
+export const FIRST_KEY_INDEX = 1;
+
+export const IS_READ_ONLY = true;
+
+type BitFieldType = string; // TODO 'i[1-64]' | 'u[1-63]'
+
+interface BitFieldOperation {
+ 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 {
+ 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 const transformReply = transformReplyNumberNullArray;
diff --git a/lib/commands/BITOP.spec.ts b/lib/commands/BITOP.spec.ts
new file mode 100644
index 00000000000..aa863e5f2d2
--- /dev/null
+++ b/lib/commands/BITOP.spec.ts
@@ -0,0 +1,35 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } 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']
+ );
+ });
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'client.bitOp', async client => {
+ assert.equal(
+ await client.bitOp('AND', 'destKey', 'key'),
+ 0
+ );
+ });
+
+ itWithCluster(TestRedisClusters.OPEN, 'cluster.bitOp', async cluster => {
+ assert.equal(
+ await cluster.bitOp('AND', '{tag}destKey', '{tag}key'),
+ 0
+ );
+ });
+});
diff --git a/lib/commands/BITOP.ts b/lib/commands/BITOP.ts
new file mode 100644
index 00000000000..fe7d339f5d1
--- /dev/null
+++ b/lib/commands/BITOP.ts
@@ -0,0 +1,11 @@
+import { pushVerdictArguments, transformReplyNumber } 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): Array {
+ return pushVerdictArguments(['BITOP', operation, destKey], key);
+}
+
+export const transformReply = transformReplyNumber;
diff --git a/lib/commands/BITPOS.spec.ts b/lib/commands/BITPOS.spec.ts
new file mode 100644
index 00000000000..ad08e708c54
--- /dev/null
+++ b/lib/commands/BITPOS.spec.ts
@@ -0,0 +1,42 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } 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']
+ );
+ });
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'client.bitPos', async client => {
+ assert.equal(
+ await client.bitPos('key', 1, 1),
+ -1
+ );
+ });
+
+ itWithCluster(TestRedisClusters.OPEN, 'cluster.bitPos', async cluster => {
+ assert.equal(
+ await cluster.bitPos('key', 1, 1),
+ -1
+ );
+ });
+});
diff --git a/lib/commands/BITPOS.ts b/lib/commands/BITPOS.ts
new file mode 100644
index 00000000000..bebc45b03c3
--- /dev/null
+++ b/lib/commands/BITPOS.ts
@@ -0,0 +1,21 @@
+import { BitValue, transformReplyNumber } 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 {
+ 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 const transformReply = transformReplyNumber;
\ No newline at end of file
diff --git a/lib/commands/BLMOVE.spec.ts b/lib/commands/BLMOVE.spec.ts
new file mode 100644
index 00000000000..b942864758f
--- /dev/null
+++ b/lib/commands/BLMOVE.spec.ts
@@ -0,0 +1,43 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters, describeHandleMinimumRedisVersion } from '../test-utils';
+import { transformArguments } from './BLMOVE';
+import { commandOptions } from '../../index';
+
+describe('BLMOVE', () => {
+ describeHandleMinimumRedisVersion([6, 2]);
+
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments('source', 'destination', 'LEFT', 'RIGHT', 0),
+ ['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0']
+ );
+ });
+
+ itWithClient(TestRedisServers.OPEN, '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'
+ );
+ });
+
+ itWithCluster(TestRedisClusters.OPEN, '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'
+ );
+ });
+});
diff --git a/lib/commands/BLMOVE.ts b/lib/commands/BLMOVE.ts
new file mode 100644
index 00000000000..74a2eed4aa5
--- /dev/null
+++ b/lib/commands/BLMOVE.ts
@@ -0,0 +1,23 @@
+import { transformReplyStringNull } from './generic-transformers';
+import { LMoveSide } from './LMOVE';
+
+export const FIRST_KEY_INDEX = 1;
+
+export function transformArguments(
+ source: string,
+ destination: string,
+ sourceDirection: LMoveSide,
+ destinationDirection: LMoveSide,
+ timeout: number
+): Array {
+ return [
+ 'BLMOVE',
+ source,
+ destination,
+ sourceDirection,
+ destinationDirection,
+ timeout.toString()
+ ];
+}
+
+export const transformReply = transformReplyStringNull;
diff --git a/lib/commands/BLPOP.spec.ts b/lib/commands/BLPOP.spec.ts
new file mode 100644
index 00000000000..651dd09eaf3
--- /dev/null
+++ b/lib/commands/BLPOP.spec.ts
@@ -0,0 +1,79 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } 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'
+ }
+ );
+ });
+ });
+
+ itWithClient(TestRedisServers.OPEN, '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'
+ }
+ );
+ });
+
+ itWithCluster(TestRedisClusters.OPEN, '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'
+ }
+ );
+ });
+});
diff --git a/lib/commands/BLPOP.ts b/lib/commands/BLPOP.ts
new file mode 100644
index 00000000000..7c352951fb3
--- /dev/null
+++ b/lib/commands/BLPOP.ts
@@ -0,0 +1,25 @@
+import { pushVerdictArguments } from './generic-transformers';
+
+export const FIRST_KEY_INDEX = 1;
+
+export function transformArguments(keys: string | Array, timeout: number): Array {
+ 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]
+ };
+}
diff --git a/lib/commands/BRPOP.spec.ts b/lib/commands/BRPOP.spec.ts
new file mode 100644
index 00000000000..9a7d0bbc37d
--- /dev/null
+++ b/lib/commands/BRPOP.spec.ts
@@ -0,0 +1,79 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } 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'
+ }
+ );
+ });
+ });
+
+ itWithClient(TestRedisServers.OPEN, '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'
+ }
+ );
+ });
+
+ itWithCluster(TestRedisClusters.OPEN, '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'
+ }
+ );
+ });
+});
diff --git a/lib/commands/BRPOP.ts b/lib/commands/BRPOP.ts
new file mode 100644
index 00000000000..a03c278309a
--- /dev/null
+++ b/lib/commands/BRPOP.ts
@@ -0,0 +1,25 @@
+import { pushVerdictArguments } from './generic-transformers';
+
+export const FIRST_KEY_INDEX = 1;
+
+export function transformArguments(key: string | Array, timeout: number): Array {
+ 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]
+ };
+}
diff --git a/lib/commands/BRPOPLPUSH.spec.ts b/lib/commands/BRPOPLPUSH.spec.ts
new file mode 100644
index 00000000000..08bcf5e4d94
--- /dev/null
+++ b/lib/commands/BRPOPLPUSH.spec.ts
@@ -0,0 +1,47 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } 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']
+ );
+ });
+
+ itWithClient(TestRedisServers.OPEN, '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'
+ );
+ });
+
+ itWithCluster(TestRedisClusters.OPEN, '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'
+ );
+ });
+});
diff --git a/lib/commands/BRPOPLPUSH.ts b/lib/commands/BRPOPLPUSH.ts
new file mode 100644
index 00000000000..f6a3bc1b8a6
--- /dev/null
+++ b/lib/commands/BRPOPLPUSH.ts
@@ -0,0 +1,9 @@
+import { transformReplyNumberNull } from './generic-transformers';
+
+export const FIRST_KEY_INDEX = 1;
+
+export function transformArguments(source: string, destination: string, timeout: number): Array {
+ return ['BRPOPLPUSH', source, destination, timeout.toString()];
+}
+
+export const transformReply = transformReplyNumberNull;
diff --git a/lib/commands/BZPOPMAX.spec.ts b/lib/commands/BZPOPMAX.spec.ts
new file mode 100644
index 00000000000..c4bcc321b29
--- /dev/null
+++ b/lib/commands/BZPOPMAX.spec.ts
@@ -0,0 +1,66 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient } from '../test-utils';
+import { transformArguments, transformReply } from './BZPOPMAX';
+import { commandOptions } from '../../index';
+import { describe } from 'mocha';
+
+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
+ }
+ );
+ });
+ });
+
+ itWithClient(TestRedisServers.OPEN, '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
+ }
+ );
+ });
+});
diff --git a/lib/commands/BZPOPMAX.ts b/lib/commands/BZPOPMAX.ts
new file mode 100644
index 00000000000..ccd84272a50
--- /dev/null
+++ b/lib/commands/BZPOPMAX.ts
@@ -0,0 +1,27 @@
+import { pushVerdictArguments, transformReplyNumberInfinity, ZMember } from './generic-transformers';
+
+export const FIRST_KEY_INDEX = 1;
+
+export function transformArguments(key: string | Array, timeout: number): Array {
+ 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])
+ };
+}
diff --git a/lib/commands/BZPOPMIN.spec.ts b/lib/commands/BZPOPMIN.spec.ts
new file mode 100644
index 00000000000..8b8977f9b3a
--- /dev/null
+++ b/lib/commands/BZPOPMIN.spec.ts
@@ -0,0 +1,65 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient } 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
+ }
+ );
+ });
+ });
+
+ itWithClient(TestRedisServers.OPEN, '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
+ }
+ );
+ });
+});
diff --git a/lib/commands/BZPOPMIN.ts b/lib/commands/BZPOPMIN.ts
new file mode 100644
index 00000000000..0c299cdb9df
--- /dev/null
+++ b/lib/commands/BZPOPMIN.ts
@@ -0,0 +1,27 @@
+import { pushVerdictArguments, transformReplyNumberInfinity, ZMember } from './generic-transformers';
+
+export const FIRST_KEY_INDEX = 1;
+
+export function transformArguments(key: string | Array, timeout: number): Array {
+ 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])
+ };
+}
diff --git a/lib/commands/CLIENT_ID.spec.ts b/lib/commands/CLIENT_ID.spec.ts
new file mode 100644
index 00000000000..cb7dfd9f730
--- /dev/null
+++ b/lib/commands/CLIENT_ID.spec.ts
@@ -0,0 +1,19 @@
+import { strict as assert } from 'assert';
+import { TestRedisServers, itWithClient } from '../test-utils';
+import { transformArguments } from './CLIENT_ID';
+
+describe('CLIENT ID', () => {
+ it('transformArguments', () => {
+ assert.deepEqual(
+ transformArguments(),
+ ['CLIENT', 'ID']
+ );
+ });
+
+ itWithClient(TestRedisServers.OPEN, 'client.clientId', async client => {
+ assert.equal(
+ typeof (await client.clientId()),
+ 'number'
+ );
+ });
+});
diff --git a/lib/commands/CLIENT_ID.ts b/lib/commands/CLIENT_ID.ts
new file mode 100644
index 00000000000..baeab148eba
--- /dev/null
+++ b/lib/commands/CLIENT_ID.ts
@@ -0,0 +1,9 @@
+import { transformReplyNumber } from './generic-transformers';
+
+export const IS_READ_ONLY = true;
+
+export function transformArguments(): Array {
+ return ['CLIENT', 'ID'];
+}
+
+export const transformReply = transformReplyNumber;
diff --git a/lib/commands/CLIENT_INFO.spec.ts b/lib/commands/CLIENT_INFO.spec.ts
new file mode 100644
index 00000000000..ee87df4a199
--- /dev/null
+++ b/lib/commands/CLIENT_INFO.spec.ts
@@ -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
+ }
+ );
+ });
+});
diff --git a/lib/commands/CLIENT_INFO.ts b/lib/commands/CLIENT_INFO.ts
new file mode 100644
index 00000000000..8dd30b70590
--- /dev/null
+++ b/lib/commands/CLIENT_INFO.ts
@@ -0,0 +1,85 @@
+export function transformArguments(): Array {
+ 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)
+ };
+}
diff --git a/lib/commands/CLUSTER_ADDSLOTS.spec.ts b/lib/commands/CLUSTER_ADDSLOTS.spec.ts
new file mode 100644
index 00000000000..c16476de436
--- /dev/null
+++ b/lib/commands/CLUSTER_ADDSLOTS.spec.ts
@@ -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']
+ );
+ });
+ });
+});
diff --git a/lib/commands/CLUSTER_ADDSLOTS.ts b/lib/commands/CLUSTER_ADDSLOTS.ts
new file mode 100644
index 00000000000..594eae77c75
--- /dev/null
+++ b/lib/commands/CLUSTER_ADDSLOTS.ts
@@ -0,0 +1,15 @@
+import { transformReplyString } from './generic-transformers';
+
+export function transformArguments(slots: number | Array