From 4e6d018d77c259f7ff7cc9137153b4ffc4cdbba0 Mon Sep 17 00:00:00 2001 From: Leibale Eidelman Date: Thu, 2 Sep 2021 10:04:48 -0400 Subject: [PATCH] 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 | 1092 +- 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, 28847 insertions(+), 14559 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 34bfad2947..0000000000 --- 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 fd16de3088..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/** -coverage/** -**.md -**.log diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 81e5e9b9cf..0000000000 --- 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 1893f87aad..0000000000 --- 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 c74eb12b80..9b181d4314 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 98e3d31260..d4f8b8f2d9 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 3ec398bb62..b6e5802a91 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 98c615d0c5..0000000000 --- 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 0000000000..16ca16b560 --- /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 d110707ee0..0000000000 --- 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 06b0e57ec3..028600f1a1 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 7a2e00a9c9..0000000000 --- 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 64b4143dc6..0bdff14c7f 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 605ed4543c..fa2d884112 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 0000000000..925d954248 --- /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 1ca516a86c..0000000000 --- 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 186da332a4..d009571401 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 2adee6acb3..0000000000 --- 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 d9fac6a450..fbad520508 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 db86cc4de7..5cb3bb4180 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 10f0c04488..acc229b69c 100644 --- a/README.md +++ b/README.md @@ -1,1008 +1,290 @@

- - + +

Node Redis

-

A high performance Node.js Redis client.

---- - -

- NPM downloads - NPM version - Build Status - Windows Build Status - Coverage Status - - Coverage Status - -

+ --- ## Installation ```bash -npm install redis +npm install redis@next ``` +> :warning: The new interface is clean and cool, but if you have an existing code base, you'll want to read the [migration guide](./docs/v3-to-v4.md). + ## Usage -#### Example +### Basic Example -```js -const redis = require("redis"); -const client = redis.createClient(); +```typescript +import { createClient } from 'redis'; -client.on("error", function(error) { - console.error(error); -}); +(async () => { + const client = createClient(); -client.set("key", "value", redis.print); -client.get("key", redis.print); + client.on('error', (err) => console.log('Redis Client Error', err)); + + await client.connect(); + + await client.set('key', 'value'); + const value = await client.get('key'); +})(); ``` -Note that the API is entirely asynchronous. To get data back from the server, -you'll need to use a callback. +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]`: -### 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); -``` - -### 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"` - -`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 - -| 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. | - -**`detect_buffers` example:** - -```js -const redis = require("redis"); -const client = redis.createClient({ detect_buffers: true }); - -client.set("foo_rand000000000000", "OK"); - -// This will return a JavaScript String -client.get("foo_rand000000000000", function(err, reply) { - console.log(reply.toString()); // Will print `OK` -}); - -// 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 `` -}); -``` - -**`retry_strategy` example:** - -```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"); +```typescript +createClient({ + socket: { + url: 'redis://alice:foobared@awesome.redis.server:6380' } - 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; - } - // reconnect after - return Math.min(options.attempt * 100, 3000); - }, }); ``` -### client.auth(password[, callback]) +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). -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`. +### Redis Commands -### client.quit(callback) +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.): -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. +```typescript +// raw Redis commands +await client.HSET('key', 'field', 'value'); +await client.HGETALL('key'); -### client.end(flush) +// friendly JavaScript commands +await client.hSet('key', 'field', 'value'); +await client.hGetAll('key'); +``` -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. +Modifiers to commands are specified using a JavaScript object: -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. - -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.' +```typescript +await client.set('key', 'value', { + EX: 10, + NX: true }); ``` -`client.end()` without the flush parameter set to true should NOT be used in production! +Replies will be transformed into useful data structures: -### 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"); - -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.hGetAll('key'); // { field1: 'value1', field2: 'value2' } +await client.hVals('key'); // ['value1', 'value2'] ``` -Every `ReplyError` contains the `command` name in all-caps and the arguments (`args`). +### Unsupported Redis Commands -If Node Redis emits a library error because of another error, the triggering -error is added to the returned error as `origin` attribute. +If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`: -**_Error codes_** +```typescript +await client.sendCommand(['SET', 'key', 'value', 'NX']); // 'OK' -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(); - -/* - * 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); -}); +await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2'] ``` -### Hash Commands +### Transactions (Multi/Exec) -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. +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: -#### client.hgetall(hash, callback) +```typescript +await client.set('another-key', 'another-value'); -The reply from an `HGETALL` command will be converted into a JavaScript Object. That way you can interact with the -responses using JavaScript syntax. - -**Example:** - -```js -client.hmset("key", "foo", "bar", "hello", "world"); - -client.hgetall("key", function(err, value) { - console.log(value.foo); // > "bar" - console.log(value.hello); // > "world" -}); +const [ setKeyReply, otherKeyValue ] = await client.multi() + .set('key', 'value') + .get('another-key') + .exec() +]); // ['OK', 'another-value'] ``` -#### client.hmset(hash, key1, val1, ...keyN, valN, [callback]) +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. -Multiple values may also be set by supplying more arguments. +To dig deeper into transactions, check out the [Isolated Execution Guide](./docs/isolated-execution.md). -**Example:** +### Blocking Commands -```js -// key -// 1) foo => bar -// 2) hello => world -client.HMSET("key", "foo", "bar", "hello", "world"); +Any command can be run on a new connection by specifying the `isolated` option. The newly created connection is closed when the command's `Promise` is fulfilled. + +This pattern works especially well for blocking commands—such as `BLPOP` and `BLMOVE`: + +```typescript +import { commandOptions } from 'redis'; + +const blPopPromise = client.blPop( + commandOptions({ isolated: true }), + 'key' +); + +await client.lPush('key', ['1', '2']); + +await blPopPromise; // '2' ``` -### PubSub +To learn more about isolated execution, check out the [guide](./docs/isolated-execution.md). -#### Example +### Pub/Sub -This example opens two client connections, subscribes to a channel on one of them, and publishes to that -channel on the other. +Subscribing to a channel requires a dedicated stand-alone connection. You can easily get one by `.duplicate()`ing an existing Redis connection. -```js -const redis = require("redis"); +```typescript +const subscriber = client.duplicate(); -const subscriber = redis.createClient(); -const publisher = redis.createClient(); - -let messageCount = 0; - -subscriber.on("subscribe", function(channel, count) { - publisher.publish("a channel", "a message"); - publisher.publish("a channel", "another message"); -}); - -subscriber.on("message", function(channel, message) { - messageCount += 1; - - console.log("Subscriber received message in channel '" + channel + "': " + message); - - if (messageCount === 2) { - subscriber.unsubscribe(); - subscriber.quit(); - publisher.quit(); - } -}); - -subscriber.subscribe("a channel"); +await subscriber.connect(); ``` -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. +Once you have one, simply subscribe and unsubscribe as needed: -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). +```typescript +await subscriber.subscribe('channel', message => { + console.log(message); // 'message' +}); -#### Subscriber Events +await subscriber.pSubscribe('channe*', (message, channel) => { + console.log(message, channel); // 'message', 'channel' +}); -If a client has subscriptions active, it may emit these events: +await subscriber.unsubscribe('channel'); -**"message" (channel, message)**: +await subscriber.pUnsubscribe('channe*'); +``` -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`. +Publish a message on a channel: -**"pmessage" (pattern, channel, message)**: +```typescript +await publisher.publish('channel', 'message'); +``` -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`. +### Scan Iterator -**"message_buffer" (channel, message)**: +[`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): -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; +```typescript +for await (const key of client.scanIterator()) { + // use the key! + await client.get(key); } - -// 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()); - }); - }); ``` -#### Multi.exec([callback]) +This works with `HSCAN`, `SSCAN`, and `ZSCAN` too: -`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. +```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')) {} +``` -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 override the default options by providing a configuration object: -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); - -// 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 +client.scanIterator({ + TYPE: 'string', // `SCAN` only + MATCH: 'patter*', + COUNT: 100 }); ``` -In addition to adding commands to the `MULTI` queue individually, you can also -pass an array of commands and arguments to the constructor: +### Lua Scripts -```js -const redis = require("redis"); +Define new functions using [Lua scripts](https://redis.io/commands/eval) which execute on the Redis server: -const client = redis.createClient(); +```typescript +import { createClient, defineScript } from 'redis'; -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 - */ - }); - }); -}); -``` - -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); +(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; + } + }) } + }); - clients.watcher.quit(); - clients.modifier.quit(); - }); - }, 1000); -}); + await client.connect(); + + await client.set('key', '1'); + await client.add('key', 2); // 3 +})(); ``` -#### `WATCH` limitations +### Cluster -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. +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: -The following example would watch the keys `foo` and `hello`, not the field `hello` -of hash `foo`: +```typescript +import { createCluster } from 'redis'; -```js -const redis = require("redis"); +(async () => { + const cluster = createCluster({ + rootNodes: [{ + host: '10.0.0.1', + port: 30001 + }, { + host: '10.0.0.2', + port: 30002 + }] + }); -const client = redis.createClient(); + cluster.on('error', (err) => console.log('Redis Cluster Error', err)); -client.hget("foo", "hello", function(hashGetError, result) { - if (hashGetError) throw hashGetError; + await cluster.connect(); - //Do some processing with the value from this field and watch it after - - 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. - */ - }); -}); + await cluster.set('key', 'value'); + const value = await cluster.get('key'); +})(); ``` -This limitation also applies to sets (you can not watch individual set members) -and any other collections. +### Auto-Pipelining -### client.batch([commands]) +Node Redis will automatically pipeline requests that are made during the same "tick". -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 - -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. - -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 +client.set('Tm9kZSBSZWRpcw==', 'users:1'); +client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw=='); ``` -## Extras +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()`. -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. - -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 ] -``` - -### `redis.print()` - -A handy callback function for displaying return values when testing. Example: - -```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(); -}); -``` - -### 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"); - -client - .multi() - .script("load", "return 1") - .exec(); - -client.multi([["script", "load", "return 1"]]).exec(); -``` - -### `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. - -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. - -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. - -### `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"]; - -client.zadd(args, function(addError, addResponse) { - if (addError) throw addError; - console.log("added " + addResponse + " items."); - - // -Infinity and +Infinity also work - const args1 = ["myzset", "+inf", "-inf"]; - client.zrevrangebyscore(args1, function(rangeError, rangeResponse) { - if (rangeError) throw rangeError; - console.log("response1", rangeResponse); - // ... - }); - - 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); - // ... - }); -}); -``` - -## Performance - -Much effort has been spent to make Node Redis as fast as possible for common operations. - -``` -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_**: - -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) -``` - -Debug stack trace: - -``` -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) +```typescript +await Promise.all([ + client.set('Tm9kZSBSZWRpcw==', 'users:1'), + client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==') +]); ``` ## Contributing -Please see the [contributing guide](CONTRIBUTING.md). +If you'd like to contribute, check out the [contributing guide](CONTRIBUTING.md). + +Thank you to all the people who already contributed to Node Redis! + + ## License diff --git a/benchmark/.gitignore b/benchmark/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /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 0000000000..37f8817665 --- /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 0000000000..a16a420f8c --- /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 0000000000..5226a5b0c8 --- /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 c3ed47a5fb..0000000000 --- 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 86cf9329ce..0000000000 --- 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 0000000000..b5074e7302 --- /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 0000000000..4b93340ad8 --- /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 0000000000..78b34252a0 --- /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 0000000000..7c3e988043 --- /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 e36b700656..0000000000 --- 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 0e9140c6b2..0000000000 --- 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 cffdb548a1..0000000000 --- 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 8128b41bdc..0000000000 --- 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 0bacd723b3..0000000000 --- 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 e774ac44e0..0000000000 --- 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 93d5d100f5..0000000000 --- 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 e0e3bf13fc..0000000000 --- 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 3a0ceaec6a..0000000000 --- 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 ecd2427404..0000000000 --- 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 d0970eb502..0000000000 --- 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 e6b67ea6ae..0000000000 --- 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 2fc2c3aefb..0000000000 --- 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 b09b06fbbf..0000000000 --- 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 726e4adf92..0000000000 --- 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 5677c12919..0000000000 --- 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 355dd94abc..0000000000 --- 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 b51aef2d11..0000000000 --- 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 ba5950d038..0000000000 --- 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 fe79c5f393..0000000000 --- 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 0000000000..408cbe3b99 --- /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 0000000000..f73049d228 --- /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 0000000000..a8da7f5ddd --- /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 0000000000..3e255fc2a6 --- /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 0000000000..b7dbe50c90 --- /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 0000000000..6da6dc55f4 --- /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 0000000000..2096258046 --- /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 717115c82e..0000000000 --- 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 0000000000..a38330abad --- /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 0000000000..51adc417ba --- /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 0000000000..1890e0a00a --- /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 a3b5189698..0000000000 --- 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 0000000000..77ed1cb7a0 --- /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 0000000000..d1620ef158 --- /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 0000000000..c64e8db196 --- /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 0000000000..7fb4904be4 --- /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 0000000000..a288a4f714 --- /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 0000000000..ec55aebdb0 --- /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 0000000000..c43cdc364a --- /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 0000000000..876a723c39 --- /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 0000000000..ab6bae762f --- /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 0000000000..3f2845b907 --- /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 0000000000..d173d7f135 --- /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 0000000000..59418614ed --- /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 0000000000..3ce76ce456 --- /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 0000000000..ed0590b578 --- /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 0000000000..3f0e628d9f --- /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 0000000000..30b8ccb20c --- /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 0000000000..b34c7bb0e6 --- /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 0000000000..5b9c7b84cc --- /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 0000000000..f3badfcdca --- /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 0000000000..b2829ca964 --- /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 0000000000..14b76725fc --- /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 0000000000..f9e837a434 --- /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 0000000000..a933057ea9 --- /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 0000000000..3fc70649f8 --- /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 0000000000..283ab80795 --- /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 0000000000..f64e835113 --- /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 0000000000..3da2015199 --- /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 0000000000..3f836131ac --- /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 0000000000..1907488346 --- /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 0000000000..750f0f5435 --- /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 0000000000..d0e150e155 --- /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 0000000000..52d3f4df9d --- /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 0000000000..8e4de5eef5 --- /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 0000000000..f09f906ade --- /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 0000000000..bf4cf39cab --- /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 0000000000..1aececc377 --- /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 0000000000..4d6d9d11c1 --- /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 0000000000..445c26e28a --- /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 0000000000..aa863e5f2d --- /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 0000000000..fe7d339f5d --- /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 0000000000..ad08e708c5 --- /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 0000000000..bebc45b03c --- /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 0000000000..b942864758 --- /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 0000000000..74a2eed4aa --- /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 0000000000..651dd09eaf --- /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 0000000000..7c352951fb --- /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 0000000000..9a7d0bbc37 --- /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 0000000000..a03c278309 --- /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 0000000000..08bcf5e4d9 --- /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 0000000000..f6a3bc1b8a --- /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 0000000000..c4bcc321b2 --- /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 0000000000..ccd84272a5 --- /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 0000000000..8b8977f9b3 --- /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 0000000000..0c299cdb9d --- /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 0000000000..cb7dfd9f73 --- /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 0000000000..baeab148eb --- /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 0000000000..ee87df4a19 --- /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 0000000000..8dd30b7059 --- /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 0000000000..c16476de43 --- /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 0000000000..594eae77c7 --- /dev/null +++ b/lib/commands/CLUSTER_ADDSLOTS.ts @@ -0,0 +1,15 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(slots: number | Array): Array { + const args = ['CLUSTER', 'ADDSLOTS']; + + if (typeof slots === 'number') { + args.push(slots.toString()); + } else { + args.push(...slots.map(String)); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts b/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts new file mode 100644 index 0000000000..f91a9a70cf --- /dev/null +++ b/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './CLUSTER_FLUSHSLOTS'; + +describe('CLUSTER FLUSHSLOTS', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['CLUSTER', 'FLUSHSLOTS'] + ); + }); +}); diff --git a/lib/commands/CLUSTER_FLUSHSLOTS.ts b/lib/commands/CLUSTER_FLUSHSLOTS.ts new file mode 100644 index 0000000000..28fbcc1fab --- /dev/null +++ b/lib/commands/CLUSTER_FLUSHSLOTS.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['CLUSTER', 'FLUSHSLOTS']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts b/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts new file mode 100644 index 0000000000..bb20f7521d --- /dev/null +++ b/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './CLUSTER_GETKEYSINSLOT'; + +describe('CLUSTER GETKEYSINSLOT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(0, 10), + ['CLUSTER', 'GETKEYSINSLOT', '0', '10'] + ); + }); +}); diff --git a/lib/commands/CLUSTER_GETKEYSINSLOT.ts b/lib/commands/CLUSTER_GETKEYSINSLOT.ts new file mode 100644 index 0000000000..c5719848cf --- /dev/null +++ b/lib/commands/CLUSTER_GETKEYSINSLOT.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(slot: number, count: number): Array { + return ['CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/CLUSTER_INFO.spec.ts b/lib/commands/CLUSTER_INFO.spec.ts new file mode 100644 index 0000000000..ce41151b67 --- /dev/null +++ b/lib/commands/CLUSTER_INFO.spec.ts @@ -0,0 +1,64 @@ +import { strict as assert } from 'assert'; +import { itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments, transformReply } from './CLUSTER_INFO'; + +describe('CLUSTER INFO', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['CLUSTER', 'INFO'] + ); + }); + + it('transformReply', () => { + assert.deepEqual( + transformReply([ + 'cluster_state:ok', + 'cluster_slots_assigned:16384', + 'cluster_slots_ok:16384', + 'cluster_slots_pfail:0', + 'cluster_slots_fail:0', + 'cluster_known_nodes:6', + 'cluster_size:3', + 'cluster_current_epoch:6', + 'cluster_my_epoch:2', + 'cluster_stats_messages_sent:1483972', + 'cluster_stats_messages_received:1483968' + ].join('\r\n')), + { + state: 'ok', + slots: { + assigned: 16384, + ok: 16384, + pfail: 0, + fail: 0 + }, + knownNodes: 6, + size: 3, + currentEpoch: 6, + myEpoch: 2, + stats: { + messagesSent: 1483972, + messagesReceived: 1483968 + } + } + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.clusterInfo', async cluster => { + const info = await cluster.clusterInfo(); + assert.equal(info.state, 'ok'); + assert.deepEqual(info.slots, { + assigned: 16384, + ok: 16384, + pfail: 0, + fail: 0 + }); + assert.equal(info.knownNodes, 3); + assert.equal(info.size, 3); + assert.equal(typeof info.currentEpoch, 'number'); + assert.equal(typeof info.myEpoch, 'number'); + assert.equal(typeof info.stats.messagesReceived, 'number'); + assert.equal(typeof info.stats.messagesSent, 'number'); + }); +}); diff --git a/lib/commands/CLUSTER_INFO.ts b/lib/commands/CLUSTER_INFO.ts new file mode 100644 index 0000000000..634515f927 --- /dev/null +++ b/lib/commands/CLUSTER_INFO.ts @@ -0,0 +1,47 @@ +export function transformArguments(): Array { + return ['CLUSTER', 'INFO']; +} + +interface ClusterInfoReply { + state: string; + slots: { + assigned: number; + ok: number; + pfail: number; + fail: number; + }; + knownNodes: number; + size: number; + currentEpoch: number; + myEpoch: number; + stats: { + messagesSent: number; + messagesReceived: number; + }; +} + +export function transformReply(reply: string): ClusterInfoReply { + const lines = reply.split('\r\n'); + + return { + state: extractLineValue(lines[0]), + slots: { + assigned: Number(extractLineValue(lines[1])), + ok: Number(extractLineValue(lines[2])), + pfail: Number(extractLineValue(lines[3])), + fail: Number(extractLineValue(lines[4])) + }, + knownNodes: Number(extractLineValue(lines[5])), + size: Number(extractLineValue(lines[6])), + currentEpoch: Number(extractLineValue(lines[7])), + myEpoch: Number(extractLineValue(lines[8])), + stats: { + messagesSent: Number(extractLineValue(lines[9])), + messagesReceived: Number(extractLineValue(lines[10])) + } + }; +} + +export function extractLineValue(line: string): string { + return line.substring(line.indexOf(':') + 1); +} diff --git a/lib/commands/CLUSTER_MEET.spec.ts b/lib/commands/CLUSTER_MEET.spec.ts new file mode 100644 index 0000000000..50a5393efa --- /dev/null +++ b/lib/commands/CLUSTER_MEET.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './CLUSTER_MEET'; + +describe('CLUSTER MEET', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('127.0.0.1', 6379), + ['CLUSTER', 'MEET', '127.0.0.1', '6379'] + ); + }); +}); diff --git a/lib/commands/CLUSTER_MEET.ts b/lib/commands/CLUSTER_MEET.ts new file mode 100644 index 0000000000..19da150356 --- /dev/null +++ b/lib/commands/CLUSTER_MEET.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(ip: string, port: number): Array { + return ['CLUSTER', 'MEET', ip, port.toString()]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/CLUSTER_NODES.spec.ts b/lib/commands/CLUSTER_NODES.spec.ts new file mode 100644 index 0000000000..1f0e9dd425 --- /dev/null +++ b/lib/commands/CLUSTER_NODES.spec.ts @@ -0,0 +1,116 @@ +import { strict as assert } from 'assert'; +import { itWithCluster, TestRedisClusters } from '../test-utils'; +import { RedisClusterNodeLinkStates, transformArguments, transformReply } from './CLUSTER_NODES'; + +describe('CLUSTER NODES', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['CLUSTER', 'NODES'] + ); + }); + + describe('transformReply', () => { + it('simple', () => { + assert.deepEqual( + transformReply([ + 'master 127.0.0.1:30001@31001 myself,master - 0 0 1 connected 0-16384', + 'slave 127.0.0.1:30002@31002 slave master 0 0 1 connected', + '' + ].join('\n')), + [{ + id: 'master', + url: '127.0.0.1:30001@31001', + host: '127.0.0.1', + port: 30001, + cport: 31001, + flags: ['myself', 'master'], + pingSent: 0, + pongRecv: 0, + configEpoch: 1, + linkState: RedisClusterNodeLinkStates.CONNECTED, + slots: [{ + from: 0, + to: 16384 + }], + replicas: [{ + id: 'slave', + url: '127.0.0.1:30002@31002', + host: '127.0.0.1', + port: 30002, + cport: 31002, + flags: ['slave'], + pingSent: 0, + pongRecv: 0, + configEpoch: 1, + linkState: RedisClusterNodeLinkStates.CONNECTED + }] + }] + ); + }); + + it.skip('with importing slots', () => { + assert.deepEqual( + transformReply( + 'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0-<-16384\n' + ), + [{ + id: 'id', + url: '127.0.0.1:30001@31001', + host: '127.0.0.1', + port: 30001, + cport: 31001, + flags: ['master'], + pingSent: 0, + pongRecv: 0, + configEpoch: 0, + linkState: RedisClusterNodeLinkStates.CONNECTED, + slots: [], // TODO + replicas: [] + }] + ); + }); + + it.skip('with migrating slots', () => { + assert.deepEqual( + transformReply( + 'id 127.0.0.1:30001@31001 master - 0 0 0 connected 0->-16384\n' + ), + [{ + id: 'id', + url: '127.0.0.1:30001@31001', + host: '127.0.0.1', + port: 30001, + cport: 31001, + flags: ['master'], + pingSent: 0, + pongRecv: 0, + configEpoch: 0, + linkState: RedisClusterNodeLinkStates.CONNECTED, + slots: [], // TODO + replicas: [] + }] + ); + }); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.clusterNodes', async cluster => { + for (const node of (await cluster.clusterNodes())) { + assert.equal(typeof node.id, 'string'); + assert.equal(typeof node.url, 'string'); + assert.equal(typeof node.host, 'string'); + assert.equal(typeof node.port, 'number'); + assert.equal(typeof node.cport, 'number'); + assert.ok(Array.isArray(node.flags)); + assert.equal(typeof node.pingSent, 'number'); + assert.equal(typeof node.pongRecv, 'number'); + assert.equal(typeof node.configEpoch, 'number'); + assert.equal(typeof node.linkState, 'string'); + + for (const slot of node.slots) { + assert.equal(typeof slot.from, 'number'); + assert.equal(typeof slot.to, 'number'); + } + } + }); +}); diff --git a/lib/commands/CLUSTER_NODES.ts b/lib/commands/CLUSTER_NODES.ts new file mode 100644 index 0000000000..d04ffc10a1 --- /dev/null +++ b/lib/commands/CLUSTER_NODES.ts @@ -0,0 +1,96 @@ +export function transformArguments(): Array { + return ['CLUSTER', 'NODES']; +} + +export enum RedisClusterNodeLinkStates { + CONNECTED = 'connected', + DISCONNECTED = 'disconnected' +} + +interface RedisClusterNodeTransformedUrl { + host: string; + port: number; + cport: number; +} + +export interface RedisClusterReplicaNode extends RedisClusterNodeTransformedUrl { + id: string; + url: string; + flags: Array; + pingSent: number; + pongRecv: number; + configEpoch: number; + linkState: RedisClusterNodeLinkStates; +} + +export interface RedisClusterMasterNode extends RedisClusterReplicaNode { + slots: Array<{ + from: number; + to: number; + }>; + replicas: Array; +} + +export function transformReply(reply: string): Array { + const lines = reply.split('\n'); + lines.pop(); // last line is empty + + const mastersMap = new Map(), + replicasMap = new Map>(); + + for (const line of lines) { + const [id, url, flags, masterId, pingSent, pongRecv, configEpoch, linkState, ...slots] = line.split(' '), + node = { + id, + url, + ...transformNodeUrl(url), + flags: flags.split(','), + pingSent: Number(pingSent), + pongRecv: Number(pongRecv), + configEpoch: Number(configEpoch), + linkState: (linkState as RedisClusterNodeLinkStates) + }; + + if (masterId === '-') { + let replicas = replicasMap.get(id); + if (!replicas) { + replicas = []; + replicasMap.set(id, replicas); + } + + mastersMap.set(id, { + ...node, + slots: slots.map(slot => { + // TODO: importing & exporting (https://redis.io/commands/cluster-nodes#special-slot-entries) + const [fromString, toString] = slot.split('-', 2), + from = Number(fromString); + return { + from, + to: toString ? Number(toString) : from + }; + }), + replicas + }); + } else { + const replicas = replicasMap.get(masterId); + if (!replicas) { + replicasMap.set(masterId, [node]); + } else { + replicas.push(node); + } + } + } + + return [...mastersMap.values()]; +} + +function transformNodeUrl(url: string): RedisClusterNodeTransformedUrl { + const indexOfColon = url.indexOf(':'), + indexOfAt = url.indexOf('@', indexOfColon); + + return { + host: url.substring(0, indexOfColon), + port: Number(url.substring(indexOfColon + 1, indexOfAt)), + cport: Number(url.substring(indexOfAt + 1)) + }; +} diff --git a/lib/commands/CLUSTER_RESET.spec.ts b/lib/commands/CLUSTER_RESET.spec.ts new file mode 100644 index 0000000000..c077e7f887 --- /dev/null +++ b/lib/commands/CLUSTER_RESET.spec.ts @@ -0,0 +1,27 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './CLUSTER_RESET'; + +describe('CLUSTER RESET', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['CLUSTER', 'RESET'] + ); + }); + + it('HARD', () => { + assert.deepEqual( + transformArguments('HARD'), + ['CLUSTER', 'RESET', 'HARD'] + ); + }); + + it('SOFT', () => { + assert.deepEqual( + transformArguments('SOFT'), + ['CLUSTER', 'RESET', 'SOFT'] + ); + }); + }); +}); diff --git a/lib/commands/CLUSTER_RESET.ts b/lib/commands/CLUSTER_RESET.ts new file mode 100644 index 0000000000..ec27b45eeb --- /dev/null +++ b/lib/commands/CLUSTER_RESET.ts @@ -0,0 +1,15 @@ +import { transformReplyString } from './generic-transformers'; + +export type ClusterResetModes = 'HARD' | 'SOFT'; + +export function transformArguments(mode?: ClusterResetModes): Array { + const args = ['CLUSTER', 'RESET']; + + if (mode) { + args.push(mode); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/CLUSTER_SETSLOT.spec.ts b/lib/commands/CLUSTER_SETSLOT.spec.ts new file mode 100644 index 0000000000..0f46aafd13 --- /dev/null +++ b/lib/commands/CLUSTER_SETSLOT.spec.ts @@ -0,0 +1,20 @@ +import { strict as assert } from 'assert'; +import { ClusterSlotStates, transformArguments } from './CLUSTER_SETSLOT'; + +describe('CLUSTER SETSLOT', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(0, ClusterSlotStates.IMPORTING), + ['CLUSTER', 'SETSLOT', '0', 'IMPORTING'] + ); + }); + + it('with nodeId', () => { + assert.deepEqual( + transformArguments(0, ClusterSlotStates.IMPORTING, 'nodeId'), + ['CLUSTER', 'SETSLOT', '0', 'IMPORTING', 'nodeId'] + ); + }); + }); +}); diff --git a/lib/commands/CLUSTER_SETSLOT.ts b/lib/commands/CLUSTER_SETSLOT.ts new file mode 100644 index 0000000000..c665b34962 --- /dev/null +++ b/lib/commands/CLUSTER_SETSLOT.ts @@ -0,0 +1,20 @@ +import { transformReplyString } from './generic-transformers'; + +export enum ClusterSlotStates { + IMPORTING = 'IMPORTING', + MIGRATING = 'MIGRATING', + STABLE = 'STABLE', + NODE = 'NODE' +} + +export function transformArguments(slot: number, state: ClusterSlotStates, nodeId?: string): Array { + const args = ['CLUSTER', 'SETSLOT', slot.toString(), state]; + + if (nodeId) { + args.push(nodeId); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/CONFIG_GET.spec.ts b/lib/commands/CONFIG_GET.spec.ts new file mode 100644 index 0000000000..83b5c410cf --- /dev/null +++ b/lib/commands/CONFIG_GET.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './CONFIG_GET'; + +describe('CONFIG GET', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('*'), + ['CONFIG', 'GET', '*'] + ); + }); +}); diff --git a/lib/commands/CONFIG_GET.ts b/lib/commands/CONFIG_GET.ts new file mode 100644 index 0000000000..423683c13a --- /dev/null +++ b/lib/commands/CONFIG_GET.ts @@ -0,0 +1,7 @@ +import { transformReplyTuples } from './generic-transformers'; + +export function transformArguments(parameter: string): Array { + return ['CONFIG', 'GET', parameter]; +} + +export const transformReply = transformReplyTuples; diff --git a/lib/commands/CONFIG_RESETSTAT.spec.ts b/lib/commands/CONFIG_RESETSTAT.spec.ts new file mode 100644 index 0000000000..d3f3048b94 --- /dev/null +++ b/lib/commands/CONFIG_RESETSTAT.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './CONFIG_RESETSTAT'; + +describe('CONFIG RESETSTAT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['CONFIG', 'RESETSTAT'] + ); + }); +}); diff --git a/lib/commands/CONFIG_RESETSTAT.ts b/lib/commands/CONFIG_RESETSTAT.ts new file mode 100644 index 0000000000..3c87b08d88 --- /dev/null +++ b/lib/commands/CONFIG_RESETSTAT.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['CONFIG', 'RESETSTAT']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/CONFIG_REWRITE.spec.ts b/lib/commands/CONFIG_REWRITE.spec.ts new file mode 100644 index 0000000000..cbc3e5b59d --- /dev/null +++ b/lib/commands/CONFIG_REWRITE.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './CONFIG_REWRITE'; + +describe('CONFIG REWRITE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['CONFIG', 'REWRITE'] + ); + }); +}); diff --git a/lib/commands/CONFIG_REWRITE.ts b/lib/commands/CONFIG_REWRITE.ts new file mode 100644 index 0000000000..0624751712 --- /dev/null +++ b/lib/commands/CONFIG_REWRITE.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['CONFIG', 'REWRITE']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/CONFIG_SET.spec.ts b/lib/commands/CONFIG_SET.spec.ts new file mode 100644 index 0000000000..3127e9732b --- /dev/null +++ b/lib/commands/CONFIG_SET.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './CONFIG_SET'; + +describe('CONFIG SET', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('parameter', 'value'), + ['CONFIG', 'SET', 'parameter', 'value'] + ); + }); +}); diff --git a/lib/commands/CONFIG_SET.ts b/lib/commands/CONFIG_SET.ts new file mode 100644 index 0000000000..894a95cb1c --- /dev/null +++ b/lib/commands/CONFIG_SET.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(parameter: string, value: string): Array { + return ['CONFIG', 'SET', parameter, value]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/COPY.spec.ts b/lib/commands/COPY.spec.ts new file mode 100644 index 0000000000..fb35be863a --- /dev/null +++ b/lib/commands/COPY.spec.ts @@ -0,0 +1,67 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments, transformReply } from './COPY'; + +describe('COPY', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('source', 'destination'), + ['COPY', 'source', 'destination'] + ); + }); + + it('with destination DB flag', () => { + assert.deepEqual( + transformArguments('source', 'destination', { + destinationDb: 1 + }), + ['COPY', 'source', 'destination', 'DB', '1'] + ); + }); + + it('with replace flag', () => { + assert.deepEqual( + transformArguments('source', 'destination', { + replace: true + }), + ['COPY', 'source', 'destination', 'REPLACE'] + ); + }); + + it('with both flags', () => { + assert.deepEqual( + transformArguments('source', 'destination', { + destinationDb: 1, + replace: true + }), + ['COPY', 'source', 'destination', 'DB', '1', 'REPLACE'] + ); + }); + }); + + describe('transformReply', () => { + it('0', () => { + assert.equal( + transformReply(0), + false + ); + }); + + it('1', () => { + assert.equal( + transformReply(1), + true + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.copy', async client => { + assert.equal( + await client.copy('source', 'destination'), + false + ); + }); +}); diff --git a/lib/commands/COPY.ts b/lib/commands/COPY.ts new file mode 100644 index 0000000000..534b0d9c48 --- /dev/null +++ b/lib/commands/COPY.ts @@ -0,0 +1,24 @@ +import { transformReplyBoolean } from './generic-transformers'; + +interface CopyCommandOptions { + destinationDb?: number; + replace?: boolean; +} + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(source: string, destination: string, options?: CopyCommandOptions): Array { + const args = ['COPY', source, destination]; + + if (options?.destinationDb) { + args.push('DB', options.destinationDb.toString()); + } + + if (options?.replace) { + args.push('REPLACE'); + } + + return args; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/DBSIZE.spec.ts b/lib/commands/DBSIZE.spec.ts new file mode 100644 index 0000000000..87e3c15453 --- /dev/null +++ b/lib/commands/DBSIZE.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './DBSIZE'; + +describe('DBSIZE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['DBSIZE'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.dbSize', async client => { + assert.equal( + await client.dbSize(), + 0 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.dbSize', async cluster => { + assert.equal( + await cluster.dbSize(), + 0 + ); + }); +}); diff --git a/lib/commands/DBSIZE.ts b/lib/commands/DBSIZE.ts new file mode 100644 index 0000000000..72933930f7 --- /dev/null +++ b/lib/commands/DBSIZE.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const IS_READ_ONLY = true; + +export function transformArguments(): Array { + return ['DBSIZE']; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/DECR.spec.ts b/lib/commands/DECR.spec.ts new file mode 100644 index 0000000000..5b4b4f0fd3 --- /dev/null +++ b/lib/commands/DECR.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './DECR'; + +describe('DECR', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['DECR', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.decr', async client => { + assert.equal( + await client.decr('key'), + -1 + ); + }); +}); diff --git a/lib/commands/DECR.ts b/lib/commands/DECR.ts new file mode 100644 index 0000000000..cac6e07f05 --- /dev/null +++ b/lib/commands/DECR.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['DECR', key]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/DECRBY.spec.ts b/lib/commands/DECRBY.spec.ts new file mode 100644 index 0000000000..1c9ac69bb9 --- /dev/null +++ b/lib/commands/DECRBY.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './DECRBY'; + +describe('DECRBY', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 2), + ['DECRBY', 'key', '2'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.decrBy', async client => { + assert.equal( + await client.decrBy('key', 2), + -2 + ); + }); +}); diff --git a/lib/commands/DECRBY.ts b/lib/commands/DECRBY.ts new file mode 100644 index 0000000000..cc163cbe82 --- /dev/null +++ b/lib/commands/DECRBY.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, decrement: number): Array { + return ['DECRBY', key, decrement.toString()]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/DEL.spec.ts b/lib/commands/DEL.spec.ts new file mode 100644 index 0000000000..ec780de67a --- /dev/null +++ b/lib/commands/DEL.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './DEL'; + +describe('DEL', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['DEL', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['key1', 'key2']), + ['DEL', 'key1', 'key2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.del', async client => { + assert.equal( + await client.del('key'), + 0 + ); + }); +}); diff --git a/lib/commands/DEL.ts b/lib/commands/DEL.ts new file mode 100644 index 0000000000..3d9a78212f --- /dev/null +++ b/lib/commands/DEL.ts @@ -0,0 +1,7 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export function transformArguments(keys: string | Array): Array { + return pushVerdictArguments(['DEL'], keys); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/DISCARD.spec.ts b/lib/commands/DISCARD.spec.ts new file mode 100644 index 0000000000..b01f9d650d --- /dev/null +++ b/lib/commands/DISCARD.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './DISCARD'; + +describe('DISCARD', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['DISCARD'] + ); + }); +}); diff --git a/lib/commands/DISCARD.ts b/lib/commands/DISCARD.ts new file mode 100644 index 0000000000..b5aaf45cc8 --- /dev/null +++ b/lib/commands/DISCARD.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['DISCARD']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/DUMP.spec.ts b/lib/commands/DUMP.spec.ts new file mode 100644 index 0000000000..e3f42c5757 --- /dev/null +++ b/lib/commands/DUMP.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; + +describe('DUMP', () => { + itWithClient(TestRedisServers.OPEN, 'client.dump', async client => { + assert.equal( + await client.dump('key'), + null + ); + }); +}); diff --git a/lib/commands/DUMP.ts b/lib/commands/DUMP.ts new file mode 100644 index 0000000000..1c72110f21 --- /dev/null +++ b/lib/commands/DUMP.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(key: string): Array { + return ['DUMP', key]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/ECHO.spec.ts b/lib/commands/ECHO.spec.ts new file mode 100644 index 0000000000..4a1bf8fe37 --- /dev/null +++ b/lib/commands/ECHO.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './ECHO'; + +describe('ECHO', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('message'), + ['ECHO', 'message'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.echo', async client => { + assert.equal( + await client.echo('message'), + 'message' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.echo', async cluster => { + assert.equal( + await cluster.echo('message'), + 'message' + ); + }); +}); diff --git a/lib/commands/ECHO.ts b/lib/commands/ECHO.ts new file mode 100644 index 0000000000..007b8f2764 --- /dev/null +++ b/lib/commands/ECHO.ts @@ -0,0 +1,9 @@ +import { transformReplyString } from './generic-transformers'; + +export const IS_READ_ONLY = true; + +export function transformArguments(message: string): Array { + return ['ECHO', message]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/EVAL.spec.ts b/lib/commands/EVAL.spec.ts new file mode 100644 index 0000000000..2be1aedf08 --- /dev/null +++ b/lib/commands/EVAL.spec.ts @@ -0,0 +1,29 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './EVAL'; + +describe('EVAL', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('return KEYS[1] + ARGV[1]', { + keys: ['key'], + arguments: ['argument'] + }), + ['EVAL', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.eval', async client => { + assert.equal( + await client.eval('return 1'), + 1 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.eval', async cluster => { + assert.equal( + await cluster.eval('return 1'), + 1 + ); + }); +}); diff --git a/lib/commands/EVAL.ts b/lib/commands/EVAL.ts new file mode 100644 index 0000000000..89645df9f3 --- /dev/null +++ b/lib/commands/EVAL.ts @@ -0,0 +1,9 @@ +import { EvalOptions, pushEvalArguments } from './generic-transformers'; + +export function transformArguments(script: string, options?: EvalOptions): Array { + return pushEvalArguments(['EVAL', script], options); +} + +export function transformReply(reply: unknown): unknown { + return reply; +} diff --git a/lib/commands/EVALSHA.spec.ts b/lib/commands/EVALSHA.spec.ts new file mode 100644 index 0000000000..08b330ac4f --- /dev/null +++ b/lib/commands/EVALSHA.spec.ts @@ -0,0 +1,14 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './EVALSHA'; + +describe('EVALSHA', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('sha1', { + keys: ['key'], + arguments: ['argument'] + }), + ['EVALSHA', 'sha1', '1', 'key', 'argument'] + ); + }); +}); diff --git a/lib/commands/EVALSHA.ts b/lib/commands/EVALSHA.ts new file mode 100644 index 0000000000..a81595bc4c --- /dev/null +++ b/lib/commands/EVALSHA.ts @@ -0,0 +1,9 @@ +import { EvalOptions, pushEvalArguments } from './generic-transformers'; + +export function transformArguments(sha1: string, options?: EvalOptions): Array { + return pushEvalArguments(['EVALSHA', sha1], options); +} + +export function transformReply(reply: unknown): unknown { + return reply; +} diff --git a/lib/commands/EXISTS.spec.ts b/lib/commands/EXISTS.spec.ts new file mode 100644 index 0000000000..3cba44b563 --- /dev/null +++ b/lib/commands/EXISTS.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './EXISTS'; + +describe('EXISTS', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['EXISTS', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['EXISTS', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.exists', async client => { + assert.equal( + await client.exists('key'), + false + ); + }); +}); diff --git a/lib/commands/EXISTS.ts b/lib/commands/EXISTS.ts new file mode 100644 index 0000000000..5a76ca833f --- /dev/null +++ b/lib/commands/EXISTS.ts @@ -0,0 +1,11 @@ +import { pushVerdictArguments, transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(keys: string | Array): Array { + return pushVerdictArguments(['EXISTS'], keys); +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/EXPIRE.spec.ts b/lib/commands/EXPIRE.spec.ts new file mode 100644 index 0000000000..6550532cab --- /dev/null +++ b/lib/commands/EXPIRE.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './EXPIRE'; + +describe('EXPIRE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['EXPIRE', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.expire', async client => { + assert.equal( + await client.expire('key', 0), + false + ); + }); +}); diff --git a/lib/commands/EXPIRE.ts b/lib/commands/EXPIRE.ts new file mode 100644 index 0000000000..04b0504a6f --- /dev/null +++ b/lib/commands/EXPIRE.ts @@ -0,0 +1,7 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export function transformArguments(key: string, seconds: number): Array { + return ['EXPIRE', key, seconds.toString()]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/EXPIREAT.spec.ts b/lib/commands/EXPIREAT.spec.ts new file mode 100644 index 0000000000..cefe9fa9b8 --- /dev/null +++ b/lib/commands/EXPIREAT.spec.ts @@ -0,0 +1,29 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './EXPIREAT'; + +describe('EXPIREAT', () => { + describe('transformArguments', () => { + it('number', () => { + assert.deepEqual( + transformArguments('key', 1), + ['EXPIREAT', 'key', '1'] + ); + }); + + it('date', () => { + const d = new Date(); + assert.deepEqual( + transformArguments('key', d), + ['EXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString()] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.expireAt', async client => { + assert.equal( + await client.expireAt('key', 1), + false + ); + }); +}); diff --git a/lib/commands/EXPIREAT.ts b/lib/commands/EXPIREAT.ts new file mode 100644 index 0000000000..b7bfdcaa42 --- /dev/null +++ b/lib/commands/EXPIREAT.ts @@ -0,0 +1,11 @@ +import { transformEXAT, transformReplyBoolean } from './generic-transformers'; + +export function transformArguments(key: string, timestamp: number | Date): Array { + return [ + 'EXPIREAT', + key, + transformEXAT(timestamp) + ]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/FAILOVER.spec.ts b/lib/commands/FAILOVER.spec.ts new file mode 100644 index 0000000000..16094a0dbc --- /dev/null +++ b/lib/commands/FAILOVER.spec.ts @@ -0,0 +1,72 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './FAILOVER'; + +describe('FAILOVER', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['FAILOVER'] + ); + }); + + describe('with TO', () => { + it('simple', () => { + assert.deepEqual( + transformArguments({ + TO: { + host: 'host', + port: 6379 + } + }), + ['FAILOVER', 'TO', 'host', '6379'] + ); + }); + + it('with FORCE', () => { + assert.deepEqual( + transformArguments({ + TO: { + host: 'host', + port: 6379, + FORCE: true + } + }), + ['FAILOVER', 'TO', 'host', '6379', 'FORCE'] + ); + }); + }); + + it('with ABORT', () => { + assert.deepEqual( + transformArguments({ + ABORT: true + }), + ['FAILOVER', 'ABORT'] + ); + }); + + it('with TIMEOUT', () => { + assert.deepEqual( + transformArguments({ + TIMEOUT: 1 + }), + ['FAILOVER', 'TIMEOUT', '1'] + ); + }); + + it('with TO, ABORT, TIMEOUT', () => { + assert.deepEqual( + transformArguments({ + TO: { + host: 'host', + port: 6379 + }, + ABORT: true, + TIMEOUT: 1 + }), + ['FAILOVER', 'TO', 'host', '6379', 'ABORT', 'TIMEOUT', '1'] + ); + }); + }); +}); diff --git a/lib/commands/FAILOVER.ts b/lib/commands/FAILOVER.ts new file mode 100644 index 0000000000..11ccb32a5c --- /dev/null +++ b/lib/commands/FAILOVER.ts @@ -0,0 +1,35 @@ +import { transformReplyString } from './generic-transformers'; + +interface FailoverOptions { + TO?: { + host: string; + port: number; + FORCE?: true; + }; + ABORT?: true; + TIMEOUT?: number; +} + +export function transformArguments(options?: FailoverOptions): Array { + const args = ['FAILOVER']; + + if (options?.TO) { + args.push('TO', options.TO.host, options.TO.port.toString()); + + if (options.TO.FORCE) { + args.push('FORCE'); + } + } + + if (options?.ABORT) { + args.push('ABORT'); + } + + if (options?.TIMEOUT) { + args.push('TIMEOUT', options.TIMEOUT.toString()); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/FLUSHALL.spec.ts b/lib/commands/FLUSHALL.spec.ts new file mode 100644 index 0000000000..7f1c5ffd28 --- /dev/null +++ b/lib/commands/FLUSHALL.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { RedisFlushModes, transformArguments } from './FLUSHALL'; + +describe('FLUSHALL', () => { + describe('transformArguments', () => { + it('default', () => { + assert.deepEqual( + transformArguments(), + ['FLUSHALL'] + ); + }); + + it('ASYNC', () => { + assert.deepEqual( + transformArguments(RedisFlushModes.ASYNC), + ['FLUSHALL', 'ASYNC'] + ); + }); + + it('SYNC', () => { + assert.deepEqual( + transformArguments(RedisFlushModes.SYNC), + ['FLUSHALL', 'SYNC'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.flushAll', async client => { + assert.equal( + await client.flushAll(), + 'OK' + ); + }); +}); diff --git a/lib/commands/FLUSHALL.ts b/lib/commands/FLUSHALL.ts new file mode 100644 index 0000000000..4be3474f7e --- /dev/null +++ b/lib/commands/FLUSHALL.ts @@ -0,0 +1,18 @@ +import { transformReplyString } from './generic-transformers'; + +export enum RedisFlushModes { + ASYNC = 'ASYNC', + SYNC = 'SYNC' +} + +export function transformArguments(mode?: RedisFlushModes): Array { + const args = ['FLUSHALL']; + + if (mode) { + args.push(mode); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/FLUSHDB.spec.ts b/lib/commands/FLUSHDB.spec.ts new file mode 100644 index 0000000000..e237e52768 --- /dev/null +++ b/lib/commands/FLUSHDB.spec.ts @@ -0,0 +1,36 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { RedisFlushModes } from './FLUSHALL'; +import { transformArguments } from './FLUSHDB'; + +describe('FLUSHDB', () => { + describe('transformArguments', () => { + it('default', () => { + assert.deepEqual( + transformArguments(), + ['FLUSHDB'] + ); + }); + + it('ASYNC', () => { + assert.deepEqual( + transformArguments(RedisFlushModes.ASYNC), + ['FLUSHDB', 'ASYNC'] + ); + }); + + it('SYNC', () => { + assert.deepEqual( + transformArguments(RedisFlushModes.SYNC), + ['FLUSHDB', 'SYNC'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.flushDb', async client => { + assert.equal( + await client.flushDb(), + 'OK' + ); + }); +}); diff --git a/lib/commands/FLUSHDB.ts b/lib/commands/FLUSHDB.ts new file mode 100644 index 0000000000..a85c0933c4 --- /dev/null +++ b/lib/commands/FLUSHDB.ts @@ -0,0 +1,14 @@ +import { RedisFlushModes } from './FLUSHALL'; +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(mode?: RedisFlushModes): Array { + const args = ['FLUSHDB']; + + if (mode) { + args.push(mode); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/GEOADD.spec.ts b/lib/commands/GEOADD.spec.ts new file mode 100644 index 0000000000..673e962093 --- /dev/null +++ b/lib/commands/GEOADD.spec.ts @@ -0,0 +1,95 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './GEOADD'; + +describe('GEOADD', () => { + describe('transformArguments', () => { + it('one member', () => { + assert.deepEqual( + transformArguments('key', { + member: 'member', + longitude: 1, + latitude: 2 + }), + ['GEOADD', 'key', '1', '2', 'member'] + ); + }); + + it('multiple members', () => { + assert.deepEqual( + transformArguments('key', [{ + longitude: 1, + latitude: 2, + member: '3', + }, { + longitude: 4, + latitude: 5, + member: '6', + }]), + ['GEOADD', 'key', '1', '2', '3', '4', '5', '6'] + ); + }); + + it('with NX', () => { + assert.deepEqual( + transformArguments('key', { + longitude: 1, + latitude: 2, + member: 'member' + }, { + NX: true + }), + ['GEOADD', 'key', 'NX', '1', '2', 'member'] + ); + }); + + it('with CH', () => { + assert.deepEqual( + transformArguments('key', { + longitude: 1, + latitude: 2, + member: 'member' + }, { + CH: true + }), + ['GEOADD', 'key', 'CH', '1', '2', 'member'] + ); + }); + + it('with XX, CH', () => { + assert.deepEqual( + transformArguments('key', { + longitude: 1, + latitude: 2, + member: 'member' + }, { + XX: true, + CH: true + }), + ['GEOADD', 'key', 'XX', 'CH', '1', '2', 'member'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.geoAdd', async client => { + assert.equal( + await client.geoAdd('key', { + member: 'member', + longitude: 1, + latitude: 2 + }), + 1 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.geoAdd', async cluster => { + assert.equal( + await cluster.geoAdd('key', { + member: 'member', + longitude: 1, + latitude: 2 + }), + 1 + ); + }); +}); diff --git a/lib/commands/GEOADD.ts b/lib/commands/GEOADD.ts new file mode 100644 index 0000000000..1236563d54 --- /dev/null +++ b/lib/commands/GEOADD.ts @@ -0,0 +1,49 @@ +import { GeoCoordinates, transformReplyNumber } from './generic-transformers'; + +interface GeoMember extends GeoCoordinates { + member: string; +} + +interface NX { + NX?: true; +} + +interface XX { + XX?: true; +} + +type SetGuards = NX | XX; + +interface GeoAddCommonOptions { + CH?: true; +} + +type GeoAddOptions = SetGuards & GeoAddCommonOptions; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, toAdd: GeoMember | Array, options?: GeoAddOptions): Array { + const args = ['GEOADD', key]; + + if ((options as NX)?.NX) { + args.push('NX'); + } else if ((options as XX)?.XX) { + args.push('XX'); + } + + if (options?.CH) { + args.push('CH'); + } + + for (const { longitude, latitude, member } of (Array.isArray(toAdd) ? toAdd : [toAdd])) { + args.push( + longitude.toString(), + latitude.toString(), + member + ); + } + + return args; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/GEODIST.spec.ts b/lib/commands/GEODIST.spec.ts new file mode 100644 index 0000000000..c116825931 --- /dev/null +++ b/lib/commands/GEODIST.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './GEODIST'; + +describe('GEODIST', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key', '1', '2'), + ['GEODIST', 'key', '1', '2'] + ); + }); + + it('with unit', () => { + assert.deepEqual( + transformArguments('key', '1', '2', 'm'), + ['GEODIST', 'key', '1', '2', 'm'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.geoDist', async client => { + assert.equal( + await client.geoDist('key', '1', '2'), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.geoDist', async cluster => { + assert.equal( + await cluster.geoDist('key', '1', '2'), + null + ); + }); +}); diff --git a/lib/commands/GEODIST.ts b/lib/commands/GEODIST.ts new file mode 100644 index 0000000000..6fe6fbb47a --- /dev/null +++ b/lib/commands/GEODIST.ts @@ -0,0 +1,24 @@ +import { GeoUnits } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments( + key: string, + member1: string, + member2: string, + unit?: GeoUnits +): Array { + const args = ['GEODIST', key, member1, member2]; + + if (unit) { + args.push(unit); + } + + return args; +} + +export function transformReply(reply: string | null): number | null { + return reply === null ? null : Number(reply); +} diff --git a/lib/commands/GEOHASH.spec.ts b/lib/commands/GEOHASH.spec.ts new file mode 100644 index 0000000000..b79de23555 --- /dev/null +++ b/lib/commands/GEOHASH.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './GEOHASH'; + +describe('GEOHASH', () => { + describe('transformArguments', () => { + it('single member', () => { + assert.deepEqual( + transformArguments('key', 'member'), + ['GEOHASH', 'key', 'member'] + ); + }); + + it('multiple members', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['GEOHASH', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.geoHash', async client => { + assert.deepEqual( + await client.geoHash('key', 'member'), + [null] + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.geoHash', async cluster => { + assert.deepEqual( + await cluster.geoHash('key', 'member'), + [null] + ); + }); +}); diff --git a/lib/commands/GEOHASH.ts b/lib/commands/GEOHASH.ts new file mode 100644 index 0000000000..a46738955d --- /dev/null +++ b/lib/commands/GEOHASH.ts @@ -0,0 +1,11 @@ +import { pushVerdictArguments, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, member: string | Array): Array { + return pushVerdictArguments(['GEOHASH', key], member); +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/GEOPOS.spec.ts b/lib/commands/GEOPOS.spec.ts new file mode 100644 index 0000000000..98cfa6aa2d --- /dev/null +++ b/lib/commands/GEOPOS.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './GEOPOS'; + +describe('GEOPOS', () => { + describe('transformArguments', () => { + it('single member', () => { + assert.deepEqual( + transformArguments('key', 'member'), + ['GEOPOS', 'key', 'member'] + ); + }); + + it('multiple members', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['GEOPOS', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.geoPos', async client => { + assert.deepEqual( + await client.geoPos('key', 'member'), + [null] + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.geoPos', async cluster => { + assert.deepEqual( + await cluster.geoPos('key', 'member'), + [null] + ); + }); +}); diff --git a/lib/commands/GEOPOS.ts b/lib/commands/GEOPOS.ts new file mode 100644 index 0000000000..46b0a153ba --- /dev/null +++ b/lib/commands/GEOPOS.ts @@ -0,0 +1,21 @@ +import { pushVerdictArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, member: string | Array): Array { + return pushVerdictArguments(['GEOPOS', key], member); +} + +interface GeoCoordinates { + longitude: string; + latitude: string; +} + +export function transformReply(reply: Array<[string, string] | null>): Array { + return reply.map(coordinates => coordinates === null ? null : { + longitude: coordinates[0], + latitude: coordinates[1] + }); +} diff --git a/lib/commands/GEOSEARCH.spec.ts b/lib/commands/GEOSEARCH.spec.ts new file mode 100644 index 0000000000..a8606b3f74 --- /dev/null +++ b/lib/commands/GEOSEARCH.spec.ts @@ -0,0 +1,37 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './GEOSEARCH'; + +describe('GEOSEARCH', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'member', { + radius: 1, + unit: 'm' + }), + ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.geoSearch', async client => { + assert.deepEqual( + await client.geoSearch('key', 'member', { + radius: 1, + unit: 'm' + }), + [] + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.geoSearch', async cluster => { + assert.deepEqual( + await cluster.geoSearch('key', 'member', { + radius: 1, + unit: 'm' + }), + [] + ); + }); +}); diff --git a/lib/commands/GEOSEARCH.ts b/lib/commands/GEOSEARCH.ts new file mode 100644 index 0000000000..3872f11c6c --- /dev/null +++ b/lib/commands/GEOSEARCH.ts @@ -0,0 +1,16 @@ +import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments( + key: string, + from: GeoSearchFrom, + by: GeoSearchBy, + options?: GeoSearchOptions +): Array { + return pushGeoSearchArguments(['GEOSEARCH'], key, from, by, options); +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/GEOSEARCHSTORE.spec.ts b/lib/commands/GEOSEARCHSTORE.spec.ts new file mode 100644 index 0000000000..1983537077 --- /dev/null +++ b/lib/commands/GEOSEARCHSTORE.spec.ts @@ -0,0 +1,74 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './GEOSEARCHSTORE'; + +describe('GEOSEARCHSTORE', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('destination', 'source', 'member', { + radius: 1, + unit: 'm' + }, { + SORT: 'ASC', + COUNT: { + value: 1, + ANY: true + } + }), + ['GEOSEARCHSTORE', 'destination', 'source', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC', 'COUNT', '1', 'ANY'] + ); + }); + + it('with STOREDIST', () => { + assert.deepEqual( + transformArguments('destination', 'source', 'member', { + radius: 1, + unit: 'm' + }, { + SORT: 'ASC', + COUNT: { + value: 1, + ANY: true + }, + STOREDIST: true + }), + ['GEOSEARCHSTORE', 'destination', 'source', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC', 'COUNT', '1', 'ANY', 'STOREDIST'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.geoSearchStore', async client => { + await client.geoAdd('source', { + longitude: 1, + latitude: 1, + member: 'member' + }); + + assert.equal( + await client.geoSearchStore('destination', 'source', 'member', { + radius: 1, + unit: 'm' + }), + 1 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.geoSearchStore', async cluster => { + await cluster.geoAdd('{tag}source', { + longitude: 1, + latitude: 1, + member: 'member' + }); + + assert.equal( + await cluster.geoSearchStore('{tag}destination', '{tag}source', 'member', { + radius: 1, + unit: 'm' + }), + 1 + ); + }); +}); diff --git a/lib/commands/GEOSEARCHSTORE.ts b/lib/commands/GEOSEARCHSTORE.ts new file mode 100644 index 0000000000..e10622052b --- /dev/null +++ b/lib/commands/GEOSEARCHSTORE.ts @@ -0,0 +1,39 @@ +import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +interface GeoSearchStoreOptions extends GeoSearchOptions { + STOREDIST?: true; +} + +export function transformArguments( + destination: string, + source: string, + from: GeoSearchFrom, + by: GeoSearchBy, + options?: GeoSearchStoreOptions +): Array { + const args = pushGeoSearchArguments( + ['GEOSEARCHSTORE', destination], + source, + from, + by, + options + ); + + if (options?.STOREDIST) { + args.push('STOREDIST'); + } + + return args; +} + +export function transformReply(reply: number): number { + if (typeof reply !== 'number') { + throw new TypeError(`https://github.com/redis/redis/issues/9261`); + } + + return reply; +} diff --git a/lib/commands/GEOSEARCH_WITH.spec.ts b/lib/commands/GEOSEARCH_WITH.spec.ts new file mode 100644 index 0000000000..a400fb965c --- /dev/null +++ b/lib/commands/GEOSEARCH_WITH.spec.ts @@ -0,0 +1,42 @@ +import { strict as assert } from 'assert'; +import { TransformArgumentsReply } from '.'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster, describeHandleMinimumRedisVersion } from '../test-utils'; +import { GeoReplyWith } from './generic-transformers'; +import { transformArguments } from './GEOSEARCH_WITH'; + +describe('GEOSEARCH WITH', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + const expectedReply: TransformArgumentsReply = ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'WITHDIST'] + expectedReply.preserve = ['WITHDIST']; + + assert.deepEqual( + transformArguments('key', 'member', { + radius: 1, + unit: 'm' + }, [GeoReplyWith.DISTANCE]), + expectedReply + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.geoSearchWith', async client => { + assert.deepEqual( + await client.geoSearchWith('key', 'member', { + radius: 1, + unit: 'm' + }, [GeoReplyWith.DISTANCE]), + [] + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.geoSearchWith', async cluster => { + assert.deepEqual( + await cluster.geoSearchWith('key', 'member', { + radius: 1, + unit: 'm' + }, [GeoReplyWith.DISTANCE]), + [] + ); + }); +}); diff --git a/lib/commands/GEOSEARCH_WITH.ts b/lib/commands/GEOSEARCH_WITH.ts new file mode 100644 index 0000000000..ef19ca5dfc --- /dev/null +++ b/lib/commands/GEOSEARCH_WITH.ts @@ -0,0 +1,23 @@ +import { TransformArgumentsReply } from '.'; +import { GeoSearchFrom, GeoSearchBy, GeoReplyWith, GeoSearchOptions, transformGeoMembersWithReply } from './generic-transformers'; +import { transformArguments as geoSearchTransformArguments } from './GEOSEARCH'; + +export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEOSEARCH'; + +export function transformArguments( + key: string, + from: GeoSearchFrom, + by: GeoSearchBy, + replyWith: Array, + options?: GeoSearchOptions +): TransformArgumentsReply { + const args: TransformArgumentsReply = geoSearchTransformArguments(key, from, by, options); + + args.push(...replyWith); + + args.preserve = replyWith; + + return args; +} + +export const transformReply = transformGeoMembersWithReply; diff --git a/lib/commands/GET.spec.ts b/lib/commands/GET.spec.ts new file mode 100644 index 0000000000..303be60fe9 --- /dev/null +++ b/lib/commands/GET.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './GET'; + +describe('GET', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['GET', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.get', async client => { + assert.equal( + await client.get('key'), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.get', async cluster => { + assert.equal( + await cluster.get('key'), + null + ); + }); +}); diff --git a/lib/commands/GET.ts b/lib/commands/GET.ts new file mode 100644 index 0000000000..714ad953d8 --- /dev/null +++ b/lib/commands/GET.ts @@ -0,0 +1,11 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['GET', key]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/GETBIT.spec.ts b/lib/commands/GETBIT.spec.ts new file mode 100644 index 0000000000..7163b4ba25 --- /dev/null +++ b/lib/commands/GETBIT.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './GETBIT'; + +describe('GETBIT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0), + ['GETBIT', 'key', '0'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.getBit', async client => { + assert.equal( + await client.getBit('key', 0), + 0 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.getBit', async cluster => { + assert.equal( + await cluster.getBit('key', 0), + 0 + ); + }); +}); diff --git a/lib/commands/GETBIT.ts b/lib/commands/GETBIT.ts new file mode 100644 index 0000000000..c7e878f75a --- /dev/null +++ b/lib/commands/GETBIT.ts @@ -0,0 +1,11 @@ +import { transformReplyBit } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, offset: number): Array { + return ['GETBIT', key, offset.toString()]; +} + +export const transformReply = transformReplyBit; diff --git a/lib/commands/GETDEL.spec.ts b/lib/commands/GETDEL.spec.ts new file mode 100644 index 0000000000..232c08b950 --- /dev/null +++ b/lib/commands/GETDEL.spec.ts @@ -0,0 +1,29 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './GETDEL'; + +describe('GETDEL', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['GETDEL', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.getDel', async client => { + assert.equal( + await client.getDel('key'), + null + ); + }); + + + itWithCluster(TestRedisClusters.OPEN, 'cluster.getDel', async cluster => { + assert.equal( + await cluster.getDel('key'), + null + ); + }); +}); diff --git a/lib/commands/GETDEL.ts b/lib/commands/GETDEL.ts new file mode 100644 index 0000000000..218e057637 --- /dev/null +++ b/lib/commands/GETDEL.ts @@ -0,0 +1,9 @@ +import { transformReplyStringNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['GETDEL', key]; +} + +export const transformReply = transformReplyStringNull; diff --git a/lib/commands/GETEX.spec.ts b/lib/commands/GETEX.spec.ts new file mode 100644 index 0000000000..830f12cedf --- /dev/null +++ b/lib/commands/GETEX.spec.ts @@ -0,0 +1,96 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './GETEX'; + +describe('GETEX', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('EX', () => { + assert.deepEqual( + transformArguments('key', { + EX: 1 + }), + ['GETEX', 'key', 'EX', '1'] + ); + }); + + it('PX', () => { + assert.deepEqual( + transformArguments('key', { + PX: 1 + }), + ['GETEX', 'key', 'PX', '1'] + ); + }); + + describe('EXAT', () => { + it('number', () => { + assert.deepEqual( + transformArguments('key', { + EXAT: 1 + }), + ['GETEX', 'key', 'EXAT', '1'] + ); + }); + + it('date', () => { + const d = new Date(); + assert.deepEqual( + transformArguments('key', { + EXAT: d + }), + ['GETEX', 'key', 'EXAT', Math.floor(d.getTime() / 1000).toString()] + ); + }); + }); + + describe('PXAT', () => { + it('number', () => { + assert.deepEqual( + transformArguments('key', { + PXAT: 1 + }), + ['GETEX', 'key', 'PXAT', '1'] + ); + }); + + it('date', () => { + const d = new Date(); + assert.deepEqual( + transformArguments('key', { + PXAT: d + }), + ['GETEX', 'key', 'PXAT', d.getTime().toString()] + ); + }); + }); + + it('PERSIST', () => { + assert.deepEqual( + transformArguments('key', { + PERSIST: true + }), + ['GETEX', 'key', 'PERSIST'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.getEx', async client => { + assert.equal( + await client.getEx('key', { + PERSIST: true + }), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.getEx', async cluster => { + assert.equal( + await cluster.getEx('key', { + PERSIST: true + }), + null + ); + }); +}); diff --git a/lib/commands/GETEX.ts b/lib/commands/GETEX.ts new file mode 100644 index 0000000000..ca1465b7ee --- /dev/null +++ b/lib/commands/GETEX.ts @@ -0,0 +1,35 @@ +import { transformEXAT, transformPXAT, transformReplyStringNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +type GetExModes = { + EX: number; +} | { + PX: number; +} | { + EXAT: number | Date; +} | { + PXAT: number | Date; +} | { + PERSIST: true; +}; + +export function transformArguments(key: string, mode: GetExModes) { + const args = ['GETEX', key]; + + if ('EX' in mode) { + args.push('EX', mode.EX.toString()); + } else if ('PX' in mode) { + args.push('PX', mode.PX.toString()); + } else if ('EXAT' in mode) { + args.push('EXAT', transformEXAT(mode.EXAT)); + } else if ('PXAT' in mode) { + args.push('PXAT', transformPXAT(mode.PXAT)); + } else { // PERSIST + args.push('PERSIST'); + } + + return args; +} + +export const transformReply = transformReplyStringNull; diff --git a/lib/commands/GETRANGE.spec.ts b/lib/commands/GETRANGE.spec.ts new file mode 100644 index 0000000000..726311e684 --- /dev/null +++ b/lib/commands/GETRANGE.spec.ts @@ -0,0 +1,27 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './GETRANGE'; + +describe('GETRANGE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0, -1), + ['GETRANGE', 'key', '0', '-1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.getRange', async client => { + assert.equal( + await client.getRange('key', 0, -1), + '' + ); + }); + + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lTrim', async cluster => { + assert.equal( + await cluster.getRange('key', 0, -1), + '' + ); + }); +}); diff --git a/lib/commands/GETRANGE.ts b/lib/commands/GETRANGE.ts new file mode 100644 index 0000000000..9488dd53d5 --- /dev/null +++ b/lib/commands/GETRANGE.ts @@ -0,0 +1,11 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, start: number, end: number): Array { + return ['GETRANGE', key, start.toString(), end.toString()]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/GETSET.spec.ts b/lib/commands/GETSET.spec.ts new file mode 100644 index 0000000000..4af5ab39ca --- /dev/null +++ b/lib/commands/GETSET.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './GETSET'; + +describe('GETSET', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'value'), + ['GETSET', 'key', 'value'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.getSet', async client => { + assert.equal( + await client.getSet('key', 'value'), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.getSet', async cluster => { + assert.equal( + await cluster.getSet('key', 'value'), + null + ); + }); +}); diff --git a/lib/commands/GETSET.ts b/lib/commands/GETSET.ts new file mode 100644 index 0000000000..1b9b9d6bc7 --- /dev/null +++ b/lib/commands/GETSET.ts @@ -0,0 +1,9 @@ +import { transformReplyStringNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, value: string): Array { + return ['GETSET', key, value]; +} + +export const transformReply = transformReplyStringNull; diff --git a/lib/commands/HDEL.spec.ts b/lib/commands/HDEL.spec.ts new file mode 100644 index 0000000000..04191f51ad --- /dev/null +++ b/lib/commands/HDEL.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HDEL'; + +describe('HDEL', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['HDEL', 'key', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['HDEL', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hDel', async client => { + assert.equal( + await client.hDel('key', 'field'), + 0 + ); + }); +}); diff --git a/lib/commands/HDEL.ts b/lib/commands/HDEL.ts new file mode 100644 index 0000000000..ee96193144 --- /dev/null +++ b/lib/commands/HDEL.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, field: string | Array): Array { + return pushVerdictArguments(['HDEL', key], field); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/HELLO.spec.ts b/lib/commands/HELLO.spec.ts new file mode 100644 index 0000000000..7642f739d9 --- /dev/null +++ b/lib/commands/HELLO.spec.ts @@ -0,0 +1,77 @@ +import { strict as assert } from 'assert'; +import { REDIS_VERSION, TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './HELLO'; + +describe('HELLO', () => { + describeHandleMinimumRedisVersion([6]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['HELLO'] + ); + }); + + it('with protover', () => { + assert.deepEqual( + transformArguments({ + protover: 3 + }), + ['HELLO', '3'] + ); + }); + + it('with protover, auth', () => { + assert.deepEqual( + transformArguments({ + protover: 3, + auth: { + username: 'username', + password: 'password' + } + }), + ['HELLO', '3', 'AUTH', 'username', 'password'] + ); + }); + + it('with protover, clientName', () => { + assert.deepEqual( + transformArguments({ + protover: 3, + clientName: 'clientName' + }), + ['HELLO', '3', 'SETNAME', 'clientName'] + ); + }); + + it('with protover, auth, clientName', () => { + assert.deepEqual( + transformArguments({ + protover: 3, + auth: { + username: 'username', + password: 'password' + }, + clientName: 'clientName' + }), + ['HELLO', '3', 'AUTH', 'username', 'password', 'SETNAME', 'clientName'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hello', async client => { + assert.deepEqual( + await client.hello(), + { + server: 'redis', + version: REDIS_VERSION.join('.'), + proto: 2, + id: await client.clientId(), + mode: 'standalone', + role: 'master', + modules: [] + } + ); + }); +}); diff --git a/lib/commands/HELLO.ts b/lib/commands/HELLO.ts new file mode 100644 index 0000000000..efb96890fc --- /dev/null +++ b/lib/commands/HELLO.ts @@ -0,0 +1,64 @@ +import { AuthOptions } from './AUTH'; + +interface HelloOptions { + protover: number; + auth?: Required; + clientName?: string; +} + +export function transformArguments(options?: HelloOptions): Array { + const args = ['HELLO']; + + if (options) { + args.push(options.protover.toString()); + + if (options?.auth) { + args.push('AUTH', options.auth.username, options.auth.password); + } + + if (options.clientName) { + args.push('SETNAME', options.clientName); + } + } + + return args; +} + +type HelloRawReply = [ + _: never, + server: string, + _: never, + version: string, + _: never, + proto: number, + _: never, + id: number, + _: never, + mode: string, + _: never, + role: string, + _: never, + modules: Array +]; + +interface HelloTransformedReply { + server: string; + version: string; + proto: number; + id: number; + mode: string; + role: string; + modules: Array; +} + +export function transformReply(reply: HelloRawReply): HelloTransformedReply { + return { + server: reply[1], + version: reply[3], + proto: reply[5], + id: reply[7], + mode: reply[9], + role: reply[11], + modules: reply[13] + }; +} diff --git a/lib/commands/HEXISTS.spec.ts b/lib/commands/HEXISTS.spec.ts new file mode 100644 index 0000000000..26c411c432 --- /dev/null +++ b/lib/commands/HEXISTS.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HEXISTS'; + +describe('HEXISTS', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['HEXISTS', 'key', 'field'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hExists', async client => { + assert.equal( + await client.hExists('key', 'field'), + false + ); + }); +}); diff --git a/lib/commands/HEXISTS.ts b/lib/commands/HEXISTS.ts new file mode 100644 index 0000000000..7cf0b158d9 --- /dev/null +++ b/lib/commands/HEXISTS.ts @@ -0,0 +1,9 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, field: string): Array { + return ['HEXISTS', key, field]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/HGET.spec.ts b/lib/commands/HGET.spec.ts new file mode 100644 index 0000000000..c78550c517 --- /dev/null +++ b/lib/commands/HGET.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HGET'; + +describe('HGET', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['HGET', 'key', 'field'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hGet', async client => { + assert.equal( + await client.hGet('key', 'field'), + null + ); + }); +}); diff --git a/lib/commands/HGET.ts b/lib/commands/HGET.ts new file mode 100644 index 0000000000..edabbcd6bc --- /dev/null +++ b/lib/commands/HGET.ts @@ -0,0 +1,9 @@ +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, field: string): Array { + return ['HGET', key, field]; +} + +export function transformReply(reply?: string): string | undefined { + return reply; +} diff --git a/lib/commands/HGETALL.spec.ts b/lib/commands/HGETALL.spec.ts new file mode 100644 index 0000000000..68b51a2902 --- /dev/null +++ b/lib/commands/HGETALL.spec.ts @@ -0,0 +1,41 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformReply } from './HGETALL'; + +describe('HGETALL', () => { + describe('transformReply', () => { + it('empty', () => { + assert.deepEqual( + transformReply([]), + Object.create(null) + ); + }); + + it('with values', () => { + assert.deepEqual( + transformReply(['key1', 'value1', 'key2', 'value2']), + Object.create(null, { + key1: { + value: 'value1', + configurable: true, + enumerable: true, + writable: true + }, + key2: { + value: 'value2', + configurable: true, + enumerable: true, + writable: true + } + }) + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hGetAll', async client => { + assert.deepEqual( + await client.hGetAll('key'), + Object.create(null) + ); + }); +}); diff --git a/lib/commands/HGETALL.ts b/lib/commands/HGETALL.ts new file mode 100644 index 0000000000..8ac14ec496 --- /dev/null +++ b/lib/commands/HGETALL.ts @@ -0,0 +1,9 @@ +import { transformReplyTuples } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['HGETALL', key]; +} + +export const transformReply = transformReplyTuples; diff --git a/lib/commands/HINCRBY.spec.ts b/lib/commands/HINCRBY.spec.ts new file mode 100644 index 0000000000..898dfd1172 --- /dev/null +++ b/lib/commands/HINCRBY.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HINCRBY'; + +describe('HINCRBY', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'field', 1), + ['HINCRBY', 'key', 'field', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hIncrBy', async client => { + assert.equal( + await client.hIncrBy('key', 'field', 1), + 1 + ); + }); +}); diff --git a/lib/commands/HINCRBY.ts b/lib/commands/HINCRBY.ts new file mode 100644 index 0000000000..192dac456e --- /dev/null +++ b/lib/commands/HINCRBY.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, field: string, increment: number): Array { + return ['HINCRBY', key, field, increment.toString()]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/HINCRBYFLOAT.spec.ts b/lib/commands/HINCRBYFLOAT.spec.ts new file mode 100644 index 0000000000..83e87538c5 --- /dev/null +++ b/lib/commands/HINCRBYFLOAT.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HINCRBYFLOAT'; + +describe('HINCRBYFLOAT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'field', 1.5), + ['HINCRBYFLOAT', 'key', 'field', '1.5'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hIncrByFloat', async client => { + assert.equal( + await client.hIncrByFloat('key', 'field', 1.5), + '1.5' + ); + }); +}); diff --git a/lib/commands/HINCRBYFLOAT.ts b/lib/commands/HINCRBYFLOAT.ts new file mode 100644 index 0000000000..10c949b8d9 --- /dev/null +++ b/lib/commands/HINCRBYFLOAT.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, field: string, increment: number): Array { + return ['HINCRBYFLOAT', key, field, increment.toString()]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/HKEYS.spec.ts b/lib/commands/HKEYS.spec.ts new file mode 100644 index 0000000000..12190668b0 --- /dev/null +++ b/lib/commands/HKEYS.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HKEYS'; + +describe('HKEYS', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['HKEYS', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hKeys', async client => { + assert.deepEqual( + await client.hKeys('key'), + [] + ); + }); +}); diff --git a/lib/commands/HKEYS.ts b/lib/commands/HKEYS.ts new file mode 100644 index 0000000000..d79d2c1d13 --- /dev/null +++ b/lib/commands/HKEYS.ts @@ -0,0 +1,9 @@ +import { transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['HKEYS', key]; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/HLEN.spec.ts b/lib/commands/HLEN.spec.ts new file mode 100644 index 0000000000..e9aaa64e6e --- /dev/null +++ b/lib/commands/HLEN.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HLEN'; + +describe('HLEN', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['HLEN', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hLen', async client => { + assert.equal( + await client.hLen('key'), + 0 + ); + }); +}); diff --git a/lib/commands/HLEN.ts b/lib/commands/HLEN.ts new file mode 100644 index 0000000000..ba7ccc3aed --- /dev/null +++ b/lib/commands/HLEN.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['HLEN', key]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/HMGET.spec.ts b/lib/commands/HMGET.spec.ts new file mode 100644 index 0000000000..3b1c286e74 --- /dev/null +++ b/lib/commands/HMGET.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HMGET'; + +describe('HMGET', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['HMGET', 'key', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['field1', 'field2']), + ['HMGET', 'key', 'field1', 'field2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hmGet', async client => { + assert.deepEqual( + await client.hmGet('key', 'field'), + [null] + ); + }); +}); diff --git a/lib/commands/HMGET.ts b/lib/commands/HMGET.ts new file mode 100644 index 0000000000..fc0f91d822 --- /dev/null +++ b/lib/commands/HMGET.ts @@ -0,0 +1,11 @@ +import { pushVerdictArguments, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, fields: string | Array): Array { + return pushVerdictArguments(['HMGET', key], fields); +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/HRANDFIELD.spec.ts b/lib/commands/HRANDFIELD.spec.ts new file mode 100644 index 0000000000..70e2585cf9 --- /dev/null +++ b/lib/commands/HRANDFIELD.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './HRANDFIELD'; + +describe('HRANDFIELD', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['HRANDFIELD', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hRandField', async client => { + assert.equal( + await client.hRandField('key'), + null + ); + }); +}); diff --git a/lib/commands/HRANDFIELD.ts b/lib/commands/HRANDFIELD.ts new file mode 100644 index 0000000000..e0c6ee392d --- /dev/null +++ b/lib/commands/HRANDFIELD.ts @@ -0,0 +1,9 @@ +import { transformReplyStringNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['HRANDFIELD', key]; +} + +export const transformReply = transformReplyStringNull; \ No newline at end of file diff --git a/lib/commands/HRANDFIELD_COUNT.spec.ts b/lib/commands/HRANDFIELD_COUNT.spec.ts new file mode 100644 index 0000000000..6954bd484a --- /dev/null +++ b/lib/commands/HRANDFIELD_COUNT.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './HRANDFIELD_COUNT'; + +describe('HRANDFIELD COUNT', () => { + describeHandleMinimumRedisVersion([6, 2, 5]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['HRANDFIELD', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hRandFieldCount', async client => { + assert.deepEqual( + await client.hRandFieldCount('key', 1), + [] + ); + }); +}); diff --git a/lib/commands/HRANDFIELD_COUNT.ts b/lib/commands/HRANDFIELD_COUNT.ts new file mode 100644 index 0000000000..d615b86ee8 --- /dev/null +++ b/lib/commands/HRANDFIELD_COUNT.ts @@ -0,0 +1,13 @@ +import { transformReplyStringArray } from './generic-transformers'; +import { transformArguments as transformHRandFieldArguments } from './HRANDFIELD'; + +export { FIRST_KEY_INDEX } from './HRANDFIELD'; + +export function transformArguments(key: string, count: number): Array { + return [ + ...transformHRandFieldArguments(key), + count.toString() + ]; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts b/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts new file mode 100644 index 0000000000..0c26cbc793 --- /dev/null +++ b/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './HRANDFIELD_COUNT_WITHVALUES'; + +describe('HRANDFIELD COUNT WITHVALUES', () => { + describeHandleMinimumRedisVersion([6, 2, 5]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['HRANDFIELD', 'key', '1', 'WITHVALUES'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hRandFieldCountWithValues', async client => { + assert.deepEqual( + await client.hRandFieldCountWithValues('key', 1), + Object.create(null) + ); + }); +}); diff --git a/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts b/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts new file mode 100644 index 0000000000..53856c1984 --- /dev/null +++ b/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts @@ -0,0 +1,13 @@ +import { transformReplyTuples } from './generic-transformers'; +import { transformArguments as transformHRandFieldCountArguments } from './HRANDFIELD_COUNT'; + +export { FIRST_KEY_INDEX } from './HRANDFIELD_COUNT'; + +export function transformArguments(key: string, count: number): Array { + return [ + ...transformHRandFieldCountArguments(key, count), + 'WITHVALUES' + ]; +} + +export const transformReply = transformReplyTuples; diff --git a/lib/commands/HSCAN.spec.ts b/lib/commands/HSCAN.spec.ts new file mode 100644 index 0000000000..7441dd48d5 --- /dev/null +++ b/lib/commands/HSCAN.spec.ts @@ -0,0 +1,77 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './HSCAN'; + +describe('HSCAN', () => { + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + transformArguments('key', 0), + ['HSCAN', 'key', '0'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern' + }), + ['HSCAN', 'key', '0', 'MATCH', 'pattern'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + COUNT: 1 + }), + ['HSCAN', 'key', '0', 'COUNT', '1'] + ); + }); + + it('with MATCH & COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern', + COUNT: 1 + }), + ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] + ); + }); + }); + + describe('transformReply', () => { + it('without tuples', () => { + assert.deepEqual( + transformReply(['0', []]), + { + cursor: 0, + tuples: [] + } + ); + }); + + it('with tuples', () => { + assert.deepEqual( + transformReply(['0', ['field', 'value']]), + { + cursor: 0, + tuples: [{ + field: 'field', + value: 'value' + }] + } + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hScan', async client => { + assert.deepEqual( + await client.hScan('key', 0), + { + cursor: 0, + tuples: [] + } + ); + }); +}); diff --git a/lib/commands/HSCAN.ts b/lib/commands/HSCAN.ts new file mode 100644 index 0000000000..18b1355b59 --- /dev/null +++ b/lib/commands/HSCAN.ts @@ -0,0 +1,37 @@ +import { ScanOptions, pushScanArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, cursor: number, options?: ScanOptions): Array { + return pushScanArguments([ + 'HSCAN', + key + ], cursor, options); +} + +export interface HScanTuple { + field: string; + value: string; +} + +interface HScanReply { + cursor: number; + tuples: Array; +} + +export function transformReply([cursor, rawTuples]: [string, Array]): HScanReply { + const parsedTuples = []; + for (let i = 0; i < rawTuples.length; i += 2) { + parsedTuples.push({ + field: rawTuples[i], + value: rawTuples[i + 1] + }); + } + + return { + cursor: Number(cursor), + tuples: parsedTuples + }; +} diff --git a/lib/commands/HSET.spec.ts b/lib/commands/HSET.spec.ts new file mode 100644 index 0000000000..af7bcb6eb2 --- /dev/null +++ b/lib/commands/HSET.spec.ts @@ -0,0 +1,44 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './HSET'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; + +describe('HSET', () => { + describe('transformArguments', () => { + it('Map', () => { + assert.deepEqual( + transformArguments('key', new Map([['field', 'value']])), + ['HSET', 'key', 'field', 'value'] + ); + }); + + it('Array', () => { + assert.deepEqual( + transformArguments('key', [['field', 'value']]), + ['HSET', 'key', 'field', 'value'] + ); + }); + + it('Object', () => { + it('Array', () => { + assert.deepEqual( + transformArguments('key', { field: 'value' }), + ['HSET', 'key', 'field', 'value'] + ); + }); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hSet', async client => { + assert.equal( + await client.hSet('key', { field: 'value' }), + 1 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.hSet', async cluster => { + assert.equal( + await cluster.hSet('key', { field: 'value' }), + 1 + ); + }); +}); \ No newline at end of file diff --git a/lib/commands/HSET.ts b/lib/commands/HSET.ts new file mode 100644 index 0000000000..3edaa64b4e --- /dev/null +++ b/lib/commands/HSET.ts @@ -0,0 +1,41 @@ +import { transformReplyString } from './generic-transformers'; + +type HSETObject = Record; + +type HSETMap = Map; + +type HSETTuples = Array<[string, string]> | Array; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, value: HSETObject | HSETMap | HSETTuples): Array { + const args = ['HSET', key]; + + if (value instanceof Map) { + pushMap(args, value); + } else if (Array.isArray(value)) { + pushTuples(args, value); + } else if (typeof value === 'object' && value !== null) { + pushObject(args, value); + } + + return args; +} + +function pushMap(args: Array, map: HSETMap): void { + for (const [key, value] of map.entries()) { + args.push(key.toString(), value.toString()); + } +} + +function pushTuples(args: Array, tuples: HSETTuples): void { + args.push(...tuples.flat()); +} + +function pushObject(args: Array, object: HSETObject): void { + for (const key of Object.keys(object)) { + args.push(key.toString(), object[key].toString()); + } +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/HSETNX.spec.ts b/lib/commands/HSETNX.spec.ts new file mode 100644 index 0000000000..f810c5e2b9 --- /dev/null +++ b/lib/commands/HSETNX.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HSETNX'; + +describe('HSETNX', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'field', 'value'), + ['HSETNX', 'key', 'field', 'value'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hSetNX', async client => { + assert.equal( + await client.hSetNX('key', 'field', 'value'), + true + ); + }); +}); diff --git a/lib/commands/HSETNX.ts b/lib/commands/HSETNX.ts new file mode 100644 index 0000000000..0eef875252 --- /dev/null +++ b/lib/commands/HSETNX.ts @@ -0,0 +1,9 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, field: string, value: string): Array { + return ['HSETNX', key, field, value]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/HSTRLEN.spec.ts b/lib/commands/HSTRLEN.spec.ts new file mode 100644 index 0000000000..35bf08d54c --- /dev/null +++ b/lib/commands/HSTRLEN.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HSTRLEN'; + +describe('HSTRLEN', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['HSTRLEN', 'key', 'field'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hStrLen', async client => { + assert.equal( + await client.hStrLen('key', 'field'), + 0 + ); + }); +}); diff --git a/lib/commands/HSTRLEN.ts b/lib/commands/HSTRLEN.ts new file mode 100644 index 0000000000..4181cde851 --- /dev/null +++ b/lib/commands/HSTRLEN.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, field: string): Array { + return ['HSTRLEN', key, field]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/HVALS.spec.ts b/lib/commands/HVALS.spec.ts new file mode 100644 index 0000000000..9e6451f500 --- /dev/null +++ b/lib/commands/HVALS.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './HVALS'; + +describe('HVALS', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['HVALS', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hVals', async client => { + assert.deepEqual( + await client.hVals('key'), + [] + ); + }); +}); diff --git a/lib/commands/HVALS.ts b/lib/commands/HVALS.ts new file mode 100644 index 0000000000..7f924623cf --- /dev/null +++ b/lib/commands/HVALS.ts @@ -0,0 +1,9 @@ +import { transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['HVALS', key]; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/INCR.spec.ts b/lib/commands/INCR.spec.ts new file mode 100644 index 0000000000..d64c3696af --- /dev/null +++ b/lib/commands/INCR.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './INCR'; + +describe('INCR', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['INCR', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.incr', async client => { + assert.equal( + await client.incr('key'), + 1 + ); + }); +}); diff --git a/lib/commands/INCR.ts b/lib/commands/INCR.ts new file mode 100644 index 0000000000..00747f0f7e --- /dev/null +++ b/lib/commands/INCR.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['INCR', key]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/INCRBY.spec.ts b/lib/commands/INCRBY.spec.ts new file mode 100644 index 0000000000..875277570c --- /dev/null +++ b/lib/commands/INCRBY.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './INCRBY'; + +describe('INCR', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['INCRBY', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.incrBy', async client => { + assert.equal( + await client.incrBy('key', 1), + 1 + ); + }); +}); diff --git a/lib/commands/INCRBY.ts b/lib/commands/INCRBY.ts new file mode 100644 index 0000000000..8fd31d0338 --- /dev/null +++ b/lib/commands/INCRBY.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, increment: number): Array { + return ['INCRBY', key, increment.toString()]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/INCRBYFLOAT.spec.ts b/lib/commands/INCRBYFLOAT.spec.ts new file mode 100644 index 0000000000..fe062b6290 --- /dev/null +++ b/lib/commands/INCRBYFLOAT.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './INCRBYFLOAT'; + +describe('INCRBYFLOAT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1.5), + ['INCRBYFLOAT', 'key', '1.5'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.incrByFloat', async client => { + assert.equal( + await client.incrByFloat('key', 1.5), + '1.5' + ); + }); +}); diff --git a/lib/commands/INCRBYFLOAT.ts b/lib/commands/INCRBYFLOAT.ts new file mode 100644 index 0000000000..38912cbdc9 --- /dev/null +++ b/lib/commands/INCRBYFLOAT.ts @@ -0,0 +1,9 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, increment: number): Array { + return ['INCRBYFLOAT', key, increment.toString()]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/INFO.spec.ts b/lib/commands/INFO.spec.ts new file mode 100644 index 0000000000..118682c7da --- /dev/null +++ b/lib/commands/INFO.spec.ts @@ -0,0 +1,20 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './INFO'; + +describe('INFO', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['INFO'] + ); + }); + + it('server section', () => { + assert.deepEqual( + transformArguments('server'), + ['INFO', 'server'] + ); + }); + }); +}); diff --git a/lib/commands/INFO.ts b/lib/commands/INFO.ts new file mode 100644 index 0000000000..437b5e5b83 --- /dev/null +++ b/lib/commands/INFO.ts @@ -0,0 +1,15 @@ +import { transformReplyString } from './generic-transformers'; + +export const IS_READ_ONLY = true; + +export function transformArguments(section?: string): Array { + const args = ['INFO']; + + if (section) { + args.push(section); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/KEYS.spec.ts b/lib/commands/KEYS.spec.ts new file mode 100644 index 0000000000..d11e8a0f58 --- /dev/null +++ b/lib/commands/KEYS.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; + +describe('KEYS', () => { + itWithClient(TestRedisServers.OPEN, 'client.keys', async client => { + assert.deepEqual( + await client.keys('pattern'), + [] + ); + }); +}); diff --git a/lib/commands/KEYS.ts b/lib/commands/KEYS.ts new file mode 100644 index 0000000000..99c99c1152 --- /dev/null +++ b/lib/commands/KEYS.ts @@ -0,0 +1,7 @@ +export function transformArguments(pattern: string): Array { + return ['KEYS', pattern]; +} + +export function transformReply(keys: Array): Array { + return keys; +} \ No newline at end of file diff --git a/lib/commands/LASTSAVE.spec.ts b/lib/commands/LASTSAVE.spec.ts new file mode 100644 index 0000000000..1b13bed5d2 --- /dev/null +++ b/lib/commands/LASTSAVE.spec.ts @@ -0,0 +1,20 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LASTSAVE'; + +describe('LASTSAVE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['LASTSAVE'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lastSave', async client => { + assert.ok((await client.lastSave()) instanceof Date); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lastSave', async cluster => { + assert.ok((await cluster.lastSave()) instanceof Date); + }); +}); diff --git a/lib/commands/LASTSAVE.ts b/lib/commands/LASTSAVE.ts new file mode 100644 index 0000000000..76944d3548 --- /dev/null +++ b/lib/commands/LASTSAVE.ts @@ -0,0 +1,9 @@ +export const IS_READ_ONLY = true; + +export function transformArguments(): Array { + return ['LASTSAVE']; +} + +export function transformReply(reply: number): Date { + return new Date(reply); +} diff --git a/lib/commands/LINDEX.spec.ts b/lib/commands/LINDEX.spec.ts new file mode 100644 index 0000000000..74a6706ecd --- /dev/null +++ b/lib/commands/LINDEX.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LINDEX'; + +describe('LINDEX', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'element'), + ['LINDEX', 'key', 'element'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lIndex', async client => { + assert.equal( + await client.lIndex('key', 'element'), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lIndex', async cluster => { + assert.equal( + await cluster.lIndex('key', 'element'), + null + ); + }); +}); diff --git a/lib/commands/LINDEX.ts b/lib/commands/LINDEX.ts new file mode 100644 index 0000000000..0237a4705b --- /dev/null +++ b/lib/commands/LINDEX.ts @@ -0,0 +1,11 @@ +import { transformReplyNumberNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, element: string): Array { + return ['LINDEX', key, element]; +} + +export const transformReply = transformReplyNumberNull; diff --git a/lib/commands/LINSERT.spec.ts b/lib/commands/LINSERT.spec.ts new file mode 100644 index 0000000000..286e61d06d --- /dev/null +++ b/lib/commands/LINSERT.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LINSERT'; + +describe('LINSERT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'BEFORE', 'pivot', 'element'), + ['LINSERT', 'key', 'BEFORE', 'pivot', 'element'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lInsert', async client => { + assert.equal( + await client.lInsert('key', 'BEFORE', 'pivot', 'element'), + 0 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lLen', async cluster => { + assert.equal( + await cluster.lInsert('key', 'BEFORE', 'pivot', 'element'), + 0 + ); + }); +}); diff --git a/lib/commands/LINSERT.ts b/lib/commands/LINSERT.ts new file mode 100644 index 0000000000..40bd4e3d4d --- /dev/null +++ b/lib/commands/LINSERT.ts @@ -0,0 +1,22 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +type LInsertPosition = 'BEFORE' | 'AFTER'; + +export function transformArguments( + key: string, + position: LInsertPosition, + pivot: string, + element: string +): Array { + return [ + 'LINSERT', + key, + position, + pivot, + element + ]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/LLEN.spec.ts b/lib/commands/LLEN.spec.ts new file mode 100644 index 0000000000..6e4581ddd1 --- /dev/null +++ b/lib/commands/LLEN.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LLEN'; + +describe('LLEN', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['LLEN', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lLen', async client => { + assert.equal( + await client.lLen('key'), + 0 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lLen', async cluster => { + assert.equal( + await cluster.lLen('key'), + 0 + ); + }); +}); diff --git a/lib/commands/LLEN.ts b/lib/commands/LLEN.ts new file mode 100644 index 0000000000..61aae604c9 --- /dev/null +++ b/lib/commands/LLEN.ts @@ -0,0 +1,11 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['LLEN', key]; +} + +export const transformReply = transformReplyNumber; \ No newline at end of file diff --git a/lib/commands/LMOVE.spec.ts b/lib/commands/LMOVE.spec.ts new file mode 100644 index 0000000000..bcb897f76a --- /dev/null +++ b/lib/commands/LMOVE.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './LMOVE'; + +describe('LMOVE', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('source', 'destination', 'LEFT', 'RIGHT'), + ['LMOVE', 'source', 'destination', 'LEFT', 'RIGHT'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lMove', async client => { + assert.equal( + await client.lMove('source', 'destination', 'LEFT', 'RIGHT'), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lMove', async cluster => { + assert.equal( + await cluster.lMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT'), + null + ); + }); +}); diff --git a/lib/commands/LMOVE.ts b/lib/commands/LMOVE.ts new file mode 100644 index 0000000000..1e99297d81 --- /dev/null +++ b/lib/commands/LMOVE.ts @@ -0,0 +1,22 @@ +import { transformReplyStringNull } from './generic-transformers'; + +export type LMoveSide = 'LEFT' | 'RIGHT'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments( + source: string, + destination: string, + sourceSide: LMoveSide, + destinationSide: LMoveSide +): Array { + return [ + 'LMOVE', + source, + destination, + sourceSide, + destinationSide, + ]; +} + +export const transformReply = transformReplyStringNull; diff --git a/lib/commands/LOLWUT.spec.ts b/lib/commands/LOLWUT.spec.ts new file mode 100644 index 0000000000..8e77b85b59 --- /dev/null +++ b/lib/commands/LOLWUT.spec.ts @@ -0,0 +1,43 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LOLWUT'; + +describe('LOLWUT', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['LOLWUT'] + ); + }); + + it('with version', () => { + assert.deepEqual( + transformArguments(5), + ['LOLWUT', 'VERSION', '5'] + ); + }); + + it('with version and optional arguments', () => { + assert.deepEqual( + transformArguments(5, 1, 2, 3), + ['LOLWUT', 'VERSION', '5', '1', '2', '3'] + ); + }); + }); + + + itWithClient(TestRedisServers.OPEN, 'client.LOLWUT', async client => { + assert.equal( + typeof (await client.LOLWUT()), + 'string' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.LOLWUT', async cluster => { + assert.equal( + typeof (await cluster.LOLWUT()), + 'string' + ); + }); +}); diff --git a/lib/commands/LOLWUT.ts b/lib/commands/LOLWUT.ts new file mode 100644 index 0000000000..f0cd20d447 --- /dev/null +++ b/lib/commands/LOLWUT.ts @@ -0,0 +1,19 @@ +import { transformReplyString } from './generic-transformers'; + +export const IS_READ_ONLY = true; + +export function transformArguments(version?: number, ...optionalArguments: Array): Array { + const args = ['LOLWUT']; + + if (version) { + args.push( + 'VERSION', + version.toString(), + ...optionalArguments.map(String), + ); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/LPOP.spec.ts b/lib/commands/LPOP.spec.ts new file mode 100644 index 0000000000..b593f65742 --- /dev/null +++ b/lib/commands/LPOP.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LPOP'; + +describe('LPOP', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['LPOP', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lPop', async client => { + assert.equal( + await client.lPop('key'), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lPop', async cluster => { + assert.equal( + await cluster.lPop('key'), + null + ); + }); +}); diff --git a/lib/commands/LPOP.ts b/lib/commands/LPOP.ts new file mode 100644 index 0000000000..30595a5491 --- /dev/null +++ b/lib/commands/LPOP.ts @@ -0,0 +1,9 @@ +import { transformReplyStringNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['LPOP', key]; +} + +export const transformReply = transformReplyStringNull; diff --git a/lib/commands/LPOP_COUNT.spec.ts b/lib/commands/LPOP_COUNT.spec.ts new file mode 100644 index 0000000000..89150dbf4d --- /dev/null +++ b/lib/commands/LPOP_COUNT.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './LPOP_COUNT'; + +describe('LPOP COUNT', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['LPOP', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lPopCount', async client => { + assert.equal( + await client.lPopCount('key', 1), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lPop', async cluster => { + assert.equal( + await cluster.lPopCount('key', 1), + null + ); + }); +}); diff --git a/lib/commands/LPOP_COUNT.ts b/lib/commands/LPOP_COUNT.ts new file mode 100644 index 0000000000..432d2c47c0 --- /dev/null +++ b/lib/commands/LPOP_COUNT.ts @@ -0,0 +1,9 @@ +import { transformReplyStringArrayNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, count: number): Array { + return ['LPOP', key, count.toString()]; +} + +export const transformReply = transformReplyStringArrayNull; diff --git a/lib/commands/LPOS.spec.ts b/lib/commands/LPOS.spec.ts new file mode 100644 index 0000000000..1cf9e35209 --- /dev/null +++ b/lib/commands/LPOS.spec.ts @@ -0,0 +1,58 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './LPOS'; + +describe('LPOS', () => { + describeHandleMinimumRedisVersion([6, 0, 6]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key', 'element'), + ['LPOS', 'key', 'element'] + ); + }); + + it('with RANK', () => { + assert.deepEqual( + transformArguments('key', 'element', { + RANK: 0 + }), + ['LPOS', 'key', 'element', 'RANK', '0'] + ); + }); + + it('with MAXLEN', () => { + assert.deepEqual( + transformArguments('key', 'element', { + MAXLEN: 10 + }), + ['LPOS', 'key', 'element', 'MAXLEN', '10'] + ); + }); + + it('with RANK, MAXLEN', () => { + assert.deepEqual( + transformArguments('key', 'element', { + RANK: 0, + MAXLEN: 10 + }), + ['LPOS', 'key', 'element', 'RANK', '0', 'MAXLEN', '10'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lPos', async client => { + assert.equal( + await client.lPos('key', 'element'), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lPos', async cluster => { + assert.equal( + await cluster.lPos('key', 'element'), + null + ); + }); +}); diff --git a/lib/commands/LPOS.ts b/lib/commands/LPOS.ts new file mode 100644 index 0000000000..fc160dbcbb --- /dev/null +++ b/lib/commands/LPOS.ts @@ -0,0 +1,26 @@ +import { transformReplyNumberNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export interface LPosOptions { + RANK?: number; + MAXLEN?: number; +} + +export function transformArguments(key: string, element: string, options?: LPosOptions): Array { + const args = ['LPOS', key, element]; + + if (typeof options?.RANK === 'number') { + args.push('RANK', options.RANK.toString()); + } + + if (typeof options?.MAXLEN === 'number') { + args.push('MAXLEN', options.MAXLEN.toString()); + } + + return args; +} + +export const transformReply = transformReplyNumberNull; diff --git a/lib/commands/LPOS_COUNT.spec.ts b/lib/commands/LPOS_COUNT.spec.ts new file mode 100644 index 0000000000..1d80bd45c3 --- /dev/null +++ b/lib/commands/LPOS_COUNT.spec.ts @@ -0,0 +1,58 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './LPOS_COUNT'; + +describe('LPOS COUNT', () => { + describeHandleMinimumRedisVersion([6, 0, 6]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key', 'element', 0), + ['LPOS', 'key', 'element', 'COUNT', '0'] + ); + }); + + it('with RANK', () => { + assert.deepEqual( + transformArguments('key', 'element', 0, { + RANK: 0 + }), + ['LPOS', 'key', 'element', 'RANK', '0', 'COUNT', '0'] + ); + }); + + it('with MAXLEN', () => { + assert.deepEqual( + transformArguments('key', 'element', 0, { + MAXLEN: 10 + }), + ['LPOS', 'key', 'element', 'COUNT', '0', 'MAXLEN', '10'] + ); + }); + + it('with RANK, MAXLEN', () => { + assert.deepEqual( + transformArguments('key', 'element', 0, { + RANK: 0, + MAXLEN: 10 + }), + ['LPOS', 'key', 'element', 'RANK', '0', 'COUNT', '0', 'MAXLEN', '10'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lPosCount', async client => { + assert.deepEqual( + await client.lPosCount('key', 'element', 0), + [] + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lPosCount', async cluster => { + assert.deepEqual( + await cluster.lPosCount('key', 'element', 0), + [] + ); + }); +}); diff --git a/lib/commands/LPOS_COUNT.ts b/lib/commands/LPOS_COUNT.ts new file mode 100644 index 0000000000..2a1d306858 --- /dev/null +++ b/lib/commands/LPOS_COUNT.ts @@ -0,0 +1,22 @@ +import { transformReplyNumberArray } from './generic-transformers'; +import { LPosOptions } from './LPOS'; + +export { FIRST_KEY_INDEX, IS_READ_ONLY } from './LPOS'; + +export function transformArguments(key: string, element: string, count: number, options?: LPosOptions): Array { + const args = ['LPOS', key, element]; + + if (typeof options?.RANK === 'number') { + args.push('RANK', options.RANK.toString()); + } + + args.push('COUNT', count.toString()); + + if (typeof options?.MAXLEN === 'number') { + args.push('MAXLEN', options.MAXLEN.toString()); + } + + return args; +} + +export const transformReply = transformReplyNumberArray; diff --git a/lib/commands/LPUSH.spec.ts b/lib/commands/LPUSH.spec.ts new file mode 100644 index 0000000000..44cf8c12d5 --- /dev/null +++ b/lib/commands/LPUSH.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LPUSH'; + +describe('LPUSH', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'field'), + ['LPUSH', 'key', 'field'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['LPUSH', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lPush', async client => { + assert.equal( + await client.lPush('key', 'field'), + 1 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lPush', async cluster => { + assert.equal( + await cluster.lPush('key', 'field'), + 1 + ); + }); +}); diff --git a/lib/commands/LPUSH.ts b/lib/commands/LPUSH.ts new file mode 100644 index 0000000000..434ad619cb --- /dev/null +++ b/lib/commands/LPUSH.ts @@ -0,0 +1,8 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, elements: string | Array): Array { + return pushVerdictArguments(['LPUSH', key], elements);} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/LPUSHX.spec.ts b/lib/commands/LPUSHX.spec.ts new file mode 100644 index 0000000000..1150c4d64d --- /dev/null +++ b/lib/commands/LPUSHX.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LPUSHX'; + +describe('LPUSHX', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'element'), + ['LPUSHX', 'key', 'element'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['LPUSHX', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lPushX', async client => { + assert.equal( + await client.lPushX('key', 'element'), + 0 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lPushX', async cluster => { + assert.equal( + await cluster.lPushX('key', 'element'), + 0 + ); + }); +}); diff --git a/lib/commands/LPUSHX.ts b/lib/commands/LPUSHX.ts new file mode 100644 index 0000000000..f1a989d962 --- /dev/null +++ b/lib/commands/LPUSHX.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, element: string | Array): Array { + return pushVerdictArguments(['LPUSHX', key], element); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/LRANGE.spec.ts b/lib/commands/LRANGE.spec.ts new file mode 100644 index 0000000000..843b7b8815 --- /dev/null +++ b/lib/commands/LRANGE.spec.ts @@ -0,0 +1,27 @@ + +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LRANGE'; + +describe('LRANGE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0, -1), + ['LRANGE', 'key', '0', '-1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lRange', async client => { + assert.deepEqual( + await client.lRange('key', 0, -1), + [] + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lRange', async cluster => { + assert.deepEqual( + await cluster.lRange('key', 0, -1), + [] + ); + }); +}); diff --git a/lib/commands/LRANGE.ts b/lib/commands/LRANGE.ts new file mode 100644 index 0000000000..cbed9a75de --- /dev/null +++ b/lib/commands/LRANGE.ts @@ -0,0 +1,16 @@ +import { transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, start: number, stop: number): Array { + return [ + 'LRANGE', + key, + start.toString(), + stop.toString() + ]; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/LREM.spec.ts b/lib/commands/LREM.spec.ts new file mode 100644 index 0000000000..e2f027ffeb --- /dev/null +++ b/lib/commands/LREM.spec.ts @@ -0,0 +1,27 @@ + +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LREM'; + +describe('LREM', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0, 'element'), + ['LREM', 'key', '0', 'element'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lRem', async client => { + assert.equal( + await client.lRem('key', 0, 'element'), + 0 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lRem', async cluster => { + assert.equal( + await cluster.lRem('key', 0, 'element'), + 0 + ); + }); +}); diff --git a/lib/commands/LREM.ts b/lib/commands/LREM.ts new file mode 100644 index 0000000000..5eabbc9194 --- /dev/null +++ b/lib/commands/LREM.ts @@ -0,0 +1,14 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, count: number, element: string): Array { + return [ + 'LREM', + key, + count.toString(), + element + ]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/LSET.spec.ts b/lib/commands/LSET.spec.ts new file mode 100644 index 0000000000..a5fe78cf4c --- /dev/null +++ b/lib/commands/LSET.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LSET'; + +describe('LSET', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0, 'element'), + ['LSET', 'key', '0', 'element'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lSet', async client => { + await client.lPush('key', 'element'); + assert.equal( + await client.lSet('key', 0, 'element'), + 'OK' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lSet', async cluster => { + await cluster.lPush('key', 'element'); + assert.equal( + await cluster.lSet('key', 0, 'element'), + 'OK' + ); + }); +}); diff --git a/lib/commands/LSET.ts b/lib/commands/LSET.ts new file mode 100644 index 0000000000..0e910dd6a1 --- /dev/null +++ b/lib/commands/LSET.ts @@ -0,0 +1,14 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, index: number, element: string): Array { + return [ + 'LSET', + key, + index.toString(), + element + ]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/LTRIM.spec.ts b/lib/commands/LTRIM.spec.ts new file mode 100644 index 0000000000..8092ba6af1 --- /dev/null +++ b/lib/commands/LTRIM.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './LTRIM'; + +describe('LTRIM', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0, -1), + ['LTRIM', 'key', '0', '-1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.lTrim', async client => { + assert.equal( + await client.lTrim('key', 0, -1), + 'OK' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.lTrim', async cluster => { + assert.equal( + await cluster.lTrim('key', 0, -1), + 'OK' + ); + }); +}); diff --git a/lib/commands/LTRIM.ts b/lib/commands/LTRIM.ts new file mode 100644 index 0000000000..3ccfa751af --- /dev/null +++ b/lib/commands/LTRIM.ts @@ -0,0 +1,14 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, start: number, stop: number): Array { + return [ + 'LTRIM', + key, + start.toString(), + stop.toString() + ] +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/MEMORY_DOCTOR.spec.ts b/lib/commands/MEMORY_DOCTOR.spec.ts new file mode 100644 index 0000000000..da883deeb7 --- /dev/null +++ b/lib/commands/MEMORY_DOCTOR.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './MEMORY_DOCTOR'; + +describe('MEMORY DOCTOR', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['MEMORY', 'DOCTOR'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.memoryDoctor', async client => { + assert.equal( + typeof (await client.memoryDoctor()), + 'string' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.memoryDoctor', async cluster => { + assert.equal( + typeof (await cluster.memoryDoctor()), + 'string' + ); + }); +}); diff --git a/lib/commands/MEMORY_DOCTOR.ts b/lib/commands/MEMORY_DOCTOR.ts new file mode 100644 index 0000000000..0d02bf9336 --- /dev/null +++ b/lib/commands/MEMORY_DOCTOR.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['MEMORY', 'DOCTOR']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/MEMORY_MALLOC-STATS.spec.ts b/lib/commands/MEMORY_MALLOC-STATS.spec.ts new file mode 100644 index 0000000000..2750ebdf7a --- /dev/null +++ b/lib/commands/MEMORY_MALLOC-STATS.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './MEMORY_MALLOC-STATS'; + +describe('MEMORY MALLOC-STATS', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['MEMORY', 'MALLOC-STATS'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.memoryMallocStats', async client => { + assert.equal( + typeof (await client.memoryDoctor()), + 'string' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.memoryDoctor', async cluster => { + assert.equal( + typeof (await cluster.memoryDoctor()), + 'string' + ); + }); +}); diff --git a/lib/commands/MEMORY_MALLOC-STATS.ts b/lib/commands/MEMORY_MALLOC-STATS.ts new file mode 100644 index 0000000000..7dd997c48b --- /dev/null +++ b/lib/commands/MEMORY_MALLOC-STATS.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['MEMORY', 'MALLOC-STATS']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/MEMORY_PURGE.spec.ts b/lib/commands/MEMORY_PURGE.spec.ts new file mode 100644 index 0000000000..ac9198ccfc --- /dev/null +++ b/lib/commands/MEMORY_PURGE.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './MEMORY_PURGE'; + +describe('MEMORY PURGE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['MEMORY', 'PURGE'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.memoryPurge', async client => { + assert.equal( + await client.memoryPurge(), + 'OK' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.memoryPurge', async cluster => { + assert.equal( + await cluster.memoryPurge(), + 'OK' + ); + }); +}); diff --git a/lib/commands/MEMORY_PURGE.ts b/lib/commands/MEMORY_PURGE.ts new file mode 100644 index 0000000000..7aaeee7e6a --- /dev/null +++ b/lib/commands/MEMORY_PURGE.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['MEMORY', 'PURGE']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/MEMORY_STATS.spec.ts b/lib/commands/MEMORY_STATS.spec.ts new file mode 100644 index 0000000000..12aa21181e --- /dev/null +++ b/lib/commands/MEMORY_STATS.spec.ts @@ -0,0 +1,108 @@ +import { strict as assert } from 'assert'; +import { transformArguments, transformReply } from './MEMORY_STATS'; + +describe('MEMORY STATS', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['MEMORY', 'STATS'] + ); + }); + + it('transformReply', () => { + assert.deepEqual( + transformReply([ + 'peak.allocated', + 952728, + 'total.allocated', + 892904, + 'startup.allocated', + 809952, + 'replication.backlog', + 0, + 'clients.slaves', + 0, + 'clients.normal', + 41000, + 'aof.buffer', + 0, + 'lua.caches', + 0, + 'db.0', + [ + 'overhead.hashtable.main', + 72, + 'overhead.hashtable.expires', + 0 + ], + 'overhead.total', + 850952, + 'keys.count', + 0, + 'keys.bytes-per-key', + 0, + 'dataset.bytes', + 41952, + 'dataset.percentage', + '50.573825836181641', + 'peak.percentage', + '93.720771789550781', + 'allocator.allocated', + 937632, + 'allocator.active', + 1191936, + 'allocator.resident', + 4005888, + 'allocator-fragmentation.ratio', + '1.2712193727493286', + 'allocator-fragmentation.bytes', + 254304, + 'allocator-rss.ratio', + '3.3608248233795166', + 'allocator-rss.bytes', + 2813952, + 'rss-overhead.ratio', + '2.4488751888275146', + 'rss-overhead.bytes', + 5804032, + 'fragmentation', + '11.515504837036133', + 'fragmentation.bytes', + 8958032 + ]), + { + peakAllocated: 952728, + totalAllocated: 892904, + startupAllocated: 809952, + replicationBacklog: 0, + clientsReplicas: 0, + clientsNormal: 41000, + aofBuffer: 0, + luaCaches: 0, + overheadTotal: 850952, + keysCount: 0, + keysBytesPerKey: 0, + datasetBytes: 41952, + datasetPercentage: 50.573825836181641, + peakPercentage: 93.720771789550781, + allocatorAllocated: 937632, + allocatorActive: 1191936, + allocatorResident: 4005888, + allocatorFragmentationRatio: 1.2712193727493286, + allocatorFragmentationBytes: 254304, + allocatorRssRatio: 3.3608248233795166, + allocatorRssBytes: 2813952, + rssOverheadRatio: 2.4488751888275146, + rssOverheadBytes: 5804032, + fragmentation: 11.515504837036133, + fragmentationBytes: 8958032, + db: { + 0: { + overheadHashtableMain: 72, + overheadHashtableExpires: 0 + } + } + } + ); + }); +}); diff --git a/lib/commands/MEMORY_STATS.ts b/lib/commands/MEMORY_STATS.ts new file mode 100644 index 0000000000..8ae83d2239 --- /dev/null +++ b/lib/commands/MEMORY_STATS.ts @@ -0,0 +1,93 @@ +export function transformArguments(): Array { + return ['MEMORY', 'STATS']; +} + +interface MemoryStatsReply { + peakAllocated: number; + totalAllocated: number; + startupAllocated: number; + replicationBacklog: number; + clientsReplicas: number; + clientsNormal: number; + aofBuffer: number; + luaCaches: number; + overheadTotal: number; + keysCount: number; + keysBytesPerKey: number; + datasetBytes: number; + datasetPercentage: number; + peakPercentage: number; + allocatorAllocated?: number, + allocatorActive?: number; + allocatorResident?: number; + allocatorFragmentationRatio?: number; + allocatorFragmentationBytes?: number; + allocatorRssRatio?: number; + allocatorRssBytes?: number; + rssOverheadRatio?: number; + rssOverheadBytes?: number; + fragmentation?: number; + fragmentationBytes: number; + db: { + [key: number]: { + overheadHashtableMain: number; + overheadHashtableExpires: number; + }; + }; +} + +const FIELDS_MAPPING = { + 'peak.allocated': 'peakAllocated', + 'total.allocated': 'totalAllocated', + 'startup.allocated': 'startupAllocated', + 'replication.backlog': 'replicationBacklog', + 'clients.slaves': 'clientsReplicas', + 'clients.normal': 'clientsNormal', + 'aof.buffer': 'aofBuffer', + 'lua.caches': 'luaCaches', + 'overhead.total': 'overheadTotal', + 'keys.count': 'keysCount', + 'keys.bytes-per-key': 'keysBytesPerKey', + 'dataset.bytes': 'datasetBytes', + 'dataset.percentage': 'datasetPercentage', + 'peak.percentage': 'peakPercentage', + 'allocator.allocated': 'allocatorAllocated', + 'allocator.active': 'allocatorActive', + 'allocator.resident': 'allocatorResident', + 'allocator-fragmentation.ratio': 'allocatorFragmentationRatio', + 'allocator-fragmentation.bytes': 'allocatorFragmentationBytes', + 'allocator-rss.ratio': 'allocatorRssRatio', + 'allocator-rss.bytes': 'allocatorRssBytes', + 'rss-overhead.ratio': 'rssOverheadRatio', + 'rss-overhead.bytes': 'rssOverheadBytes', + 'fragmentation': 'fragmentation', + 'fragmentation.bytes': 'fragmentationBytes' + }, + DB_FIELDS_MAPPING = { + 'overhead.hashtable.main': 'overheadHashtableMain', + 'overhead.hashtable.expires': 'overheadHashtableExpires' + }; + +export function transformReply(rawReply: Array>): MemoryStatsReply { + const reply: any = { + db: {} + }; + + for (let i = 0; i < rawReply.length; i += 2) { + const key = rawReply[i] as string; + if (key.startsWith('db.')) { + const dbTuples = rawReply[i + 1] as Array, + db: any = {}; + for (let j = 0; j < dbTuples.length; j += 2) { + db[DB_FIELDS_MAPPING[dbTuples[j] as keyof typeof DB_FIELDS_MAPPING]] = dbTuples[j + 1]; + } + + reply.db[key.substring(3)] = db; + continue; + } + + reply[FIELDS_MAPPING[key as keyof typeof FIELDS_MAPPING]] = Number(rawReply[i + 1]); + } + + return reply as MemoryStatsReply; +} diff --git a/lib/commands/MEMORY_USAGE.spec.ts b/lib/commands/MEMORY_USAGE.spec.ts new file mode 100644 index 0000000000..7487e7e4ff --- /dev/null +++ b/lib/commands/MEMORY_USAGE.spec.ts @@ -0,0 +1,37 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './MEMORY_USAGE'; + +describe('MEMORY USAGE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key'), + ['MEMORY', 'USAGE', 'key'] + ); + }); + + it('with SAMPLES', () => { + assert.deepEqual( + transformArguments('key', { + SAMPLES: 1 + }), + ['MEMORY', 'USAGE', 'key', 'SAMPLES', '1'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.memoryUsage', async client => { + assert.equal( + await client.memoryUsage('key'), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.memoryUsage', async cluster => { + assert.equal( + await cluster.memoryUsage('key'), + null + ); + }); +}); diff --git a/lib/commands/MEMORY_USAGE.ts b/lib/commands/MEMORY_USAGE.ts new file mode 100644 index 0000000000..0868b16226 --- /dev/null +++ b/lib/commands/MEMORY_USAGE.ts @@ -0,0 +1,21 @@ +import { transformReplyNumberNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +interface MemoryUsageOptions { + SAMPLES?: number; +} + +export function transformArguments(key: string, options?: MemoryUsageOptions): Array { + const args = ['MEMORY', 'USAGE', key]; + + if (options?.SAMPLES) { + args.push('SAMPLES', options.SAMPLES.toString()); + } + + return args; +} + +export const transformReply = transformReplyNumberNull; diff --git a/lib/commands/MGET.spec.ts b/lib/commands/MGET.spec.ts new file mode 100644 index 0000000000..c8c020fe43 --- /dev/null +++ b/lib/commands/MGET.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './MGET'; + +describe('MGET', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['MGET', '1', '2'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.mGet', async client => { + assert.deepEqual( + await client.mGet(['key']), + [null] + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.mGet', async cluster => { + assert.deepEqual( + await cluster.mGet(['key']), + [null] + ); + }); +}); diff --git a/lib/commands/MGET.ts b/lib/commands/MGET.ts new file mode 100644 index 0000000000..fdf5b3dde8 --- /dev/null +++ b/lib/commands/MGET.ts @@ -0,0 +1,11 @@ +import { transformReplyStringNullArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(keys: Array): Array { + return ['MGET', ...keys]; +} + +export const transformReply = transformReplyStringNullArray; diff --git a/lib/commands/MIGRATE.spec.ts b/lib/commands/MIGRATE.spec.ts new file mode 100644 index 0000000000..eb233f2290 --- /dev/null +++ b/lib/commands/MIGRATE.spec.ts @@ -0,0 +1,76 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './MIGRATE'; + +describe('MIGRATE', () => { + describe('transformArguments', () => { + it('single key', () => { + assert.deepEqual( + transformArguments('127.0.0.1', 6379, 'key', 0, 10), + ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10'] + ); + }); + + it('multiple keys', () => { + assert.deepEqual( + transformArguments('127.0.0.1', 6379, ['1', '2'], 0, 10), + ['MIGRATE', '127.0.0.1', '6379', '""', '0', '10', 'KEYS', '1', '2'] + ); + }); + + it('with COPY', () => { + assert.deepEqual( + transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + COPY: true + }), + ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'COPY'] + ); + }); + + it('with REPLACE', () => { + assert.deepEqual( + transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + REPLACE: true + }), + ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'REPLACE'] + ); + }); + + describe('with AUTH', () => { + it('password only', () => { + assert.deepEqual( + transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + AUTH: { + password: 'password' + } + }), + ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'AUTH', 'password'] + ); + }); + + it('username & password', () => { + assert.deepEqual( + transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + AUTH: { + username: 'username', + password: 'password' + } + }), + ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'AUTH2', 'username', 'password'] + ); + }); + }); + + it('with COPY, REPLACE, AUTH', () => { + assert.deepEqual( + transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + COPY: true, + REPLACE: true, + AUTH: { + password: 'password' + } + }), + ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'COPY', 'REPLACE', 'AUTH', 'password'] + ); + }); + }); +}); diff --git a/lib/commands/MIGRATE.ts b/lib/commands/MIGRATE.ts new file mode 100644 index 0000000000..1d2fc075ef --- /dev/null +++ b/lib/commands/MIGRATE.ts @@ -0,0 +1,65 @@ +import { AuthOptions } from './AUTH'; +import { transformReplyString } from './generic-transformers'; + +interface MigrateOptions { + COPY?: true; + REPLACE?: true; + AUTH?: AuthOptions; +} + +export function transformArguments( + host: string, + port: number, + key: string | Array, + destinationDb: number, + timeout: number, + options?: MigrateOptions +): Array { + const args = ['MIGRATE', host, port.toString()], + isKeyString = typeof key === 'string'; + + if (isKeyString) { + args.push(key as string); + } else { + args.push('""'); + } + + args.push( + destinationDb.toString(), + timeout.toString() + ); + + if (options?.COPY) { + args.push('COPY'); + } + + if (options?.REPLACE) { + args.push('REPLACE'); + } + + if (options?.AUTH) { + if (options.AUTH.username) { + args.push( + 'AUTH2', + options.AUTH.username, + options.AUTH.password + ); + } else { + args.push( + 'AUTH', + options.AUTH.password + ); + } + } + + if (!isKeyString) { + args.push( + 'KEYS', + ...key + ); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/MODULE_LIST.spec.ts b/lib/commands/MODULE_LIST.spec.ts new file mode 100644 index 0000000000..eeeb774ebf --- /dev/null +++ b/lib/commands/MODULE_LIST.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './MODULE_LIST'; + +describe('MODULE LIST', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['MODULE', 'LIST'] + ); + }); +}); diff --git a/lib/commands/MODULE_LIST.ts b/lib/commands/MODULE_LIST.ts new file mode 100644 index 0000000000..53ad14b68e --- /dev/null +++ b/lib/commands/MODULE_LIST.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['MODULE', 'LIST']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/MODULE_LOAD.spec.ts b/lib/commands/MODULE_LOAD.spec.ts new file mode 100644 index 0000000000..5a99a232ca --- /dev/null +++ b/lib/commands/MODULE_LOAD.spec.ts @@ -0,0 +1,20 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './MODULE_LOAD'; + +describe('MODULE LOAD', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('path'), + ['MODULE', 'LOAD', 'path'] + ); + }); + + it('with module args', () => { + assert.deepEqual( + transformArguments('path', ['1', '2']), + ['MODULE', 'LOAD', 'path', '1', '2'] + ); + }); + }); +}); diff --git a/lib/commands/MODULE_LOAD.ts b/lib/commands/MODULE_LOAD.ts new file mode 100644 index 0000000000..cd2347af24 --- /dev/null +++ b/lib/commands/MODULE_LOAD.ts @@ -0,0 +1,13 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(path: string, moduleArgs?: Array): Array { + const args = ['MODULE', 'LOAD', path]; + + if (moduleArgs) { + args.push(...moduleArgs); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/MODULE_UNLOAD.spec.ts b/lib/commands/MODULE_UNLOAD.spec.ts new file mode 100644 index 0000000000..d8af96c54f --- /dev/null +++ b/lib/commands/MODULE_UNLOAD.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './MODULE_UNLOAD'; + +describe('MODULE UNLOAD', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('name'), + ['MODULE', 'UNLOAD', 'name'] + ); + }); +}); diff --git a/lib/commands/MODULE_UNLOAD.ts b/lib/commands/MODULE_UNLOAD.ts new file mode 100644 index 0000000000..3737784f00 --- /dev/null +++ b/lib/commands/MODULE_UNLOAD.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(name: string): Array { + return ['MODULE', 'UNLOAD', name]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/MOVE.spec.ts b/lib/commands/MOVE.spec.ts new file mode 100644 index 0000000000..a05ca4613e --- /dev/null +++ b/lib/commands/MOVE.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './MOVE'; + +describe('MOVE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['MOVE', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.move', async client => { + assert.equal( + await client.move('key', 1), + false + ); + }); +}); diff --git a/lib/commands/MOVE.ts b/lib/commands/MOVE.ts new file mode 100644 index 0000000000..74bb88c5e7 --- /dev/null +++ b/lib/commands/MOVE.ts @@ -0,0 +1,7 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export function transformArguments(key: string, db: number): Array { + return ['MOVE', key, db.toString()]; +} + +export const transformReply = transformReplyBoolean; \ No newline at end of file diff --git a/lib/commands/MSET.spec.ts b/lib/commands/MSET.spec.ts new file mode 100644 index 0000000000..4445f4a728 --- /dev/null +++ b/lib/commands/MSET.spec.ts @@ -0,0 +1,42 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './MSET'; + +describe('MSET', () => { + describe('transformArguments', () => { + it("['key1', 'value1', 'key2', 'value2']", () => { + assert.deepEqual( + transformArguments(['key1', 'value1', 'key2', 'value2']), + ['MSET', 'key1', 'value1', 'key2', 'value2'] + ); + }); + + it("[['key1', 'value1'], ['key2', 'value2']]", () => { + assert.deepEqual( + transformArguments([['key1', 'value1'], ['key2', 'value2']]), + ['MSET', 'key1', 'value1', 'key2', 'value2'] + ); + }); + + it("{key1: 'value1'. key2: 'value2'}", () => { + assert.deepEqual( + transformArguments({ key1: 'value1', key2: 'value2' }), + ['MSET', 'key1', 'value1', 'key2', 'value2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.mSet', async client => { + assert.equal( + await client.mSet(['key1', 'value1', 'key2', 'value2']), + 'OK' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.mSet', async cluster => { + assert.equal( + await cluster.mSet(['{key}1', 'value1', '{key}2', 'value2']), + 'OK' + ); + }); +}); diff --git a/lib/commands/MSET.ts b/lib/commands/MSET.ts new file mode 100644 index 0000000000..d51790caee --- /dev/null +++ b/lib/commands/MSET.ts @@ -0,0 +1,19 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(toSet: Array<[string, string]> | Array | Record): Array { + const args = ['MSET']; + + if (Array.isArray(toSet)) { + args.push(...toSet.flat()); + } else { + for (const key of Object.keys(toSet)) { + args.push(key, toSet[key]); + } + } + + return args; +} + +export const transformReply = transformReplyString; \ No newline at end of file diff --git a/lib/commands/MSETNX.spec.ts b/lib/commands/MSETNX.spec.ts new file mode 100644 index 0000000000..7f61a43e8d --- /dev/null +++ b/lib/commands/MSETNX.spec.ts @@ -0,0 +1,42 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './MSETNX'; + +describe('MSETNX', () => { + describe('transformArguments', () => { + it("['key1', 'value1', 'key2', 'value2']", () => { + assert.deepEqual( + transformArguments(['key1', 'value1', 'key2', 'value2']), + ['MSETNX', 'key1', 'value1', 'key2', 'value2'] + ); + }); + + it("[['key1', 'value1'], ['key2', 'value2']]", () => { + assert.deepEqual( + transformArguments([['key1', 'value1'], ['key2', 'value2']]), + ['MSETNX', 'key1', 'value1', 'key2', 'value2'] + ); + }); + + it("{key1: 'value1'. key2: 'value2'}", () => { + assert.deepEqual( + transformArguments({ key1: 'value1', key2: 'value2' }), + ['MSETNX', 'key1', 'value1', 'key2', 'value2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.mSetNX', async client => { + assert.equal( + await client.mSetNX(['key1', 'value1', 'key2', 'value2']), + true + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.mSetNX', async cluster => { + assert.equal( + await cluster.mSetNX(['{key}1', 'value1', '{key}2', 'value2']), + true + ); + }); +}); diff --git a/lib/commands/MSETNX.ts b/lib/commands/MSETNX.ts new file mode 100644 index 0000000000..c9c8c84037 --- /dev/null +++ b/lib/commands/MSETNX.ts @@ -0,0 +1,19 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(toSet: Array<[string, string]> | Array | Record): Array { + const args = ['MSETNX']; + + if (Array.isArray(toSet)) { + args.push(...toSet.flat()); + } else { + for (const key of Object.keys(toSet)) { + args.push(key, toSet[key]); + } + } + + return args; +} + +export const transformReply = transformReplyBoolean; \ No newline at end of file diff --git a/lib/commands/PERSIST.spec.ts b/lib/commands/PERSIST.spec.ts new file mode 100644 index 0000000000..05c0e7aed8 --- /dev/null +++ b/lib/commands/PERSIST.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './PERSIST'; + +describe('PERSIST', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['PERSIST', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.persist', async client => { + assert.equal( + await client.persist('key'), + false + ); + }); +}); diff --git a/lib/commands/PERSIST.ts b/lib/commands/PERSIST.ts new file mode 100644 index 0000000000..fc85a21c98 --- /dev/null +++ b/lib/commands/PERSIST.ts @@ -0,0 +1,9 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['PERSIST', key]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/PEXPIRE.spec.ts b/lib/commands/PEXPIRE.spec.ts new file mode 100644 index 0000000000..b7c4e1df46 --- /dev/null +++ b/lib/commands/PEXPIRE.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './PEXPIRE'; + +describe('PEXPIRE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['PEXPIRE', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.pExpire', async client => { + assert.equal( + await client.pExpire('key', 1), + false + ); + }); +}); diff --git a/lib/commands/PEXPIRE.ts b/lib/commands/PEXPIRE.ts new file mode 100644 index 0000000000..d795f2fc0d --- /dev/null +++ b/lib/commands/PEXPIRE.ts @@ -0,0 +1,9 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, milliseconds: number): Array { + return ['PEXPIRE', key, milliseconds.toString()]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/PEXPIREAT.spec.ts b/lib/commands/PEXPIREAT.spec.ts new file mode 100644 index 0000000000..6e5fc37ed5 --- /dev/null +++ b/lib/commands/PEXPIREAT.spec.ts @@ -0,0 +1,29 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './PEXPIREAT'; + +describe('PEXPIREAT', () => { + describe('transformArguments', () => { + it('number', () => { + assert.deepEqual( + transformArguments('key', 1), + ['PEXPIREAT', 'key', '1'] + ); + }); + + it('date', () => { + const d = new Date(); + assert.deepEqual( + transformArguments('key', d), + ['PEXPIREAT', 'key', d.getTime().toString()] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.pExpireAt', async client => { + assert.equal( + await client.pExpireAt('key', 1), + false + ); + }); +}); diff --git a/lib/commands/PEXPIREAT.ts b/lib/commands/PEXPIREAT.ts new file mode 100644 index 0000000000..91f38f8894 --- /dev/null +++ b/lib/commands/PEXPIREAT.ts @@ -0,0 +1,13 @@ +import { transformPXAT, transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, millisecondsTimestamp: number | Date): Array { + return [ + 'PEXPIREAT', + key, + transformPXAT(millisecondsTimestamp) + ]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/PFADD.spec.ts b/lib/commands/PFADD.spec.ts new file mode 100644 index 0000000000..74f03ea3ce --- /dev/null +++ b/lib/commands/PFADD.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './PFADD'; + +describe('PFADD', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'element'), + ['PFADD', 'key', 'element'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['PFADD', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.pfAdd', async client => { + assert.equal( + await client.pfAdd('key', '1'), + true + ); + }); +}); diff --git a/lib/commands/PFADD.ts b/lib/commands/PFADD.ts new file mode 100644 index 0000000000..3348a98852 --- /dev/null +++ b/lib/commands/PFADD.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, element: string | Array): Array { + return pushVerdictArguments(['PFADD', key], element); +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/PFCOUNT.spec.ts b/lib/commands/PFCOUNT.spec.ts new file mode 100644 index 0000000000..049fa2c200 --- /dev/null +++ b/lib/commands/PFCOUNT.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './PFCOUNT'; + +describe('PFCOUNT', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['PFCOUNT', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['PFCOUNT', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.pfCount', async client => { + assert.equal( + await client.pfCount('key'), + 0 + ); + }); +}); diff --git a/lib/commands/PFCOUNT.ts b/lib/commands/PFCOUNT.ts new file mode 100644 index 0000000000..eac710a354 --- /dev/null +++ b/lib/commands/PFCOUNT.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string | Array): Array { + return pushVerdictArguments(['PFCOUNT'], key); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/PFMERGE.spec.ts b/lib/commands/PFMERGE.spec.ts new file mode 100644 index 0000000000..1f6ed24bcd --- /dev/null +++ b/lib/commands/PFMERGE.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './PFMERGE'; + +describe('PFMERGE', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('destination', 'source'), + ['PFMERGE', 'destination', 'source'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('destination', ['1', '2']), + ['PFMERGE', 'destination', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.pfMerge', async client => { + assert.equal( + await client.pfMerge('destination', 'source'), + 'OK' + ); + }); +}); diff --git a/lib/commands/PFMERGE.ts b/lib/commands/PFMERGE.ts new file mode 100644 index 0000000000..73a4a2edb9 --- /dev/null +++ b/lib/commands/PFMERGE.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(destination: string, source: string | Array): Array { + return pushVerdictArguments(['PFMERGE', destination], source); +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/PING.spec.ts b/lib/commands/PING.spec.ts new file mode 100644 index 0000000000..87d9359a36 --- /dev/null +++ b/lib/commands/PING.spec.ts @@ -0,0 +1,18 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; + +describe('PING', () => { + itWithClient(TestRedisServers.OPEN, 'client.ping', async client => { + assert.equal( + await client.ping(), + 'PONG' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.ping', async cluster => { + assert.equal( + await cluster.ping(), + 'PONG' + ); + }); +}); diff --git a/lib/commands/PING.ts b/lib/commands/PING.ts new file mode 100644 index 0000000000..36e92a08cf --- /dev/null +++ b/lib/commands/PING.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['PING']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/PSETEX.spec.ts b/lib/commands/PSETEX.spec.ts new file mode 100644 index 0000000000..c98142effa --- /dev/null +++ b/lib/commands/PSETEX.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './PSETEX'; + +describe('PSETEX', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1, 'value'), + ['PSETEX', 'key', '1', 'value'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.pSetEx', async client => { + assert.equal( + await client.pSetEx('key', 1, 'value'), + 'OK' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.pSetEx', async cluster => { + assert.equal( + await cluster.pSetEx('key', 1, 'value'), + 'OK' + ); + }); +}); diff --git a/lib/commands/PSETEX.ts b/lib/commands/PSETEX.ts new file mode 100644 index 0000000000..101030d2e6 --- /dev/null +++ b/lib/commands/PSETEX.ts @@ -0,0 +1,14 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, milliseconds: number, value: string): Array { + return [ + 'PSETEX', + key, + milliseconds.toString(), + value + ]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/PTTL.spec.ts b/lib/commands/PTTL.spec.ts new file mode 100644 index 0000000000..35f48c2cc3 --- /dev/null +++ b/lib/commands/PTTL.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './PTTL'; + +describe('PTTL', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['PTTL', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.pTTL', async client => { + assert.equal( + await client.pTTL('key'), + -2 + ); + }); +}); diff --git a/lib/commands/PTTL.ts b/lib/commands/PTTL.ts new file mode 100644 index 0000000000..8356c75bbd --- /dev/null +++ b/lib/commands/PTTL.ts @@ -0,0 +1,11 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['PTTL', key]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/PUBLISH.spec.ts b/lib/commands/PUBLISH.spec.ts new file mode 100644 index 0000000000..e746b9490e --- /dev/null +++ b/lib/commands/PUBLISH.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './PUBLISH'; + +describe('PUBLISH', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('channel', 'message'), + ['PUBLISH', 'channel', 'message'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.publish', async client => { + assert.equal( + await client.publish('channel', 'message'), + 0 + ); + }); +}); diff --git a/lib/commands/PUBLISH.ts b/lib/commands/PUBLISH.ts new file mode 100644 index 0000000000..51387a6803 --- /dev/null +++ b/lib/commands/PUBLISH.ts @@ -0,0 +1,7 @@ +import { transformReplyNumber } from './generic-transformers'; + +export function transformArguments(channel: string, message: string): Array { + return ['PUBLISH', channel, message]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/PUBSUB_CHANNELS.spec.ts b/lib/commands/PUBSUB_CHANNELS.spec.ts new file mode 100644 index 0000000000..5ff9db60df --- /dev/null +++ b/lib/commands/PUBSUB_CHANNELS.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './PUBSUB_CHANNELS'; + +describe('PUBSUB CHANNELS', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['PUBSUB', 'CHANNELS'] + ); + }); + + it('with pattern', () => { + assert.deepEqual( + transformArguments('patter*'), + ['PUBSUB', 'CHANNELS', 'patter*'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.pubSubChannels', async client => { + assert.deepEqual( + await client.pubSubChannels(), + [] + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.pubSubChannels', async cluster => { + assert.deepEqual( + await cluster.pubSubChannels(), + [] + ); + }); +}); diff --git a/lib/commands/PUBSUB_CHANNELS.ts b/lib/commands/PUBSUB_CHANNELS.ts new file mode 100644 index 0000000000..aa7a0749fc --- /dev/null +++ b/lib/commands/PUBSUB_CHANNELS.ts @@ -0,0 +1,15 @@ +import { transformReplyStringArray } from './generic-transformers'; + +export const IS_READ_ONLY = true; + +export function transformArguments(pattern?: string): Array { + const args = ['PUBSUB', 'CHANNELS']; + + if (pattern) { + args.push(pattern); + } + + return args; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/PUBSUB_NUMPAT.spec.ts b/lib/commands/PUBSUB_NUMPAT.spec.ts new file mode 100644 index 0000000000..49a39eedae --- /dev/null +++ b/lib/commands/PUBSUB_NUMPAT.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './PUBSUB_NUMPAT'; + +describe('PUBSUB NUMPAT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['PUBSUB', 'NUMPAT'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.pubSubNumPat', async client => { + assert.equal( + await client.pubSubNumPat(), + 0 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.pubSubNumPat', async cluster => { + assert.equal( + await cluster.pubSubNumPat(), + 0 + ); + }); +}); diff --git a/lib/commands/PUBSUB_NUMPAT.ts b/lib/commands/PUBSUB_NUMPAT.ts new file mode 100644 index 0000000000..966a8d237c --- /dev/null +++ b/lib/commands/PUBSUB_NUMPAT.ts @@ -0,0 +1,9 @@ +import { transformReplyString } from './generic-transformers'; + +export const IS_READ_ONLY = true; + +export function transformArguments(): Array { + return ['PUBSUB', 'NUMPAT']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/PUBSUB_NUMSUB.spec.ts b/lib/commands/PUBSUB_NUMSUB.spec.ts new file mode 100644 index 0000000000..74065dbb48 --- /dev/null +++ b/lib/commands/PUBSUB_NUMSUB.spec.ts @@ -0,0 +1,42 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './PUBSUB_NUMSUB'; + +describe('PUBSUB NUMSUB', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['PUBSUB', 'NUMSUB'] + ); + }); + + it('string', () => { + assert.deepEqual( + transformArguments('channel'), + ['PUBSUB', 'NUMSUB', 'channel'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['PUBSUB', 'NUMSUB', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.pubSubNumSub', async client => { + assert.deepEqual( + await client.pubSubNumSub(), + Object.create(null) + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.pubSubNumPat', async cluster => { + assert.deepEqual( + await cluster.pubSubNumSub(), + Object.create(null) + ); + }); +}); diff --git a/lib/commands/PUBSUB_NUMSUB.ts b/lib/commands/PUBSUB_NUMSUB.ts new file mode 100644 index 0000000000..c68b0d9a7f --- /dev/null +++ b/lib/commands/PUBSUB_NUMSUB.ts @@ -0,0 +1,23 @@ +import { pushVerdictArguments } from './generic-transformers'; + +export const IS_READ_ONLY = true; + +export function transformArguments(channels?: Array | string): Array { + const args = ['PUBSUB', 'NUMSUB']; + + if (channels) { + pushVerdictArguments(args, channels); + } + + return args; +} + +export function transformReply(rawReply: Array): Record { + const transformedReply = Object.create(null); + + for (let i = 0; i < rawReply.length; i +=2) { + transformedReply[rawReply[i]] = rawReply[i + 1]; + } + + return transformedReply; +} diff --git a/lib/commands/RANDOMKEY.spec.ts b/lib/commands/RANDOMKEY.spec.ts new file mode 100644 index 0000000000..171c42be11 --- /dev/null +++ b/lib/commands/RANDOMKEY.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './RANDOMKEY'; + +describe('RANDOMKEY', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['RANDOMKEY'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.randomKey', async client => { + assert.equal( + await client.randomKey(), + null + ); + }); +}); diff --git a/lib/commands/RANDOMKEY.ts b/lib/commands/RANDOMKEY.ts new file mode 100644 index 0000000000..fad0b073c7 --- /dev/null +++ b/lib/commands/RANDOMKEY.ts @@ -0,0 +1,9 @@ +export const IS_READ_ONLY = true; + +export function transformArguments(): Array { + return ['RANDOMKEY']; +} + +export function transformReply(reply: string | null): string | null { + return reply; +} diff --git a/lib/commands/READONLY.spec.ts b/lib/commands/READONLY.spec.ts new file mode 100644 index 0000000000..aa4db47f81 --- /dev/null +++ b/lib/commands/READONLY.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './READONLY'; + +describe('READONLY', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['READONLY'] + ); + }); +}); diff --git a/lib/commands/READONLY.ts b/lib/commands/READONLY.ts new file mode 100644 index 0000000000..00fbe4e435 --- /dev/null +++ b/lib/commands/READONLY.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['READONLY']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/READWRITE.spec.ts b/lib/commands/READWRITE.spec.ts new file mode 100644 index 0000000000..6ce4a3ee56 --- /dev/null +++ b/lib/commands/READWRITE.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './READWRITE'; + +describe('READWRITE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['READWRITE'] + ); + }); +}); diff --git a/lib/commands/READWRITE.ts b/lib/commands/READWRITE.ts new file mode 100644 index 0000000000..16f9560440 --- /dev/null +++ b/lib/commands/READWRITE.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['READWRITE']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/RENAME.spec.ts b/lib/commands/RENAME.spec.ts new file mode 100644 index 0000000000..9d447c600b --- /dev/null +++ b/lib/commands/RENAME.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './RENAME'; + +describe('RENAME', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('from', 'to'), + ['RENAME', 'from', 'to'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.rename', async client => { + await client.set('from', 'value'); + + assert.equal( + await client.rename('from', 'to'), + 'OK' + ); + }); +}); diff --git a/lib/commands/RENAME.ts b/lib/commands/RENAME.ts new file mode 100644 index 0000000000..0f9582677f --- /dev/null +++ b/lib/commands/RENAME.ts @@ -0,0 +1,9 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, newKey: string): Array { + return ['RENAME', key, newKey]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/RENAMENX.spec.ts b/lib/commands/RENAMENX.spec.ts new file mode 100644 index 0000000000..f438834b90 --- /dev/null +++ b/lib/commands/RENAMENX.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './RENAMENX'; + +describe('RENAMENX', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('from', 'to'), + ['RENAMENX', 'from', 'to'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.renameNX', async client => { + await client.set('from', 'value'); + + assert.equal( + await client.renameNX('from', 'to'), + true + ); + }); +}); diff --git a/lib/commands/RENAMENX.ts b/lib/commands/RENAMENX.ts new file mode 100644 index 0000000000..883d2ca296 --- /dev/null +++ b/lib/commands/RENAMENX.ts @@ -0,0 +1,9 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, newKey: string): Array { + return ['RENAMENX', key, newKey]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/REPLICAOF.spec.ts b/lib/commands/REPLICAOF.spec.ts new file mode 100644 index 0000000000..ab1906944c --- /dev/null +++ b/lib/commands/REPLICAOF.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './REPLICAOF'; + +describe('REPLICAOF', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('host', 1), + ['REPLICAOF', 'host', '1'] + ); + }); +}); diff --git a/lib/commands/REPLICAOF.ts b/lib/commands/REPLICAOF.ts new file mode 100644 index 0000000000..0b56bd74dc --- /dev/null +++ b/lib/commands/REPLICAOF.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(host: string, port: number): Array { + return ['REPLICAOF', host, port.toString()]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/RESTORE-ASKING.spec.ts b/lib/commands/RESTORE-ASKING.spec.ts new file mode 100644 index 0000000000..de9fce5c62 --- /dev/null +++ b/lib/commands/RESTORE-ASKING.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './RESTORE-ASKING'; + +describe('RESTORE-ASKING', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['RESTORE-ASKING'] + ); + }); +}); diff --git a/lib/commands/RESTORE-ASKING.ts b/lib/commands/RESTORE-ASKING.ts new file mode 100644 index 0000000000..4d178cb1f0 --- /dev/null +++ b/lib/commands/RESTORE-ASKING.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['RESTORE-ASKING']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/ROLE.spec.ts b/lib/commands/ROLE.spec.ts new file mode 100644 index 0000000000..5b647e07ca --- /dev/null +++ b/lib/commands/ROLE.spec.ts @@ -0,0 +1,69 @@ +import { strict as assert } from 'assert'; +import { itWithClient, TestRedisServers } from '../test-utils'; +import { transformArguments, transformReply } from './ROLE'; + +describe('ROLE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['ROLE'] + ); + }); + + describe('transformReply', () => { + it('master', () => { + assert.deepEqual( + transformReply(['master', 3129659, [['127.0.0.1', '9001', '3129242'], ['127.0.0.1', '9002', '3129543']]]), + { + role: 'master', + replicationOffest: 3129659, + replicas: [{ + ip: '127.0.0.1', + port: 9001, + replicationOffest: 3129242 + }, { + ip: '127.0.0.1', + port: 9002, + replicationOffest: 3129543 + }] + } + ); + }); + + it('replica', () => { + assert.deepEqual( + transformReply(['slave', '127.0.0.1', 9000, 'connected', 3167038]), + { + role: 'slave', + master: { + ip: '127.0.0.1', + port: 9000 + }, + state: 'connected', + dataReceived: 3167038 + } + ); + }); + + it('sentinel', () => { + assert.deepEqual( + transformReply(['sentinel', ['resque-master', 'html-fragments-master', 'stats-master', 'metadata-master']]), + { + role: 'sentinel', + masterNames: ['resque-master', 'html-fragments-master', 'stats-master', 'metadata-master'] + } + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.role', async client => { + assert.deepEqual( + await client.role(), + { + role: 'master', + replicationOffest: 0, + replicas: [] + } + ); + }); +}); diff --git a/lib/commands/ROLE.ts b/lib/commands/ROLE.ts new file mode 100644 index 0000000000..b1d6041fdf --- /dev/null +++ b/lib/commands/ROLE.ts @@ -0,0 +1,75 @@ +export const IS_READ_ONLY = true; + +export function transformArguments(): Array { + return ['ROLE']; +} + +interface RoleReplyInterface { + role: T; +} + +type RoleMasterRawReply = ['master', number, Array<[string, string, string]>]; + +interface RoleMasterReply extends RoleReplyInterface<'master'> { + replicationOffest: number; + replicas: Array<{ + ip: string; + port: number; + replicationOffest: number; + }>; +} + +type RoleReplicaState = 'connect' | 'connecting' | 'sync' | 'connected'; + +type RoleReplicaRawReply = ['slave', string, number, RoleReplicaState, number]; + +interface RoleReplicaReply extends RoleReplyInterface<'slave'> { + master: { + ip: string; + port: number; + }; + state: RoleReplicaState; + dataReceived: number; +} + +type RoleSentinelRawReply = ['sentinel', Array]; + +interface RoleSentinelReply extends RoleReplyInterface<'sentinel'> { + masterNames: Array; +} + +type RoleRawReply = RoleMasterRawReply | RoleReplicaRawReply | RoleSentinelRawReply; + +type RoleReply = RoleMasterReply | RoleReplicaReply | RoleSentinelReply; + +export function transformReply(reply: RoleRawReply): RoleReply { + switch (reply[0]) { + case 'master': + return { + role: 'master', + replicationOffest: reply[1], + replicas: reply[2].map(([ip, port, replicationOffest]) => ({ + ip, + port: Number(port), + replicationOffest: Number(replicationOffest) + })) + }; + + case 'slave': + return { + role: 'slave', + master: { + ip: reply[1], + port: reply[2] + }, + state: reply[3], + dataReceived: reply[4] + }; + + case 'sentinel': + return { + role: 'sentinel', + masterNames: reply[1] + }; + } +} diff --git a/lib/commands/RPOP.spec.ts b/lib/commands/RPOP.spec.ts new file mode 100644 index 0000000000..2a753ff1a6 --- /dev/null +++ b/lib/commands/RPOP.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './RPOP'; + +describe('RPOP', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['RPOP', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.rPop', async client => { + assert.equal( + await client.rPop('key'), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.rPop', async cluster => { + assert.equal( + await cluster.rPop('key'), + null + ); + }); +}); diff --git a/lib/commands/RPOP.ts b/lib/commands/RPOP.ts new file mode 100644 index 0000000000..daccbf5d42 --- /dev/null +++ b/lib/commands/RPOP.ts @@ -0,0 +1,9 @@ +import { transformReplyStringNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['RPOP', key]; +} + +export const transformReply = transformReplyStringNull; diff --git a/lib/commands/RPOPLPUSH.spec.ts b/lib/commands/RPOPLPUSH.spec.ts new file mode 100644 index 0000000000..75b5f2e18f --- /dev/null +++ b/lib/commands/RPOPLPUSH.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './RPOPLPUSH'; + +describe('RPOPLPUSH', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('source', 'destination'), + ['RPOPLPUSH', 'source', 'destination'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.rPopLPush', async client => { + assert.equal( + await client.rPopLPush('source', 'destination'), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.rPopLPush', async cluster => { + assert.equal( + await cluster.rPopLPush('{tag}source', '{tag}destination'), + null + ); + }); +}); diff --git a/lib/commands/RPOPLPUSH.ts b/lib/commands/RPOPLPUSH.ts new file mode 100644 index 0000000000..db388906d3 --- /dev/null +++ b/lib/commands/RPOPLPUSH.ts @@ -0,0 +1,9 @@ +import { transformReplyNumberNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(source: string, destination: string): Array { + return ['RPOPLPUSH', source, destination]; +} + +export const transformReply = transformReplyNumberNull; diff --git a/lib/commands/RPOP_COUNT.spec.ts b/lib/commands/RPOP_COUNT.spec.ts new file mode 100644 index 0000000000..2624540f12 --- /dev/null +++ b/lib/commands/RPOP_COUNT.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './RPOP_COUNT'; + +describe('RPOP COUNT', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['RPOP', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.rPopCount', async client => { + assert.equal( + await client.rPopCount('key', 1), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.rPopCount', async cluster => { + assert.equal( + await cluster.rPopCount('key', 1), + null + ); + }); +}); diff --git a/lib/commands/RPOP_COUNT.ts b/lib/commands/RPOP_COUNT.ts new file mode 100644 index 0000000000..205704274f --- /dev/null +++ b/lib/commands/RPOP_COUNT.ts @@ -0,0 +1,9 @@ +import { transformReplyStringArrayNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, count: number): Array { + return ['RPOP', key, count.toString()]; +} + +export const transformReply = transformReplyStringArrayNull; diff --git a/lib/commands/RPUSH.spec.ts b/lib/commands/RPUSH.spec.ts new file mode 100644 index 0000000000..4336d10c9a --- /dev/null +++ b/lib/commands/RPUSH.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './RPUSH'; + +describe('RPUSH', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'element'), + ['RPUSH', 'key', 'element'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['RPUSH', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.rPush', async client => { + assert.equal( + await client.rPush('key', 'element'), + 1 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.rPush', async cluster => { + assert.equal( + await cluster.rPush('key', 'element'), + 1 + ); + }); +}); diff --git a/lib/commands/RPUSH.ts b/lib/commands/RPUSH.ts new file mode 100644 index 0000000000..191d2704e0 --- /dev/null +++ b/lib/commands/RPUSH.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, element: string | Array): Array { + return pushVerdictArguments(['RPUSH', key], element); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/RPUSHX.spec.ts b/lib/commands/RPUSHX.spec.ts new file mode 100644 index 0000000000..18f91e8bef --- /dev/null +++ b/lib/commands/RPUSHX.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './RPUSHX'; + +describe('RPUSHX', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'element'), + ['RPUSHX', 'key', 'element'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['RPUSHX', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.rPushX', async client => { + assert.equal( + await client.rPushX('key', 'element'), + 0 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.rPushX', async cluster => { + assert.equal( + await cluster.rPushX('key', 'element'), + 0 + ); + }); +}); diff --git a/lib/commands/RPUSHX.ts b/lib/commands/RPUSHX.ts new file mode 100644 index 0000000000..a07615a58e --- /dev/null +++ b/lib/commands/RPUSHX.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, element: string | Array): Array { + return pushVerdictArguments(['RPUSHX', key], element); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/SADD.spec.ts b/lib/commands/SADD.spec.ts new file mode 100644 index 0000000000..bf1ee48fe7 --- /dev/null +++ b/lib/commands/SADD.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SADD'; + +describe('SADD', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'member'), + ['SADD', 'key', 'member'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['SADD', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sAdd', async client => { + assert.equal( + await client.sAdd('key', 'member'), + 1 + ); + }); +}); diff --git a/lib/commands/SADD.ts b/lib/commands/SADD.ts new file mode 100644 index 0000000000..a14ba1686c --- /dev/null +++ b/lib/commands/SADD.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, members: string | Array): Array { + return pushVerdictArguments(['SADD', key], members); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/SAVE.spec.ts b/lib/commands/SAVE.spec.ts new file mode 100644 index 0000000000..1e1987b5ab --- /dev/null +++ b/lib/commands/SAVE.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './SAVE'; + +describe('SAVE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['SAVE'] + ); + }); +}); diff --git a/lib/commands/SAVE.ts b/lib/commands/SAVE.ts new file mode 100644 index 0000000000..38a397892f --- /dev/null +++ b/lib/commands/SAVE.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['SAVE']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/SCAN.spec.ts b/lib/commands/SCAN.spec.ts new file mode 100644 index 0000000000..975c4cb6d2 --- /dev/null +++ b/lib/commands/SCAN.spec.ts @@ -0,0 +1,84 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './SCAN'; + +describe('SCAN', () => { + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + transformArguments(0), + ['SCAN', '0'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + transformArguments(0, { + MATCH: 'pattern' + }), + ['SCAN', '0', 'MATCH', 'pattern'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments(0, { + COUNT: 1 + }), + ['SCAN', '0', 'COUNT', '1'] + ); + }); + + it('with TYPE', () => { + assert.deepEqual( + transformArguments(0, { + TYPE: 'stream' + }), + ['SCAN', '0', 'TYPE', 'stream'] + ); + }); + + it('with MATCH & COUNT & TYPE', () => { + assert.deepEqual( + transformArguments(0, { + MATCH: 'pattern', + COUNT: 1, + TYPE: 'stream' + }), + ['SCAN', '0', 'MATCH', 'pattern', 'COUNT', '1', 'TYPE', 'stream'] + ); + }); + }); + + describe('transformReply', () => { + it('without keys', () => { + assert.deepEqual( + transformReply(['0', []]), + { + cursor: 0, + keys: [] + } + ); + }); + + it('with keys', () => { + assert.deepEqual( + transformReply(['0', ['key']]), + { + cursor: 0, + keys: ['key'] + } + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.scan', async client => { + assert.deepEqual( + await client.scan(0), + { + cursor: 0, + keys: [] + } + ); + }); +}); diff --git a/lib/commands/SCAN.ts b/lib/commands/SCAN.ts new file mode 100644 index 0000000000..e3541ea9a7 --- /dev/null +++ b/lib/commands/SCAN.ts @@ -0,0 +1,28 @@ +import { ScanOptions, pushScanArguments } from './generic-transformers'; + +export const IS_READ_ONLY = true; +export interface ScanCommandOptions extends ScanOptions { + TYPE?: string; +} + +export function transformArguments(cursor: number, options?: ScanCommandOptions): Array { + const args = pushScanArguments(['SCAN'], cursor, options); + + if (options?.TYPE) { + args.push('TYPE', options.TYPE); + } + + return args; +} + +export interface ScanReply { + cursor: number; + keys: Array; +} + +export function transformReply([cursor, keys]: [string, Array]): ScanReply { + return { + cursor: Number(cursor), + keys + }; +} diff --git a/lib/commands/SCARD.spec.ts b/lib/commands/SCARD.spec.ts new file mode 100644 index 0000000000..b668169381 --- /dev/null +++ b/lib/commands/SCARD.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SCARD'; + +describe('SCARD', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['SCARD', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sCard', async client => { + assert.equal( + await client.sCard('key'), + 0 + ); + }); +}); diff --git a/lib/commands/SCARD.ts b/lib/commands/SCARD.ts new file mode 100644 index 0000000000..8a90bd3b02 --- /dev/null +++ b/lib/commands/SCARD.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['SCARD', key]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/SCRIPT_DEBUG.spec.ts b/lib/commands/SCRIPT_DEBUG.spec.ts new file mode 100644 index 0000000000..9096605143 --- /dev/null +++ b/lib/commands/SCRIPT_DEBUG.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './SCRIPT_DEBUG'; + +describe('SCRIPT DEBUG', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('NO'), + ['SCRIPT', 'DEBUG', 'NO'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.scriptDebug', async client => { + assert.equal( + await client.scriptDebug('NO'), + 'OK' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.scriptDebug', async cluster => { + assert.equal( + await cluster.scriptDebug('NO'), + 'OK' + ); + }); +}); diff --git a/lib/commands/SCRIPT_DEBUG.ts b/lib/commands/SCRIPT_DEBUG.ts new file mode 100644 index 0000000000..e93443a586 --- /dev/null +++ b/lib/commands/SCRIPT_DEBUG.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(mode: 'YES' | 'SYNC' | 'NO'): Array { + return ['SCRIPT', 'DEBUG', mode]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/SCRIPT_EXISTS.spec.ts b/lib/commands/SCRIPT_EXISTS.spec.ts new file mode 100644 index 0000000000..d03521a5c6 --- /dev/null +++ b/lib/commands/SCRIPT_EXISTS.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './SCRIPT_EXISTS'; + +describe('SCRIPT EXISTS', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('sha1'), + ['SCRIPT', 'EXISTS', 'sha1'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['SCRIPT', 'EXISTS', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.scriptExists', async client => { + assert.deepEqual( + await client.scriptExists('sha1'), + [false] + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.scriptExists', async cluster => { + assert.deepEqual( + await cluster.scriptExists('sha1'), + [false] + ); + }); +}); diff --git a/lib/commands/SCRIPT_EXISTS.ts b/lib/commands/SCRIPT_EXISTS.ts new file mode 100644 index 0000000000..b127a0b261 --- /dev/null +++ b/lib/commands/SCRIPT_EXISTS.ts @@ -0,0 +1,7 @@ +import { pushVerdictArguments, transformReplyBooleanArray } from './generic-transformers'; + +export function transformArguments(sha1: string | Array): Array { + return pushVerdictArguments(['SCRIPT', 'EXISTS'], sha1); +} + +export const transformReply = transformReplyBooleanArray; diff --git a/lib/commands/SCRIPT_FLUSH.spec.ts b/lib/commands/SCRIPT_FLUSH.spec.ts new file mode 100644 index 0000000000..c1321676eb --- /dev/null +++ b/lib/commands/SCRIPT_FLUSH.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './SCRIPT_FLUSH'; + +describe('SCRIPT FLUSH', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['SCRIPT', 'FLUSH'] + ); + }); + + it('with mode', () => { + assert.deepEqual( + transformArguments('SYNC'), + ['SCRIPT', 'FLUSH', 'SYNC'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.scriptFlush', async client => { + assert.equal( + await client.scriptFlush(), + 'OK' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.scriptFlush', async cluster => { + assert.equal( + await cluster.scriptFlush(), + 'OK' + ); + }); +}); diff --git a/lib/commands/SCRIPT_FLUSH.ts b/lib/commands/SCRIPT_FLUSH.ts new file mode 100644 index 0000000000..83bc9e2b5d --- /dev/null +++ b/lib/commands/SCRIPT_FLUSH.ts @@ -0,0 +1,13 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(mode?: 'ASYNC' | 'SYNC'): Array { + const args = ['SCRIPT', 'FLUSH']; + + if (mode) { + args.push(mode); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/SCRIPT_KILL.spec.ts b/lib/commands/SCRIPT_KILL.spec.ts new file mode 100644 index 0000000000..e57265aa61 --- /dev/null +++ b/lib/commands/SCRIPT_KILL.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './SCRIPT_KILL'; + +describe('SCRIPT KILL', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['SCRIPT', 'KILL'] + ); + }); +}); diff --git a/lib/commands/SCRIPT_KILL.ts b/lib/commands/SCRIPT_KILL.ts new file mode 100644 index 0000000000..5c175b74d6 --- /dev/null +++ b/lib/commands/SCRIPT_KILL.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['SCRIPT', 'KILL']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/SCRIPT_LOAD.spec.ts b/lib/commands/SCRIPT_LOAD.spec.ts new file mode 100644 index 0000000000..46490f35c8 --- /dev/null +++ b/lib/commands/SCRIPT_LOAD.spec.ts @@ -0,0 +1,30 @@ +import { strict as assert } from 'assert'; +import { scriptSha1 } from '../lua-script'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './SCRIPT_LOAD'; + +describe('SCRIPT LOAD', () => { + const SCRIPT = 'return 1;', + SCRIPT_SHA1 = scriptSha1(SCRIPT); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments(SCRIPT), + ['SCRIPT', 'LOAD', SCRIPT] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.scriptLoad', async client => { + assert.equal( + await client.scriptLoad(SCRIPT), + SCRIPT_SHA1 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.scriptLoad', async cluster => { + assert.equal( + await cluster.scriptLoad(SCRIPT), + SCRIPT_SHA1 + ); + }); +}); diff --git a/lib/commands/SCRIPT_LOAD.ts b/lib/commands/SCRIPT_LOAD.ts new file mode 100644 index 0000000000..378fbf1e76 --- /dev/null +++ b/lib/commands/SCRIPT_LOAD.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(script: string): Array { + return ['SCRIPT', 'LOAD', script]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/SDIFF.spec.ts b/lib/commands/SDIFF.spec.ts new file mode 100644 index 0000000000..82ef2dac6f --- /dev/null +++ b/lib/commands/SDIFF.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SDIFF'; + +describe('SDIFF', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['SDIFF', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['SDIFF', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sDiff', async client => { + assert.deepEqual( + await client.sDiff('key'), + [] + ); + }); +}); diff --git a/lib/commands/SDIFF.ts b/lib/commands/SDIFF.ts new file mode 100644 index 0000000000..496ed59337 --- /dev/null +++ b/lib/commands/SDIFF.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(keys: string | Array): Array { + return pushVerdictArguments(['SDIFF'], keys); +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/SDIFFSTORE.spec.ts b/lib/commands/SDIFFSTORE.spec.ts new file mode 100644 index 0000000000..1e7f5f6f32 --- /dev/null +++ b/lib/commands/SDIFFSTORE.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SDIFFSTORE'; + +describe('SDIFFSTORE', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('destination', 'key'), + ['SDIFFSTORE', 'destination', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('destination', ['1', '2']), + ['SDIFFSTORE', 'destination', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sDiffStore', async client => { + assert.equal( + await client.sDiffStore('destination', 'key'), + 0 + ); + }); +}); diff --git a/lib/commands/SDIFFSTORE.ts b/lib/commands/SDIFFSTORE.ts new file mode 100644 index 0000000000..295433602f --- /dev/null +++ b/lib/commands/SDIFFSTORE.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(destination: string, keys: string | Array): Array { + return pushVerdictArguments(['SDIFFSTORE', destination], keys); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/SET.spec.ts b/lib/commands/SET.spec.ts new file mode 100644 index 0000000000..a587f6c312 --- /dev/null +++ b/lib/commands/SET.spec.ts @@ -0,0 +1,121 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SET'; + +describe('SET', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key', 'value'), + ['SET', 'key', 'value'] + ); + }); + + describe('TTL', () => { + it('with EX', () => { + assert.deepEqual( + transformArguments('key', 'value', { + EX: 1 + }), + ['SET', 'key', 'value', 'EX', '1'] + ); + }); + + it('with PX', () => { + assert.deepEqual( + transformArguments('key', 'value', { + PX: 1 + }), + ['SET', 'key', 'value', 'PX', '1'] + ); + }); + + it('with EXAT', () => { + assert.deepEqual( + transformArguments('key', 'value', { + EXAT: 1 + }), + ['SET', 'key', 'value', 'EXAT', '1'] + ); + }); + + it('with PXAT', () => { + assert.deepEqual( + transformArguments('key', 'value', { + PXAT: 1 + }), + ['SET', 'key', 'value', 'PXAT', '1'] + ); + }); + + it('with KEEPTTL', () => { + assert.deepEqual( + transformArguments('key', 'value', { + KEEPTTL: true + }), + ['SET', 'key', 'value', 'KEEPTTL'] + ); + }); + }); + + describe('Guards', () => { + it('with NX', () => { + assert.deepEqual( + transformArguments('key', 'value', { + NX: true + }), + ['SET', 'key', 'value', 'NX'] + ); + }); + + it('with XX', () => { + assert.deepEqual( + transformArguments('key', 'value', { + XX: true + }), + ['SET', 'key', 'value', 'XX'] + ); + }); + }); + + it('with GET', () => { + assert.deepEqual( + transformArguments('key', 'value', { + GET: true + }), + ['SET', 'key', 'value', 'GET'] + ); + }); + + it('with EX, NX, GET', () => { + assert.deepEqual( + transformArguments('key', 'value', { + EX: 1, + NX: true, + GET: true + }), + ['SET', 'key', 'value', 'EX', '1', 'NX', 'GET'] + ); + }); + }); + + describe('client.set', () => { + itWithClient(TestRedisServers.OPEN, 'simple', async client => { + assert.equal( + await client.set('key', 'value'), + 'OK' + ); + }); + + itWithClient(TestRedisServers.OPEN, 'with GET on empty key', async client => { + assert.equal( + await client.set('key', 'value', { + GET: true + }), + null + ); + }, { + minimumRedisVersion: [6, 2] + }); + }); +}); diff --git a/lib/commands/SET.ts b/lib/commands/SET.ts new file mode 100644 index 0000000000..4d5919cde2 --- /dev/null +++ b/lib/commands/SET.ts @@ -0,0 +1,75 @@ +export const FIRST_KEY_INDEX = 1; + +interface EX { + EX: number; +} + +interface PX { + PX: number +} + +interface EXAT { + EXAT: number; +} + +interface PXAT { + PXAT: number; +} + +interface KEEPTTL { + KEEPTTL: true; +} + +type SetTTL = EX | PX | EXAT | PXAT | KEEPTTL | {}; + +interface NX { + NX: true; +} + +interface XX { + XX: true; +} + +type SetGuards = NX | XX | {}; + +interface SetCommonOptions { + GET: true +} + +type SetOptions = SetTTL & SetGuards & (SetCommonOptions | {}); + +export function transformArguments(key: string, value: string, options?: SetOptions): Array { + const args = ['SET', key, value]; + + if (!options) { + return args; + } + + if ('EX' in options) { + args.push('EX', options.EX.toString()); + } else if ('PX' in options) { + args.push('PX', options.PX.toString()); + } else if ('EXAT' in options) { + args.push('EXAT', options.EXAT.toString()); + } else if ('PXAT' in options) { + args.push('PXAT', options.PXAT.toString()); + } else if ((options).KEEPTTL) { + args.push('KEEPTTL'); + } + + if ((options).NX) { + args.push('NX'); + } else if ((options).XX) { + args.push('XX'); + } + + if ((options).GET) { + args.push('GET'); + } + + return args; +} + +export function transformReply(reply?: string): string | null { + return reply ?? null; +} diff --git a/lib/commands/SETBIT.spec.ts b/lib/commands/SETBIT.spec.ts new file mode 100644 index 0000000000..7347913f29 --- /dev/null +++ b/lib/commands/SETBIT.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './SETBIT'; + +describe('SETBIT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0, 1), + ['SETBIT', 'key', '0', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.setBit', async client => { + assert.equal( + await client.setBit('key', 0, 1), + 0 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.setBit', async cluster => { + assert.equal( + await cluster.setBit('key', 0, 1), + 0 + ); + }); +}); diff --git a/lib/commands/SETBIT.ts b/lib/commands/SETBIT.ts new file mode 100644 index 0000000000..0cd41d1b97 --- /dev/null +++ b/lib/commands/SETBIT.ts @@ -0,0 +1,9 @@ +import { BitValue, transformReplyBit } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, offset: number, value: BitValue) { + return ['SETBIT', key, offset.toString(), value.toString()]; +} + +export const transformReply = transformReplyBit; diff --git a/lib/commands/SETEX.spec.ts b/lib/commands/SETEX.spec.ts new file mode 100644 index 0000000000..7ea55eba83 --- /dev/null +++ b/lib/commands/SETEX.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './SETEX'; + +describe('SETEX', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1, 'value'), + ['SETEX', 'key', '1', 'value'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.setEx', async client => { + assert.equal( + await client.setEx('key', 1, 'value'), + 'OK' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.setEx', async cluster => { + assert.equal( + await cluster.setEx('key', 1, 'value'), + 'OK' + ); + }); +}); diff --git a/lib/commands/SETEX.ts b/lib/commands/SETEX.ts new file mode 100644 index 0000000000..57c32db6ff --- /dev/null +++ b/lib/commands/SETEX.ts @@ -0,0 +1,14 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, seconds: number, value: string): Array { + return [ + 'SETEX', + key, + seconds.toString(), + value + ]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/SETNX .spec.ts b/lib/commands/SETNX .spec.ts new file mode 100644 index 0000000000..daf3ca6e76 --- /dev/null +++ b/lib/commands/SETNX .spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './SETNX'; + +describe('SETNX', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'value'), + ['SETNX', 'key', 'value'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.setNX', async client => { + assert.equal( + await client.setNX('key', 'value'), + true + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.setNX', async cluster => { + assert.equal( + await cluster.setNX('key', 'value'), + true + ); + }); +}); diff --git a/lib/commands/SETNX.ts b/lib/commands/SETNX.ts new file mode 100644 index 0000000000..f009783658 --- /dev/null +++ b/lib/commands/SETNX.ts @@ -0,0 +1,9 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, value: string): Array { + return ['SETNX', key, value]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/SETRANGE.spec.ts b/lib/commands/SETRANGE.spec.ts new file mode 100644 index 0000000000..766c56c5ff --- /dev/null +++ b/lib/commands/SETRANGE.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './SETRANGE'; + +describe('SETRANGE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0, 'value'), + ['SETRANGE', 'key', '0', 'value'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.setRange', async client => { + assert.equal( + await client.setRange('key', 0, 'value'), + 5 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.setRange', async cluster => { + assert.equal( + await cluster.setRange('key', 0, 'value'), + 5 + ); + }); +}); diff --git a/lib/commands/SETRANGE.ts b/lib/commands/SETRANGE.ts new file mode 100644 index 0000000000..a303487ddd --- /dev/null +++ b/lib/commands/SETRANGE.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, offset: number, value: string): Array { + return ['SETRANGE', key, offset.toString(), value]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/SHUTDOWN.spec.ts b/lib/commands/SHUTDOWN.spec.ts new file mode 100644 index 0000000000..d58cf4443c --- /dev/null +++ b/lib/commands/SHUTDOWN.spec.ts @@ -0,0 +1,27 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './SHUTDOWN'; + +describe('SHUTDOWN', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['SHUTDOWN'] + ); + }); + + it('NOSAVE', () => { + assert.deepEqual( + transformArguments('NOSAVE'), + ['SHUTDOWN', 'NOSAVE'] + ); + }); + + it('SAVE', () => { + assert.deepEqual( + transformArguments('SAVE'), + ['SHUTDOWN', 'SAVE'] + ); + }); + }); +}); diff --git a/lib/commands/SHUTDOWN.ts b/lib/commands/SHUTDOWN.ts new file mode 100644 index 0000000000..0dd2cf3a5b --- /dev/null +++ b/lib/commands/SHUTDOWN.ts @@ -0,0 +1,13 @@ +import { transformReplyVoid } from './generic-transformers'; + +export function transformArguments(mode?: 'NOSAVE' | 'SAVE'): Array { + const args = ['SHUTDOWN']; + + if (mode) { + args.push(mode); + } + + return args; +} + +export const transformReply = transformReplyVoid; diff --git a/lib/commands/SINTER.spec.ts b/lib/commands/SINTER.spec.ts new file mode 100644 index 0000000000..8fee35427c --- /dev/null +++ b/lib/commands/SINTER.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SINTER'; + +describe('SINTER', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['SINTER', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['SINTER', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sInter', async client => { + assert.deepEqual( + await client.sInter('key'), + [] + ); + }); +}); diff --git a/lib/commands/SINTER.ts b/lib/commands/SINTER.ts new file mode 100644 index 0000000000..104e81b921 --- /dev/null +++ b/lib/commands/SINTER.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(keys: string | Array): Array { + return pushVerdictArguments(['SINTER'], keys); +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/SINTERSTORE.spec.ts b/lib/commands/SINTERSTORE.spec.ts new file mode 100644 index 0000000000..013931d231 --- /dev/null +++ b/lib/commands/SINTERSTORE.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SINTERSTORE'; + +describe('SINTERSTORE', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('destination', 'key'), + ['SINTERSTORE', 'destination', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('destination', ['1', '2']), + ['SINTERSTORE', 'destination', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sInterStore', async client => { + assert.equal( + await client.sInterStore('destination', 'key'), + 0 + ); + }); +}); diff --git a/lib/commands/SINTERSTORE.ts b/lib/commands/SINTERSTORE.ts new file mode 100644 index 0000000000..a7a4d4fd10 --- /dev/null +++ b/lib/commands/SINTERSTORE.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(destination: string, keys: string | Array): Array { + return pushVerdictArguments(['SINTERSTORE', destination], keys); +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/SISMEMBER.spec.ts b/lib/commands/SISMEMBER.spec.ts new file mode 100644 index 0000000000..fec4ebfc57 --- /dev/null +++ b/lib/commands/SISMEMBER.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './SISMEMBER'; + +describe('SISMEMBER', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'member'), + ['SISMEMBER', 'key', 'member'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sIsMember', async client => { + assert.equal( + await client.sIsMember('key', 'member'), + false + ); + }); +}); diff --git a/lib/commands/SISMEMBER.ts b/lib/commands/SISMEMBER.ts new file mode 100644 index 0000000000..661410fce0 --- /dev/null +++ b/lib/commands/SISMEMBER.ts @@ -0,0 +1,9 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, member: string): Array { + return ['SISMEMBER', key, member]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/SMEMBERS.spec.ts b/lib/commands/SMEMBERS.spec.ts new file mode 100644 index 0000000000..2398dbaa8c --- /dev/null +++ b/lib/commands/SMEMBERS.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SMEMBERS'; + +describe('SMEMBERS', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['SMEMBERS', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sMembers', async client => { + assert.deepEqual( + await client.sMembers('key'), + [] + ); + }); +}); diff --git a/lib/commands/SMEMBERS.ts b/lib/commands/SMEMBERS.ts new file mode 100644 index 0000000000..d7e75daaa3 --- /dev/null +++ b/lib/commands/SMEMBERS.ts @@ -0,0 +1,9 @@ +import { transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['SMEMBERS', key]; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/SMISMEMBER.spec.ts b/lib/commands/SMISMEMBER.spec.ts new file mode 100644 index 0000000000..320f60d4ba --- /dev/null +++ b/lib/commands/SMISMEMBER.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './SMISMEMBER'; + +describe('SMISMEMBER', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['SMISMEMBER', 'key', '1', '2'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.smIsMember', async client => { + assert.deepEqual( + await client.smIsMember('key', ['1', '2']), + [false, false] + ); + }); +}); diff --git a/lib/commands/SMISMEMBER.ts b/lib/commands/SMISMEMBER.ts new file mode 100644 index 0000000000..07637a689b --- /dev/null +++ b/lib/commands/SMISMEMBER.ts @@ -0,0 +1,9 @@ +import { transformReplyBooleanArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, members: Array): Array { + return ['SMISMEMBER', key, ...members]; +} + +export const transformReply = transformReplyBooleanArray; diff --git a/lib/commands/SMOVE.spec.ts b/lib/commands/SMOVE.spec.ts new file mode 100644 index 0000000000..97e938a46b --- /dev/null +++ b/lib/commands/SMOVE.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SMOVE'; + +describe('SMOVE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('source', 'destination', 'member'), + ['SMOVE', 'source', 'destination', 'member'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sMove', async client => { + assert.equal( + await client.sMove('source', 'destination', 'member'), + false + ); + }); +}); diff --git a/lib/commands/SMOVE.ts b/lib/commands/SMOVE.ts new file mode 100644 index 0000000000..f8922f6ca3 --- /dev/null +++ b/lib/commands/SMOVE.ts @@ -0,0 +1,9 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(source: string, destination: string, member: string): Array { + return ['SMOVE', source, destination, member]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/SORT.spec.ts b/lib/commands/SORT.spec.ts new file mode 100644 index 0000000000..c449e0511f --- /dev/null +++ b/lib/commands/SORT.spec.ts @@ -0,0 +1,106 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SORT'; + +describe('SORT', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key'), + ['SORT', 'key'] + ); + }); + + it('with BY', () => { + assert.deepEqual( + transformArguments('key', { + BY: 'pattern' + }), + ['SORT', 'key', 'BY', 'pattern'] + ); + }); + + it('with LIMIT', () => { + assert.deepEqual( + transformArguments('key', { + LIMIT: { + offset: 0, + count: 1 + } + }), + ['SORT', 'key', 'LIMIT', '0', '1'] + ); + }); + + describe('with GET', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', { + GET: 'pattern' + }), + ['SORT', 'key', 'GET', 'pattern'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', { + GET: ['1', '2'] + }), + ['SORT', 'key', 'GET', '1', 'GET', '2'] + ); + }); + }); + + it('with DIRECTION', () => { + assert.deepEqual( + transformArguments('key', { + DIRECTION: 'ASC' + }), + ['SORT', 'key', 'ASC'] + ); + }); + + it('with ALPHA', () => { + assert.deepEqual( + transformArguments('key', { + ALPHA: true + }), + ['SORT', 'key', 'ALPHA'] + ); + }); + + it('with STORE', () => { + assert.deepEqual( + transformArguments('key', { + STORE: 'destination' + }), + ['SORT', 'key', 'STORE', 'destination'] + ); + }); + + it('with BY, LIMIT, GET, DIRECTION, ALPHA, STORE', () => { + assert.deepEqual( + transformArguments('key', { + BY: 'pattern', + LIMIT: { + offset: 0, + count: 1 + }, + GET: 'pattern', + DIRECTION: 'ASC', + ALPHA: true, + STORE: 'destination' + }), + ['SORT', 'key', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA', 'STORE', 'destination'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sort', async client => { + assert.deepEqual( + await client.sort('key'), + [] + ); + }); +}); diff --git a/lib/commands/SORT.ts b/lib/commands/SORT.ts new file mode 100644 index 0000000000..0305d93144 --- /dev/null +++ b/lib/commands/SORT.ts @@ -0,0 +1,57 @@ +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + + +interface SortOptions { + BY?: string; + LIMIT?: { + offset: number; + count: number; + }, + GET?: string | Array; + DIRECTION?: 'ASC' | 'DESC'; + ALPHA?: true; + STORE?: string; +} + +export function transformArguments(key: string, options?: SortOptions): Array { + const args = ['SORT', key]; + + if (options?.BY) { + args.push('BY', options.BY); + } + + if (options?.LIMIT) { + args.push( + 'LIMIT', + options.LIMIT.offset.toString(), + options.LIMIT.count.toString() + ); + } + + if (options?.GET) { + for (const pattern of (typeof options.GET === 'string' ? [options.GET] : options.GET)) { + args.push('GET', pattern); + } + } + + if (options?.DIRECTION) { + args.push(options.DIRECTION); + } + + if (options?.ALPHA) { + args.push('ALPHA'); + } + + if (options?.STORE) { + args.push('STORE', options.STORE); + } + + return args; +} + +// integer when using `STORE` +export function transformReply(reply: Array | number): Array | number { + return reply; +} diff --git a/lib/commands/SPOP.spec.ts b/lib/commands/SPOP.spec.ts new file mode 100644 index 0000000000..238c58f479 --- /dev/null +++ b/lib/commands/SPOP.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SPOP'; + +describe('SPOP', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key'), + ['SPOP', 'key'] + ); + }); + + it('with count', () => { + assert.deepEqual( + transformArguments('key', 2), + ['SPOP', 'key', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sPop', async client => { + assert.equal( + await client.sPop('key'), + null + ); + }); +}); diff --git a/lib/commands/SPOP.ts b/lib/commands/SPOP.ts new file mode 100644 index 0000000000..a389fed5cc --- /dev/null +++ b/lib/commands/SPOP.ts @@ -0,0 +1,15 @@ +import { transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, count?: number): Array { + const args = ['SPOP', key]; + + if (typeof count === 'number') { + args.push(count.toString()); + } + + return args; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/SRANDMEMBER.spec.ts b/lib/commands/SRANDMEMBER.spec.ts new file mode 100644 index 0000000000..5c359f73f9 --- /dev/null +++ b/lib/commands/SRANDMEMBER.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SRANDMEMBER'; + +describe('SRANDMEMBER', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['SRANDMEMBER', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sRandMember', async client => { + assert.equal( + await client.sRandMember('key'), + null + ); + }); +}); diff --git a/lib/commands/SRANDMEMBER.ts b/lib/commands/SRANDMEMBER.ts new file mode 100644 index 0000000000..2e8cd53927 --- /dev/null +++ b/lib/commands/SRANDMEMBER.ts @@ -0,0 +1,9 @@ +import { transformReplyStringNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return ['SRANDMEMBER', key]; +} + +export const transformReply = transformReplyStringNull; diff --git a/lib/commands/SRANDMEMBER_COUNT.spec.ts b/lib/commands/SRANDMEMBER_COUNT.spec.ts new file mode 100644 index 0000000000..81a4fd45f3 --- /dev/null +++ b/lib/commands/SRANDMEMBER_COUNT.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SRANDMEMBER_COUNT'; + +describe('SRANDMEMBER COUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['SRANDMEMBER', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sRandMemberCount', async client => { + assert.deepEqual( + await client.sRandMemberCount('key', 1), + [] + ); + }); +}); diff --git a/lib/commands/SRANDMEMBER_COUNT.ts b/lib/commands/SRANDMEMBER_COUNT.ts new file mode 100644 index 0000000000..b7fa8ebeb3 --- /dev/null +++ b/lib/commands/SRANDMEMBER_COUNT.ts @@ -0,0 +1,13 @@ +import { transformReplyStringArray } from './generic-transformers'; +import { transformArguments as transformSRandMemberArguments } from './SRANDMEMBER'; + +export { FIRST_KEY_INDEX } from './SRANDMEMBER'; + +export function transformArguments(key: string, count: number): Array { + return [ + ...transformSRandMemberArguments(key), + count.toString() + ]; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/SREM.spec.ts b/lib/commands/SREM.spec.ts new file mode 100644 index 0000000000..c9270624ae --- /dev/null +++ b/lib/commands/SREM.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SREM'; + +describe('SREM', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'member'), + ['SREM', 'key', 'member'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['SREM', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sRem', async client => { + assert.equal( + await client.sRem('key', 'member'), + 0 + ); + }); +}); diff --git a/lib/commands/SREM.ts b/lib/commands/SREM.ts new file mode 100644 index 0000000000..d1021bb3a1 --- /dev/null +++ b/lib/commands/SREM.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, members: string | Array): Array { + return pushVerdictArguments(['SREM', key], members); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/SSCAN.spec.ts b/lib/commands/SSCAN.spec.ts new file mode 100644 index 0000000000..9b203ffb83 --- /dev/null +++ b/lib/commands/SSCAN.spec.ts @@ -0,0 +1,74 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './SSCAN'; + +describe('SSCAN', () => { + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + transformArguments('key', 0), + ['SSCAN', 'key', '0'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern' + }), + ['SSCAN', 'key', '0', 'MATCH', 'pattern'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + COUNT: 1 + }), + ['SSCAN', 'key', '0', 'COUNT', '1'] + ); + }); + + it('with MATCH & COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern', + COUNT: 1 + }), + ['SSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] + ); + }); + }); + + describe('transformReply', () => { + it('without members', () => { + assert.deepEqual( + transformReply(['0', []]), + { + cursor: 0, + members: [] + } + ); + }); + + it('with members', () => { + assert.deepEqual( + transformReply(['0', ['member']]), + { + cursor: 0, + members: ['member'] + } + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sScan', async client => { + assert.deepEqual( + await client.sScan('key', 0), + { + cursor: 0, + members: [] + } + ); + }); +}); diff --git a/lib/commands/SSCAN.ts b/lib/commands/SSCAN.ts new file mode 100644 index 0000000000..9b881f5d88 --- /dev/null +++ b/lib/commands/SSCAN.ts @@ -0,0 +1,24 @@ +import { ScanOptions, pushScanArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, cursor: number, options?: ScanOptions): Array { + return pushScanArguments([ + 'SSCAN', + key, + ], cursor, options); +} + +interface SScanReply { + cursor: number; + members: Array; +} + +export function transformReply([cursor, members]: [string, Array]): SScanReply { + return { + cursor: Number(cursor), + members + }; +} diff --git a/lib/commands/STRLEN.spec.ts b/lib/commands/STRLEN.spec.ts new file mode 100644 index 0000000000..3d24e36037 --- /dev/null +++ b/lib/commands/STRLEN.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './STRLEN'; + +describe('STRLEN', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['STRLEN', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.strLen', async client => { + assert.equal( + await client.strLen('key'), + 0 + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.strLen', async cluster => { + assert.equal( + await cluster.strLen('key'), + 0 + ); + }); +}); diff --git a/lib/commands/STRLEN.ts b/lib/commands/STRLEN.ts new file mode 100644 index 0000000000..d8112ce7d1 --- /dev/null +++ b/lib/commands/STRLEN.ts @@ -0,0 +1,11 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['STRLEN', key]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/SUNION.spec.ts b/lib/commands/SUNION.spec.ts new file mode 100644 index 0000000000..fdf9766897 --- /dev/null +++ b/lib/commands/SUNION.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SUNION'; + +describe('SUNION', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['SUNION', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['SUNION', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sUnion', async client => { + assert.deepEqual( + await client.sUnion('key'), + [] + ); + }); +}); diff --git a/lib/commands/SUNION.ts b/lib/commands/SUNION.ts new file mode 100644 index 0000000000..3f06138b1b --- /dev/null +++ b/lib/commands/SUNION.ts @@ -0,0 +1,11 @@ +import { pushVerdictArguments, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(keys: string | Array): Array { + return pushVerdictArguments(['SUNION'], keys); +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/SUNIONSTORE.spec.ts b/lib/commands/SUNIONSTORE.spec.ts new file mode 100644 index 0000000000..82c9a03a0b --- /dev/null +++ b/lib/commands/SUNIONSTORE.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './SUNIONSTORE'; + +describe('SUNIONSTORE', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('destination', 'key'), + ['SUNIONSTORE', 'destination', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('destination', ['1', '2']), + ['SUNIONSTORE', 'destination', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sUnionStore', async client => { + assert.equal( + await client.sUnionStore('destination', 'key'), + 0 + ); + }); +}); diff --git a/lib/commands/SUNIONSTORE.ts b/lib/commands/SUNIONSTORE.ts new file mode 100644 index 0000000000..7a1aab8011 --- /dev/null +++ b/lib/commands/SUNIONSTORE.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(destination: string, keys: string | Array): Array { + return pushVerdictArguments(['SUNIONSTORE', destination], keys); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/SWAPDB.spec.ts b/lib/commands/SWAPDB.spec.ts new file mode 100644 index 0000000000..1a5637ae43 --- /dev/null +++ b/lib/commands/SWAPDB.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { itWithClient, TestRedisServers } from '../test-utils'; +import { transformArguments } from './SWAPDB'; + +describe('SWAPDB', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(0, 1), + ['SWAPDB', '0', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.swapDb', async client => { + assert.equal( + await client.swapDb(0, 1), + 'OK' + ); + }); +}); diff --git a/lib/commands/SWAPDB.ts b/lib/commands/SWAPDB.ts new file mode 100644 index 0000000000..f0d4dacf3f --- /dev/null +++ b/lib/commands/SWAPDB.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(index1: number, index2: number): Array { + return ['SWAPDB', index1.toString(), index2.toString()]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/TIME.spec.ts b/lib/commands/TIME.spec.ts new file mode 100644 index 0000000000..1a07114af4 --- /dev/null +++ b/lib/commands/TIME.spec.ts @@ -0,0 +1,18 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './TIME'; + +describe('TIME', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['TIME'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.time', async client => { + const reply = await client.time(); + assert.ok(reply instanceof Date); + assert.ok(typeof reply.microseconds === 'number'); + }); +}); diff --git a/lib/commands/TIME.ts b/lib/commands/TIME.ts new file mode 100644 index 0000000000..4579845339 --- /dev/null +++ b/lib/commands/TIME.ts @@ -0,0 +1,15 @@ +export function transformArguments(): Array { + return ['TIME']; +} + +interface TimeReply extends Date { + microseconds: number; +} + +export function transformReply(reply: [string, string]): TimeReply { + const seconds = Number(reply[0]), + microseconds = Number(reply[1]), + d: Partial = new Date(seconds + Math.round(microseconds / 1000)); + d.microseconds = microseconds; + return d as TimeReply; +} diff --git a/lib/commands/TOUCH.spec.ts b/lib/commands/TOUCH.spec.ts new file mode 100644 index 0000000000..c4cb435629 --- /dev/null +++ b/lib/commands/TOUCH.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './TOUCH'; + +describe('TOUCH', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['TOUCH', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['TOUCH', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.touch', async client => { + assert.equal( + await client.touch('key'), + 0 + ); + }); +}); diff --git a/lib/commands/TOUCH.ts b/lib/commands/TOUCH.ts new file mode 100644 index 0000000000..f2fb054897 --- /dev/null +++ b/lib/commands/TOUCH.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string | Array): Array { + return pushVerdictArguments(['TOUCH'], key); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/TTL.spec.ts b/lib/commands/TTL.spec.ts new file mode 100644 index 0000000000..bcabe8d39e --- /dev/null +++ b/lib/commands/TTL.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './TTL'; + +describe('TTL', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['TTL', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.ttl', async client => { + assert.equal( + await client.ttl('key'), + -2 + ); + }); +}); diff --git a/lib/commands/TTL.ts b/lib/commands/TTL.ts new file mode 100644 index 0000000000..aa8462dfea --- /dev/null +++ b/lib/commands/TTL.ts @@ -0,0 +1,11 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['TTL', key]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/TYPE.spec.ts b/lib/commands/TYPE.spec.ts new file mode 100644 index 0000000000..d40f724242 --- /dev/null +++ b/lib/commands/TYPE.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './TYPE'; + +describe('TYPE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['TYPE', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.type', async client => { + assert.equal( + await client.type('key'), + 'none' + ); + }); +}); diff --git a/lib/commands/TYPE.ts b/lib/commands/TYPE.ts new file mode 100644 index 0000000000..4f27b29d2b --- /dev/null +++ b/lib/commands/TYPE.ts @@ -0,0 +1,11 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['TYPE', key]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/UNLINK.spec.ts b/lib/commands/UNLINK.spec.ts new file mode 100644 index 0000000000..a0dddf54f2 --- /dev/null +++ b/lib/commands/UNLINK.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './UNLINK'; + +describe('UNLINK', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['UNLINK', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['UNLINK', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.unlink', async client => { + assert.equal( + await client.unlink('key'), + 0 + ); + }); +}); diff --git a/lib/commands/UNLINK.ts b/lib/commands/UNLINK.ts new file mode 100644 index 0000000000..9dfe0ca48e --- /dev/null +++ b/lib/commands/UNLINK.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string | Array): Array { + return pushVerdictArguments(['UNLINK'], key); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/UNWATCH.spec.ts b/lib/commands/UNWATCH.spec.ts new file mode 100644 index 0000000000..238ffdc59b --- /dev/null +++ b/lib/commands/UNWATCH.spec.ts @@ -0,0 +1,26 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils'; +import { transformArguments } from './UNWATCH'; + +describe('UNWATCH', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['UNWATCH'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.unwatch', async client => { + assert.equal( + await client.unwatch(), + 'OK' + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.unwatch', async cluster => { + assert.equal( + await cluster.unwatch(), + 'OK' + ); + }); +}); diff --git a/lib/commands/UNWATCH.ts b/lib/commands/UNWATCH.ts new file mode 100644 index 0000000000..d0ede556f3 --- /dev/null +++ b/lib/commands/UNWATCH.ts @@ -0,0 +1,7 @@ +import { transformReplyString } from './generic-transformers'; + +export function transformArguments(): Array { + return ['UNWATCH']; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/WAIT.spec.ts b/lib/commands/WAIT.spec.ts new file mode 100644 index 0000000000..c3f53b7db7 --- /dev/null +++ b/lib/commands/WAIT.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './WAIT'; + +describe('WAIT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments(0, 1), + ['WAIT', '0', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.wait', async client => { + assert.equal( + await client.wait(0, 1), + 0 + ); + }); +}); diff --git a/lib/commands/WAIT.ts b/lib/commands/WAIT.ts new file mode 100644 index 0000000000..214fb35668 --- /dev/null +++ b/lib/commands/WAIT.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(numberOfReplicas: number, timeout: number): Array { + return ['WAIT', numberOfReplicas.toString(), timeout.toString()]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/WATCH.spec.ts b/lib/commands/WATCH.spec.ts new file mode 100644 index 0000000000..acaa062874 --- /dev/null +++ b/lib/commands/WATCH.spec.ts @@ -0,0 +1,20 @@ +import { strict as assert } from 'assert'; +import { transformArguments } from './WATCH'; + +describe('WATCH', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['WATCH', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['WATCH', '1', '2'] + ); + }); + }); +}); diff --git a/lib/commands/WATCH.ts b/lib/commands/WATCH.ts new file mode 100644 index 0000000000..5e24ca3795 --- /dev/null +++ b/lib/commands/WATCH.ts @@ -0,0 +1,7 @@ +import { pushVerdictArguments, transformReplyString } from './generic-transformers'; + +export function transformArguments(key: string | Array): Array { + return pushVerdictArguments(['WATCH'], key); +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/XACK.spec.ts b/lib/commands/XACK.spec.ts new file mode 100644 index 0000000000..fb267c355e --- /dev/null +++ b/lib/commands/XACK.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XACK'; + +describe('XACK', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'group', '1-0'), + ['XACK', 'key', 'group', '1-0'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', 'group', ['1-0', '2-0']), + ['XACK', 'key', 'group', '1-0', '2-0'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xAck', async client => { + assert.equal( + await client.xAck('key', 'group', '1-0'), + 0 + ); + }); +}); diff --git a/lib/commands/XACK.ts b/lib/commands/XACK.ts new file mode 100644 index 0000000000..969f9b6a8b --- /dev/null +++ b/lib/commands/XACK.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, group: string, id: string | Array): Array { + return pushVerdictArguments(['XACK', key, group], id); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/XADD.spec.ts b/lib/commands/XADD.spec.ts new file mode 100644 index 0000000000..02e6888051 --- /dev/null +++ b/lib/commands/XADD.spec.ts @@ -0,0 +1,118 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XADD'; + +describe('XADD', () => { + describe('transformArguments', () => { + it('single field', () => { + assert.deepEqual( + transformArguments('key', '*', { + field: 'value' + }), + ['XADD', 'key', '*', 'field', 'value'] + ); + }); + + it('multiple fields', () => { + assert.deepEqual( + transformArguments('key', '*', { + '1': 'I', + '2': 'II' + }), + ['XADD', 'key', '*', '1', 'I', '2', 'II'] + ); + }); + + it('with NOMKSTREAM', () => { + assert.deepEqual( + transformArguments('key', '*', { + field: 'value' + }, { + NOMKSTREAM: true + }), + ['XADD', 'key', 'NOMKSTREAM', '*', 'field', 'value'] + ); + }); + + it('with TRIM', () => { + assert.deepEqual( + transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000 + } + }), + ['XADD', 'key', '1000', '*', 'field', 'value'] + ); + }); + + it('with TRIM.strategy', () => { + assert.deepEqual( + transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + strategy: 'MAXLEN', + threshold: 1000 + } + }), + ['XADD', 'key', 'MAXLEN', '1000', '*','field', 'value'] + ); + }); + + it('with TRIM.strategyModifier', () => { + assert.deepEqual( + transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + strategyModifier: '=', + threshold: 1000 + } + }), + ['XADD', 'key', '=', '1000', '*', 'field', 'value'] + ); + }); + + it('with TRIM.limit', () => { + assert.deepEqual( + transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000, + limit: 1 + } + }), + ['XADD', 'key', '1000', 'LIMIT', '1', '*', 'field', 'value'] + ); + }); + + it('with NOMKSTREAM, TRIM, TRIM.*', () => { + assert.deepEqual( + transformArguments('key', '*', { + field: 'value' + }, { + NOMKSTREAM: true, + TRIM: { + strategy: 'MAXLEN', + strategyModifier: '=', + threshold: 1000, + limit: 1 + } + }), + ['XADD', 'key', 'NOMKSTREAM', 'MAXLEN', '=', '1000', 'LIMIT', '1', '*', 'field', 'value'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xAdd', async client => { + assert.equal( + typeof await client.xAdd('key', '*', { + field: 'value' + }), + 'string' + ); + }); +}); diff --git a/lib/commands/XADD.ts b/lib/commands/XADD.ts new file mode 100644 index 0000000000..0500a2fde6 --- /dev/null +++ b/lib/commands/XADD.ts @@ -0,0 +1,48 @@ +import { TuplesObject, transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + + +interface XAddOptions { + NOMKSTREAM?: true; + TRIM?: { + strategy?: 'MAXLEN' | 'MINID'; + strategyModifier?: '=' | '~'; + threshold: number; + limit?: number; + } +} + +export function transformArguments(key: string, id: string, message: TuplesObject, options?: XAddOptions): Array { + const args = ['XADD', key]; + + if (options?.NOMKSTREAM) { + args.push('NOMKSTREAM'); + } + + if (options?.TRIM) { + if (options.TRIM.strategy) { + args.push(options.TRIM.strategy); + } + + if (options.TRIM.strategyModifier) { + args.push(options.TRIM.strategyModifier); + } + + args.push(options.TRIM.threshold.toString()); + + if (options.TRIM.limit) { + args.push('LIMIT', options.TRIM.limit.toString()); + } + } + + args.push(id); + + for (const [key, value] of Object.entries(message)) { + args.push(key, value); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/XAUTOCLAIM.spec.ts b/lib/commands/XAUTOCLAIM.spec.ts new file mode 100644 index 0000000000..a0818d5c2c --- /dev/null +++ b/lib/commands/XAUTOCLAIM.spec.ts @@ -0,0 +1,42 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './XAUTOCLAIM'; + +describe('XAUTOCLAIM', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0'), + ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0', { + COUNT: 1 + }), + ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'COUNT', '1'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xAutoClaim', async client => { + await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xGroupCreateConsumer('key', 'group', 'consumer'), + ]); + + assert.deepEqual( + await client.xAutoClaim('key', 'group', 'consumer', 1, '0-0'), + { + nextId: '0-0', + messages: [] + } + ); + }); +}); diff --git a/lib/commands/XAUTOCLAIM.ts b/lib/commands/XAUTOCLAIM.ts new file mode 100644 index 0000000000..f02ccbaa2e --- /dev/null +++ b/lib/commands/XAUTOCLAIM.ts @@ -0,0 +1,36 @@ +import { StreamMessagesReply, transformReplyStreamMessages } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export interface XAutoClaimOptions { + COUNT?: number; +} + +export function transformArguments( + key: string, + group: string, + consumer: string, + minIdleTime: number, + start: string, + options?: XAutoClaimOptions +): Array { + const args = ['XAUTOCLAIM', key, group, consumer, minIdleTime.toString(), start]; + + if (options?.COUNT) { + args.push('COUNT', options.COUNT.toString()); + } + + return args; +} + +interface XAutoClaimReply { + nextId: string; + messages: StreamMessagesReply; +} + +export function transformReply(reply: [string, Array]): XAutoClaimReply { + return { + nextId: reply[0], + messages: transformReplyStreamMessages(reply[1]) + }; +} diff --git a/lib/commands/XAUTOCLAIM_JUSTID.spec.ts b/lib/commands/XAUTOCLAIM_JUSTID.spec.ts new file mode 100644 index 0000000000..d076f28751 --- /dev/null +++ b/lib/commands/XAUTOCLAIM_JUSTID.spec.ts @@ -0,0 +1,31 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './XAUTOCLAIM_JUSTID'; + +describe('XAUTOCLAIM JUSTID', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0'), + ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xAutoClaimJustId', async client => { + await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xGroupCreateConsumer('key', 'group', 'consumer'), + ]); + + assert.deepEqual( + await client.xAutoClaimJustId('key', 'group', 'consumer', 1, '0-0'), + { + nextId: '0-0', + messages: [] + } + ); + }); +}); diff --git a/lib/commands/XAUTOCLAIM_JUSTID.ts b/lib/commands/XAUTOCLAIM_JUSTID.ts new file mode 100644 index 0000000000..70c1a07f92 --- /dev/null +++ b/lib/commands/XAUTOCLAIM_JUSTID.ts @@ -0,0 +1,22 @@ +import { transformArguments as transformXAutoClaimArguments } from './XAUTOCLAIM'; + +export { FIRST_KEY_INDEX } from './XAUTOCLAIM'; + +export function transformArguments(...args: Parameters): Array { + return [ + ...transformXAutoClaimArguments(...args), + 'JUSTID' + ]; +} + +interface XAutoClaimJustIdReply { + nextId: string; + messages: Array; +} + +export function transformReply(reply: [string, Array]): XAutoClaimJustIdReply { + return { + nextId: reply[0], + messages: reply[1] + }; +} diff --git a/lib/commands/XCLAIM.spec.ts b/lib/commands/XCLAIM.spec.ts new file mode 100644 index 0000000000..ff4b445dcf --- /dev/null +++ b/lib/commands/XCLAIM.spec.ts @@ -0,0 +1,90 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XCLAIM'; + +describe('XCLAIM', () => { + describe('transformArguments', () => { + it('single id (string)', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0'), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0'] + ); + }); + + it('multiple ids (array)', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, ['0-0', '1-0']), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', '1-0'] + ); + }); + + it('with IDLE', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0', { + IDLE: 1 + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'IDLE', '1'] + ); + }); + + it('with TIME (number)', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0', { + TIME: 1 + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'TIME', '1'] + ); + }); + + it('with TIME (date)', () => { + const d = new Date(); + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0', { + TIME: d + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'TIME', d.getTime().toString()] + ); + }); + + it('with RETRYCOUNT', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0', { + RETRYCOUNT: 1 + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'RETRYCOUNT', '1'] + ); + }); + + it('with FORCE', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0', { + FORCE: true + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'FORCE'] + ); + }); + + it('with IDLE, TIME, RETRYCOUNT, FORCE, JUSTID', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0', { + IDLE: 1, + TIME: 1, + RETRYCOUNT: 1, + FORCE: true + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'IDLE', '1', 'TIME', '1', 'RETRYCOUNT', '1', 'FORCE'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xClaim', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.deepEqual( + await client.xClaim('key', 'group', 'consumer', 1, '0-0'), + [] + ); + }); +}); diff --git a/lib/commands/XCLAIM.ts b/lib/commands/XCLAIM.ts new file mode 100644 index 0000000000..c5890a7579 --- /dev/null +++ b/lib/commands/XCLAIM.ts @@ -0,0 +1,46 @@ +import { pushVerdictArguments, transformReplyStreamMessages } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export interface XClaimOptions { + IDLE?: number; + TIME?: number | Date; + RETRYCOUNT?: number; + FORCE?: true; +} + +export function transformArguments( + key: string, + group: string, + consumer: string, + minIdleTime: number, + id: string | Array, + options?: XClaimOptions +): Array { + const args = ['XCLAIM', key, group, consumer, minIdleTime.toString()]; + + pushVerdictArguments(args, id); + + if (options?.IDLE) { + args.push('IDLE', options.IDLE.toString()); + } + + if (options?.TIME) { + args.push( + 'TIME', + (typeof options.TIME === 'number' ? options.TIME : options.TIME.getTime()).toString() + ); + } + + if (options?.RETRYCOUNT) { + args.push('RETRYCOUNT', options.RETRYCOUNT.toString()); + } + + if (options?.FORCE) { + args.push('FORCE'); + } + + return args; +} + +export const transformReply = transformReplyStreamMessages; diff --git a/lib/commands/XCLAIM_JUSTID.spec.ts b/lib/commands/XCLAIM_JUSTID.spec.ts new file mode 100644 index 0000000000..bb31f2c453 --- /dev/null +++ b/lib/commands/XCLAIM_JUSTID.spec.ts @@ -0,0 +1,23 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XCLAIM_JUSTID'; + +describe('XCLAIM JUSTID', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer', 1, '0-0'), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xClaimJustId', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.deepEqual( + await client.xClaimJustId('key', 'group', 'consumer', 1, '0-0'), + [] + ); + }); +}); diff --git a/lib/commands/XCLAIM_JUSTID.ts b/lib/commands/XCLAIM_JUSTID.ts new file mode 100644 index 0000000000..dcf274ed82 --- /dev/null +++ b/lib/commands/XCLAIM_JUSTID.ts @@ -0,0 +1,13 @@ +import { transformReplyStringArray } from './generic-transformers'; +import { transformArguments as transformArgumentsXClaim } from './XCLAIM'; + +export { FIRST_KEY_INDEX } from './XCLAIM'; + +export function transformArguments(...args: Parameters): Array { + return [ + ...transformArgumentsXClaim(...args), + 'JUSTID' + ]; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/XDEL.spec.ts b/lib/commands/XDEL.spec.ts new file mode 100644 index 0000000000..1a3015538f --- /dev/null +++ b/lib/commands/XDEL.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XDEL'; + +describe('XDEL', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', '0-0'), + ['XDEL', 'key', '0-0'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['0-0', '1-0']), + ['XDEL', 'key', '0-0', '1-0'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xDel', async client => { + assert.equal( + await client.xDel('key', '0-0'), + 0 + ); + }); +}); diff --git a/lib/commands/XDEL.ts b/lib/commands/XDEL.ts new file mode 100644 index 0000000000..9d173271c2 --- /dev/null +++ b/lib/commands/XDEL.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, id: string | Array): Array { + return pushVerdictArguments(['XDEL', key], id); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/XGROUP_CREATE.spec.ts b/lib/commands/XGROUP_CREATE.spec.ts new file mode 100644 index 0000000000..fdbb796f10 --- /dev/null +++ b/lib/commands/XGROUP_CREATE.spec.ts @@ -0,0 +1,32 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XGROUP_CREATE'; + +describe('XGROUP CREATE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key', 'group', '$'), + ['XGROUP', 'CREATE', 'key', 'group', '$'] + ); + }); + + it('with MKSTREAM', () => { + assert.deepEqual( + transformArguments('key', 'group', '$', { + MKSTREAM: true + }), + ['XGROUP', 'CREATE', 'key', 'group', '$', 'MKSTREAM'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xGroupCreate', async client => { + assert.equal( + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + 'OK' + ); + }); +}); diff --git a/lib/commands/XGROUP_CREATE.ts b/lib/commands/XGROUP_CREATE.ts new file mode 100644 index 0000000000..167197b263 --- /dev/null +++ b/lib/commands/XGROUP_CREATE.ts @@ -0,0 +1,19 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 2; + +interface XGroupCreateOptions { + MKSTREAM?: true; +} + +export function transformArguments(key: string, group: string, id: string, options?: XGroupCreateOptions): Array { + const args = ['XGROUP', 'CREATE', key, group, id]; + + if (options?.MKSTREAM) { + args.push('MKSTREAM'); + } + + return args; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/XGROUP_CREATECONSUMER.spec.ts b/lib/commands/XGROUP_CREATECONSUMER.spec.ts new file mode 100644 index 0000000000..5b06188e30 --- /dev/null +++ b/lib/commands/XGROUP_CREATECONSUMER.spec.ts @@ -0,0 +1,25 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './XGROUP_CREATECONSUMER'; + +describe('XGROUP CREATECONSUMER', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer'), + ['XGROUP', 'CREATECONSUMER', 'key', 'group', 'consumer'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xGroupCreateConsumer', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.equal( + await client.xGroupCreateConsumer('key', 'group', 'consumer'), + true + ); + }); +}); diff --git a/lib/commands/XGROUP_CREATECONSUMER.ts b/lib/commands/XGROUP_CREATECONSUMER.ts new file mode 100644 index 0000000000..395688706e --- /dev/null +++ b/lib/commands/XGROUP_CREATECONSUMER.ts @@ -0,0 +1,9 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 2; + +export function transformArguments(key: string, group: string, consumer: string): Array { + return ['XGROUP', 'CREATECONSUMER', key, group, consumer]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/XGROUP_DELCONSUMER.spec.ts b/lib/commands/XGROUP_DELCONSUMER.spec.ts new file mode 100644 index 0000000000..c3cf3c2378 --- /dev/null +++ b/lib/commands/XGROUP_DELCONSUMER.spec.ts @@ -0,0 +1,23 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XGROUP_DELCONSUMER'; + +describe('XGROUP DELCONSUMER', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'group', 'consumer'), + ['XGROUP', 'DELCONSUMER', 'key', 'group', 'consumer'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xGroupDelConsumer', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.equal( + await client.xGroupDelConsumer('key', 'group', 'consumer'), + 0 + ); + }); +}); diff --git a/lib/commands/XGROUP_DELCONSUMER.ts b/lib/commands/XGROUP_DELCONSUMER.ts new file mode 100644 index 0000000000..91a36f91a3 --- /dev/null +++ b/lib/commands/XGROUP_DELCONSUMER.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 2; + +export function transformArguments(key: string, group: string, consumer: string): Array { + return ['XGROUP', 'DELCONSUMER', key, group, consumer]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/XGROUP_DESTROY.spec.ts b/lib/commands/XGROUP_DESTROY.spec.ts new file mode 100644 index 0000000000..e991bc0d66 --- /dev/null +++ b/lib/commands/XGROUP_DESTROY.spec.ts @@ -0,0 +1,23 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XGROUP_DESTROY'; + +describe('XGROUP DESTROY', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'group'), + ['XGROUP', 'DESTROY', 'key', 'group'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xGroupDestroy', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.equal( + await client.xGroupDestroy('key', 'group'), + true + ); + }); +}); diff --git a/lib/commands/XGROUP_DESTROY.ts b/lib/commands/XGROUP_DESTROY.ts new file mode 100644 index 0000000000..1fd25550c3 --- /dev/null +++ b/lib/commands/XGROUP_DESTROY.ts @@ -0,0 +1,9 @@ +import { transformReplyBoolean } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 2; + +export function transformArguments(key: string, group: string): Array { + return ['XGROUP', 'DESTROY', key, group]; +} + +export const transformReply = transformReplyBoolean; diff --git a/lib/commands/XGROUP_SETID.spec.ts b/lib/commands/XGROUP_SETID.spec.ts new file mode 100644 index 0000000000..0fa10cdb0b --- /dev/null +++ b/lib/commands/XGROUP_SETID.spec.ts @@ -0,0 +1,23 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XGROUP_SETID'; + +describe('XGROUP SETID', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'group', '0'), + ['XGROUP', 'SETID', 'key', 'group', '0'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xGroupSetId', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.equal( + await client.xGroupSetId('key', 'group', '0'), + 'OK' + ); + }); +}); diff --git a/lib/commands/XGROUP_SETID.ts b/lib/commands/XGROUP_SETID.ts new file mode 100644 index 0000000000..ce4a13086d --- /dev/null +++ b/lib/commands/XGROUP_SETID.ts @@ -0,0 +1,9 @@ +import { transformReplyString } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 2; + +export function transformArguments(key: string, group: string, id: string): Array { + return ['XGROUP', 'SETID', key, group, id]; +} + +export const transformReply = transformReplyString; diff --git a/lib/commands/XINFO_CONSUMERS.spec.ts b/lib/commands/XINFO_CONSUMERS.spec.ts new file mode 100644 index 0000000000..08ef17e51a --- /dev/null +++ b/lib/commands/XINFO_CONSUMERS.spec.ts @@ -0,0 +1,41 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './XINFO_CONSUMERS'; + +describe('XINFO CONSUMERS', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'group'), + ['XINFO', 'CONSUMERS', 'key', 'group'] + ); + }); + + it('transformReply', () => { + assert.deepEqual( + transformReply([ + ['name', 'Alice', 'pending', 1, 'idle', 9104628], + ['name', 'Bob', 'pending', 1, 'idle', 83841983] + ]), + [{ + name: 'Alice', + pending: 1, + idle: 9104628 + }, { + name: 'Bob', + pending: 1, + idle: 83841983 + }] + ); + }) + + itWithClient(TestRedisServers.OPEN, 'client.xInfoConsumers', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.deepEqual( + await client.xInfoConsumers('key', 'group'), + [] + ); + }); +}); diff --git a/lib/commands/XINFO_CONSUMERS.ts b/lib/commands/XINFO_CONSUMERS.ts new file mode 100644 index 0000000000..57e60db832 --- /dev/null +++ b/lib/commands/XINFO_CONSUMERS.ts @@ -0,0 +1,21 @@ +export const FIRST_KEY_INDEX = 2; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, group: string): Array { + return ['XINFO', 'CONSUMERS', key, group]; +} + +type XInfoConsumersReply = Array<{ + name: string; + pending: number; + idle: number; +}>; + +export function transformReply(rawReply: Array): XInfoConsumersReply { + return rawReply.map(consumer => ({ + name: consumer[1], + pending: consumer[3], + idle: consumer[5] + })); +} diff --git a/lib/commands/XINFO_GROUPS.spec.ts b/lib/commands/XINFO_GROUPS.spec.ts new file mode 100644 index 0000000000..8fbd86ee3e --- /dev/null +++ b/lib/commands/XINFO_GROUPS.spec.ts @@ -0,0 +1,48 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './XINFO_GROUPS'; + +describe('XINFO GROUPS', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['XINFO', 'GROUPS', 'key'] + ); + }); + + it('transformReply', () => { + assert.deepEqual( + transformReply([ + ['name', 'mygroup', 'consumers', 2, 'pending', 2, 'last-delivered-id', '1588152489012-0'], + ['name', 'some-other-group', 'consumers', 1, 'pending', 0, 'last-delivered-id', '1588152498034-0'] + ]), + [{ + name: 'mygroup', + consumers: 2, + pending: 2, + lastDeliveredId: '1588152489012-0' + }, { + name: 'some-other-group', + consumers: 1, + pending: 0, + lastDeliveredId: '1588152498034-0' + }] + ); + }) + + itWithClient(TestRedisServers.OPEN, 'client.xInfoGroups', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.deepEqual( + await client.xInfoGroups('key'), + [{ + name: 'group', + consumers: 0, + pending: 0, + lastDeliveredId: '0-0' + }] + ); + }); +}); diff --git a/lib/commands/XINFO_GROUPS.ts b/lib/commands/XINFO_GROUPS.ts new file mode 100644 index 0000000000..04fb9ca39a --- /dev/null +++ b/lib/commands/XINFO_GROUPS.ts @@ -0,0 +1,23 @@ +export const FIRST_KEY_INDEX = 2; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['XINFO', 'GROUPS', key]; +} + +type XInfoGroupsReply = Array<{ + name: string; + consumers: number; + pending: number; + lastDeliveredId: string; +}>; + +export function transformReply(rawReply: Array): XInfoGroupsReply { + return rawReply.map(group => ({ + name: group[1], + consumers: group[3], + pending: group[5], + lastDeliveredId: group[7] + })); +} diff --git a/lib/commands/XINFO_STREAM.spec.ts b/lib/commands/XINFO_STREAM.spec.ts new file mode 100644 index 0000000000..ecab605e4e --- /dev/null +++ b/lib/commands/XINFO_STREAM.spec.ts @@ -0,0 +1,72 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './XINFO_STREAM'; + +describe('XINFO STREAM', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['XINFO', 'STREAM', 'key'] + ); + }); + + it('transformReply', () => { + assert.deepEqual( + transformReply([ + 'length', 2, + 'radix-tree-keys', 1, + 'radix-tree-nodes', 2, + 'last-generated-id', '1538385846314-0', + 'groups', 2, + 'first-entry', ['1538385820729-0', ['foo', 'bar']], + 'last-entry', ['1538385846314-0', ['field', 'value']] + ]), + { + length: 2, + radixTreeKeys: 1, + radixTreeNodes: 2, + groups: 2, + lastGeneratedId: '1538385846314-0', + firstEntry: { + id: '1538385820729-0', + message: Object.create(null, { + foo: { + value: 'bar', + configurable: true, + enumerable: true + } + }) + }, + lastEntry: { + id: '1538385846314-0', + message: Object.create(null, { + field: { + value: 'value', + configurable: true, + enumerable: true + } + }) + } + } + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xInfoStream', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.deepEqual( + await client.xInfoStream('key'), + { + length: 0, + radixTreeKeys: 0, + radixTreeNodes: 1, + groups: 1, + lastGeneratedId: '0-0', + firstEntry: null, + lastEntry: null + } + ); + }); +}); diff --git a/lib/commands/XINFO_STREAM.ts b/lib/commands/XINFO_STREAM.ts new file mode 100644 index 0000000000..0bb4472187 --- /dev/null +++ b/lib/commands/XINFO_STREAM.ts @@ -0,0 +1,63 @@ +import { StreamMessageReply, transformReplyTuples } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 2; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['XINFO', 'STREAM', key]; +} + +interface XInfoStreamReply { + length: number; + radixTreeKeys: number; + radixTreeNodes: number; + groups: number; + lastGeneratedId: string; + firstEntry: StreamMessageReply | null; + lastEntry: StreamMessageReply | null; +} + +export function transformReply(rawReply: Array): XInfoStreamReply { + const parsedReply: Partial = {}; + + for (let i = 0; i < rawReply.length; i+= 2) { + switch (rawReply[i]) { + case 'length': + parsedReply.length = rawReply[i + 1]; + break; + + case 'radix-tree-keys': + parsedReply.radixTreeKeys = rawReply[i + 1]; + break; + + case 'radix-tree-nodes': + parsedReply.radixTreeNodes = rawReply[i + 1]; + break; + + case 'groups': + parsedReply.groups = rawReply[i + 1]; + break; + + case 'last-generated-id': + parsedReply.lastGeneratedId = rawReply[i + 1]; + break; + + case 'first-entry': + parsedReply.firstEntry = rawReply[i + 1] ? { + id: rawReply[i + 1][0], + message: transformReplyTuples(rawReply[i + 1][1]) + } : null; + break; + + case 'last-entry': + parsedReply.lastEntry = rawReply[i + 1] ? { + id: rawReply[i + 1][0], + message: transformReplyTuples(rawReply[i + 1][1]) + } : null; + break; + } + } + + return parsedReply as XInfoStreamReply; +} diff --git a/lib/commands/XLEN.spec.ts b/lib/commands/XLEN.spec.ts new file mode 100644 index 0000000000..c4f62dbc4f --- /dev/null +++ b/lib/commands/XLEN.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XLEN'; + +describe('XLEN', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['XLEN', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xLen', async client => { + assert.equal( + await client.xLen('key'), + 0 + ); + }); +}); diff --git a/lib/commands/XLEN.ts b/lib/commands/XLEN.ts new file mode 100644 index 0000000000..d7ba033e61 --- /dev/null +++ b/lib/commands/XLEN.ts @@ -0,0 +1,11 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['XLEN', key]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/XPENDING.spec.ts b/lib/commands/XPENDING.spec.ts new file mode 100644 index 0000000000..31ffeeb423 --- /dev/null +++ b/lib/commands/XPENDING.spec.ts @@ -0,0 +1,30 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XPENDING'; + +describe('XPENDING', () => { + describe('transformArguments', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'group'), + ['XPENDING', 'key', 'group'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xPending', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.deepEqual( + await client.xPending('key', 'group'), + { + pending: 0, + firstId: null, + lastId: null, + consumers: null + } + ); + }); +}); diff --git a/lib/commands/XPENDING.ts b/lib/commands/XPENDING.ts new file mode 100644 index 0000000000..6695ab6614 --- /dev/null +++ b/lib/commands/XPENDING.ts @@ -0,0 +1,23 @@ +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, group: string): Array { + return ['XPENDING', key, group]; +} + +interface XPendingReply { + pending: number; + firstId: string | null; + lastId: number | null + consumers: Array | null; +} + +export function transformReply(reply: [number, string | null, number | null, Array | null]): XPendingReply { + return { + pending: reply[0], + firstId: reply[1], + lastId: reply[2], + consumers: reply[3] + }; +} diff --git a/lib/commands/XPENDING_RANGE.spec.ts b/lib/commands/XPENDING_RANGE.spec.ts new file mode 100644 index 0000000000..76a582d3db --- /dev/null +++ b/lib/commands/XPENDING_RANGE.spec.ts @@ -0,0 +1,53 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XPENDING_RANGE'; + +describe('XPENDING RANGE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key', 'group', '-', '+', 1), + ['XPENDING', 'key', 'group', '-', '+', '1'] + ); + }); + + it('with IDLE', () => { + assert.deepEqual( + transformArguments('key', 'group', '-', '+', 1, { + IDLE: 1, + }), + ['XPENDING', 'key', 'group', 'IDLE', '1', '-', '+', '1'] + ); + }); + + it('with consumer', () => { + assert.deepEqual( + transformArguments('key', 'group', '-', '+', 1, { + consumer: 'consumer' + }), + ['XPENDING', 'key', 'group', '-', '+', '1', 'consumer'] + ); + }); + + it('with IDLE, consumer', () => { + assert.deepEqual( + transformArguments('key', 'group', '-', '+', 1, { + IDLE: 1, + consumer: 'consumer' + }), + ['XPENDING', 'key', 'group', 'IDLE', '1', '-', '+', '1', 'consumer'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xPendingRange', async client => { + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }); + + assert.deepEqual( + await client.xPendingRange('key', 'group', '-', '+', 1), + [] + ); + }); +}); diff --git a/lib/commands/XPENDING_RANGE.ts b/lib/commands/XPENDING_RANGE.ts new file mode 100644 index 0000000000..c9e8d898e8 --- /dev/null +++ b/lib/commands/XPENDING_RANGE.ts @@ -0,0 +1,35 @@ +import { transformReplyStreamMessages } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +interface XPendingRangeOptions { + IDLE?: number; + consumer?: string; +} + +export function transformArguments( + key: string, + group: string, + start: string, + end: string, + count: number, + options?: XPendingRangeOptions +): Array { + const args = ['XPENDING', key, group]; + + if (options?.IDLE) { + args.push('IDLE', options.IDLE.toString()); + } + + args.push(start, end, count.toString()); + + if (options?.consumer) { + args.push(options.consumer); + } + + return args; +} + +export const transformReply = transformReplyStreamMessages; diff --git a/lib/commands/XRANGE.spec.ts b/lib/commands/XRANGE.spec.ts new file mode 100644 index 0000000000..55efa9d772 --- /dev/null +++ b/lib/commands/XRANGE.spec.ts @@ -0,0 +1,30 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XRANGE'; + +describe('XRANGE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key', '-', '+'), + ['XRANGE', 'key', '-', '+'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('key', '-', '+', { + COUNT: 1 + }), + ['XRANGE', 'key', '-', '+', 'COUNT', '1'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xRange', async client => { + assert.deepEqual( + await client.xRange('key', '+', '-'), + [] + ); + }); +}); diff --git a/lib/commands/XRANGE.ts b/lib/commands/XRANGE.ts new file mode 100644 index 0000000000..2902d0743d --- /dev/null +++ b/lib/commands/XRANGE.ts @@ -0,0 +1,21 @@ +import { transformReplyStreamMessages } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +interface XRangeOptions { + COUNT?: number; +} + +export function transformArguments(key: string, start: string, end: string, options?: XRangeOptions): Array { + const args = ['XRANGE', key, start, end]; + + if (options?.COUNT) { + args.push('COUNT', options.COUNT.toString()); + } + + return args; +} + +export const transformReply = transformReplyStreamMessages; diff --git a/lib/commands/XREAD.spec.ts b/lib/commands/XREAD.spec.ts new file mode 100644 index 0000000000..e7bf127a90 --- /dev/null +++ b/lib/commands/XREAD.spec.ts @@ -0,0 +1,87 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './XREAD'; + +describe('XREAD', () => { + describe('transformArguments', () => { + it('single stream', () => { + assert.deepEqual( + transformArguments({ + key: 'key', + id: '0' + }), + ['XREAD', 'STREAMS', 'key', '0'] + ); + }); + + it('multiple streams', () => { + assert.deepEqual( + transformArguments([{ + key: '1', + id: '0' + }, { + key: '2', + id: '0' + }]), + ['XREAD', 'STREAMS', '1', '2', '0', '0'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments({ + key: 'key', + id: '0' + }, { + COUNT: 1 + }), + ['XREAD', 'COUNT', '1', 'STREAMS', 'key', '0'] + ); + }); + + it('with BLOCK', () => { + assert.deepEqual( + transformArguments({ + key: 'key', + id: '0' + }, { + BLOCK: 0 + }), + ['XREAD', 'BLOCK', '0', 'STREAMS', 'key', '0'] + ); + }); + + it('with COUNT, BLOCK', () => { + assert.deepEqual( + transformArguments({ + key: 'key', + id: '0' + }, { + COUNT: 1, + BLOCK: 0 + }), + ['XREAD', 'COUNT', '1', 'BLOCK', '0', 'STREAMS', 'key', '0'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xRead', async client => { + assert.equal( + await client.xRead({ + key: 'key', + id: '0' + }), + null + ); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.xRead', async cluster => { + assert.equal( + await cluster.xRead({ + key: 'key', + id: '0' + }), + null + ); + }); +}); diff --git a/lib/commands/XREAD.ts b/lib/commands/XREAD.ts new file mode 100644 index 0000000000..1f4932837a --- /dev/null +++ b/lib/commands/XREAD.ts @@ -0,0 +1,43 @@ +import { transformReplyStreamsMessages } from './generic-transformers'; + +export const FIRST_KEY_INDEX = (streams: Array | XReadStream): string => { + return Array.isArray(streams) ? streams[0].key : streams.key; +}; + +export const IS_READ_ONLY = true; + +interface XReadStream { + key: string; + id: string; +} + +interface XReadOptions { + COUNT?: number; + BLOCK?: number; +} + +export function transformArguments(streams: Array | XReadStream, options?: XReadOptions): Array { + const args = ['XREAD']; + + if (options?.COUNT) { + args.push('COUNT', options.COUNT.toString()); + } + + if (typeof options?.BLOCK === 'number') { + args.push('BLOCK', options.BLOCK.toString()); + } + + args.push('STREAMS'); + + const streamsArray = Array.isArray(streams) ? streams : [streams], + argsLength = args.length; + for (let i = 0; i < streamsArray.length; i++) { + const stream = streamsArray[i]; + args[argsLength + i] = stream.key; + args[argsLength + streamsArray.length + i] = stream.id; + } + + return args; +} + +export const transformReply = transformReplyStreamsMessages; diff --git a/lib/commands/XREADGROUP.spec.ts b/lib/commands/XREADGROUP.spec.ts new file mode 100644 index 0000000000..8f7693c333 --- /dev/null +++ b/lib/commands/XREADGROUP.spec.ts @@ -0,0 +1,137 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, itWithCluster, TestRedisClusters } from '../test-utils'; +import { transformArguments } from './XREADGROUP'; + +describe('XREADGROUP', () => { + describe('transformArguments', () => { + it('single stream', () => { + assert.deepEqual( + transformArguments('group', 'consumer', { + key: 'key', + id: '0' + }), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'STREAMS', 'key', '0'] + ); + }); + + it('multiple streams', () => { + assert.deepEqual( + transformArguments('group', 'consumer', [{ + key: '1', + id: '0' + }, { + key: '2', + id: '0' + }]), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'STREAMS', '1', '2', '0', '0'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('group', 'consumer', { + key: 'key', + id: '0' + }, { + COUNT: 1 + }), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'COUNT', '1', 'STREAMS', 'key', '0'] + ); + }); + + it('with BLOCK', () => { + assert.deepEqual( + transformArguments('group', 'consumer', { + key: 'key', + id: '0' + }, { + BLOCK: 0 + }), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'BLOCK', '0', 'STREAMS', 'key', '0'] + ); + }); + + it('with NOACK', () => { + assert.deepEqual( + transformArguments('group', 'consumer', { + key: 'key', + id: '0' + }, { + NOACK: true + }), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'NOACK', 'STREAMS', 'key', '0'] + ); + }); + + it('with COUNT, BLOCK, NOACK', () => { + assert.deepEqual( + transformArguments('group', 'consumer', { + key: 'key', + id: '0' + }, { + COUNT: 1, + BLOCK: 0, + NOACK: true + }), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'COUNT', '1', 'BLOCK', '0', 'NOACK', 'STREAMS', 'key', '0'] + ); + }); + }); + + describe('client.xReadGroup', () => { + itWithClient(TestRedisServers.OPEN, 'null', async client => { + const [, readGroupReply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }) + ]); + + assert.equal(readGroupReply, null); + }); + + itWithClient(TestRedisServers.OPEN, 'with a message', async client => { + const [, id, readGroupReply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xAdd('key', '*', { field: 'value' }), + client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }) + ]); + + assert.deepEqual(readGroupReply, [{ + name: 'key', + messages: [{ + id, + message: Object.create(null, { + field: { + value: 'value', + configurable: true, + enumerable: true + } + }) + }] + }]); + }); + }); + + itWithCluster(TestRedisClusters.OPEN, 'cluster.xReadGroup', async cluster => { + const [, readGroupReply] = await Promise.all([ + cluster.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + cluster.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }) + ]); + + assert.equal(readGroupReply, null); + }); +}); diff --git a/lib/commands/XREADGROUP.ts b/lib/commands/XREADGROUP.ts new file mode 100644 index 0000000000..b01385e7c2 --- /dev/null +++ b/lib/commands/XREADGROUP.ts @@ -0,0 +1,57 @@ +import { transformReplyStreamsMessages } from './generic-transformers'; + +export interface XReadGroupStream { + key: string; + id: string; +} + +export interface XReadGroupOptions { + COUNT?: number; + BLOCK?: number; + NOACK?: true; +} + +export const FIRST_KEY_INDEX = ( + _group: string, + _consumer: string, + streams: Array | XReadGroupStream +): string => { + return Array.isArray(streams) ? streams[0].key : streams.key; +}; + +export const IS_READ_ONLY = true; + +export function transformArguments( + group: string, + consumer: string, + streams: Array | XReadGroupStream, + options?: XReadGroupOptions +): Array { + const args = ['XREADGROUP', 'GROUP', group, consumer]; + + if (options?.COUNT) { + args.push('COUNT', options.COUNT.toString()); + } + + if (typeof options?.BLOCK === 'number') { + args.push('BLOCK', options.BLOCK.toString()); + } + + if (options?.NOACK) { + args.push('NOACK'); + } + + args.push('STREAMS'); + + const streamsArray = Array.isArray(streams) ? streams : [streams], + argsLength = args.length; + for (let i = 0; i < streamsArray.length; i++) { + const stream = streamsArray[i]; + args[argsLength + i] = stream.key; + args[argsLength + streamsArray.length + i] = stream.id; + } + + return args; +} + +export const transformReply = transformReplyStreamsMessages; diff --git a/lib/commands/XREVRANGE.spec.ts b/lib/commands/XREVRANGE.spec.ts new file mode 100644 index 0000000000..ba009cc2bb --- /dev/null +++ b/lib/commands/XREVRANGE.spec.ts @@ -0,0 +1,30 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XREVRANGE'; + +describe('XREVRANGE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key', '-', '+'), + ['XREVRANGE', 'key', '-', '+'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('key', '-', '+', { + COUNT: 1 + }), + ['XREVRANGE', 'key', '-', '+', 'COUNT', '1'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xRevRange', async client => { + assert.deepEqual( + await client.xRevRange('key', '+', '-'), + [] + ); + }); +}); diff --git a/lib/commands/XREVRANGE.ts b/lib/commands/XREVRANGE.ts new file mode 100644 index 0000000000..a1fbbbc128 --- /dev/null +++ b/lib/commands/XREVRANGE.ts @@ -0,0 +1,17 @@ +import { transformReplyStreamMessages } from './generic-transformers'; + +interface XRangeRevOptions { + COUNT?: number; +} + +export function transformArguments(key: string, start: string, end: string, options?: XRangeRevOptions): Array { + const args = ['XREVRANGE', key, start, end]; + + if (options?.COUNT) { + args.push('COUNT', options.COUNT.toString()); + } + + return args; +} + +export const transformReply = transformReplyStreamMessages; diff --git a/lib/commands/XTRIM.spec.ts b/lib/commands/XTRIM.spec.ts new file mode 100644 index 0000000000..0b48fd6a2d --- /dev/null +++ b/lib/commands/XTRIM.spec.ts @@ -0,0 +1,49 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './XTRIM'; + +describe('XTRIM', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('key', 'MAXLEN', 1), + ['XTRIM', 'key', 'MAXLEN', '1'] + ); + }); + + it('with strategyModifier', () => { + assert.deepEqual( + transformArguments('key', 'MAXLEN', 1, { + strategyModifier: '=' + }), + ['XTRIM', 'key', 'MAXLEN', '=', '1'] + ); + }); + + it('with LIMIT', () => { + assert.deepEqual( + transformArguments('key', 'MAXLEN', 1, { + LIMIT: 1 + }), + ['XTRIM', 'key', 'MAXLEN', '1', 'LIMIT', '1'] + ); + }); + + it('with strategyModifier, LIMIT', () => { + assert.deepEqual( + transformArguments('key', 'MAXLEN', 1, { + strategyModifier: '=', + LIMIT: 1 + }), + ['XTRIM', 'key', 'MAXLEN', '=', '1', 'LIMIT', '1'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.xTrim', async client => { + assert.equal( + await client.xTrim('key', 'MAXLEN', 1), + 0 + ); + }); +}); diff --git a/lib/commands/XTRIM.ts b/lib/commands/XTRIM.ts new file mode 100644 index 0000000000..8175ba70df --- /dev/null +++ b/lib/commands/XTRIM.ts @@ -0,0 +1,26 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +interface XTrimOptions { + strategyModifier?: '=' | '~'; + LIMIT?: number; +} + +export function transformArguments(key: string, strategy: 'MAXLEN' | 'MINID', threshold: number, options?: XTrimOptions): Array { + const args = ['XTRIM', key, strategy]; + + if (options?.strategyModifier) { + args.push(options.strategyModifier); + } + + args.push(threshold.toString()); + + if (options?.LIMIT) { + args.push('LIMIT', options.LIMIT.toString()); + } + + return args; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZADD.spec.ts b/lib/commands/ZADD.spec.ts new file mode 100644 index 0000000000..7c017e4541 --- /dev/null +++ b/lib/commands/ZADD.spec.ts @@ -0,0 +1,127 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZADD'; + +describe('ZADD', () => { + describe('transformArguments', () => { + it('single member', () => { + assert.deepEqual( + transformArguments('key', { + value: '1', + score: 1 + }), + ['ZADD', 'key', '1', '1'] + ); + }); + + it('multiple members', () => { + assert.deepEqual( + transformArguments('key', [{ + value: '1', + score: 1 + }, { + value: '2', + score: 2 + }]), + ['ZADD', 'key', '1', '1', '2', '2'] + ); + }); + + it('with NX', () => { + assert.deepEqual( + transformArguments('key', { + value: '1', + score: 1 + }, { + NX: true + }), + ['ZADD', 'key', 'NX', '1', '1'] + ); + }); + + it('with XX', () => { + assert.deepEqual( + transformArguments('key', { + value: '1', + score: 1 + }, { + XX: true + }), + ['ZADD', 'key', 'XX', '1', '1'] + ); + }); + + it('with GT', () => { + assert.deepEqual( + transformArguments('key', { + value: '1', + score: 1 + }, { + GT: true + }), + ['ZADD', 'key', 'GT', '1', '1'] + ); + }); + + it('with LT', () => { + assert.deepEqual( + transformArguments('key', { + value: '1', + score: 1 + }, { + LT: true + }), + ['ZADD', 'key', 'LT', '1', '1'] + ); + }); + + it('with CH', () => { + assert.deepEqual( + transformArguments('key', { + value: '1', + score: 1 + }, { + CH: true + }), + ['ZADD', 'key', 'CH', '1', '1'] + ); + }); + + it('with INCR', () => { + assert.deepEqual( + transformArguments('key', { + value: '1', + score: 1 + }, { + INCR: true + }), + ['ZADD', 'key', 'INCR', '1', '1'] + ); + }); + + it('with XX, GT, CH, INCR', () => { + assert.deepEqual( + transformArguments('key', { + value: '1', + score: 1 + }, { + XX: true, + GT: true, + CH: true, + INCR: true + }), + ['ZADD', 'key', 'XX', 'GT', 'CH', 'INCR', '1', '1'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zAdd', async client => { + assert.equal( + await client.zAdd('key', { + value: '1', + score: 1 + }), + 1 + ); + }); +}); diff --git a/lib/commands/ZADD.ts b/lib/commands/ZADD.ts new file mode 100644 index 0000000000..0e43ecaf50 --- /dev/null +++ b/lib/commands/ZADD.ts @@ -0,0 +1,66 @@ +import { transformArgumentNumberInfinity, transformReplyNumberInfinity, ZMember } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +interface NX { + NX?: true; +} + +interface XX { + XX?: true; +} + +interface LT { + LT?: true; +} + +interface GT { + GT?: true; +} + +interface CH { + CH?: true; +} + +interface INCR { + INCR?: true; +} + +type ZAddOptions = (NX | (XX & LT & GT)) & CH & INCR; + +export function transformArguments(key: string, members: ZMember | Array, options?: ZAddOptions): Array { + const args = ['ZADD', key]; + + if ((options)?.NX) { + args.push('NX'); + } else { + if ((options)?.XX) { + args.push('XX'); + } + + if ((options)?.GT) { + args.push('GT'); + } else if ((options)?.LT) { + args.push('LT'); + } + } + + if ((options)?.CH) { + args.push('CH'); + } + + if ((options)?.INCR) { + args.push('INCR'); + } + + for (const { score, value } of (Array.isArray(members) ? members : [members])) { + args.push( + transformArgumentNumberInfinity(score), + value + ); + } + + return args; +} + +export const transformReply = transformReplyNumberInfinity; diff --git a/lib/commands/ZCARD.spec.ts b/lib/commands/ZCARD.spec.ts new file mode 100644 index 0000000000..03bfe59cfc --- /dev/null +++ b/lib/commands/ZCARD.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZCARD'; + +describe('ZCARD', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['ZCARD', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zCard', async client => { + assert.equal( + await client.zCard('key'), + 0 + ); + }); +}); diff --git a/lib/commands/ZCARD.ts b/lib/commands/ZCARD.ts new file mode 100644 index 0000000000..f6e4ea5f6c --- /dev/null +++ b/lib/commands/ZCARD.ts @@ -0,0 +1,11 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['ZCARD', key]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZCOUNT.spec.ts b/lib/commands/ZCOUNT.spec.ts new file mode 100644 index 0000000000..e461241ce1 --- /dev/null +++ b/lib/commands/ZCOUNT.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZCOUNT'; + +describe('ZCOUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0, 1), + ['ZCOUNT', 'key', '0', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zCount', async client => { + assert.equal( + await client.zCount('key', 0, 1), + 0 + ); + }); +}); diff --git a/lib/commands/ZCOUNT.ts b/lib/commands/ZCOUNT.ts new file mode 100644 index 0000000000..fd73c38448 --- /dev/null +++ b/lib/commands/ZCOUNT.ts @@ -0,0 +1,16 @@ +import { transformArgumentNumberInfinity, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, min: number, max: number): Array { + return [ + 'ZCOUNT', + key, + transformArgumentNumberInfinity(min), + transformArgumentNumberInfinity(max) + ]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZDIFF.spec.ts b/lib/commands/ZDIFF.spec.ts new file mode 100644 index 0000000000..f45b2af7ed --- /dev/null +++ b/lib/commands/ZDIFF.spec.ts @@ -0,0 +1,30 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZDIFF'; + +describe('ZDIFF', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['ZDIFF', '1', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['ZDIFF', '2', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zDiff', async client => { + assert.deepEqual( + await client.zDiff('key'), + [] + ); + }); +}); diff --git a/lib/commands/ZDIFF.ts b/lib/commands/ZDIFF.ts new file mode 100644 index 0000000000..f557b597ec --- /dev/null +++ b/lib/commands/ZDIFF.ts @@ -0,0 +1,11 @@ +import { pushVerdictArgument, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 2; + +export const IS_READ_ONLY = true; + +export function transformArguments(keys: Array | string): Array { + return pushVerdictArgument(['ZDIFF'], keys); +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/ZDIFFSTORE.spec.ts b/lib/commands/ZDIFFSTORE.spec.ts new file mode 100644 index 0000000000..5fbeebaf50 --- /dev/null +++ b/lib/commands/ZDIFFSTORE.spec.ts @@ -0,0 +1,30 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZDIFFSTORE'; + +describe('ZDIFFSTORE', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('destination', 'key'), + ['ZDIFFSTORE', 'destination', '1', 'key'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('destination', ['1', '2']), + ['ZDIFFSTORE', 'destination', '2', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zDiffStore', async client => { + assert.equal( + await client.zDiffStore('destination', 'key'), + 0 + ); + }); +}); diff --git a/lib/commands/ZDIFFSTORE.ts b/lib/commands/ZDIFFSTORE.ts new file mode 100644 index 0000000000..de409c0939 --- /dev/null +++ b/lib/commands/ZDIFFSTORE.ts @@ -0,0 +1,9 @@ +import { pushVerdictArgument, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(destination: string, keys: Array | string): Array { + return pushVerdictArgument(['ZDIFFSTORE', destination], keys); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZDIFF_WITHSCORES.spec.ts b/lib/commands/ZDIFF_WITHSCORES.spec.ts new file mode 100644 index 0000000000..99c2310829 --- /dev/null +++ b/lib/commands/ZDIFF_WITHSCORES.spec.ts @@ -0,0 +1,30 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZDIFF_WITHSCORES'; + +describe('ZDIFF WITHSCORES', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key'), + ['ZDIFF', '1', 'key', 'WITHSCORES'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['ZDIFF', '2', '1', '2', 'WITHSCORES'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zDiffWithScores', async client => { + assert.deepEqual( + await client.zDiffWithScores('key'), + [] + ); + }); +}); diff --git a/lib/commands/ZDIFF_WITHSCORES.ts b/lib/commands/ZDIFF_WITHSCORES.ts new file mode 100644 index 0000000000..26effab718 --- /dev/null +++ b/lib/commands/ZDIFF_WITHSCORES.ts @@ -0,0 +1,13 @@ +import { transformReplySortedSetWithScores } from './generic-transformers'; +import { transformArguments as transformZDiffArguments } from './ZDIFF'; + +export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZDIFF'; + +export function transformArguments(...args: Parameters): Array { + return [ + ...transformZDiffArguments(...args), + 'WITHSCORES' + ]; +} + +export const transformReply = transformReplySortedSetWithScores; diff --git a/lib/commands/ZINCRBY.spec.ts b/lib/commands/ZINCRBY.spec.ts new file mode 100644 index 0000000000..2196c63ec0 --- /dev/null +++ b/lib/commands/ZINCRBY.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZINCRBY'; + +describe('ZINCRBY', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1, 'member'), + ['ZINCRBY', 'key', '1', 'member'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zIncrBy', async client => { + assert.equal( + await client.zIncrBy('destination', 1, 'member'), + 1 + ); + }); +}); diff --git a/lib/commands/ZINCRBY.ts b/lib/commands/ZINCRBY.ts new file mode 100644 index 0000000000..39f32c165f --- /dev/null +++ b/lib/commands/ZINCRBY.ts @@ -0,0 +1,14 @@ +import { transformArgumentNumberInfinity, transformReplyNumberInfinity } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, increment: number, member: string): Array { + return [ + 'ZINCRBY', + key, + transformArgumentNumberInfinity(increment), + member + ]; +} + +export const transformReply = transformReplyNumberInfinity; diff --git a/lib/commands/ZINTER.spec.ts b/lib/commands/ZINTER.spec.ts new file mode 100644 index 0000000000..998c46fd3e --- /dev/null +++ b/lib/commands/ZINTER.spec.ts @@ -0,0 +1,58 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZINTER'; + +describe('ZINTER', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + transformArguments('key'), + ['ZINTER', '1', 'key'] + ); + }); + + it('keys (array)', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['ZINTER', '2', '1', '2'] + ); + }); + + it('with WEIGHTS', () => { + assert.deepEqual( + transformArguments('key', { + WEIGHTS: [1] + }), + ['ZINTER', '1', 'key', 'WEIGHTS', '1'] + ); + }); + + it('with AGGREGATE', () => { + assert.deepEqual( + transformArguments('key', { + AGGREGATE: 'SUM' + }), + ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM'] + ); + }); + + it('with WEIGHTS, AGGREGATE', () => { + assert.deepEqual( + transformArguments('key', { + WEIGHTS: [1], + AGGREGATE: 'SUM' + }), + ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zInter', async client => { + assert.deepEqual( + await client.zInter('key'), + [] + ); + }); +}); diff --git a/lib/commands/ZINTER.ts b/lib/commands/ZINTER.ts new file mode 100644 index 0000000000..90a42eda0d --- /dev/null +++ b/lib/commands/ZINTER.ts @@ -0,0 +1,29 @@ +import { pushVerdictArgument, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 2; + +export const IS_READ_ONLY = true; + +interface ZInterOptions { + WEIGHTS?: Array; + AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; +} + +export function transformArguments(keys: Array | string, options?: ZInterOptions): Array { + const args = pushVerdictArgument(['ZINTER'], keys); + + if (options?.WEIGHTS) { + args.push( + 'WEIGHTS', + ...options.WEIGHTS.map(weight => weight.toString()) + ); + } + + if (options?.AGGREGATE) { + args.push('AGGREGATE', options?.AGGREGATE); + } + + return args; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/ZINTERSTORE.spec.ts b/lib/commands/ZINTERSTORE.spec.ts new file mode 100644 index 0000000000..fca03157cb --- /dev/null +++ b/lib/commands/ZINTERSTORE.spec.ts @@ -0,0 +1,56 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZINTERSTORE'; + +describe('ZINTERSTORE', () => { + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + transformArguments('destination', 'key'), + ['ZINTERSTORE', 'destination', '1', 'key'] + ); + }); + + it('keys (array)', () => { + assert.deepEqual( + transformArguments('destination', ['1', '2']), + ['ZINTERSTORE', 'destination', '2', '1', '2'] + ); + }); + + it('with WEIGHTS', () => { + assert.deepEqual( + transformArguments('destination', 'key', { + WEIGHTS: [1] + }), + ['ZINTERSTORE', 'destination', '1', 'key', 'WEIGHTS', '1'] + ); + }); + + it('with AGGREGATE', () => { + assert.deepEqual( + transformArguments('destination', 'key', { + AGGREGATE: 'SUM' + }), + ['ZINTERSTORE', 'destination', '1', 'key', 'AGGREGATE', 'SUM'] + ); + }); + + it('with WEIGHTS, AGGREGATE', () => { + assert.deepEqual( + transformArguments('destination', 'key', { + WEIGHTS: [1], + AGGREGATE: 'SUM' + }), + ['ZINTERSTORE', 'destination', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zInterStore', async client => { + assert.equal( + await client.zInterStore('destination', 'key'), + 0 + ); + }); +}); diff --git a/lib/commands/ZINTERSTORE.ts b/lib/commands/ZINTERSTORE.ts new file mode 100644 index 0000000000..a026916ce1 --- /dev/null +++ b/lib/commands/ZINTERSTORE.ts @@ -0,0 +1,27 @@ +import { pushVerdictArgument, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +interface ZInterStoreOptions { + WEIGHTS?: Array; + AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; +} + +export function transformArguments(destination: string, keys: Array | string, options?: ZInterStoreOptions): Array { + const args = pushVerdictArgument(['ZINTERSTORE', destination], keys); + + if (options?.WEIGHTS) { + args.push( + 'WEIGHTS', + ...options.WEIGHTS.map(weight => weight.toString()) + ); + } + + if (options?.AGGREGATE) { + args.push('AGGREGATE', options?.AGGREGATE); + } + + return args; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZINTER_WITHSCORES.spec.ts b/lib/commands/ZINTER_WITHSCORES.spec.ts new file mode 100644 index 0000000000..f66787e3de --- /dev/null +++ b/lib/commands/ZINTER_WITHSCORES.spec.ts @@ -0,0 +1,58 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZINTER_WITHSCORES'; + +describe('ZINTER WITHSCORES', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + transformArguments('key'), + ['ZINTER', '1', 'key', 'WITHSCORES'] + ); + }); + + it('keys (array)', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['ZINTER', '2', '1', '2', 'WITHSCORES'] + ); + }); + + it('with WEIGHTS', () => { + assert.deepEqual( + transformArguments('key', { + WEIGHTS: [1] + }), + ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES'] + ); + }); + + it('with AGGREGATE', () => { + assert.deepEqual( + transformArguments('key', { + AGGREGATE: 'SUM' + }), + ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES'] + ); + }); + + it('with WEIGHTS, AGGREGATE', () => { + assert.deepEqual( + transformArguments('key', { + WEIGHTS: [1], + AGGREGATE: 'SUM' + }), + ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM', 'WITHSCORES'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zInterWithScores', async client => { + assert.deepEqual( + await client.zInterWithScores('key'), + [] + ); + }); +}); diff --git a/lib/commands/ZINTER_WITHSCORES.ts b/lib/commands/ZINTER_WITHSCORES.ts new file mode 100644 index 0000000000..0a82228fce --- /dev/null +++ b/lib/commands/ZINTER_WITHSCORES.ts @@ -0,0 +1,13 @@ +import { transformReplySortedSetWithScores } from './generic-transformers'; +import { transformArguments as transformZInterArguments } from './ZINTER'; + +export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZINTER'; + +export function transformArguments(...args: Parameters): Array { + return [ + ...transformZInterArguments(...args), + 'WITHSCORES' + ]; +} + +export const transformReply = transformReplySortedSetWithScores; diff --git a/lib/commands/ZLEXCOUNT.spec.ts b/lib/commands/ZLEXCOUNT.spec.ts new file mode 100644 index 0000000000..b106ba0cdc --- /dev/null +++ b/lib/commands/ZLEXCOUNT.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZLEXCOUNT'; + +describe('ZLEXCOUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', '[a', '[b'), + ['ZLEXCOUNT', 'key', '[a', '[b'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zLexCount', async client => { + assert.equal( + await client.zLexCount('key', '[a', '[b'), + 0 + ); + }); +}); diff --git a/lib/commands/ZLEXCOUNT.ts b/lib/commands/ZLEXCOUNT.ts new file mode 100644 index 0000000000..2ba50dda73 --- /dev/null +++ b/lib/commands/ZLEXCOUNT.ts @@ -0,0 +1,16 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, min: string, max: string): Array { + return [ + 'ZLEXCOUNT', + key, + min, + max + ]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZMSCORE.spec.ts b/lib/commands/ZMSCORE.spec.ts new file mode 100644 index 0000000000..3cf3845392 --- /dev/null +++ b/lib/commands/ZMSCORE.spec.ts @@ -0,0 +1,30 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZMSCORE'; + +describe('ZMSCORE', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'member'), + ['ZMSCORE', 'key', 'member'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['ZMSCORE', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zmScore', async client => { + assert.deepEqual( + await client.zmScore('key', 'member'), + [null] + ); + }); +}); diff --git a/lib/commands/ZMSCORE.ts b/lib/commands/ZMSCORE.ts new file mode 100644 index 0000000000..8a6f73c783 --- /dev/null +++ b/lib/commands/ZMSCORE.ts @@ -0,0 +1,11 @@ +import { pushVerdictArguments, transformReplyNumberInfinityNullArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, member: string | Array): Array { + return pushVerdictArguments(['ZMSCORE', key], member); +} + +export const transformReply = transformReplyNumberInfinityNullArray; diff --git a/lib/commands/ZPOPMAX.spec.ts b/lib/commands/ZPOPMAX.spec.ts new file mode 100644 index 0000000000..ceab3cad1d --- /dev/null +++ b/lib/commands/ZPOPMAX.spec.ts @@ -0,0 +1,41 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './ZPOPMAX'; + +describe('ZPOPMAX', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['ZPOPMAX', 'key'] + ); + }); + + it('transformReply', () => { + assert.deepEqual( + transformReply(['value', '1']), + { + value: 'value', + score: 1 + } + ); + }); + + describe('client.zPopMax', () => { + itWithClient(TestRedisServers.OPEN, 'null', async client => { + assert.equal( + await client.zPopMax('key'), + null + ); + }); + + itWithClient(TestRedisServers.OPEN, 'member', async client => { + const member = { score: 1, value: 'value' }, + [, zPopMaxReply] = await Promise.all([ + client.zAdd('key', member), + client.zPopMax('key') + ]); + + assert.deepEqual(zPopMaxReply, member); + }); + }); +}); diff --git a/lib/commands/ZPOPMAX.ts b/lib/commands/ZPOPMAX.ts new file mode 100644 index 0000000000..8da20731ab --- /dev/null +++ b/lib/commands/ZPOPMAX.ts @@ -0,0 +1,19 @@ +import { transformReplyNumberInfinity, ZMember } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return [ + 'ZPOPMAX', + key + ]; +} + +export function transformReply(reply: [string, string] | []): ZMember | null { + if (!reply.length) return null; + + return { + value: reply[0], + score: transformReplyNumberInfinity(reply[1]) + }; +} diff --git a/lib/commands/ZPOPMAX_COUNT.spec.ts b/lib/commands/ZPOPMAX_COUNT.spec.ts new file mode 100644 index 0000000000..c0e71977ee --- /dev/null +++ b/lib/commands/ZPOPMAX_COUNT.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZPOPMAX_COUNT'; + +describe('ZPOPMAX COUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['ZPOPMAX', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zPopMaxCount', async client => { + assert.deepEqual( + await client.zPopMaxCount('key', 1), + [] + ); + }); +}); diff --git a/lib/commands/ZPOPMAX_COUNT.ts b/lib/commands/ZPOPMAX_COUNT.ts new file mode 100644 index 0000000000..abfa8494ec --- /dev/null +++ b/lib/commands/ZPOPMAX_COUNT.ts @@ -0,0 +1,13 @@ +import { transformReplySortedSetWithScores } from './generic-transformers'; +import { transformArguments as transformZPopMaxArguments } from './ZPOPMAX'; + +export { FIRST_KEY_INDEX } from './ZPOPMAX'; + +export function transformArguments(key: string, count: number): Array { + return [ + ...transformZPopMaxArguments(key), + count.toString() + ]; +} + +export const transformReply = transformReplySortedSetWithScores; diff --git a/lib/commands/ZPOPMIN.spec.ts b/lib/commands/ZPOPMIN.spec.ts new file mode 100644 index 0000000000..c69ca7c27f --- /dev/null +++ b/lib/commands/ZPOPMIN.spec.ts @@ -0,0 +1,41 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './ZPOPMIN'; + +describe('ZPOPMIN', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['ZPOPMIN', 'key'] + ); + }); + + it('transformReply', () => { + assert.deepEqual( + transformReply(['value', '1']), + { + value: 'value', + score: 1 + } + ); + }); + + describe('client.zPopMin', () => { + itWithClient(TestRedisServers.OPEN, 'null', async client => { + assert.equal( + await client.zPopMin('key'), + null + ); + }); + + itWithClient(TestRedisServers.OPEN, 'member', async client => { + const member = { score: 1, value: 'value' }, + [, zPopMinReply] = await Promise.all([ + client.zAdd('key', member), + client.zPopMin('key') + ]); + + assert.deepEqual(zPopMinReply, member); + }); + }); +}); diff --git a/lib/commands/ZPOPMIN.ts b/lib/commands/ZPOPMIN.ts new file mode 100644 index 0000000000..9e1a7bdfa3 --- /dev/null +++ b/lib/commands/ZPOPMIN.ts @@ -0,0 +1,19 @@ +import { transformReplyNumberInfinity, ZMember } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string): Array { + return [ + 'ZPOPMIN', + key + ]; +} + +export function transformReply(reply: [string, string] | []): ZMember | null { + if (!reply.length) return null; + + return { + value: reply[0], + score: transformReplyNumberInfinity(reply[1]) + }; +} diff --git a/lib/commands/ZPOPMIN_COUNT.spec.ts b/lib/commands/ZPOPMIN_COUNT.spec.ts new file mode 100644 index 0000000000..1c2745a0fd --- /dev/null +++ b/lib/commands/ZPOPMIN_COUNT.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZPOPMIN_COUNT'; + +describe('ZPOPMIN COUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['ZPOPMIN', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zPopMinCount', async client => { + assert.deepEqual( + await client.zPopMinCount('key', 1), + [] + ); + }); +}); diff --git a/lib/commands/ZPOPMIN_COUNT.ts b/lib/commands/ZPOPMIN_COUNT.ts new file mode 100644 index 0000000000..e313b32dc8 --- /dev/null +++ b/lib/commands/ZPOPMIN_COUNT.ts @@ -0,0 +1,13 @@ +import { transformReplySortedSetWithScores } from './generic-transformers'; +import { transformArguments as transformZPopMinArguments } from './ZPOPMIN'; + +export { FIRST_KEY_INDEX } from './ZPOPMIN'; + +export function transformArguments(key: string, count: number): Array { + return [ + ...transformZPopMinArguments(key), + count.toString() + ]; +} + +export const transformReply = transformReplySortedSetWithScores; diff --git a/lib/commands/ZRANDMEMBER.spec.ts b/lib/commands/ZRANDMEMBER.spec.ts new file mode 100644 index 0000000000..da31641a18 --- /dev/null +++ b/lib/commands/ZRANDMEMBER.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZRANDMEMBER'; + +describe('ZRANDMEMBER', () => { + describeHandleMinimumRedisVersion([6, 2]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key'), + ['ZRANDMEMBER', 'key'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRandMember', async client => { + assert.equal( + await client.zRandMember('key'), + null + ); + }); +}); diff --git a/lib/commands/ZRANDMEMBER.ts b/lib/commands/ZRANDMEMBER.ts new file mode 100644 index 0000000000..27bb7cefa5 --- /dev/null +++ b/lib/commands/ZRANDMEMBER.ts @@ -0,0 +1,11 @@ +import { transformReplyStringNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string): Array { + return ['ZRANDMEMBER', key]; +} + +export const transformReply = transformReplyStringNull; diff --git a/lib/commands/ZRANDMEMBER_COUNT.spec.ts b/lib/commands/ZRANDMEMBER_COUNT.spec.ts new file mode 100644 index 0000000000..4c873c82d9 --- /dev/null +++ b/lib/commands/ZRANDMEMBER_COUNT.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZRANDMEMBER_COUNT'; + +describe('ZRANDMEMBER COUNT', () => { + describeHandleMinimumRedisVersion([6, 2, 5]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['ZRANDMEMBER', 'key', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRandMemberCount', async client => { + assert.deepEqual( + await client.zRandMemberCount('key', 1), + [] + ); + }); +}); diff --git a/lib/commands/ZRANDMEMBER_COUNT.ts b/lib/commands/ZRANDMEMBER_COUNT.ts new file mode 100644 index 0000000000..f7eef456d0 --- /dev/null +++ b/lib/commands/ZRANDMEMBER_COUNT.ts @@ -0,0 +1,13 @@ +import { transformReplyStringArray } from './generic-transformers'; +import { transformArguments as transformZRandMemberArguments } from './ZRANDMEMBER'; + +export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANDMEMBER'; + +export function transformArguments(key: string, count: number): Array { + return [ + ...transformZRandMemberArguments(key), + count.toString() + ]; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts b/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts new file mode 100644 index 0000000000..55624361fb --- /dev/null +++ b/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZRANDMEMBER_COUNT_WITHSCORES'; + +describe('ZRANDMEMBER COUNT WITHSCORES', () => { + describeHandleMinimumRedisVersion([6, 2, 5]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 1), + ['ZRANDMEMBER', 'key', '1', 'WITHSCORES'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRandMemberCountWithScores', async client => { + assert.deepEqual( + await client.zRandMemberCountWithScores('key', 1), + [] + ); + }); +}); diff --git a/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts b/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts new file mode 100644 index 0000000000..6d79f41c95 --- /dev/null +++ b/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts @@ -0,0 +1,13 @@ +import { transformReplySortedSetWithScores } from './generic-transformers'; +import { transformArguments as transformZRandMemberCountArguments } from './ZRANDMEMBER_COUNT'; + +export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANDMEMBER_COUNT'; + +export function transformArguments(...args: Parameters): Array { + return [ + ...transformZRandMemberCountArguments(...args), + 'WITHSCORES' + ]; +} + +export const transformReply = transformReplySortedSetWithScores; diff --git a/lib/commands/ZRANGE.spec.ts b/lib/commands/ZRANGE.spec.ts new file mode 100644 index 0000000000..72d83931ff --- /dev/null +++ b/lib/commands/ZRANGE.spec.ts @@ -0,0 +1,74 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZRANGE'; + +describe('ZRANGE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('src', 0, 1), + ['ZRANGE', 'src', '0', '1'] + ); + }); + + it('with BYSCORE', () => { + assert.deepEqual( + transformArguments('src', 0, 1, { + BY: 'SCORE' + }), + ['ZRANGE', 'src', '0', '1', 'BYSCORE'] + ); + }); + + it('with BYLEX', () => { + assert.deepEqual( + transformArguments('src', 0, 1, { + BY: 'LEX' + }), + ['ZRANGE', 'src', '0', '1', 'BYLEX'] + ); + }); + + it('with REV', () => { + assert.deepEqual( + transformArguments('src', 0, 1, { + REV: true + }), + ['ZRANGE', 'src', '0', '1', 'REV'] + ); + }); + + it('with LIMIT', () => { + assert.deepEqual( + transformArguments('src', 0, 1, { + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGE', 'src', '0', '1', 'LIMIT', '0', '1'] + ); + }); + + it('with BY & REV & LIMIT', () => { + assert.deepEqual( + transformArguments('src', 0, 1, { + BY: 'SCORE', + REV: true, + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRange', async client => { + assert.deepEqual( + await client.zRange('src', 0, 1), + [] + ); + }); +}); diff --git a/lib/commands/ZRANGE.ts b/lib/commands/ZRANGE.ts new file mode 100644 index 0000000000..9037210d69 --- /dev/null +++ b/lib/commands/ZRANGE.ts @@ -0,0 +1,45 @@ +import { transformArgumentNumberInfinity, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +interface ZRangeOptions { + BY?: 'SCORE' | 'LEX'; + REV?: true; + LIMIT?: { + offset: number; + count: number; + }; +} + +export function transformArguments(key: string, min: string | number, max: string | number, options?: ZRangeOptions): Array { + const args = [ + 'ZRANGE', + key, + typeof min === 'string' ? min : transformArgumentNumberInfinity(min), + typeof max === 'string' ? max : transformArgumentNumberInfinity(max) + ]; + + switch (options?.BY) { + case 'SCORE': + args.push('BYSCORE'); + break; + + case 'LEX': + args.push('BYLEX'); + break; + } + + if (options?.REV) { + args.push('REV'); + } + + if (options?.LIMIT) { + args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); + } + + return args; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/ZRANGESTORE.spec.ts b/lib/commands/ZRANGESTORE.spec.ts new file mode 100644 index 0000000000..30dee7c0b5 --- /dev/null +++ b/lib/commands/ZRANGESTORE.spec.ts @@ -0,0 +1,82 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZRANGESTORE'; + +describe('ZRANGESTORE', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('dst', 'src', 0, 1), + ['ZRANGESTORE', 'dst', 'src', '0', '1'] + ); + }); + + it('with BYSCORE', () => { + assert.deepEqual( + transformArguments('dst', 'src', 0, 1, { + BY: 'SCORE' + }), + ['ZRANGESTORE', 'dst', 'src', '0', '1', 'BYSCORE'] + ); + }); + + it('with BYLEX', () => { + assert.deepEqual( + transformArguments('dst', 'src', 0, 1, { + BY: 'LEX' + }), + ['ZRANGESTORE', 'dst', 'src', '0', '1', 'BYLEX'] + ); + }); + + it('with REV', () => { + assert.deepEqual( + transformArguments('dst', 'src', 0, 1, { + REV: true + }), + ['ZRANGESTORE', 'dst', 'src', '0', '1', 'REV'] + ); + }); + + it('with LIMIT', () => { + assert.deepEqual( + transformArguments('dst', 'src', 0, 1, { + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGESTORE', 'dst', 'src', '0', '1', 'LIMIT', '0', '1'] + ); + }); + + it('with BY & REV & LIMIT', () => { + assert.deepEqual( + transformArguments('dst', 'src', 0, 1, { + BY: 'SCORE', + REV: true, + LIMIT: { + offset: 0, + count: 1 + }, + WITHSCORES: true + }), + ['ZRANGESTORE', 'dst', 'src', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1', 'WITHSCORES'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRangeStore', async client => { + await client.zAdd('src', { + score: 0.5, + value: 'value' + }); + + assert.equal( + await client.zRangeStore('dst', 'src', 0, 1), + 1 + ); + }); +}); diff --git a/lib/commands/ZRANGESTORE.ts b/lib/commands/ZRANGESTORE.ts new file mode 100644 index 0000000000..6ad7566166 --- /dev/null +++ b/lib/commands/ZRANGESTORE.ts @@ -0,0 +1,55 @@ +import { transformArgumentNumberInfinity } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +interface ZRangeStoreOptions { + BY?: 'SCORE' | 'LEX'; + REV?: true; + LIMIT?: { + offset: number; + count: number; + }; + WITHSCORES?: true; +} + +export function transformArguments(dst: string, src: string, min: number, max: number, options?: ZRangeStoreOptions): Array { + const args = [ + 'ZRANGESTORE', + dst, + src, + transformArgumentNumberInfinity(min), + transformArgumentNumberInfinity(max) + ]; + + switch (options?.BY) { + case 'SCORE': + args.push('BYSCORE'); + break; + + case 'LEX': + args.push('BYLEX'); + break; + } + + if (options?.REV) { + args.push('REV'); + } + + if (options?.LIMIT) { + args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); + } + + if (options?.WITHSCORES) { + args.push('WITHSCORES'); + } + + return args; +} + +export function transformReply(reply: number): number { + if (typeof reply !== 'number') { + throw new TypeError(`Upgrade to Redis 6.2.5 and up (https://github.com/redis/redis/pull/9089)`); + } + + return reply; +} diff --git a/lib/commands/ZRANGE_WITHSCORES.spec.ts b/lib/commands/ZRANGE_WITHSCORES.spec.ts new file mode 100644 index 0000000000..4c739b3d3b --- /dev/null +++ b/lib/commands/ZRANGE_WITHSCORES.spec.ts @@ -0,0 +1,65 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZRANGE_WITHSCORES'; + +describe('ZRANGE WITHSCORES', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('src', 0, 1), + ['ZRANGE', 'src', '0', '1', 'WITHSCORES'] + ); + }); + + it('with BY', () => { + assert.deepEqual( + transformArguments('src', 0, 1, { + BY: 'SCORE' + }), + ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'WITHSCORES'] + ); + }); + + it('with REV', () => { + assert.deepEqual( + transformArguments('src', 0, 1, { + REV: true + }), + ['ZRANGE', 'src', '0', '1', 'REV', 'WITHSCORES'] + ); + }); + + it('with LIMIT', () => { + assert.deepEqual( + transformArguments('src', 0, 1, { + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGE', 'src', '0', '1', 'LIMIT', '0', '1', 'WITHSCORES'] + ); + }); + + it('with BY & REV & LIMIT', () => { + assert.deepEqual( + transformArguments('src', 0, 1, { + BY: 'SCORE', + REV: true, + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1', 'WITHSCORES'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRangeWithScores', async client => { + assert.deepEqual( + await client.zRangeWithScores('src', 0, 1), + [] + ); + }); +}); diff --git a/lib/commands/ZRANGE_WITHSCORES.ts b/lib/commands/ZRANGE_WITHSCORES.ts new file mode 100644 index 0000000000..0f777132f2 --- /dev/null +++ b/lib/commands/ZRANGE_WITHSCORES.ts @@ -0,0 +1,13 @@ +import { transformReplySortedSetWithScores } from './generic-transformers'; +import { transformArguments as transformZRangeArguments } from './ZRANGE'; + +export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANGE'; + +export function transformArguments(...args: Parameters): Array { + return [ + ...transformZRangeArguments(...args), + 'WITHSCORES' + ]; +} + +export const transformReply = transformReplySortedSetWithScores; diff --git a/lib/commands/ZRANK.spec.ts b/lib/commands/ZRANK.spec.ts new file mode 100644 index 0000000000..8dd9c924cc --- /dev/null +++ b/lib/commands/ZRANK.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZRANK'; + +describe('ZRANK', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'member'), + ['ZRANK', 'key', 'member'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRank', async client => { + assert.equal( + await client.zRank('key', 'member'), + null + ); + }); +}); diff --git a/lib/commands/ZRANK.ts b/lib/commands/ZRANK.ts new file mode 100644 index 0000000000..84f9c7d4a9 --- /dev/null +++ b/lib/commands/ZRANK.ts @@ -0,0 +1,11 @@ +import { transformReplyNumberNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, member: string): Array { + return ['ZRANK', key, member]; +} + +export const transformReply = transformReplyNumberNull; diff --git a/lib/commands/ZREM.spec.ts b/lib/commands/ZREM.spec.ts new file mode 100644 index 0000000000..d613832035 --- /dev/null +++ b/lib/commands/ZREM.spec.ts @@ -0,0 +1,28 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZREM'; + +describe('ZREM', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + transformArguments('key', 'member'), + ['ZREM', 'key', 'member'] + ); + }); + + it('array', () => { + assert.deepEqual( + transformArguments('key', ['1', '2']), + ['ZREM', 'key', '1', '2'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRem', async client => { + assert.equal( + await client.zRem('key', 'member'), + 0 + ); + }); +}); diff --git a/lib/commands/ZREM.ts b/lib/commands/ZREM.ts new file mode 100644 index 0000000000..089b6136af --- /dev/null +++ b/lib/commands/ZREM.ts @@ -0,0 +1,9 @@ +import { pushVerdictArguments, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, member: string | Array): Array { + return pushVerdictArguments(['ZREM', key], member); +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZREMRANGEBYLEX.spec.ts b/lib/commands/ZREMRANGEBYLEX.spec.ts new file mode 100644 index 0000000000..7aae059480 --- /dev/null +++ b/lib/commands/ZREMRANGEBYLEX.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZREMRANGEBYLEX'; + +describe('ZREMRANGEBYLEX', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', '[a', '[b'), + ['ZREMRANGEBYLEX', 'key', '[a', '[b'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRemRangeByLex', async client => { + assert.equal( + await client.zRemRangeByLex('key', '[a', '[b'), + 0 + ); + }); +}); diff --git a/lib/commands/ZREMRANGEBYLEX.ts b/lib/commands/ZREMRANGEBYLEX.ts new file mode 100644 index 0000000000..aaf92992f9 --- /dev/null +++ b/lib/commands/ZREMRANGEBYLEX.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, min: string, max: string): Array { + return ['ZREMRANGEBYLEX', key, min, max]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZREMRANGEBYRANK.spec.ts b/lib/commands/ZREMRANGEBYRANK.spec.ts new file mode 100644 index 0000000000..401b57c8e2 --- /dev/null +++ b/lib/commands/ZREMRANGEBYRANK.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZREMRANGEBYRANK'; + +describe('ZREMRANGEBYRANK', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0, 1), + ['ZREMRANGEBYRANK', 'key', '0', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRemRangeByRank', async client => { + assert.equal( + await client.zRemRangeByRank('key', 0, 1), + 0 + ); + }); +}); diff --git a/lib/commands/ZREMRANGEBYRANK.ts b/lib/commands/ZREMRANGEBYRANK.ts new file mode 100644 index 0000000000..89bf63d8e3 --- /dev/null +++ b/lib/commands/ZREMRANGEBYRANK.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, start: number, stop: number): Array { + return ['ZREMRANGEBYRANK', key, start.toString(), stop.toString()]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZREMRANGEBYSCORE.spec.ts b/lib/commands/ZREMRANGEBYSCORE.spec.ts new file mode 100644 index 0000000000..141392e772 --- /dev/null +++ b/lib/commands/ZREMRANGEBYSCORE.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZREMRANGEBYSCORE'; + +describe('ZREMRANGEBYSCORE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 0, 1), + ['ZREMRANGEBYSCORE', 'key', '0', '1'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRemRangeByScore', async client => { + assert.equal( + await client.zRemRangeByScore('key', 0, 1), + 0 + ); + }); +}); diff --git a/lib/commands/ZREMRANGEBYSCORE.ts b/lib/commands/ZREMRANGEBYSCORE.ts new file mode 100644 index 0000000000..64d14a4eb4 --- /dev/null +++ b/lib/commands/ZREMRANGEBYSCORE.ts @@ -0,0 +1,9 @@ +import { transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export function transformArguments(key: string, min: number, max: number): Array { + return ['ZREMRANGEBYSCORE', key, min.toString(), max.toString()]; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZREVRANK.spec.ts b/lib/commands/ZREVRANK.spec.ts new file mode 100644 index 0000000000..727a61a35a --- /dev/null +++ b/lib/commands/ZREVRANK.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZREVRANK'; + +describe('ZREVRANK', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'member'), + ['ZREVRANK', 'key', 'member'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zRevRank', async client => { + assert.equal( + await client.zRevRank('key', 'member'), + null + ); + }); +}); diff --git a/lib/commands/ZREVRANK.ts b/lib/commands/ZREVRANK.ts new file mode 100644 index 0000000000..7d4c4ce2ab --- /dev/null +++ b/lib/commands/ZREVRANK.ts @@ -0,0 +1,11 @@ +import { transformReplyNumberNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, member: string): Array { + return ['ZREVRANK', key, member]; +} + +export const transformReply = transformReplyNumberNull; diff --git a/lib/commands/ZSCAN.spec.ts b/lib/commands/ZSCAN.spec.ts new file mode 100644 index 0000000000..3ff0c0a52b --- /dev/null +++ b/lib/commands/ZSCAN.spec.ts @@ -0,0 +1,77 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './ZSCAN'; + +describe('ZSCAN', () => { + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + transformArguments('key', 0), + ['ZSCAN', 'key', '0'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern' + }), + ['ZSCAN', 'key', '0', 'MATCH', 'pattern'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + COUNT: 1 + }), + ['ZSCAN', 'key', '0', 'COUNT', '1'] + ); + }); + + it('with MATCH & COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern', + COUNT: 1 + }), + ['ZSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] + ); + }); + }); + + describe('transformReply', () => { + it('without members', () => { + assert.deepEqual( + transformReply(['0', []]), + { + cursor: 0, + members: [] + } + ); + }); + + it('with members', () => { + assert.deepEqual( + transformReply(['0', ['member', '-inf']]), + { + cursor: 0, + members: [{ + value: 'member', + score: -Infinity + }] + } + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zScan', async client => { + assert.deepEqual( + await client.zScan('key', 0), + { + cursor: 0, + members: [] + } + ); + }); +}); diff --git a/lib/commands/ZSCAN.ts b/lib/commands/ZSCAN.ts new file mode 100644 index 0000000000..79e5464db6 --- /dev/null +++ b/lib/commands/ZSCAN.ts @@ -0,0 +1,32 @@ +import { ScanOptions, transformReplyNumberInfinity, pushScanArguments, ZMember } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, cursor: number, options?: ScanOptions): Array { + return pushScanArguments([ + 'ZSCAN', + key + ], cursor, options); +} + +interface ZScanReply { + cursor: number; + members: Array; +} + +export function transformReply([cursor, rawMembers]: [string, Array]): ZScanReply { + const parsedMembers: Array = []; + for (let i = 0; i < rawMembers.length; i += 2) { + parsedMembers.push({ + value: rawMembers[i], + score: transformReplyNumberInfinity(rawMembers[i + 1]) + }); + } + + return { + cursor: Number(cursor), + members: parsedMembers + }; +} diff --git a/lib/commands/ZSCORE.spec.ts b/lib/commands/ZSCORE.spec.ts new file mode 100644 index 0000000000..d346a2e2c8 --- /dev/null +++ b/lib/commands/ZSCORE.spec.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZSCORE'; + +describe('ZSCORE', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'member'), + ['ZSCORE', 'key', 'member'] + ); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zScore', async client => { + assert.equal( + await client.zScore('key', 'member'), + null + ); + }); +}); diff --git a/lib/commands/ZSCORE.ts b/lib/commands/ZSCORE.ts new file mode 100644 index 0000000000..18b664a321 --- /dev/null +++ b/lib/commands/ZSCORE.ts @@ -0,0 +1,11 @@ +import { transformReplyNumberInfinityNull } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, member: string): Array { + return ['ZSCORE', key, member]; +} + +export const transformReply = transformReplyNumberInfinityNull; diff --git a/lib/commands/ZUNION.spec.ts b/lib/commands/ZUNION.spec.ts new file mode 100644 index 0000000000..12e9283393 --- /dev/null +++ b/lib/commands/ZUNION.spec.ts @@ -0,0 +1,48 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZUNION'; + +describe('ZUNION', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + transformArguments('key'), + ['ZUNION', '1', 'key'] + ); + }); + + it('keys (array)', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['ZUNION', '2', '1', '2'] + ); + }); + + it('with WEIGHTS', () => { + assert.deepEqual( + transformArguments('key', { + WEIGHTS: [1] + }), + ['ZUNION', '1', 'key', 'WEIGHTS', '1'] + ); + }); + + it('with AGGREGATE', () => { + assert.deepEqual( + transformArguments('key', { + AGGREGATE: 'SUM' + }), + ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zUnion', async client => { + assert.deepEqual( + await client.zUnion('key'), + [] + ); + }); +}); diff --git a/lib/commands/ZUNION.ts b/lib/commands/ZUNION.ts new file mode 100644 index 0000000000..efdfccb1ff --- /dev/null +++ b/lib/commands/ZUNION.ts @@ -0,0 +1,26 @@ +import { pushVerdictArgument, transformReplyStringArray } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 2; + +export const IS_READ_ONLY = true; + +interface ZUnionOptions { + WEIGHTS?: Array; + AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; +} + +export function transformArguments(keys: Array | string, options?: ZUnionOptions): Array { + const args = pushVerdictArgument(['ZUNION'], keys); + + if (options?.WEIGHTS) { + args.push('WEIGHTS', ...options.WEIGHTS.map(weight => weight.toString())); + } + + if (options?.AGGREGATE) { + args.push('AGGREGATE', options.AGGREGATE); + } + + return args; +} + +export const transformReply = transformReplyStringArray; diff --git a/lib/commands/ZUNIONSTORE.spec.ts b/lib/commands/ZUNIONSTORE.spec.ts new file mode 100644 index 0000000000..0c4d7a3006 --- /dev/null +++ b/lib/commands/ZUNIONSTORE.spec.ts @@ -0,0 +1,56 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments } from './ZUNIONSTORE'; + +describe('ZUNIONSTORE', () => { + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + transformArguments('destination', 'key'), + ['ZUNIONSTORE', 'destination', '1', 'key'] + ); + }); + + it('keys (array)', () => { + assert.deepEqual( + transformArguments('destination', ['1', '2']), + ['ZUNIONSTORE', 'destination', '2', '1', '2'] + ); + }); + + it('with WEIGHTS', () => { + assert.deepEqual( + transformArguments('destination', 'key', { + WEIGHTS: [1] + }), + ['ZUNIONSTORE', 'destination', '1', 'key', 'WEIGHTS', '1'] + ); + }); + + it('with AGGREGATE', () => { + assert.deepEqual( + transformArguments('destination', 'key', { + AGGREGATE: 'SUM' + }), + ['ZUNIONSTORE', 'destination', '1', 'key', 'AGGREGATE', 'SUM'] + ); + }); + + it('with WEIGHTS, AGGREGATE', () => { + assert.deepEqual( + transformArguments('destination', 'key', { + WEIGHTS: [1], + AGGREGATE: 'SUM' + }), + ['ZUNIONSTORE', 'destination', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zUnionStore', async client => { + assert.equal( + await client.zUnionStore('destination', 'key'), + 0 + ); + }); +}); diff --git a/lib/commands/ZUNIONSTORE.ts b/lib/commands/ZUNIONSTORE.ts new file mode 100644 index 0000000000..c03f120370 --- /dev/null +++ b/lib/commands/ZUNIONSTORE.ts @@ -0,0 +1,24 @@ +import { pushVerdictArgument, transformReplyNumber } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +interface ZUnionOptions { + WEIGHTS?: Array; + AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; +} + +export function transformArguments(destination: string, keys: Array | string, options?: ZUnionOptions): Array { + const args = pushVerdictArgument(['ZUNIONSTORE', destination], keys); + + if (options?.WEIGHTS) { + args.push('WEIGHTS', ...options.WEIGHTS.map(weight => weight.toString())); + } + + if (options?.AGGREGATE) { + args.push('AGGREGATE', options.AGGREGATE); + } + + return args; +} + +export const transformReply = transformReplyNumber; diff --git a/lib/commands/ZUNION_WITHSCORES.spec.ts b/lib/commands/ZUNION_WITHSCORES.spec.ts new file mode 100644 index 0000000000..d9c65ba5e4 --- /dev/null +++ b/lib/commands/ZUNION_WITHSCORES.spec.ts @@ -0,0 +1,48 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient, describeHandleMinimumRedisVersion } from '../test-utils'; +import { transformArguments } from './ZUNION_WITHSCORES'; + +describe('ZUNION WITHSCORES', () => { + describeHandleMinimumRedisVersion([6, 2]); + + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + transformArguments('key'), + ['ZUNION', '1', 'key', 'WITHSCORES'] + ); + }); + + it('keys (array)', () => { + assert.deepEqual( + transformArguments(['1', '2']), + ['ZUNION', '2', '1', '2', 'WITHSCORES'] + ); + }); + + it('with WEIGHTS', () => { + assert.deepEqual( + transformArguments('key', { + WEIGHTS: [1] + }), + ['ZUNION', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES'] + ); + }); + + it('with AGGREGATE', () => { + assert.deepEqual( + transformArguments('key', { + AGGREGATE: 'SUM' + }), + ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES'] + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zUnionWithScores', async client => { + assert.deepEqual( + await client.zUnionWithScores('key'), + [] + ); + }); +}); diff --git a/lib/commands/ZUNION_WITHSCORES.ts b/lib/commands/ZUNION_WITHSCORES.ts new file mode 100644 index 0000000000..d0cef45cfb --- /dev/null +++ b/lib/commands/ZUNION_WITHSCORES.ts @@ -0,0 +1,13 @@ +import { transformReplySortedSetWithScores } from './generic-transformers'; +import { transformArguments as transformZUnionArguments } from './ZUNION'; + +export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZUNION'; + +export function transformArguments(...args: Parameters): Array { + return [ + ...transformZUnionArguments(...args), + 'WITHSCORES' + ]; +} + +export const transformReply = transformReplySortedSetWithScores; diff --git a/lib/commands/generic-transformers.spec.ts b/lib/commands/generic-transformers.spec.ts new file mode 100644 index 0000000000..5335255f91 --- /dev/null +++ b/lib/commands/generic-transformers.spec.ts @@ -0,0 +1,623 @@ +import { strict as assert } from 'assert'; +import { isObject } from 'util'; +import { + transformReplyBoolean, + transformReplyBooleanArray, + pushScanArguments, + transformReplyNumberInfinity, + transformReplyNumberInfinityArray, + transformReplyNumberInfinityNull, + transformArgumentNumberInfinity, + transformReplyTuples, + transformReplyStreamMessages, + transformReplyStreamsMessages, + transformReplySortedSetWithScores, + pushGeoCountArgument, + pushGeoSearchArguments, + GeoReplyWith, + transformGeoMembersWithReply, + transformEXAT, + transformPXAT, + pushEvalArguments, + pushStringTuplesArguments, + pushVerdictArguments, + pushVerdictArgument, + pushOptionalVerdictArgument +} from './generic-transformers'; + +describe('Generic Transformers', () => { + describe('transformReplyBoolean', () => { + it('0', () => { + assert.equal( + transformReplyBoolean(0), + false + ); + }); + + it('1', () => { + assert.equal( + transformReplyBoolean(1), + true + ); + }); + }); + + describe('transformReplyBooleanArray', () => { + it('empty array', () => { + assert.deepEqual( + transformReplyBooleanArray([]), + [] + ); + }); + + it('0, 1', () => { + assert.deepEqual( + transformReplyBooleanArray([0, 1]), + [false, true] + ); + }); + }); + + describe('pushScanArguments', () => { + it('cusror only', () => { + assert.deepEqual( + pushScanArguments([], 0), + ['0'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + pushScanArguments([], 0, { + MATCH: 'pattern' + }), + ['0', 'MATCH', 'pattern'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + pushScanArguments([], 0, { + COUNT: 1 + }), + ['0', 'COUNT', '1'] + ); + }); + + it('with MATCH & COUNT', () => { + assert.deepEqual( + pushScanArguments([], 0, { + MATCH: 'pattern', + COUNT: 1 + }), + ['0', 'MATCH', 'pattern', 'COUNT', '1'] + ); + }); + }); + + describe('transformReplyNumberInfinity', () => { + it('0.5', () => { + assert.equal( + transformReplyNumberInfinity('0.5'), + 0.5 + ); + }); + + it('+inf', () => { + assert.equal( + transformReplyNumberInfinity('+inf'), + Infinity + ); + }); + + it('-inf', () => { + assert.equal( + transformReplyNumberInfinity('-inf'), + -Infinity + ); + }); + }); + + describe('transformReplyNumberInfinityArray', () => { + it('empty array', () => { + assert.deepEqual( + transformReplyNumberInfinityArray([]), + [] + ); + }); + + it('0.5, +inf, -inf', () => { + assert.deepEqual( + transformReplyNumberInfinityArray(['0.5', '+inf', '-inf']), + [0.5, Infinity, -Infinity] + ); + }); + }); + + describe('transformReplyNumberInfinityNull', () => { + it('null', () => { + assert.equal( + transformReplyNumberInfinityNull(null), + null + ); + }); + + it('1', () => { + assert.equal( + transformReplyNumberInfinityNull('1'), + 1 + ); + }); + }); + + describe('transformArgumentNumberInfinity', () => { + it('0.5', () => { + assert.equal( + transformArgumentNumberInfinity(0.5), + '0.5' + ); + }); + + it('Infinity', () => { + assert.equal( + transformArgumentNumberInfinity(Infinity), + '+inf' + ); + }); + + it('-Infinity', () => { + assert.equal( + transformArgumentNumberInfinity(-Infinity), + '-inf' + ); + }); + }); + + it('transformReplyTuples', () => { + assert.deepEqual( + transformReplyTuples(['key1', 'value1', 'key2', 'value2']), + Object.create(null, { + key1: { + value: 'value1', + configurable: true, + enumerable: true + }, + key2: { + value: 'value2', + configurable: true, + enumerable: true + } + }) + ); + }); + + it('transformReplyStreamMessages', () => { + assert.deepEqual( + transformReplyStreamMessages([['0-0', ['0key', '0value']], ['1-0', ['1key', '1value']]]), + [{ + id: '0-0', + message: Object.create(null, { + '0key': { + value: '0value', + configurable: true, + enumerable: true + } + }) + }, { + id: '1-0', + message: Object.create(null, { + '1key': { + value: '1value', + configurable: true, + enumerable: true + } + }) + }] + ); + }); + + describe('transformReplyStreamsMessages', () => { + it('null', () => { + assert.equal( + transformReplyStreamsMessages(null), + null + ); + }); + + it('with messages', () => { + assert.deepEqual( + transformReplyStreamsMessages([['stream1', [['0-1', ['11key', '11value']], ['1-1', ['12key', '12value']]]], ['stream2', [['0-2', ['2key1', '2value1', '2key2', '2value2']]]]]), + [{ + name: 'stream1', + messages: [{ + id: '0-1', + message: Object.create(null, { + '11key': { + value: '11value', + configurable: true, + enumerable: true + } + }) + }, { + id: '1-1', + message: Object.create(null, { + '12key': { + value: '12value', + configurable: true, + enumerable: true + } + }) + }] + }, { + name: 'stream2', + messages: [{ + id: '0-2', + message: Object.create(null, { + '2key1': { + value: '2value1', + configurable: true, + enumerable: true + }, + '2key2': { + value: '2value2', + configurable: true, + enumerable: true + } + }) + }] + }] + ) + }); + }); + + it('transformReplySortedSetWithScores', () => { + assert.deepEqual( + transformReplySortedSetWithScores(['member1', '0.5', 'member2', '+inf', 'member3', '-inf']), + [{ + value: 'member1', + score: 0.5 + }, { + value: 'member2', + score: Infinity + }, { + value: 'member3', + score: -Infinity + }] + ); + }); + + describe('pushGeoCountArgument', () => { + it('undefined', () => { + assert.deepEqual( + pushGeoCountArgument([], undefined), + [] + ); + }); + + it('number', () => { + assert.deepEqual( + pushGeoCountArgument([], 1), + ['COUNT', '1'] + ); + }); + + it('with ANY', () => { + assert.deepEqual( + pushGeoCountArgument([], { + value: 1, + ANY: true + }), + ['COUNT', '1', 'ANY'] + ); + }); + }); + + describe('pushGeoSearchArguments', () => { + it('FROMMEMBER, BYRADIUS', () => { + assert.deepEqual( + pushGeoSearchArguments([], 'key', 'member', { + radius: 1, + unit: 'm' + }), + ['key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm'] + ); + }); + + it('FROMLONLAT, BYBOX', () => { + assert.deepEqual( + pushGeoSearchArguments([], 'key', { + longitude: 1, + latitude: 2 + }, { + width: 1, + height: 2, + unit: 'm' + }), + ['key', 'FROMLONLAT', '1', '2', 'BYBOX', '1', '2', 'm'] + ); + }); + + it('with SORT', () => { + assert.deepEqual( + pushGeoSearchArguments([], 'key', 'member', { + radius: 1, + unit: 'm' + }, { + SORT: 'ASC' + }), + ['key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC'] + ); + }); + }); + + describe('transformGeoMembersWithReply', () => { + it('DISTANCE', () => { + assert.deepEqual( + transformGeoMembersWithReply([ + [ + '1', + '2' + ], + [ + '3', + '4' + ] + ], [GeoReplyWith.DISTANCE]), + [{ + member: '1', + distance: '2' + }, { + member: '3', + distance: '4' + }] + ); + }); + + it('HASH', () => { + assert.deepEqual( + transformGeoMembersWithReply([ + [ + '1', + 2 + ], + [ + '3', + 4 + ] + ], [GeoReplyWith.HASH]), + [{ + member: '1', + hash: 2 + }, { + member: '3', + hash: 4 + }] + ); + }); + + it('COORDINATES', () => { + assert.deepEqual( + transformGeoMembersWithReply([ + [ + '1', + [ + '2', + '3' + ] + ], + [ + '4', + [ + '5', + '6' + ] + ] + ], [GeoReplyWith.COORDINATES]), + [{ + member: '1', + coordinates: { + longitude: '2', + latitude: '3' + } + }, { + member: '4', + coordinates: { + longitude: '5', + latitude: '6' + } + }] + ); + }); + + it('DISTANCE, HASH, COORDINATES', () => { + assert.deepEqual( + transformGeoMembersWithReply([ + [ + '1', + '2', + 3, + [ + '4', + '5' + ] + ], + [ + '6', + '7', + 8, + [ + '9', + '10' + ] + ] + ], [GeoReplyWith.DISTANCE, GeoReplyWith.HASH, GeoReplyWith.COORDINATES]), + [{ + member: '1', + distance: '2', + hash: 3, + coordinates: { + longitude: '4', + latitude: '5' + } + }, { + member: '6', + distance: '7', + hash: 8, + coordinates: { + longitude: '9', + latitude: '10' + } + }] + ); + }); + }); + + describe('transformEXAT', () => { + it('number', () => { + assert.equal( + transformEXAT(1), + '1' + ); + }); + + it('date', () => { + const d = new Date(); + assert.equal( + transformEXAT(d), + Math.floor(d.getTime() / 1000).toString() + ); + }); + }); + + describe('transformPXAT', () => { + it('number', () => { + assert.equal( + transformPXAT(1), + '1' + ); + }); + + it('date', () => { + const d = new Date(); + assert.equal( + transformPXAT(d), + d.getTime().toString() + ); + }); + }); + + describe('pushEvalArguments', () => { + it('empty', () => { + assert.deepEqual( + pushEvalArguments([]), + ['0'] + ); + }); + + it('with keys', () => { + assert.deepEqual( + pushEvalArguments([], { + keys: ['key'] + }), + ['1', 'key'] + ); + }); + + it('with arguments', () => { + assert.deepEqual( + pushEvalArguments([], { + arguments: ['argument'] + }), + ['0', 'argument'] + ); + }); + + it('with keys and arguments', () => { + assert.deepEqual( + pushEvalArguments([], { + keys: ['key'], + arguments: ['argument'] + }), + ['1', 'key', 'argument'] + ); + }); + }); + + describe('pushStringTuplesArguments', () => { + it("['key1', 'value1', 'key2', 'value2']", () => { + assert.deepEqual( + pushStringTuplesArguments([], ['key1', 'value1', 'key2', 'value2']), + ['key1', 'value1', 'key2', 'value2'] + ); + }); + + it("[['key1', 'value1'], ['key2', 'value2']]", () => { + assert.deepEqual( + pushStringTuplesArguments([], [['key1', 'value1'], ['key2', 'value2']]), + ['key1', 'value1', 'key2', 'value2'] + ); + }); + + it("{key1: 'value1'. key2: 'value2'}", () => { + assert.deepEqual( + pushStringTuplesArguments([], { key1: 'value1', key2: 'value2' }), + ['key1', 'value1', 'key2', 'value2'] + ); + }); + }); + + describe('pushVerdictArguments', () => { + it('string', () => { + assert.deepEqual( + pushVerdictArguments([], 'string'), + ['string'] + ); + }); + + it('array', () => { + assert.deepEqual( + pushVerdictArguments([], ['1', '2']), + ['1', '2'] + ); + }); + }); + + describe('pushVerdictArgument', () => { + it('string', () => { + assert.deepEqual( + pushVerdictArgument([], 'string'), + ['1', 'string'] + ); + }); + + it('array', () => { + assert.deepEqual( + pushVerdictArgument([], ['1', '2']), + ['2', '1', '2'] + ); + }); + }); + + describe('pushOptionalVerdictArgument', () => { + it('undefined', () => { + assert.deepEqual( + pushOptionalVerdictArgument([], 'name', undefined), + [] + ); + }); + + it('string', () => { + assert.deepEqual( + pushOptionalVerdictArgument([], 'name', 'string'), + ['name', '1', 'string'] + ); + }); + + it('array', () => { + assert.deepEqual( + pushOptionalVerdictArgument([], 'name', ['1', '2']), + ['name', '2', '1', '2'] + ); + }); + }); +}); diff --git a/lib/commands/generic-transformers.ts b/lib/commands/generic-transformers.ts new file mode 100644 index 0000000000..8105bfe903 --- /dev/null +++ b/lib/commands/generic-transformers.ts @@ -0,0 +1,381 @@ +import { TransformArgumentsReply } from '.'; + +export function transformReplyNumber(reply: number): number { + return reply; +} + +export function transformReplyNumberNull(reply: number | null): number | null { + return reply; +} + +export function transformReplyNumberArray(reply: Array): Array { + return reply; +} + +export function transformReplyNumberNullArray(reply: Array): Array { + return reply; +} + +export function transformReplyString(reply: string): string { + return reply; +} + +export function transformReplyStringNull(reply: string | null): string | null { + return reply; +} + +export function transformReplyStringArray(reply: Array): Array { + return reply; +} + +export function transformReplyStringArrayNull(reply: Array | null): Array | null { + return reply; +} + +export function transformReplyStringNullArray(reply: Array): Array { + return reply; +} + +export function transformReplyBoolean(reply: number): boolean { + return reply === 1; +} + +export function transformReplyBooleanArray(reply: Array): Array { + return reply.map(transformReplyBoolean); +} + +export type BitValue = 0 | 1; + +export function transformReplyBit(reply: BitValue): BitValue { + return reply; +} + +export function transformReplyVoid(): void {} + +export interface ScanOptions { + MATCH?: string; + COUNT?: number; +} + +export function pushScanArguments(args: Array, cursor: number, options?: ScanOptions): Array { + args.push(cursor.toString()); + + if (options?.MATCH) { + args.push('MATCH', options.MATCH); + } + + if (options?.COUNT) { + args.push('COUNT', options.COUNT.toString()); + } + + return args; +} + +export function transformReplyNumberInfinity(reply: string): number { + switch (reply) { + case '+inf': + return Infinity; + + case '-inf': + return -Infinity; + + default: + return Number(reply); + } +} + +export function transformReplyNumberInfinityArray(reply: Array): Array { + return reply.map(transformReplyNumberInfinity); +} + +export function transformReplyNumberInfinityNull(reply: string | null): number | null { + if (reply === null) return null; + + return transformReplyNumberInfinity(reply); +} + +export function transformReplyNumberInfinityNullArray(reply: Array): Array { + return reply.map(transformReplyNumberInfinityNull); +} + +export function transformArgumentNumberInfinity(num: number): string { + switch (num) { + case Infinity: + return '+inf'; + + case -Infinity: + return '-inf'; + + default: + return num.toString(); + } +} + +export interface TuplesObject { + [field: string]: string; +} + +export function transformReplyTuples(reply: Array): TuplesObject { + const message = Object.create(null); + + for (let i = 0; i < reply.length; i += 2) { + message[reply[i]] = reply[i + 1]; + } + + return message; +} + +export interface StreamMessageReply { + id: string; + message: TuplesObject; +} + +export type StreamMessagesReply = Array; + +export function transformReplyStreamMessages(reply: Array): StreamMessagesReply { + const messages = []; + + for (const [id, message] of reply) { + messages.push({ + id, + message: transformReplyTuples(message) + }); + } + + return messages; +} + +export type StreamsMessagesReply = Array<{ + name: string; + messages: StreamMessagesReply; +}> | null; + +export function transformReplyStreamsMessages(reply: Array | null): StreamsMessagesReply | null { + if (reply === null) return null; + + return reply.map(([name, rawMessages]) => ({ + name, + messages: transformReplyStreamMessages(rawMessages) + })); +} + +export interface ZMember { + score: number; + value: string; +} + +export function transformReplySortedSetWithScores(reply: Array): Array { + const members = []; + + for (let i = 0; i < reply.length; i += 2) { + members.push({ + value: reply[i], + score: transformReplyNumberInfinity(reply[i + 1]) + }); + } + + return members; +} + +type GeoCountArgument = number | { + value: number; + ANY?: true +}; + +export function pushGeoCountArgument(args: Array, count: GeoCountArgument | undefined): Array { + if (typeof count === 'number') { + args.push('COUNT', count.toString()); + } else if (count) { + args.push('COUNT', count.value.toString()); + + if (count.ANY) { + args.push('ANY'); + } + } + + return args; +} + +export type GeoUnits = 'm' | 'km' | 'mi' | 'ft'; + +export interface GeoCoordinates { + longitude: string | number; + latitude: string | number; +} + +type GeoSearchFromMember = string; + +export type GeoSearchFrom = GeoSearchFromMember | GeoCoordinates; + +interface GeoSearchByRadius { + radius: number; + unit: GeoUnits; +} + +interface GeoSearchByBox { + width: number; + height: number; + unit: GeoUnits; +} + +export type GeoSearchBy = GeoSearchByRadius | GeoSearchByBox; + +export interface GeoSearchOptions { + SORT?: 'ASC' | 'DESC'; + COUNT?: GeoCountArgument; +} + +export function pushGeoSearchArguments( + args: Array, + key: string, + from: GeoSearchFrom, + by: GeoSearchBy, + options?: GeoSearchOptions +): Array { + args.push(key); + + if (typeof from === 'string') { + args.push('FROMMEMBER', from); + } else { + args.push('FROMLONLAT', from.longitude.toString(), from.latitude.toString()); + } + + if ('radius' in by) { + args.push('BYRADIUS', by.radius.toString()); + } else { + args.push('BYBOX', by.width.toString(), by.height.toString()); + } + + if (by.unit) { + args.push(by.unit); + } + + if (options?.SORT) { + args.push(options?.SORT); + } + + pushGeoCountArgument(args, options?.COUNT); + + return args; +} + +export enum GeoReplyWith { + DISTANCE = 'WITHDIST', + HASH = 'WITHHASH', + COORDINATES = 'WITHCOORD' +} + +export interface GeoReplyWithMember { + member: string; + distance?: number; + hash?: string; + coordinates?: { + longitude: string; + latitude: string; + }; +} + +export function transformGeoMembersWithReply(reply: Array>, replyWith: Array): Array { + const replyWithSet = new Set(replyWith); + + let index = 0; + const distanceIndex = replyWithSet.has(GeoReplyWith.DISTANCE) && ++index, + hashIndex = replyWithSet.has(GeoReplyWith.HASH) && ++index, + coordinatesIndex = replyWithSet.has(GeoReplyWith.COORDINATES) && ++index; + + return reply.map(member => { + const transformedMember: GeoReplyWithMember = { + member: member[0] + }; + + if (distanceIndex) { + transformedMember.distance = member[distanceIndex]; + } + + if (hashIndex) { + transformedMember.hash = member[hashIndex]; + } + + if (coordinatesIndex) { + const [longitude, latitude] = member[coordinatesIndex]; + transformedMember.coordinates = { + longitude, + latitude + }; + } + + return transformedMember; + }); +} + +export function transformEXAT(EXAT: number | Date): string { + return (typeof EXAT === 'number' ? EXAT : Math.floor(EXAT.getTime() / 1000)).toString(); +} + +export function transformPXAT(PXAT: number | Date): string { + return (typeof PXAT === 'number' ? PXAT : PXAT.getTime()).toString(); +} + +export interface EvalOptions { + keys?: Array; + arguments?: Array; +} + +export function pushEvalArguments(args: Array, options?: EvalOptions): Array { + if (options?.keys) { + args.push( + options.keys.length.toString(), + ...options.keys + ); + } else { + args.push('0'); + } + + if (options?.arguments) { + args.push(...options.arguments); + } + + return args; +} + +export type StringTuplesArguments = Array<[string, string]> | Array | Record; + +export function pushStringTuplesArguments(args: Array, tuples: StringTuplesArguments): Array { + if (Array.isArray(tuples)) { + args.push(...tuples.flat()); + } else { + for (const key of Object.keys(tuples)) { + args.push(key, tuples[key]); + } + } + + return args; +} + +export function pushVerdictArguments(args: TransformArgumentsReply, value: string | Array): TransformArgumentsReply { + if (typeof value === 'string') { + args.push(value); + } else { + args.push(...value); + } + + return args; +} + +export function pushVerdictArgument(args: TransformArgumentsReply, value: string | Array): TransformArgumentsReply { + if (typeof value === 'string') { + args.push('1', value); + } else { + args.push(value.length.toString(), ...value); + } + + return args; +} + +export function pushOptionalVerdictArgument(args: TransformArgumentsReply, name: string, value: undefined | string | Array): TransformArgumentsReply { + if (value === undefined) return args; + + args.push(name); + + return pushVerdictArgument(args, value); +} diff --git a/lib/commands/index.ts b/lib/commands/index.ts new file mode 100644 index 0000000000..cffb47c668 --- /dev/null +++ b/lib/commands/index.ts @@ -0,0 +1,755 @@ +import * as ACL_CAT from './ACL_CAT'; +import * as ACL_DELUSER from './ACL_DELUSER'; +import * as ACL_GENPASS from './ACL_GENPASS'; +import * as ACL_GETUSER from './ACL_GETUSER'; +import * as ACL_LIST from './ACL_LIST'; +import * as ACL_LOAD from './ACL_LOAD'; +import * as ACL_LOG_RESET from './ACL_LOG_RESET'; +import * as ACL_LOG from './ACL_LOG'; +import * as ACL_SAVE from './ACL_SAVE'; +import * as ACL_SETUSER from './ACL_SETUSER'; +import * as ACL_USERS from './ACL_USERS'; +import * as ACL_WHOAMI from './ACL_WHOAMI'; +import * as APPEND from './APPEND'; +import * as ASKING from './ASKING'; +import * as AUTH from './AUTH'; +import * as BGREWRITEAOF from './BGREWRITEAOF'; +import * as BGSAVE from './BGSAVE'; +import * as BITCOUNT from './BITCOUNT'; +import * as BITFIELD from './BITFIELD'; +import * as BITOP from './BITOP'; +import * as BITPOS from './BITPOS'; +import * as BLMOVE from './BLMOVE'; +import * as BLPOP from './BLPOP'; +import * as BRPOP from './BRPOP'; +import * as BRPOPLPUSH from './BRPOPLPUSH'; +import * as BZPOPMAX from './BZPOPMAX'; +import * as BZPOPMIN from './BZPOPMIN'; +import * as CLIENT_ID from './CLIENT_ID'; +import * as CLIENT_INFO from './CLIENT_INFO'; +import * as CLUSTER_ADDSLOTS from './CLUSTER_ADDSLOTS'; +import * as CLUSTER_FLUSHSLOTS from './CLUSTER_FLUSHSLOTS'; +import * as CLUSTER_INFO from './CLUSTER_INFO'; +import * as CLUSTER_NODES from './CLUSTER_NODES'; +import * as CLUSTER_MEET from './CLUSTER_MEET'; +import * as CLUSTER_RESET from './CLUSTER_RESET'; +import * as CLUSTER_SETSLOT from './CLUSTER_SETSLOT'; +import * as CONFIG_GET from './CONFIG_GET'; +import * as CONFIG_RESETASTAT from './CONFIG_RESETSTAT'; +import * as CONFIG_REWRITE from './CONFIG_REWRITE'; +import * as CONFIG_SET from './CONFIG_SET'; +import * as COPY from './COPY'; +import * as DBSIZE from './DBSIZE'; +import * as DECR from './DECR'; +import * as DECRBY from './DECRBY'; +import * as DEL from './DEL'; +import * as DISCARD from './DISCARD'; +import * as DUMP from './DUMP'; +import * as ECHO from './ECHO'; +import * as EVAL from './EVAL'; +import * as EVALSHA from './EVALSHA'; +import * as EXISTS from './EXISTS'; +import * as EXPIRE from './EXPIRE'; +import * as EXPIREAT from './EXPIREAT'; +import * as FAILOVER from './FAILOVER'; +import * as FLUSHALL from './FLUSHALL'; +import * as FLUSHDB from './FLUSHDB'; +import * as GEOADD from './GEOADD'; +import * as GEODIST from './GEODIST'; +import * as GEOHASH from './GEOHASH'; +import * as GEOPOS from './GEOPOS'; +import * as GEOSEARCH_WITH from './GEOSEARCH_WITH'; +import * as GEOSEARCH from './GEOSEARCH'; +import * as GEOSEARCHSTORE from './GEOSEARCHSTORE'; +import * as GET from './GET'; +import * as GETBIT from './GETBIT'; +import * as GETDEL from './GETDEL'; +import * as GETEX from './GETEX'; +import * as GETRANGE from './GETRANGE'; +import * as GETSET from './GETSET'; +import * as HDEL from './HDEL'; +import * as HELLO from './HELLO'; +import * as HEXISTS from './HEXISTS'; +import * as HGET from './HGET'; +import * as HGETALL from './HGETALL'; +import * as HINCRBY from './HINCRBY'; +import * as HINCRBYFLOAT from './HINCRBYFLOAT'; +import * as HKEYS from './HKEYS'; +import * as HLEN from './HLEN'; +import * as HMGET from './HMGET'; +import * as HRANDFIELD_COUNT_WITHVALUES from './HRANDFIELD_COUNT_WITHVALUES'; +import * as HRANDFIELD_COUNT from './HRANDFIELD_COUNT'; +import * as HRANDFIELD from './HRANDFIELD'; +import * as HSCAN from './HSCAN'; +import * as HSET from './HSET'; +import * as HSETNX from './HSETNX'; +import * as HSTRLEN from './HSTRLEN'; +import * as HVALS from './HVALS'; +import * as INCR from './INCR'; +import * as INCRBY from './INCRBY'; +import * as INCRBYFLOAT from './INCRBYFLOAT'; +import * as INFO from './INFO'; +import * as KEYS from './KEYS'; +import * as LASTSAVE from './LASTSAVE'; +import * as LINDEX from './LINDEX'; +import * as LINSERT from './LINSERT'; +import * as LLEN from './LLEN'; +import * as LMOVE from './LMOVE'; +import * as LOLWUT from './LOLWUT'; +import * as LPOP_COUNT from './LPOP_COUNT'; +import * as LPOP from './LPOP'; +import * as LPOS_COUNT from './LPOS_COUNT'; +import * as LPOS from './LPOS'; +import * as LPUSH from './LPUSH'; +import * as LPUSHX from './LPUSHX'; +import * as LRANGE from './LRANGE'; +import * as LREM from './LREM'; +import * as LSET from './LSET'; +import * as LTRIM from './LTRIM'; +import * as MEMOERY_DOCTOR from './MEMORY_DOCTOR'; +import * as MEMORY_MALLOC_STATS from './MEMORY_MALLOC-STATS'; +import * as MEMORY_PURGE from './MEMORY_PURGE'; +import * as MEMORY_STATS from './MEMORY_STATS'; +import * as MEMORY_USAGE from './MEMORY_USAGE'; +import * as MGET from './MGET'; +import * as MIGRATE from './MIGRATE'; +import * as MODULE_LIST from './MODULE_LIST'; +import * as MODULE_LOAD from './MODULE_LOAD'; +import * as MODULE_UNLOAD from './MODULE_UNLOAD'; +import * as MOVE from './MOVE'; +import * as MSET from './MSET'; +import * as MSETNX from './MSETNX'; +import * as PERSIST from './PERSIST'; +import * as PEXPIRE from './PEXPIRE'; +import * as PEXPIREAT from './PEXPIREAT'; +import * as PFADD from './PFADD'; +import * as PFCOUNT from './PFCOUNT'; +import * as PFMERGE from './PFMERGE'; +import * as PING from './PING'; +import * as PSETEX from './PSETEX'; +import * as PTTL from './PTTL'; +import * as PUBLISH from './PUBLISH'; +import * as PUBSUB_CHANNELS from './PUBSUB_CHANNELS'; +import * as PUBSUB_NUMPAT from './PUBSUB_NUMPAT'; +import * as PUBSUB_NUMSUB from './PUBSUB_NUMSUB'; +import * as RANDOMKEY from './RANDOMKEY'; +import * as READONLY from './READONLY'; +import * as READWRITE from './READWRITE'; +import * as RENAME from './RENAME'; +import * as RENAMENX from './RENAMENX'; +import * as REPLICAOF from './REPLICAOF'; +import * as RESTORE_ASKING from './RESTORE-ASKING'; +import * as ROLE from './ROLE'; +import * as RPOP_COUNT from './RPOP_COUNT'; +import * as RPOP from './RPOP'; +import * as RPOPLPUSH from './RPOPLPUSH'; +import * as RPUSH from './RPUSH'; +import * as RPUSHX from './RPUSHX'; +import * as SADD from './SADD'; +import * as SAVE from './SAVE'; +import * as SCAN from './SCAN'; +import * as SCARD from './SCARD'; +import * as SCRIPT_DEBUG from './SCRIPT_DEBUG'; +import * as SCRIPT_EXISTS from './SCRIPT_EXISTS'; +import * as SCRIPT_FLUSH from './SCRIPT_FLUSH'; +import * as SCRIPT_KILL from './SCRIPT_KILL'; +import * as SCRIPT_LOAD from './SCRIPT_LOAD'; +import * as SDIFF from './SDIFF'; +import * as SDIFFSTORE from './SDIFFSTORE'; +import * as SET from './SET'; +import * as SETBIT from './SETBIT'; +import * as SETEX from './SETEX'; +import * as SETNX from './SETNX'; +import * as SETRANGE from './SETRANGE'; +import * as SHUTDOWN from './SHUTDOWN'; +import * as SINTER from './SINTER'; +import * as SINTERSTORE from './SINTERSTORE'; +import * as SISMEMBER from './SISMEMBER'; +import * as SMEMBERS from './SMEMBERS'; +import * as SMISMEMBER from './SMISMEMBER'; +import * as SMOVE from './SMOVE'; +import * as SORT from './SORT'; +import * as SPOP from './SPOP'; +import * as SRANDMEMBER_COUNT from './SRANDMEMBER_COUNT'; +import * as SRANDMEMBER from './SRANDMEMBER'; +import * as SREM from './SREM'; +import * as SSCAN from './SSCAN'; +import * as STRLEN from './STRLEN'; +import * as SUNION from './SUNION'; +import * as SUNIONSTORE from './SUNIONSTORE'; +import * as SWAPDB from './SWAPDB'; +import * as TIME from './TIME'; +import * as TOUCH from './TOUCH'; +import * as TTL from './TTL'; +import * as TYPE from './TYPE'; +import * as UNLINK from './UNLINK'; +import * as UNWATCH from './UNWATCH'; +import * as WAIT from './WAIT'; +import * as WATCH from './WATCH'; +import * as XACK from './XACK'; +import * as XADD from './XADD'; +import * as XAUTOCLAIM_JUSTID from './XAUTOCLAIM_JUSTID'; +import * as XAUTOCLAIM from './XAUTOCLAIM'; +import * as XCLAIM from './XCLAIM'; +import * as XCLAIM_JUSTID from './XCLAIM_JUSTID'; +import * as XDEL from './XDEL'; +import * as XGROUP_CREATE from './XGROUP_CREATE'; +import * as XGROUP_CREATECONSUMER from './XGROUP_CREATECONSUMER'; +import * as XGROUP_DELCONSUMER from './XGROUP_DELCONSUMER'; +import * as XGROUP_DESTROY from './XGROUP_DESTROY'; +import * as XGROUP_SETID from './XGROUP_SETID'; +import * as XINFO_CONSUMERS from './XINFO_CONSUMERS'; +import * as XINFO_GROUPS from './XINFO_GROUPS'; +import * as XINFO_STREAM from './XINFO_STREAM'; +import * as XLEN from './XLEN'; +import * as XPENDING_RANGE from './XPENDING_RANGE'; +import * as XPENDING from './XPENDING'; +import * as XRANGE from './XRANGE'; +import * as XREAD from './XREAD'; +import * as XREADGROUP from './XREADGROUP'; +import * as XREVRANGE from './XREVRANGE'; +import * as XTRIM from './XTRIM'; +import * as ZADD from './ZADD'; +import * as ZCARD from './ZCARD'; +import * as ZCOUNT from './ZCOUNT'; +import * as ZDIFF_WITHSCORES from './ZDIFF_WITHSCORES'; +import * as ZDIFF from './ZDIFF'; +import * as ZDIFFSTORE from './ZDIFFSTORE'; +import * as ZINCRBY from './ZINCRBY'; +import * as ZINTER_WITHSCORES from './ZINTER_WITHSCORES'; +import * as ZINTER from './ZINTER'; +import * as ZINTERSTORE from './ZINTERSTORE'; +import * as ZLEXCOUNT from './ZLEXCOUNT'; +import * as ZMSCORE from './ZMSCORE'; +import * as ZPOPMAX_COUNT from './ZPOPMAX_COUNT'; +import * as ZPOPMAX from './ZPOPMAX'; +import * as ZPOPMIN_COUNT from './ZPOPMIN_COUNT'; +import * as ZPOPMIN from './ZPOPMIN'; +import * as ZRANDMEMBER_COUNT_WITHSCORES from './ZRANDMEMBER_COUNT_WITHSCORES'; +import * as ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; +import * as ZRANDMEMBER from './ZRANDMEMBER'; +import * as ZRANGE_WITHSCORES from './ZRANGE_WITHSCORES'; +import * as ZRANGE from './ZRANGE'; +import * as ZRANGESTORE from './ZRANGESTORE'; +import * as ZRANK from './ZRANK'; +import * as ZREM from './ZREM'; +import * as ZREMRANGEBYLEX from './ZREMRANGEBYLEX'; +import * as ZREMRANGEBYRANK from './ZREMRANGEBYRANK'; +import * as ZREMRANGEBYSCORE from './ZREMRANGEBYSCORE'; +import * as ZREVRANK from './ZREVRANK'; +import * as ZSCAN from './ZSCAN'; +import * as ZSCORE from './ZSCORE'; +import * as ZUNION_WITHSCORES from './ZUNION_WITHSCORES'; +import * as ZUNION from './ZUNION'; +import * as ZUNIONSTORE from './ZUNIONSTORE'; + +export default { + ACL_CAT, + aclCat: ACL_CAT, + ACL_DELUSER, + aclDelUser: ACL_DELUSER, + ACL_GENPASS, + aclGenPass: ACL_GENPASS, + ACL_GETUSER, + aclGetUser: ACL_GETUSER, + ACL_LIST, + aclList: ACL_LIST, + ACL_LOAD, + aclLoad: ACL_LOAD, + ACL_LOG_RESET, + aclLogReset: ACL_LOG_RESET, + ACL_LOG, + aclLog: ACL_LOG, + ACL_SAVE, + aclSave: ACL_SAVE, + ACL_SETUSER, + aclSetUser: ACL_SETUSER, + ACL_USERS, + aclUsers: ACL_USERS, + ACL_WHOAMI, + aclWhoAmI: ACL_WHOAMI, + APPEND, + append: APPEND, + ASKING, + asking: ASKING, + AUTH, + auth: AUTH, + BGREWRITEAOF, + bgRewriteAof: BGREWRITEAOF, + BGSAVE, + bgSave: BGSAVE, + BITCOUNT, + bitCount: BITCOUNT, + BITFIELD, + bitField: BITFIELD, + BITOP, + bitOp: BITOP, + BITPOS, + bitPos: BITPOS, + BLMOVE, + blMove: BLMOVE, + BLPOP, + blPop: BLPOP, + BRPOP, + brPop: BRPOP, + BRPOPLPUSH, + brPopLPush: BRPOPLPUSH, + BZPOPMAX, + bzPopMax: BZPOPMAX, + BZPOPMIN, + bzPopMin: BZPOPMIN, + CLIENT_ID, + clientId: CLIENT_ID, + CLIENT_INFO, + clientInfo: CLIENT_INFO, + CLUSTER_ADDSLOTS, + clusterAddSlots: CLUSTER_ADDSLOTS, + CLUSTER_FLUSHSLOTS, + clusterFlushSlots: CLUSTER_FLUSHSLOTS, + CLUSTER_INFO, + clusterInfo: CLUSTER_INFO, + CLUSTER_NODES, + clusterNodes: CLUSTER_NODES, + CLUSTER_MEET, + clusterMeet: CLUSTER_MEET, + CLUSTER_RESET, + clusterReset: CLUSTER_RESET, + CLUSTER_SETSLOT, + clusterSetSlot: CLUSTER_SETSLOT, + CONFIG_GET, + configGet: CONFIG_GET, + CONFIG_RESETASTAT, + configResetStat: CONFIG_RESETASTAT, + CONFIG_REWRITE, + configRewrite: CONFIG_REWRITE, + CONFIG_SET, + configSet: CONFIG_SET, + COPY, + copy: COPY, + DBSIZE, + dbSize: DBSIZE, + DECR, + decr: DECR, + DECRBY, + decrBy: DECRBY, + DEL, + del: DEL, + DISCARD, + discard: DISCARD, + DUMP, + dump: DUMP, + ECHO, + echo: ECHO, + EVAL, + eval: EVAL, + EVALSHA, + evalSha: EVALSHA, + EXISTS, + exists: EXISTS, + EXPIRE, + expire: EXPIRE, + EXPIREAT, + expireAt: EXPIREAT, + FAILOVER, + failover: FAILOVER, + FLUSHALL, + flushAll: FLUSHALL, + FLUSHDB, + flushDb: FLUSHDB, + GEOADD, + geoAdd: GEOADD, + GEODIST, + geoDist: GEODIST, + GEOHASH, + geoHash: GEOHASH, + GEOPOS, + geoPos: GEOPOS, + GEOSEARCH_WITH, + geoSearchWith: GEOSEARCH_WITH, + GEOSEARCH, + geoSearch: GEOSEARCH, + GEOSEARCHSTORE, + geoSearchStore: GEOSEARCHSTORE, + GET, + get: GET, + GETBIT, + getBit: GETBIT, + GETDEL, + getDel: GETDEL, + GETEX, + getEx: GETEX, + GETRANGE, + getRange: GETRANGE, + GETSET, + getSet: GETSET, + HDEL, + hDel: HDEL, + HELLO, + hello: HELLO, + HEXISTS, + hExists: HEXISTS, + HGET, + hGet: HGET, + HGETALL, + hGetAll: HGETALL, + HINCRBY, + hIncrBy: HINCRBY, + HINCRBYFLOAT, + hIncrByFloat: HINCRBYFLOAT, + HKEYS, + hKeys: HKEYS, + HLEN, + hLen: HLEN, + HMGET, + hmGet: HMGET, + HRANDFIELD_COUNT_WITHVALUES, + hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES, + HRANDFIELD_COUNT, + hRandFieldCount: HRANDFIELD_COUNT, + HRANDFIELD, + hRandField: HRANDFIELD, + HSCAN, + hScan: HSCAN, + HSET, + hSet: HSET, + HSETNX, + hSetNX: HSETNX, + HSTRLEN, + hStrLen: HSTRLEN, + HVALS, + hVals: HVALS, + INCR, + incr: INCR, + INCRBY, + incrBy: INCRBY, + INCRBYFLOAT, + incrByFloat: INCRBYFLOAT, + INFO, + info: INFO, + KEYS, + keys: KEYS, + LASTSAVE, + lastSave: LASTSAVE, + LINDEX, + lIndex: LINDEX, + LINSERT, + lInsert: LINSERT, + LLEN, + lLen: LLEN, + LMOVE, + lMove: LMOVE, + LOLWUT, + LPOP_COUNT, + lPopCount: LPOP_COUNT, + LPOP, + lPop: LPOP, + LPOS_COUNT, + lPosCount: LPOS_COUNT, + LPOS, + lPos: LPOS, + LPUSH, + lPush: LPUSH, + LPUSHX, + lPushX: LPUSHX, + LRANGE, + lRange: LRANGE, + LREM, + lRem: LREM, + LSET, + lSet: LSET, + LTRIM, + lTrim: LTRIM, + MEMOERY_DOCTOR, + memoryDoctor: MEMOERY_DOCTOR, + 'MEMORY_MALLOC-STATS': MEMORY_MALLOC_STATS, + memoryMallocStats: MEMORY_MALLOC_STATS, + MEMORY_PURGE, + memoryPurge: MEMORY_PURGE, + MEMORY_STATS, + memoryStats: MEMORY_STATS, + MEMORY_USAGE, + memoryUsage: MEMORY_USAGE, + MGET, + mGet: MGET, + MIGRATE, + migrate: MIGRATE, + MODULE_LIST, + moduleList: MODULE_LIST, + MODULE_LOAD, + moduleLoad: MODULE_LOAD, + MODULE_UNLOAD, + moduleUnload: MODULE_UNLOAD, + MOVE, + move: MOVE, + MSET, + mSet: MSET, + MSETNX, + mSetNX: MSETNX, + PERSIST, + persist: PERSIST, + PEXPIRE, + pExpire: PEXPIRE, + PEXPIREAT, + pExpireAt: PEXPIREAT, + PFADD, + pfAdd: PFADD, + PFCOUNT, + pfCount: PFCOUNT, + PFMERGE, + pfMerge: PFMERGE, + PING, + ping: PING, + PSETEX, + pSetEx: PSETEX, + PTTL, + pTTL: PTTL, + PUBLISH, + publish: PUBLISH, + PUBSUB_CHANNELS, + pubSubChannels: PUBSUB_CHANNELS, + PUBSUB_NUMPAT, + pubSubNumPat: PUBSUB_NUMPAT, + PUBSUB_NUMSUB, + pubSubNumSub: PUBSUB_NUMSUB, + RANDOMKEY, + randomKey: RANDOMKEY, + READONLY, + readonly: READONLY, + READWRITE, + readwrite: READWRITE, + RENAME, + rename: RENAME, + RENAMENX, + renameNX: RENAMENX, + REPLICAOF, + replicaOf: REPLICAOF, + 'RESTORE-ASKING': RESTORE_ASKING, + restoreAsking: RESTORE_ASKING, + ROLE, + role: ROLE, + RPOP_COUNT, + rPopCount: RPOP_COUNT, + RPOP, + rPop: RPOP, + RPOPLPUSH, + rPopLPush: RPOPLPUSH, + RPUSH, + rPush: RPUSH, + RPUSHX, + rPushX: RPUSHX, + SADD, + sAdd: SADD, + SAVE, + save: SAVE, + SCAN, + scan: SCAN, + SCARD, + sCard: SCARD, + SCRIPT_DEBUG, + scriptDebug: SCRIPT_DEBUG, + SCRIPT_EXISTS, + scriptExists: SCRIPT_EXISTS, + SCRIPT_FLUSH, + scriptFlush: SCRIPT_FLUSH, + SCRIPT_KILL, + scriptKill: SCRIPT_KILL, + SCRIPT_LOAD, + scriptLoad: SCRIPT_LOAD, + SDIFF, + sDiff: SDIFF, + SDIFFSTORE, + sDiffStore: SDIFFSTORE, + SINTER, + sInter: SINTER, + SINTERSTORE, + sInterStore: SINTERSTORE, + SET, + set: SET, + SETBIT, + setBit: SETBIT, + SETEX, + setEx: SETEX, + SETNX, + setNX: SETNX, + SETRANGE, + setRange: SETRANGE, + SHUTDOWN, + shutdown: SHUTDOWN, + SISMEMBER, + sIsMember: SISMEMBER, + SMEMBERS, + sMembers: SMEMBERS, + SMISMEMBER, + smIsMember: SMISMEMBER, + SMOVE, + sMove: SMOVE, + SORT, + sort: SORT, + SPOP, + sPop: SPOP, + SRANDMEMBER_COUNT, + sRandMemberCount: SRANDMEMBER_COUNT, + SRANDMEMBER, + sRandMember: SRANDMEMBER, + SREM, + sRem: SREM, + SSCAN, + sScan: SSCAN, + STRLEN, + strLen: STRLEN, + SUNION, + sUnion: SUNION, + SUNIONSTORE, + sUnionStore: SUNIONSTORE, + SWAPDB, + swapDb: SWAPDB, + TIME, + time: TIME, + TOUCH, + touch: TOUCH, + TTL, + ttl: TTL, + TYPE, + type: TYPE, + UNLINK, + unlink: UNLINK, + UNWATCH, + unwatch: UNWATCH, + WAIT, + wait: WAIT, + WATCH, + watch: WATCH, + XACK, + xAck: XACK, + XADD, + xAdd: XADD, + XAUTOCLAIM_JUSTID, + xAutoClaimJustId: XAUTOCLAIM_JUSTID, + XAUTOCLAIM, + xAutoClaim: XAUTOCLAIM, + XCLAIM, + xClaim: XCLAIM, + XCLAIM_JUSTID, + xClaimJustId: XCLAIM_JUSTID, + XDEL, + xDel: XDEL, + XGROUP_CREATE, + xGroupCreate: XGROUP_CREATE, + XGROUP_CREATECONSUMER, + xGroupCreateConsumer: XGROUP_CREATECONSUMER, + XGROUP_DELCONSUMER, + xGroupDelConsumer: XGROUP_DELCONSUMER, + XGROUP_DESTROY, + xGroupDestroy: XGROUP_DESTROY, + XGROUP_SETID, + xGroupSetId: XGROUP_SETID, + XINFO_CONSUMERS, + xInfoConsumers: XINFO_CONSUMERS, + XINFO_GROUPS, + xInfoGroups: XINFO_GROUPS, + XINFO_STREAM, + xInfoStream: XINFO_STREAM, + XLEN, + xLen: XLEN, + XPENDING_RANGE, + xPendingRange: XPENDING_RANGE, + XPENDING, + xPending: XPENDING, + XRANGE, + xRange: XRANGE, + XREAD, + xRead: XREAD, + XREADGROUP, + xReadGroup: XREADGROUP, + XREVRANGE, + xRevRange: XREVRANGE, + XTRIM, + xTrim: XTRIM, + ZADD, + zAdd: ZADD, + ZCARD, + zCard: ZCARD, + ZCOUNT, + zCount: ZCOUNT, + ZDIFF_WITHSCORES, + zDiffWithScores: ZDIFF_WITHSCORES, + ZDIFF, + zDiff: ZDIFF, + ZDIFFSTORE, + zDiffStore: ZDIFFSTORE, + ZINCRBY, + zIncrBy: ZINCRBY, + ZINTER_WITHSCORES, + zInterWithScores: ZINTER_WITHSCORES, + ZINTER, + zInter: ZINTER, + ZINTERSTORE, + zInterStore: ZINTERSTORE, + ZLEXCOUNT, + zLexCount: ZLEXCOUNT, + ZMSCORE, + zmScore: ZMSCORE, + ZPOPMAX_COUNT, + zPopMaxCount: ZPOPMAX_COUNT, + ZPOPMAX, + zPopMax: ZPOPMAX, + ZPOPMIN_COUNT, + zPopMinCount: ZPOPMIN_COUNT, + ZPOPMIN, + zPopMin: ZPOPMIN, + ZRANDMEMBER_COUNT_WITHSCORES, + zRandMemberCountWithScores: ZRANDMEMBER_COUNT_WITHSCORES, + ZRANDMEMBER_COUNT, + zRandMemberCount: ZRANDMEMBER_COUNT, + ZRANDMEMBER, + zRandMember: ZRANDMEMBER, + ZRANGE_WITHSCORES, + zRangeWithScores: ZRANGE_WITHSCORES, + ZRANGE, + zRange: ZRANGE, + ZRANGESTORE, + zRangeStore: ZRANGESTORE, + ZRANK, + zRank: ZRANK, + ZREM, + zRem: ZREM, + ZREMRANGEBYLEX, + zRemRangeByLex: ZREMRANGEBYLEX, + ZREMRANGEBYRANK, + zRemRangeByRank: ZREMRANGEBYRANK, + ZREMRANGEBYSCORE, + zRemRangeByScore: ZREMRANGEBYSCORE, + ZREVRANK, + zRevRank: ZREVRANK, + ZSCAN, + zScan: ZSCAN, + ZSCORE, + zScore: ZSCORE, + ZUNION_WITHSCORES, + zUnionWithScores: ZUNION_WITHSCORES, + ZUNION, + zUnion: ZUNION, + ZUNIONSTORE, + zUnionStore: ZUNIONSTORE +}; + +export type RedisReply = string | number | Array | null | undefined; + +export type TransformArgumentsReply = Array & { preserve?: unknown }; + +export interface RedisCommand { + FIRST_KEY_INDEX?: number | ((...args: Array) => string); + IS_READ_ONLY?: boolean; + transformArguments(...args: Array): TransformArgumentsReply; + transformReply(reply: RedisReply, preserved: unknown): any; +} + +export interface RedisCommands { + [command: string]: RedisCommand; +} + +export interface RedisModule { + [key: string]: RedisCommand; +} + +export type RedisModules = Record; diff --git a/lib/createClient.js b/lib/createClient.js deleted file mode 100644 index b03bb57539..0000000000 --- a/lib/createClient.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict'; - -var utils = require('./utils'); -var URL = require('url'); - -module.exports = function createClient (port_arg, host_arg, options) { - - if (typeof port_arg === 'number' || typeof port_arg === 'string' && /^\d+$/.test(port_arg)) { - - var host; - if (typeof host_arg === 'string') { - host = host_arg; - } else { - if (options && host_arg) { - throw new TypeError('Unknown type of connection in createClient()'); - } - options = options || host_arg; - } - options = utils.clone(options); - options.host = host || options.host; - options.port = port_arg; - - } else if (typeof port_arg === 'string' || port_arg && port_arg.url) { - - options = utils.clone(port_arg.url ? port_arg : host_arg || options); - var url = port_arg.url || port_arg; - var parsed = URL.parse(url, true, true); - - // [redis:]//[[user][:password]@][host][:port][/db-number][?db=db-number[&password=bar[&option=value]]] - if (parsed.slashes) { // We require slashes - if (parsed.auth) { - var columnIndex = parsed.auth.indexOf(':'); - options.password = parsed.auth.slice(columnIndex + 1); - if (columnIndex > 0) { - options.user = parsed.auth.slice(0, columnIndex); - } - } - if (parsed.protocol) { - if (parsed.protocol === 'rediss:') { - options.tls = options.tls || {}; - } else if (parsed.protocol !== 'redis:') { - console.warn('node_redis: WARNING: You passed "' + parsed.protocol.substring(0, parsed.protocol.length - 1) + '" as protocol instead of the "redis" protocol!'); - } - } - if (parsed.pathname && parsed.pathname !== '/') { - options.db = parsed.pathname.substr(1); - } - if (parsed.hostname) { - options.host = parsed.hostname; - } - if (parsed.port) { - options.port = parsed.port; - } - if (parsed.search !== '') { - var elem; - for (elem in parsed.query) { - // If options are passed twice, only the parsed options will be used - if (elem in options) { - if (options[elem] === parsed.query[elem]) { - console.warn('node_redis: WARNING: You passed the ' + elem + ' option twice!'); - } else { - throw new RangeError('The ' + elem + ' option is added twice and does not match'); - } - } - options[elem] = parsed.query[elem]; - } - } - } else if (parsed.hostname) { - throw new RangeError('The redis url must begin with slashes "//" or contain slashes after the redis protocol'); - } else { - options.path = url; - } - - } else if (typeof port_arg === 'object' || port_arg === undefined) { - options = utils.clone(port_arg || options); - options.host = options.host || host_arg; - - if (port_arg && arguments.length !== 1) { - throw new TypeError('Too many arguments passed to createClient. Please only pass the options object'); - } - } - - if (!options) { - throw new TypeError('Unknown type of connection in createClient()'); - } - - return options; -}; diff --git a/lib/customErrors.js b/lib/customErrors.js deleted file mode 100644 index 2483db0d8d..0000000000 --- a/lib/customErrors.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -var util = require('util'); -var assert = require('assert'); -var RedisError = require('redis-errors').RedisError; -var ADD_STACKTRACE = false; - -function AbortError (obj, stack) { - assert(obj, 'The options argument is required'); - assert.strictEqual(typeof obj, 'object', 'The options argument has to be of type object'); - - Object.defineProperty(this, 'message', { - value: obj.message || '', - configurable: true, - writable: true - }); - if (stack || stack === undefined) { - Error.captureStackTrace(this, AbortError); - } - for (var keys = Object.keys(obj), key = keys.pop(); key; key = keys.pop()) { - this[key] = obj[key]; - } -} - -function AggregateError (obj) { - assert(obj, 'The options argument is required'); - assert.strictEqual(typeof obj, 'object', 'The options argument has to be of type object'); - - AbortError.call(this, obj, ADD_STACKTRACE); - Object.defineProperty(this, 'message', { - value: obj.message || '', - configurable: true, - writable: true - }); - Error.captureStackTrace(this, AggregateError); - for (var keys = Object.keys(obj), key = keys.pop(); key; key = keys.pop()) { - this[key] = obj[key]; - } -} - -util.inherits(AbortError, RedisError); -util.inherits(AggregateError, AbortError); - -Object.defineProperty(AbortError.prototype, 'name', { - value: 'AbortError', - configurable: true, - writable: true -}); -Object.defineProperty(AggregateError.prototype, 'name', { - value: 'AggregateError', - configurable: true, - writable: true -}); - -module.exports = { - AbortError: AbortError, - AggregateError: AggregateError -}; diff --git a/lib/debug.js b/lib/debug.js deleted file mode 100644 index d69c1ea624..0000000000 --- a/lib/debug.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -var index = require('../'); - -function debug () { - if (index.debug_mode) { - var data = Array.prototype.slice.call(arguments); - data.unshift(new Date().toISOString()); - console.error.apply(null, data); - } -} - -module.exports = debug; diff --git a/lib/errors.ts b/lib/errors.ts new file mode 100644 index 0000000000..86a65587cf --- /dev/null +++ b/lib/errors.ts @@ -0,0 +1,23 @@ +export class AbortError extends Error { + constructor() { + super('The command was aborted'); + } +} + +export class WatchError extends Error { + constructor() { + super('One (or more) of the watched keys has been changed'); + } +} + +export class ConnectionTimeoutError extends Error { + constructor() { + super('Connection timeout'); + } +} + +export class ClientClosedError extends Error { + constructor() { + super('The client is closed'); + } +} diff --git a/lib/extendedApi.js b/lib/extendedApi.js deleted file mode 100644 index 27ed4215d6..0000000000 --- a/lib/extendedApi.js +++ /dev/null @@ -1,113 +0,0 @@ -'use strict'; - -var utils = require('./utils'); -var debug = require('./debug'); -var RedisClient = require('../').RedisClient; -var Command = require('./command'); -var noop = function () {}; - -/********************************************** -All documented and exposed API belongs in here -**********************************************/ - -// Redirect calls to the appropriate function and use to send arbitrary / not supported commands -RedisClient.prototype.send_command = RedisClient.prototype.sendCommand = function (command, args, callback) { - // Throw to fail early instead of relying in order in this case - if (typeof command !== 'string') { - throw new TypeError('Wrong input type "' + (command !== null && command !== undefined ? command.constructor.name : command) + '" for command name'); - } - command = command.toLowerCase(); - if (!Array.isArray(args)) { - if (args === undefined || args === null) { - args = []; - } else if (typeof args === 'function' && callback === undefined) { - callback = args; - args = []; - } else { - throw new TypeError('Wrong input type "' + args.constructor.name + '" for args'); - } - } - if (typeof callback !== 'function' && callback !== undefined) { - throw new TypeError('Wrong input type "' + (callback !== null ? callback.constructor.name : 'null') + '" for callback function'); - } - - // Using the raw multi command is only possible with this function - // If the command is not yet added to the client, the internal function should be called right away - // Otherwise we need to redirect the calls to make sure the internal functions don't get skipped - // The internal functions could actually be used for any non hooked function - // but this might change from time to time and at the moment there's no good way to distinguish them - // from each other, so let's just do it do it this way for the time being - if (command === 'multi' || typeof this[command] !== 'function') { - return this.internal_send_command(new Command(command, args, callback)); - } - if (typeof callback === 'function') { - args = args.concat([callback]); // Prevent manipulating the input array - } - return this[command].apply(this, args); -}; - -RedisClient.prototype.end = function (flush) { - // Flush queue if wanted - if (flush) { - this.flush_and_error({ - message: 'Connection forcefully ended and command aborted.', - code: 'NR_CLOSED' - }); - } else if (arguments.length === 0) { - this.warn( - 'Using .end() without the flush parameter is deprecated and throws from v.3.0.0 on.\n' + - 'Please check the doku (https://github.com/NodeRedis/node_redis) and explictly use flush.' - ); - } - // Clear retry_timer - if (this.retry_timer) { - clearTimeout(this.retry_timer); - this.retry_timer = null; - } - this.stream.removeAllListeners(); - this.stream.on('error', noop); - this.connected = false; - this.ready = false; - this.closing = true; - return this.stream.destroySoon(); -}; - -RedisClient.prototype.unref = function () { - if (this.connected) { - debug("Unref'ing the socket connection"); - this.stream.unref(); - } else { - debug('Not connected yet, will unref later'); - this.once('connect', function () { - this.unref(); - }); - } -}; - -RedisClient.prototype.duplicate = function (options, callback) { - if (typeof options === 'function') { - callback = options; - options = null; - } - var existing_options = utils.clone(this.options); - options = utils.clone(options); - for (var elem in options) { - existing_options[elem] = options[elem]; - } - var client = new RedisClient(existing_options); - client.selected_db = options.db || this.selected_db; - if (typeof callback === 'function') { - var ready_listener = function () { - callback(null, client); - client.removeAllListeners(error_listener); - }; - var error_listener = function (err) { - callback(err); - client.end(true); - }; - client.once('ready', ready_listener); - client.once('error', error_listener); - return; - } - return client; -}; diff --git a/lib/individualCommands.js b/lib/individualCommands.js deleted file mode 100644 index c3ea3da0df..0000000000 --- a/lib/individualCommands.js +++ /dev/null @@ -1,629 +0,0 @@ -'use strict'; - -var utils = require('./utils'); -var debug = require('./debug'); -var Multi = require('./multi'); -var Command = require('./command'); -var no_password_is_set = /no password is set|called without any password configured/; -var loading = /LOADING/; -var RedisClient = require('../').RedisClient; - -/******************************************************************************************** - Replace built-in redis functions - - The callback may be hooked as needed. The same does not apply to the rest of the function. - State should not be set outside of the callback if not absolutly necessary. - This is important to make sure it works the same as single command or in a multi context. - To make sure everything works with the offline queue use the "call_on_write" function. - This is going to be executed while writing to the stream. - - TODO: Implement individal command generation as soon as possible to prevent divergent code - on single and multi calls! -********************************************************************************************/ - -RedisClient.prototype.multi = RedisClient.prototype.MULTI = function multi (args) { - var multi = new Multi(this, args); - multi.exec = multi.EXEC = multi.exec_transaction; - return multi; -}; - -// ATTENTION: This is not a native function but is still handled as a individual command as it behaves just the same as multi -RedisClient.prototype.batch = RedisClient.prototype.BATCH = function batch (args) { - return new Multi(this, args); -}; - -function select_callback (self, db, callback) { - return function (err, res) { - if (err === null) { - // Store db in this.select_db to restore it on reconnect - self.selected_db = db; - } - utils.callback_or_emit(self, callback, err, res); - }; -} - -RedisClient.prototype.select = RedisClient.prototype.SELECT = function select (db, callback) { - return this.internal_send_command(new Command('select', [db], select_callback(this, db, callback))); -}; - -Multi.prototype.select = Multi.prototype.SELECT = function select (db, callback) { - this.queue.push(new Command('select', [db], select_callback(this._client, db, callback))); - return this; -}; - -RedisClient.prototype.monitor = RedisClient.prototype.MONITOR = function monitor (callback) { - // Use a individual command, as this is a special case that does not has to be checked for any other command - var self = this; - var call_on_write = function () { - // Activating monitor mode has to happen before Redis returned the callback. The monitor result is returned first. - // Therefore we expect the command to be properly processed. If this is not the case, it's not an issue either. - self.monitoring = true; - }; - return this.internal_send_command(new Command('monitor', [], callback, call_on_write)); -}; - -// Only works with batch, not in a transaction -Multi.prototype.monitor = Multi.prototype.MONITOR = function monitor (callback) { - // Use a individual command, as this is a special case that does not has to be checked for any other command - if (this.exec !== this.exec_transaction) { - var self = this; - var call_on_write = function () { - self._client.monitoring = true; - }; - this.queue.push(new Command('monitor', [], callback, call_on_write)); - return this; - } - // Set multi monitoring to indicate the exec that it should abort - // Remove this "hack" as soon as Redis might fix this - this.monitoring = true; - return this; -}; - -function quit_callback (self, callback) { - return function (err, res) { - if (err && err.code === 'NR_CLOSED') { - // Pretent the quit command worked properly in this case. - // Either the quit landed in the offline queue and was flushed at the reconnect - // or the offline queue is deactivated and the command was rejected right away - // or the stream is not writable - // or while sending the quit, the connection ended / closed - err = null; - res = 'OK'; - } - utils.callback_or_emit(self, callback, err, res); - if (self.stream.writable) { - // If the socket is still alive, kill it. This could happen if quit got a NR_CLOSED error code - self.stream.destroy(); - } - }; -} - -RedisClient.prototype.QUIT = RedisClient.prototype.quit = function quit (callback) { - // TODO: Consider this for v.3 - // Allow the quit command to be fired as soon as possible to prevent it landing in the offline queue. - // this.ready = this.offline_queue.length === 0; - var backpressure_indicator = this.internal_send_command(new Command('quit', [], quit_callback(this, callback))); - // Calling quit should always end the connection, no matter if there's a connection or not - this.closing = true; - this.ready = false; - return backpressure_indicator; -}; - -// Only works with batch, not in a transaction -Multi.prototype.QUIT = Multi.prototype.quit = function quit (callback) { - var self = this._client; - var call_on_write = function () { - // If called in a multi context, we expect redis is available - self.closing = true; - self.ready = false; - }; - this.queue.push(new Command('quit', [], quit_callback(self, callback), call_on_write)); - return this; -}; - -function info_callback (self, callback) { - return function (err, res) { - if (res) { - var obj = {}; - var lines = res.toString().split('\r\n'); - var line, parts, sub_parts; - - for (var i = 0; i < lines.length; i++) { - parts = lines[i].split(':'); - if (parts[1]) { - if (parts[0].indexOf('db') === 0) { - sub_parts = parts[1].split(','); - obj[parts[0]] = {}; - while (line = sub_parts.pop()) { - line = line.split('='); - obj[parts[0]][line[0]] = +line[1]; - } - } else { - obj[parts[0]] = parts[1]; - } - } - } - obj.versions = []; - if (obj.redis_version) { - obj.redis_version.split('.').forEach(function (num) { - obj.versions.push(+num); - }); - } - // Expose info key/vals to users - self.server_info = obj; - } else { - self.server_info = {}; - } - utils.callback_or_emit(self, callback, err, res); - }; -} - -// Store info in this.server_info after each call -RedisClient.prototype.info = RedisClient.prototype.INFO = function info (section, callback) { - var args = []; - if (typeof section === 'function') { - callback = section; - } else if (section !== undefined) { - args = Array.isArray(section) ? section : [section]; - } - return this.internal_send_command(new Command('info', args, info_callback(this, callback))); -}; - -Multi.prototype.info = Multi.prototype.INFO = function info (section, callback) { - var args = []; - if (typeof section === 'function') { - callback = section; - } else if (section !== undefined) { - args = Array.isArray(section) ? section : [section]; - } - this.queue.push(new Command('info', args, info_callback(this._client, callback))); - return this; -}; - -function auth_callback (self, pass, user, callback) { - return function (err, res) { - if (err) { - if (no_password_is_set.test(err.message)) { - self.warn('Warning: Redis server does not require a password, but a password was supplied.'); - err = null; - res = 'OK'; - } else if (loading.test(err.message)) { - // If redis is still loading the db, it will not authenticate and everything else will fail - debug('Redis still loading, trying to authenticate later'); - setTimeout(function () { - self.auth(pass, user, callback); - }, 100); - return; - } - } - utils.callback_or_emit(self, callback, err, res); - }; -} - -RedisClient.prototype.auth = RedisClient.prototype.AUTH = function auth (pass, user, callback) { - debug('Sending auth to ' + this.address + ' id ' + this.connection_id); - - // Backward compatibility support for auth with password only - if (user instanceof Function) { - callback = user; - user = null; - } - // Stash auth for connect and reconnect. - this.auth_pass = pass; - this.auth_user = user; - var ready = this.ready; - this.ready = ready || this.offline_queue.length === 0; - var tmp = this.internal_send_command(new Command('auth', user ? [user, pass] : [pass], auth_callback(this, pass, user, callback))); - this.ready = ready; - return tmp; -}; - -// Only works with batch, not in a transaction -Multi.prototype.auth = Multi.prototype.AUTH = function auth (pass, user, callback) { - debug('Sending auth to ' + this.address + ' id ' + this.connection_id); - - // Backward compatibility support for auth with password only - if (user instanceof Function) { - callback = user; - user = null; - } - // Stash auth for connect and reconnect. - this.auth_pass = pass; - this.auth_user = user; - this.queue.push(new Command('auth', user ? [user, pass] : [pass], auth_callback(this._client, pass, user, callback))); - return this; -}; - -RedisClient.prototype.client = RedisClient.prototype.CLIENT = function client () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0]; - callback = arguments[1]; - } else if (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 { - len = arguments.length; - // 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]; - } - } - var self = this; - var call_on_write = undefined; - // CLIENT REPLY ON|OFF|SKIP - /* istanbul ignore next: TODO: Remove this as soon as Travis runs Redis 3.2 */ - if (arr.length === 2 && arr[0].toString().toUpperCase() === 'REPLY') { - var reply_on_off = arr[1].toString().toUpperCase(); - if (reply_on_off === 'ON' || reply_on_off === 'OFF' || reply_on_off === 'SKIP') { - call_on_write = function () { - self.reply = reply_on_off; - }; - } - } - return this.internal_send_command(new Command('client', arr, callback, call_on_write)); -}; - -Multi.prototype.client = Multi.prototype.CLIENT = function client () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0]; - callback = arguments[1]; - } else if (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 { - len = arguments.length; - // 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]; - } - } - var self = this._client; - var call_on_write = undefined; - // CLIENT REPLY ON|OFF|SKIP - /* istanbul ignore next: TODO: Remove this as soon as Travis runs Redis 3.2 */ - if (arr.length === 2 && arr[0].toString().toUpperCase() === 'REPLY') { - var reply_on_off = arr[1].toString().toUpperCase(); - if (reply_on_off === 'ON' || reply_on_off === 'OFF' || reply_on_off === 'SKIP') { - call_on_write = function () { - self.reply = reply_on_off; - }; - } - } - this.queue.push(new Command('client', arr, callback, call_on_write)); - return this; -}; - -RedisClient.prototype.hmset = RedisClient.prototype.HMSET = function hmset () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0]; - callback = arguments[1]; - } else if (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 if (typeof arguments[1] === 'object' && (arguments.length === 2 || arguments.length === 3 && (typeof arguments[2] === 'function' || typeof arguments[2] === 'undefined'))) { - arr = [arguments[0]]; - for (var field in arguments[1]) { - arr.push(field, arguments[1][field]); - } - callback = arguments[2]; - } else { - len = arguments.length; - // 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('hmset', arr, callback)); -}; - -Multi.prototype.hmset = Multi.prototype.HMSET = function hmset () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0]; - callback = arguments[1]; - } else if (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 if (typeof arguments[1] === 'object' && (arguments.length === 2 || arguments.length === 3 && (typeof arguments[2] === 'function' || typeof arguments[2] === 'undefined'))) { - arr = [arguments[0]]; - for (var field in arguments[1]) { - arr.push(field, arguments[1][field]); - } - callback = arguments[2]; - } else { - len = arguments.length; - // 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('hmset', arr, callback)); - return this; -}; - -RedisClient.prototype.subscribe = RedisClient.prototype.SUBSCRIBE = function subscribe () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0].slice(0); - callback = arguments[1]; - } else { - len = arguments.length; - // 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]; - } - } - var self = this; - var call_on_write = function () { - self.pub_sub_mode = self.pub_sub_mode || self.command_queue.length + 1; - }; - return this.internal_send_command(new Command('subscribe', arr, callback, call_on_write)); -}; - -Multi.prototype.subscribe = Multi.prototype.SUBSCRIBE = function subscribe () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0].slice(0); - callback = arguments[1]; - } else { - len = arguments.length; - // 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]; - } - } - var self = this._client; - var call_on_write = function () { - self.pub_sub_mode = self.pub_sub_mode || self.command_queue.length + 1; - }; - this.queue.push(new Command('subscribe', arr, callback, call_on_write)); - return this; -}; - -RedisClient.prototype.unsubscribe = RedisClient.prototype.UNSUBSCRIBE = function unsubscribe () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0].slice(0); - callback = arguments[1]; - } else { - len = arguments.length; - // 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]; - } - } - var self = this; - var call_on_write = function () { - // Pub sub has to be activated even if not in pub sub mode, as the return value is manipulated in the callback - self.pub_sub_mode = self.pub_sub_mode || self.command_queue.length + 1; - }; - return this.internal_send_command(new Command('unsubscribe', arr, callback, call_on_write)); -}; - -Multi.prototype.unsubscribe = Multi.prototype.UNSUBSCRIBE = function unsubscribe () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0].slice(0); - callback = arguments[1]; - } else { - len = arguments.length; - // 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]; - } - } - var self = this._client; - var call_on_write = function () { - // Pub sub has to be activated even if not in pub sub mode, as the return value is manipulated in the callback - self.pub_sub_mode = self.pub_sub_mode || self.command_queue.length + 1; - }; - this.queue.push(new Command('unsubscribe', arr, callback, call_on_write)); - return this; -}; - -RedisClient.prototype.psubscribe = RedisClient.prototype.PSUBSCRIBE = function psubscribe () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0].slice(0); - callback = arguments[1]; - } else { - len = arguments.length; - // 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]; - } - } - var self = this; - var call_on_write = function () { - self.pub_sub_mode = self.pub_sub_mode || self.command_queue.length + 1; - }; - return this.internal_send_command(new Command('psubscribe', arr, callback, call_on_write)); -}; - -Multi.prototype.psubscribe = Multi.prototype.PSUBSCRIBE = function psubscribe () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0].slice(0); - callback = arguments[1]; - } else { - len = arguments.length; - // 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]; - } - } - var self = this._client; - var call_on_write = function () { - self.pub_sub_mode = self.pub_sub_mode || self.command_queue.length + 1; - }; - this.queue.push(new Command('psubscribe', arr, callback, call_on_write)); - return this; -}; - -RedisClient.prototype.punsubscribe = RedisClient.prototype.PUNSUBSCRIBE = function punsubscribe () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0].slice(0); - callback = arguments[1]; - } else { - len = arguments.length; - // 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]; - } - } - var self = this; - var call_on_write = function () { - // Pub sub has to be activated even if not in pub sub mode, as the return value is manipulated in the callback - self.pub_sub_mode = self.pub_sub_mode || self.command_queue.length + 1; - }; - return this.internal_send_command(new Command('punsubscribe', arr, callback, call_on_write)); -}; - -Multi.prototype.punsubscribe = Multi.prototype.PUNSUBSCRIBE = function punsubscribe () { - var arr, - len = arguments.length, - callback, - i = 0; - if (Array.isArray(arguments[0])) { - arr = arguments[0].slice(0); - callback = arguments[1]; - } else { - len = arguments.length; - // 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]; - } - } - var self = this._client; - var call_on_write = function () { - // Pub sub has to be activated even if not in pub sub mode, as the return value is manipulated in the callback - self.pub_sub_mode = self.pub_sub_mode || self.command_queue.length + 1; - }; - this.queue.push(new Command('punsubscribe', arr, callback, call_on_write)); - return this; -}; diff --git a/lib/lua-script.ts b/lib/lua-script.ts new file mode 100644 index 0000000000..183c42f219 --- /dev/null +++ b/lib/lua-script.ts @@ -0,0 +1,28 @@ +import { createHash } from 'crypto'; +import { RedisCommand } from './commands'; + +export interface RedisLuaScriptConfig extends RedisCommand { + SCRIPT: string; + NUMBER_OF_KEYS: number; +} + +export interface SHA1 { + SHA1: string; +} + +export type RedisLuaScript = RedisLuaScriptConfig & SHA1; + +export interface RedisLuaScripts { + [key: string]: RedisLuaScript; +} + +export function defineScript(script: S): S & SHA1 { + return { + ...script, + SHA1: scriptSha1(script.SCRIPT) + }; +} + +export function scriptSha1(script: string): string { + return createHash('sha1').update(script).digest('hex'); +} diff --git a/lib/multi-command.spec.ts b/lib/multi-command.spec.ts new file mode 100644 index 0000000000..a78cc8b2e0 --- /dev/null +++ b/lib/multi-command.spec.ts @@ -0,0 +1,127 @@ +import { strict as assert } from 'assert'; +import RedisMultiCommand from './multi-command'; +import { encodeCommand } from './commander'; +import { WatchError } from './errors'; +import { spy } from 'sinon'; +import { SQUARE_SCRIPT } from './client.spec'; + +describe('Multi Command', () => { + describe('exec', () => { + it('simple', async () => { + const multi = RedisMultiCommand.create((queue, symbol) => { + assert.deepEqual( + queue.map(({encodedCommand}) => encodedCommand), + [ + encodeCommand(['MULTI']), + encodeCommand(['PING']), + encodeCommand(['EXEC']), + ] + ); + + assert.equal( + typeof symbol, + 'symbol' + ); + + return Promise.resolve(['QUEUED', 'QUEUED', ['PONG']]); + }); + + multi.ping(); + + assert.deepEqual( + await multi.exec(), + ['PONG'] + ); + }); + + it('executing an empty queue should resolve without executing on the server', async () => { + const executor = spy(); + + assert.deepEqual( + await RedisMultiCommand.create(executor).exec(), + [] + ); + + assert.ok(executor.notCalled); + }); + + it('WatchError', () => { + return assert.rejects( + RedisMultiCommand.create(() => Promise.resolve([null])).ping().exec(), + WatchError + ); + }); + + it('execAsPipeline', async () => { + const multi = RedisMultiCommand.create(queue => { + assert.deepEqual( + queue.map(({encodedCommand}) => encodedCommand), + [encodeCommand(['PING'])] + ); + + return Promise.resolve(['PONG']); + }); + + multi.ping(); + + assert.deepEqual( + await multi.exec(true), + ['PONG'] + ); + }); + }); + + describe('execAsPipeline', () => { + it('simple', async () => { + const multi = RedisMultiCommand.create(queue => { + assert.deepEqual( + queue.map(({encodedCommand}) => encodedCommand), + [encodeCommand(['PING'])] + ); + + return Promise.resolve(['PONG']); + }); + + multi.ping(); + + assert.deepEqual( + await multi.execAsPipeline(), + ['PONG'] + ); + }); + + it('executing an empty queue should resolve without executing on the server', async () => { + const executor = spy(); + + assert.deepEqual( + await RedisMultiCommand.create(executor).execAsPipeline(), + [] + ); + + assert.ok(executor.notCalled); + }); + + it('with scripts', async () => { + const MultiWithScript = RedisMultiCommand.extend({ + scripts: { + square: SQUARE_SCRIPT + } + }); + + assert.deepEqual( + await new MultiWithScript(queue => { + assert.deepEqual( + queue.map(({encodedCommand}) => encodedCommand), + [ + encodeCommand(['EVAL', SQUARE_SCRIPT.SCRIPT, '0', '2']), + encodeCommand(['EVALSHA', SQUARE_SCRIPT.SHA1, '0', '3']), + ] + ); + + return Promise.resolve([4, 9]); + }).square(2).square(3).execAsPipeline(), + [4, 9] + ); + }); + }); +}); diff --git a/lib/multi-command.ts b/lib/multi-command.ts new file mode 100644 index 0000000000..c8a5076596 --- /dev/null +++ b/lib/multi-command.ts @@ -0,0 +1,210 @@ +import COMMANDS, { TransformArgumentsReply } from './commands'; +import { RedisCommand, RedisModules, RedisReply } from './commands'; +import { RedisLuaScript, RedisLuaScripts } from './lua-script'; +import { RedisClientOptions } from './client'; +import { extendWithModulesAndScripts, extendWithDefaultCommands, encodeCommand } from './commander'; +import { WatchError } from './errors'; + +type RedisMultiCommandSignature = (...args: Parameters) => RedisMultiCommandType; + +type WithCommands = { + [P in keyof typeof COMMANDS]: RedisMultiCommandSignature<(typeof COMMANDS)[P], M, S> +}; + +type WithModules = { + [P in keyof M]: { + [C in keyof M[P]]: RedisMultiCommandSignature; + }; +}; + +type WithScripts = { + [P in keyof S]: RedisMultiCommandSignature +}; + +export type RedisMultiCommandType = RedisMultiCommand & WithCommands & WithModules & WithScripts; + +export interface MultiQueuedCommand { + encodedCommand: string; + preservedArguments?: unknown; + transformReply?: RedisCommand['transformReply']; +} + +export type RedisMultiExecutor = (queue: Array, chainId?: symbol) => Promise>; + +export default class RedisMultiCommand { + static commandsExecutor(this: RedisMultiCommand, command: RedisCommand, args: Array): RedisMultiCommand { + return this.addCommand( + command.transformArguments(...args), + command.transformReply + ); + } + + static #scriptsExecutor( + this: RedisMultiCommand, + script: RedisLuaScript, + args: Array + ): RedisMultiCommand { + const transformedArguments: TransformArgumentsReply = []; + if (this.#scriptsInUse.has(script.SHA1)) { + transformedArguments.push( + 'EVALSHA', + script.SHA1 + ); + } else { + this.#scriptsInUse.add(script.SHA1); + transformedArguments.push( + 'EVAL', + script.SCRIPT + ); + } + + transformedArguments.push(script.NUMBER_OF_KEYS.toString()); + + const scriptArguments = script.transformArguments(...args); + transformedArguments.push(...scriptArguments); + transformedArguments.preserve = scriptArguments.preserve; + + return this.addCommand( + transformedArguments, + script.transformReply + ); + } + + static extend( + clientOptions?: RedisClientOptions + ): new (...args: ConstructorParameters) => RedisMultiCommandType { + return extendWithModulesAndScripts({ + BaseClass: RedisMultiCommand, + modules: clientOptions?.modules, + modulesCommandsExecutor: RedisMultiCommand.commandsExecutor, + scripts: clientOptions?.scripts, + scriptsExecutor: RedisMultiCommand.#scriptsExecutor + }); + } + + static create( + executor: RedisMultiExecutor, + clientOptions?: RedisClientOptions + ): RedisMultiCommandType { + return new this(executor, clientOptions); + } + + readonly #executor: RedisMultiExecutor; + + readonly #clientOptions: RedisClientOptions | undefined; + + readonly #queue: Array = []; + + readonly #scriptsInUse = new Set(); + + readonly #v4: Record = {}; + + get v4(): Record { + if (!this.#clientOptions?.legacyMode) { + throw new Error('client is not in "legacy mode"'); + } + + return this.#v4; + } + + constructor(executor: RedisMultiExecutor, clientOptions?: RedisClientOptions) { + this.#executor = executor; + this.#clientOptions = clientOptions; + this.#legacyMode(); + } + + #legacyMode(): void { + if (!this.#clientOptions?.legacyMode) return; + + this.#v4.addCommand = this.addCommand.bind(this); + (this as any).addCommand = (...args: Array): this => { + this.#queue.push({ + encodedCommand: encodeCommand(args.flat() as Array) + }); + return this; + } + this.#v4.exec = this.exec.bind(this); + (this as any).exec = (callback?: (err: Error | null, replies?: Array) => unknown): void => { + this.#v4.exec() + .then((reply: Array) => { + if (!callback) return; + + callback(null, reply); + }) + .catch((err: Error) => { + if (!callback) { + // this.emit('error', err); + return; + } + + callback(err); + }); + }; + + for (const name of Object.keys(COMMANDS)) { + this.#defineLegacyCommand(name); + } + } + + #defineLegacyCommand(name: string): void { + (this as any).#v4[name] = (this as any)[name].bind(this.#v4); + (this as any)[name] = (...args: Array): void => (this as any).addCommand(name, args); + } + + addCommand(args: TransformArgumentsReply, transformReply?: RedisCommand['transformReply']): this { + this.#queue.push({ + encodedCommand: encodeCommand(args), + preservedArguments: args.preserve, + transformReply + }); + + return this; + } + + async exec(execAsPipeline = false): Promise> { + if (execAsPipeline) { + return this.execAsPipeline(); + } else if (!this.#queue.length) { + return []; + } + + const queue = this.#queue.splice(0), + rawReplies = await this.#executor([ + { + encodedCommand: encodeCommand(['MULTI']) + }, + ...queue, + { + encodedCommand: encodeCommand(['EXEC']) + } + ], Symbol('[RedisMultiCommand] Chain ID')), + execReply = rawReplies[rawReplies.length - 1] as (null | Array); + + if (execReply === null) { + throw new WatchError(); + } + + return this.#transformReplies(execReply, queue); + } + + async execAsPipeline(): Promise> { + if (!this.#queue.length) { + return []; + } + + const queue = this.#queue.splice(0); + return this.#transformReplies( + await this.#executor(queue), + queue + ); + } + + #transformReplies(rawReplies: Array, queue: Array): Array { + return rawReplies.map((reply, i) => { + const { transformReply, preservedArguments } = queue[i]; + return transformReply ? transformReply(reply, preservedArguments) : reply; + }); + } +} + +extendWithDefaultCommands(RedisMultiCommand, RedisMultiCommand.commandsExecutor); diff --git a/lib/multi.js b/lib/multi.js deleted file mode 100644 index d89cffbbd4..0000000000 --- a/lib/multi.js +++ /dev/null @@ -1,187 +0,0 @@ -'use strict'; - -var Queue = require('denque'); -var utils = require('./utils'); -var Command = require('./command'); - -function Multi (client, args) { - this._client = client; - this.queue = new Queue(); - var command, tmp_args; - if (args) { // Either undefined or an array. Fail hard if it's not an array - for (var i = 0; i < args.length; i++) { - command = args[i][0]; - tmp_args = args[i].slice(1); - if (Array.isArray(command)) { - this[command[0]].apply(this, command.slice(1).concat(tmp_args)); - } else { - this[command].apply(this, tmp_args); - } - } - } -} - -function pipeline_transaction_command (self, command_obj, index) { - // Queueing is done first, then the commands are executed - var tmp = command_obj.callback; - command_obj.callback = function (err, reply) { - // Ignore the multi command. This is applied by node_redis and the user does not benefit by it - if (err && index !== -1) { - if (tmp) { - tmp(err); - } - err.position = index; - self.errors.push(err); - } - // Keep track of who wants buffer responses: - // By the time the callback is called the command_obj got the buffer_args attribute attached - self.wants_buffers[index] = command_obj.buffer_args; - command_obj.callback = tmp; - }; - self._client.internal_send_command(command_obj); -} - -Multi.prototype.exec_atomic = Multi.prototype.EXEC_ATOMIC = Multi.prototype.execAtomic = function exec_atomic (callback) { - if (this.queue.length < 2) { - return this.exec_batch(callback); - } - return this.exec(callback); -}; - -function multi_callback (self, err, replies) { - var i = 0, command_obj; - - if (err) { - err.errors = self.errors; - if (self.callback) { - self.callback(err); - // Exclude connection errors so that those errors won't be emitted twice - } else if (err.code !== 'CONNECTION_BROKEN') { - self._client.emit('error', err); - } - return; - } - - if (replies) { - while (command_obj = self.queue.shift()) { - if (replies[i] instanceof Error) { - var match = replies[i].message.match(utils.err_code); - // LUA script could return user errors that don't behave like all other errors! - if (match) { - replies[i].code = match[1]; - } - replies[i].command = command_obj.command.toUpperCase(); - if (typeof command_obj.callback === 'function') { - command_obj.callback(replies[i]); - } - } else { - // If we asked for strings, even in detect_buffers mode, then return strings: - replies[i] = self._client.handle_reply(replies[i], command_obj.command, self.wants_buffers[i]); - if (typeof command_obj.callback === 'function') { - command_obj.callback(null, replies[i]); - } - } - i++; - } - } - - if (self.callback) { - self.callback(null, replies); - } -} - -Multi.prototype.exec_transaction = function exec_transaction (callback) { - if (this.monitoring || this._client.monitoring) { - var err = new RangeError( - 'Using transaction with a client that is in monitor mode does not work due to faulty return values of Redis.' - ); - err.command = 'EXEC'; - err.code = 'EXECABORT'; - return utils.reply_in_order(this._client, callback, err); - } - var self = this; - var len = self.queue.length; - self.errors = []; - self.callback = callback; - self._client.cork(); - self.wants_buffers = new Array(len); - pipeline_transaction_command(self, new Command('multi', []), -1); - // Drain queue, callback will catch 'QUEUED' or error - for (var index = 0; index < len; index++) { - // The commands may not be shifted off, since they are needed in the result handler - pipeline_transaction_command(self, self.queue.get(index), index); - } - - self._client.internal_send_command(new Command('exec', [], function (err, replies) { - multi_callback(self, err, replies); - })); - self._client.uncork(); - return !self._client.should_buffer; -}; - -function batch_callback (self, cb, i) { - return function batch_callback (err, res) { - if (err) { - self.results[i] = err; - // Add the position to the error - self.results[i].position = i; - } else { - self.results[i] = res; - } - cb(err, res); - }; -} - -Multi.prototype.exec = Multi.prototype.EXEC = Multi.prototype.exec_batch = function exec_batch (callback) { - var self = this; - var len = self.queue.length; - var index = 0; - var command_obj; - if (len === 0) { - utils.reply_in_order(self._client, callback, null, []); - return !self._client.should_buffer; - } - self._client.cork(); - if (!callback) { - while (command_obj = self.queue.shift()) { - self._client.internal_send_command(command_obj); - } - self._client.uncork(); - return !self._client.should_buffer; - } - var callback_without_own_cb = function (err, res) { - if (err) { - self.results.push(err); - // Add the position to the error - var i = self.results.length - 1; - self.results[i].position = i; - } else { - self.results.push(res); - } - // Do not emit an error here. Otherwise each error would result in one emit. - // The errors will be returned in the result anyway - }; - var last_callback = function (cb) { - return function (err, res) { - cb(err, res); - callback(null, self.results); - }; - }; - self.results = []; - while (command_obj = self.queue.shift()) { - if (typeof command_obj.callback === 'function') { - command_obj.callback = batch_callback(self, command_obj.callback, index); - } else { - command_obj.callback = callback_without_own_cb; - } - if (typeof callback === 'function' && index === len - 1) { - command_obj.callback = last_callback(command_obj.callback); - } - this._client.internal_send_command(command_obj); - index++; - } - self._client.uncork(); - return !self._client.should_buffer; -}; - -module.exports = Multi; diff --git a/lib/socket.spec.ts b/lib/socket.spec.ts new file mode 100644 index 0000000000..11c02d0885 --- /dev/null +++ b/lib/socket.spec.ts @@ -0,0 +1,38 @@ +import { strict as assert } from 'assert'; +import { SinonFakeTimers, useFakeTimers, spy } from 'sinon'; +import RedisSocket from './socket'; + +describe('Socket', () => { + describe('reconnectStrategy', () => { + let clock: SinonFakeTimers; + beforeEach(() => clock = useFakeTimers()); + afterEach(() => clock.uninstall()); + + it('custom strategy', () => { + const reconnectStrategy = spy((retries: number): number | Error => { + assert.equal(retries + 1, reconnectStrategy.callCount); + + if (retries === 50) { + return Error('50'); + } + + const time = retries * 2; + queueMicrotask(() => clock.tick(time)); + return time; + }); + + const socket = new RedisSocket(undefined, { + host: 'error', + reconnectStrategy + }); + + socket.on('error', () => { + // ignore errors + }); + + return assert.rejects(socket.connect(), { + message: '50' + }); + }) + }); +}); diff --git a/lib/socket.ts b/lib/socket.ts new file mode 100644 index 0000000000..66cd28d91d --- /dev/null +++ b/lib/socket.ts @@ -0,0 +1,254 @@ +import EventEmitter from 'events'; +import net from 'net'; +import tls from 'tls'; +import { URL } from 'url'; +import { ConnectionTimeoutError, ClientClosedError } from './errors'; +import { promiseTimeout } from './utils'; + +export interface RedisSocketCommonOptions { + username?: string; + password?: string; + connectTimeout?: number; + noDelay?: boolean; + keepAlive?: number | false; + reconnectStrategy?(retries: number): number | Error; +} + +export interface RedisNetSocketOptions extends RedisSocketCommonOptions { + port?: number; + host?: string; +} + +export interface RedisUrlSocketOptions extends RedisSocketCommonOptions { + url: string; +} + +export interface RedisUnixSocketOptions extends RedisSocketCommonOptions { + path: string; +} + +export interface RedisTlsSocketOptions extends RedisNetSocketOptions, tls.SecureContextOptions { + tls: true; +} + +export type RedisSocketOptions = RedisNetSocketOptions | RedisUrlSocketOptions | RedisUnixSocketOptions | RedisTlsSocketOptions; + +interface CreateSocketReturn { + connectEvent: string; + socket: T; +} + +export type RedisSocketInitiator = () => Promise; + +export default class RedisSocket extends EventEmitter { + static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions { + options ??= {}; + if (!RedisSocket.#isUnixSocket(options)) { + if (RedisSocket.#isUrlSocket(options)) { + const url = new URL(options.url); + (options as RedisNetSocketOptions).port = Number(url.port); + (options as RedisNetSocketOptions).host = url.hostname; + options.username = url.username; + options.password = url.password; + } + + (options as RedisNetSocketOptions).port ??= 6379; + (options as RedisNetSocketOptions).host ??= '127.0.0.1'; + } + + options.connectTimeout ??= 5000; + options.keepAlive ??= 5000; + options.noDelay ??= true; + + return options; + } + + static #defaultReconnectStrategy(retries: number): number { + return Math.min(retries * 50, 500); + } + + static #isUrlSocket(options: RedisSocketOptions): options is RedisUrlSocketOptions { + return Object.prototype.hasOwnProperty.call(options, 'url'); + } + + static #isUnixSocket(options: RedisSocketOptions): options is RedisUnixSocketOptions { + return Object.prototype.hasOwnProperty.call(options, 'path'); + } + + static #isTlsSocket(options: RedisSocketOptions): options is RedisTlsSocketOptions { + return (options as RedisTlsSocketOptions).tls === true; + } + + readonly #initiator?: RedisSocketInitiator; + + readonly #options: RedisSocketOptions; + + #socket?: net.Socket | tls.TLSSocket; + + #isOpen = false; + + get isOpen(): boolean { + return this.#isOpen; + } + + get chunkRecommendedSize(): number { + if (!this.#socket) return 0; + + return this.#socket.writableHighWaterMark - this.#socket.writableLength; + } + + constructor(initiator?: RedisSocketInitiator, options?: RedisSocketOptions) { + super(); + + this.#initiator = initiator; + this.#options = RedisSocket.#initiateOptions(options); + } + + async connect(): Promise { + if (this.#isOpen) { + throw new Error('Socket is connection/connecting'); + } + + this.#isOpen = true; + + try { + await this.#connect(); + } catch (err) { + this.#isOpen = false; + throw err; + } + } + + async #connect(hadError?: boolean): Promise { + this.#socket = await this.#retryConnection(0, hadError); + this.emit('connect'); + + if (this.#initiator) { + try { + await this.#initiator(); + } catch (err) { + this.#socket.end(); + this.#socket = undefined; + throw err; + } + } + + this.emit('ready'); + } + + async #retryConnection(retries: number, hadError?: boolean): Promise { + if (retries > 0 || hadError) { + this.emit('reconnecting'); + } + + try { + return await this.#createSocket(); + } catch (err) { + this.emit('error', err); + + if (!this.#isOpen) { + throw err; + } + + const retryIn = (this.#options?.reconnectStrategy ?? RedisSocket.#defaultReconnectStrategy)(retries); + if (retryIn instanceof Error) { + throw retryIn; + } + + await promiseTimeout(retryIn); + return this.#retryConnection(retries + 1); + } + } + + #createSocket(): Promise { + return new Promise((resolve, reject) => { + const {connectEvent, socket} = RedisSocket.#isTlsSocket(this.#options) ? + this.#createTlsSocket() : + this.#createNetSocket(); + + if (this.#options.connectTimeout) { + socket.setTimeout(this.#options.connectTimeout, () => socket.destroy(new ConnectionTimeoutError())); + } + + socket + .setNoDelay(this.#options.noDelay) + .setKeepAlive(this.#options.keepAlive !== false, this.#options.keepAlive || 0) + .once('error', reject) + .once(connectEvent, () => { + socket + .setTimeout(0) + .off('error', reject) + .once('error', (err: Error) => this.#onSocketError(err)) + .once('close', hadError => { + if (!hadError && this.#isOpen) { + this.#onSocketError(new Error('Socket closed unexpectedly')); + } + }) + .on('drain', () => this.emit('drain')) + .on('data', (data: Buffer) => this.emit('data', data)); + + resolve(socket); + }); + }); + } + + #createNetSocket(): CreateSocketReturn { + return { + connectEvent: 'connect', + socket: net.connect(this.#options as net.NetConnectOpts) // TODO + }; + } + + #createTlsSocket(): CreateSocketReturn { + return { + connectEvent: 'secureConnect', + socket: tls.connect(this.#options as tls.ConnectionOptions) // TODO + }; + } + + #onSocketError(err: Error): void { + this.#socket = undefined; + this.emit('error', err); + + this.#connect(true) + .catch(err => this.emit('error', err)); + } + + write(encodedCommands: string): boolean { + if (!this.#socket) { + throw new ClientClosedError(); + } + + return this.#socket.write(encodedCommands); + } + + async disconnect(ignoreIsOpen = false): Promise { + if ((!ignoreIsOpen && !this.#isOpen) || !this.#socket) { + throw new ClientClosedError(); + } else { + this.#isOpen = false; + } + + this.#socket.end(); + await EventEmitter.once(this.#socket, 'end'); + this.#socket = undefined; + this.emit('end'); + } + + async quit(fn: () => Promise): Promise { + if (!this.#isOpen) { + throw new ClientClosedError(); + } + + this.#isOpen = false; + + + try { + await fn(); + await this.disconnect(true); + } catch (err) { + this.#isOpen = true; + throw err; + } + } +} diff --git a/lib/test-utils.ts b/lib/test-utils.ts new file mode 100644 index 0000000000..e23d90d47e --- /dev/null +++ b/lib/test-utils.ts @@ -0,0 +1,373 @@ +import { strict as assert } from 'assert'; +import RedisClient, { RedisClientType } from './client'; +import { RedisModules } from './commands'; +import { RedisLuaScripts } from './lua-script'; +import { execSync, spawn } from 'child_process'; +import { once } from 'events'; +import { RedisSocketOptions } from './socket'; +import which from 'which'; +import { SinonSpy } from 'sinon'; +import RedisCluster, { RedisClusterType } from './cluster'; +import { promises as fs } from 'fs'; +import { Context as MochaContext } from 'mocha'; +import { promiseTimeout } from './utils'; + +type RedisVersion = [major: number, minor: number, patch: number]; + +type PartialRedisVersion = RedisVersion | [major: number, minor: number] | [major: number]; + +const REDIS_PATH = which.sync('redis-server'); +export const REDIS_VERSION = getRedisVersion(); + +function getRedisVersion(): RedisVersion { + const raw = execSync(`${REDIS_PATH} -v`).toString(), + indexOfVersion = raw.indexOf('v='); + + if (indexOfVersion === -1) { + throw new Error('Unknown redis version'); + } + + const start = indexOfVersion + 2; + return raw.substring( + start, + raw.indexOf(' ', start) + ).split('.', 3).map(Number) as RedisVersion; +} + +export function isRedisVersionGreaterThan(minimumVersion: PartialRedisVersion | undefined): boolean { + if (minimumVersion === undefined) return true; + + const lastIndex = minimumVersion.length - 1; + for (let i = 0; i < lastIndex; i++) { + if (REDIS_VERSION[i] > minimumVersion[i]) { + return true; + } else if (minimumVersion[i] > REDIS_VERSION[i]) { + return false; + } + } + + return REDIS_VERSION[lastIndex] >= minimumVersion[lastIndex]; +} + +export enum TestRedisServers { + OPEN, + PASSWORD +} + +export const TEST_REDIS_SERVERS: Record = {}; + +export enum TestRedisClusters { + OPEN +} + +export const TEST_REDIS_CLUSTERES: Record> = {}; + +let port = 6379; + +interface SpawnRedisServerResult { + port: number; + cleanup: () => Promise; +} + +async function spawnRedisServer(args?: Array): Promise { + const currentPort = port++, + process = spawn(REDIS_PATH, [ + '--save', + '', + '--port', + currentPort.toString(), + ...(args ?? []) + ]); + + process + .once('error', err => console.error('Redis process error', err)) + .once('close', code => console.error(`Redis process closed unexpectedly with code ${code}`)); + + for await (const chunk of process.stdout) { + if (chunk.toString().includes('Ready to accept connections')) { + break; + } + } + + if (process.exitCode !== null) { + throw new Error('Error while spawning redis server'); + } + + return { + port: currentPort, + async cleanup(): Promise { + process.removeAllListeners('close'); + assert.ok(process.kill()); + await once(process, 'close'); + } + }; +} + +async function spawnGlobalRedisServer(args?: Array): Promise { + const { port, cleanup } = await spawnRedisServer(args); + after(cleanup); + return port; +} + +const SLOTS = 16384; + +interface SpawnRedisClusterNodeResult extends SpawnRedisServerResult { + client: RedisClientType +} + +async function spawnRedisClusterNode( + type: TestRedisClusters | null, + nodeIndex: number, + fromSlot: number, + toSlot: number, + args?: Array +): Promise { + const clusterConfigFile = `/tmp/${type}-${nodeIndex}.conf`, + { port, cleanup: originalCleanup } = await spawnRedisServer([ + '--cluster-enabled', + 'yes', + '--cluster-node-timeout', + '5000', + '--cluster-config-file', + clusterConfigFile, + ...(args ?? []) + ]); + + const client = RedisClient.create({ + socket: { + port + } + }); + + await client.connect(); + + const range = []; + for (let i = fromSlot; i < toSlot; i++) { + range.push(i); + } + + await Promise.all([ + client.clusterFlushSlots(), + client.clusterAddSlots(range) + ]); + + return { + port, + async cleanup(): Promise { + await originalCleanup(); + + try { + await fs.unlink(clusterConfigFile); + } catch (err: any) { + if (err.code === 'ENOENT') return; + + throw err; + } + }, + client + }; +} + +export async function spawnRedisCluster(type: TestRedisClusters | null, numberOfNodes: number, args?: Array): Promise> { + const spawnPromises = [], + slotsPerNode = Math.floor(SLOTS / numberOfNodes); + for (let i = 0; i < numberOfNodes; i++) { + const fromSlot = i * slotsPerNode; + spawnPromises.push( + spawnRedisClusterNode( + type, + i, + fromSlot, + i === numberOfNodes - 1 ? SLOTS : fromSlot + slotsPerNode, + args + ) + ); + } + + const spawnResults = await Promise.all(spawnPromises), + meetPromises = []; + for (let i = 1; i < spawnResults.length; i++) { + meetPromises.push( + spawnResults[i].client.clusterMeet( + '127.0.0.1', + spawnResults[i - 1].port + ) + ); + } + + await Promise.all(meetPromises); + + while (!(await clusterIsReady(spawnResults))) { + await promiseTimeout(100); + } + + await Promise.all( + spawnResults.map(result => result.client.disconnect()) + ); + + return spawnResults; +} + +async function clusterIsReady(spawnResults: Array): Promise { + const nodesClusetrInfo = await Promise.all( + spawnResults.map(result => result.client.clusterInfo()) + ); + + return nodesClusetrInfo.every(({ state }) => state === 'ok'); +} + +export async function spawnGlobalRedisCluster(type: TestRedisClusters | null, numberOfNodes: number, args?: Array): Promise> { + const results = await spawnRedisCluster(type, numberOfNodes, args); + + after(() => Promise.all( + results.map(({ cleanup }) => cleanup()) + )); + + return results.map(({ port }) => port); +} + +async function spawnOpenServer(): Promise { + TEST_REDIS_SERVERS[TestRedisServers.OPEN] = { + port: await spawnGlobalRedisServer() + }; +} + +async function spawnPasswordServer(): Promise { + TEST_REDIS_SERVERS[TestRedisServers.PASSWORD] = { + port: await spawnGlobalRedisServer(['--requirepass', 'password']), + password: 'password' + }; + + if (isRedisVersionGreaterThan([6])) { + TEST_REDIS_SERVERS[TestRedisServers.PASSWORD].username = 'default'; + } +} + +async function spawnOpenCluster(): Promise { + TEST_REDIS_CLUSTERES[TestRedisClusters.OPEN] = (await spawnGlobalRedisCluster(TestRedisClusters.OPEN, 3)).map(port => ({ + port + })); +} + +before(function () { + this.timeout(10000); + + return Promise.all([ + spawnOpenServer(), + spawnPasswordServer(), + spawnOpenCluster() + ]); +}); + +interface RedisTestOptions { + minimumRedisVersion?: PartialRedisVersion; +} + +export function handleMinimumRedisVersion(mochaContext: MochaContext, minimumVersion: PartialRedisVersion | undefined): boolean { + if (isRedisVersionGreaterThan(minimumVersion)) { + return false; + } + + mochaContext.skip(); + return true; +} + +export function describeHandleMinimumRedisVersion(minimumVersion: PartialRedisVersion): void { + before(function () { + handleMinimumRedisVersion(this, minimumVersion); + }); +} + +export function itWithClient( + type: TestRedisServers, + title: string, + fn: (client: RedisClientType) => Promise, + options?: RedisTestOptions +): void { + it(title, async function () { + if (handleMinimumRedisVersion(this, options?.minimumRedisVersion)) return; + + const client = RedisClient.create({ + socket: TEST_REDIS_SERVERS[type] + }); + + await client.connect(); + + try { + await client.flushAll(); + await fn(client); + } finally { + await client.flushAll(); + await client.disconnect(); + } + }); +} + +export function itWithCluster( + type: TestRedisClusters, + title: string, + fn: (cluster: RedisClusterType) => Promise, + options?: RedisTestOptions +): void { + it(title, async function () { + if (handleMinimumRedisVersion(this, options?.minimumRedisVersion)) return; + + const cluster = RedisCluster.create({ + rootNodes: TEST_REDIS_CLUSTERES[type] + }); + + await cluster.connect(); + + try { + await clusterFlushAll(cluster); + await fn(cluster); + } finally { + await clusterFlushAll(cluster); + await cluster.disconnect(); + } + }); +} + +export function itWithDedicatedCluster(title: string, fn: (cluster: RedisClusterType) => Promise): void { + it(title, async function () { + this.timeout(10000); + + const spawnResults = await spawnRedisCluster(null, 3), + cluster = RedisCluster.create({ + rootNodes: [{ + port: spawnResults[0].port + }] + }); + + await cluster.connect(); + + try { + await fn(cluster); + } finally { + await cluster.disconnect(); + + for (const { cleanup } of spawnResults) { + await cleanup(); + } + } + }); +} + +async function clusterFlushAll(cluster: RedisCluster): Promise { + await Promise.all( + cluster.getMasters().map(({ client }) => client.flushAll()) + ); +} + +export async function waitTillBeenCalled(spy: SinonSpy): Promise { + const start = process.hrtime.bigint(), + calls = spy.callCount; + + do { + if (process.hrtime.bigint() - start > 1_000_000_000) { + throw new Error('Waiting for more than 1 second'); + } + + await promiseTimeout(1); + } while (spy.callCount === calls) +} \ No newline at end of file diff --git a/lib/ts-declarations/cluster-key-slot.d.ts b/lib/ts-declarations/cluster-key-slot.d.ts new file mode 100644 index 0000000000..5774c50fbd --- /dev/null +++ b/lib/ts-declarations/cluster-key-slot.d.ts @@ -0,0 +1,3 @@ +declare module 'cluster-key-slot' { + export default function calculateSlot(key: string): number; +} diff --git a/lib/ts-declarations/redis-parser.d.ts b/lib/ts-declarations/redis-parser.d.ts new file mode 100644 index 0000000000..68659616b9 --- /dev/null +++ b/lib/ts-declarations/redis-parser.d.ts @@ -0,0 +1,13 @@ +declare module 'redis-parser' { + interface RedisParserCallbacks { + returnReply(reply: unknown): void; + returnError(err: Error): void; + returnFatalError?(err: Error): void; + } + + export default class RedisParser { + constructor(callbacks: RedisParserCallbacks); + + execute(buffer: Buffer): void; + } +} diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index d0336ae9c1..0000000000 --- a/lib/utils.js +++ /dev/null @@ -1,134 +0,0 @@ -'use strict'; - -// hgetall converts its replies to an Object. If the reply is empty, null is returned. -// These function are only called with internal data and have therefore always the same instanceof X -function replyToObject (reply) { - // The reply might be a string or a buffer if this is called in a transaction (multi) - if (reply.length === 0 || !(reply instanceof Array)) { - return null; - } - var obj = {}; - for (var i = 0; i < reply.length; i += 2) { - obj[reply[i].toString('binary')] = reply[i + 1]; - } - return obj; -} - -function replyToStrings (reply) { - if (reply instanceof Buffer) { - return reply.toString(); - } - if (reply instanceof Array) { - var res = new Array(reply.length); - for (var i = 0; i < reply.length; i++) { - // Recusivly call the function as slowlog returns deep nested replies - res[i] = replyToStrings(reply[i]); - } - return res; - } - - return reply; -} - -function print (err, reply) { - if (err) { - // A error always begins with Error: - console.log(err.toString()); - } else { - console.log('Reply: ' + reply); - } -} - -var camelCase; -// Deep clone arbitrary objects with arrays. Can't handle cyclic structures (results in a range error) -// Any attribute with a non primitive value besides object and array will be passed by reference (e.g. Buffers, Maps, Functions) -// All capital letters are going to be replaced with a lower case letter and a underscore infront of it -function clone (obj) { - var copy; - if (Array.isArray(obj)) { - copy = new Array(obj.length); - for (var i = 0; i < obj.length; i++) { - copy[i] = clone(obj[i]); - } - return copy; - } - if (Object.prototype.toString.call(obj) === '[object Object]') { - copy = {}; - var elems = Object.keys(obj); - var elem; - while (elem = elems.pop()) { - if (elem === 'tls') { // special handle tls - copy[elem] = obj[elem]; - continue; - } - // Accept camelCase options and convert them to snake_case - var snake_case = elem.replace(/[A-Z][^A-Z]/g, '_$&').toLowerCase(); - // If camelCase is detected, pass it to the client, so all variables are going to be camelCased - // There are no deep nested options objects yet, but let's handle this future proof - if (snake_case !== elem.toLowerCase()) { - camelCase = true; - } - copy[snake_case] = clone(obj[elem]); - } - return copy; - } - return obj; -} - -function convenienceClone (obj) { - camelCase = false; - obj = clone(obj) || {}; - if (camelCase) { - obj.camel_case = true; - } - return obj; -} - -function callbackOrEmit (self, callback, err, res) { - if (callback) { - callback(err, res); - } else if (err) { - self.emit('error', err); - } -} - -function replyInOrder (self, callback, err, res, queue) { - // If the queue is explicitly passed, use that, otherwise fall back to the offline queue first, - // as there might be commands in both queues at the same time - var command_obj; - /* istanbul ignore if: TODO: Remove this as soon as we test Redis 3.2 on travis */ - if (queue) { - command_obj = queue.peekBack(); - } else { - command_obj = self.offline_queue.peekBack() || self.command_queue.peekBack(); - } - if (!command_obj) { - process.nextTick(function () { - callbackOrEmit(self, callback, err, res); - }); - } else { - var tmp = command_obj.callback; - command_obj.callback = tmp ? - function (e, r) { - tmp(e, r); - callbackOrEmit(self, callback, err, res); - } : - function (e, r) { - if (e) { - self.emit('error', e); - } - callbackOrEmit(self, callback, err, res); - }; - } -} - -module.exports = { - reply_to_strings: replyToStrings, - reply_to_object: replyToObject, - print: print, - err_code: /^([A-Z]+)\s+(.+)$/, - monitor_regex: /^[0-9]{10,11}\.[0-9]+ \[[0-9]+ .+\].*"$/, - clone: convenienceClone, - callback_or_emit: callbackOrEmit, - reply_in_order: replyInOrder -}; diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000000..55bed41981 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,3 @@ +export function promiseTimeout(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..3b7397b61e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9965 @@ +{ + "name": "redis", + "version": "4.0.0-rc.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "redis", + "version": "4.0.0-rc.0", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.0", + "generic-pool": "3.8.2", + "redis-parser": "3.0.0", + "yallist": "4.0.0" + }, + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.1", + "@types/mocha": "^9.0.0", + "@types/node": "^16.7.8", + "@types/sinon": "^10.0.2", + "@types/which": "^2.0.1", + "@types/yallist": "^4.0.1", + "mocha": "^9.1.1", + "nyc": "^15.1.0", + "release-it": "^14.11.5", + "sinon": "^11.1.2", + "source-map-support": "^0.5.19", + "ts-node": "^10.2.1", + "typedoc": "^0.21.9", + "typedoc-github-wiki-theme": "^0.5.1", + "typedoc-plugin-markdown": "^3.10.4", + "typescript": "^4.4.2", + "which": "^2.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.0.tgz", + "integrity": "sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.0", + "@babel/helper-compilation-targets": "^7.15.0", + "@babel/helper-module-transforms": "^7.15.0", + "@babel/helpers": "^7.14.8", + "@babel/parser": "^7.15.0", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.0.tgz", + "integrity": "sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz", + "integrity": "sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", + "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", + "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", + "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.0.tgz", + "integrity": "sha512-Jq8H8U2kYiafuj2xMTPQwkTBnEEdGKpT35lJEQsRRjnG0LW3neucsaMWLgKcwu3OHKNeYugfw+Z20BXBSEs2Lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", + "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz", + "integrity": "sha512-RkGiW5Rer7fpXv9m1B3iHIFDZdItnO2/BLfWVW/9q7+KqQSDY5kUfQEbzdXM1MVhJGcugKV7kRrNVzNxmk7NBg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-replace-supers": "^7.15.0", + "@babel/helper-simple-access": "^7.14.8", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.9", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", + "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.0.tgz", + "integrity": "sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.15.0", + "@babel/helper-optimise-call-expression": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.14.8", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz", + "integrity": "sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.14.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", + "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.3.tgz", + "integrity": "sha512-HwJiz52XaS96lX+28Tnbu31VeFSQJGOeKHJeaEPQlTl7PnlhFElWPj8tUXtqFIzeN86XxXoBr+WFAyK2PPVz6g==", + "dev": true, + "dependencies": { + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz", + "integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.0.tgz", + "integrity": "sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.0", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/parser": "^7.15.0", + "@babel/types": "^7.15.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz", + "integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", + "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/nyc-config-typescript": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.1.tgz", + "integrity": "sha512-/gz6LgVpky205LuoOfwEZmnUtaSmdk0QIMcNFj9OvxhiMhPpKftMgZmGN7jNj7jR+lr8IB1Yks3QSSSNSxfoaQ==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "nyc": ">=15", + "source-map-support": "*", + "ts-node": "*" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/auth-token": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz", + "integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==", + "dev": true, + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@octokit/core": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", + "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", + "dev": true, + "dependencies": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.0", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dev": true, + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.7.0.tgz", + "integrity": "sha512-diY0qMPyQjfu4rDu3kDhJ9qIZadIm4IISO3RJSv9ajYUWJUCO0AykbgzLcg1xclxtXgzY583u3gAv66M6zz5SA==", + "dev": true, + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-9.7.0.tgz", + "integrity": "sha512-TUJ16DJU8mekne6+KVcMV5g6g/rJlrnIKn7aALG9QrNpnEipFc1xjoarh0PKaAWf2Hf+HwthRKYt+9mCm5RsRg==", + "dev": true + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.1.tgz", + "integrity": "sha512-47r52KkhQDkmvUKZqXzA1lKvcyJEfYh3TKAIe5+EzMeyDM3d+/s5v11i2gTk8/n6No6DPi3k5Ind6wtDbo/AEg==", + "dev": true, + "dependencies": { + "@octokit/types": "^6.24.0" + }, + "peerDependencies": { + "@octokit/core": ">=2" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "dev": true, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.7.0.tgz", + "integrity": "sha512-G7sgccWRYQMwcHJXkDY/sDxbXeKiZkFQqUtzBCwmrzCNj2GQf3VygQ4T/BFL2crLVpIbenkE/c0ErhYOte2MPw==", + "dev": true, + "dependencies": { + "@octokit/types": "^6.24.0", + "deprecation": "^2.3.1" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/request": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.1.tgz", + "integrity": "sha512-Ls2cfs1OfXaOKzkcxnqw5MR6drMA/zWX/LIS/p8Yjdz7QKTPQLMsB3R+OvoxE6XnXeXEE2X7xe4G4l4X0gRiKQ==", + "dev": true, + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.1", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dev": true, + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/rest": { + "version": "18.9.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.9.0.tgz", + "integrity": "sha512-VrmrE8gjpuOoDAGjrQq2j9ZhOE6LxaqxaQg0yMrrEnnQZy2ZcAnr5qbVfKsMF0up/48PRV/VFS/2GSMhA7nTdA==", + "dev": true, + "dependencies": { + "@octokit/core": "^3.5.0", + "@octokit/plugin-paginate-rest": "^2.6.2", + "@octokit/plugin-request-log": "^1.0.2", + "@octokit/plugin-rest-endpoint-methods": "5.7.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.25.0.tgz", + "integrity": "sha512-bNvyQKfngvAd/08COlYIN54nRgxskmejgywodizQNyiKoXmWRAjKup2/LYwm+T9V0gsKH6tuld1gM0PzmOiB4Q==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^9.5.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz", + "integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", + "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "dev": true + }, + "node_modules/@types/keyv": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.2.tgz", + "integrity": "sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mocha": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", + "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.7.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.8.tgz", + "integrity": "sha512-8upnoQU0OPzbIkm+ZMM0zCeFCkw2s3mS0IWdx0+AAaWqm4fkBb0UJp8Edl7FVKRamYbpJC/aVsHpKWBIbiC7Zg==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/sinon": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.2.tgz", + "integrity": "sha512-BHn8Bpkapj8Wdfxvh2jWIUoaYB/9/XhsL0oOvBfRagJtKlSl9NWPcFOz2lRukI9szwGxFtYZCTejJSqsGDbdmw==", + "dev": true, + "dependencies": { + "@sinonjs/fake-timers": "^7.1.0" + } + }, + "node_modules/@types/which": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.1.tgz", + "integrity": "sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==", + "dev": true + }, + "node_modules/@types/yallist": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/yallist/-/yallist-4.0.1.tgz", + "integrity": "sha512-G3FNJfaYtN8URU6wd6+uwFI62KO79j7n3XTYcwcFncP8gkfoi0b821GoVVt0oqKVnCqKYOMNKIGpakPoFhzAGA==", + "dev": true + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.1.tgz", + "integrity": "sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "dev": true, + "dependencies": { + "string-width": "^3.0.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/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==", + "dev": true, + "engines": { + "node": ">=10" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz", + "integrity": "sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA==", + "dev": true, + "dependencies": { + "retry": "0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/before-after-hook": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", + "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boxen": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.0.1.tgz", + "integrity": "sha512-49VBlw+PrWEF51aCmy7QIteYPIFZxSpvqBdP/2itCPPlJ49kj9zg/XPRFrdkne2W+CfwXUls8exMvu1RysZpKA==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.0", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.16.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz", + "integrity": "sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001251", + "colorette": "^1.3.0", + "electron-to-chromium": "^1.3.811", + "escalade": "^3.1.1", + "node-releases": "^1.1.75" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001252", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz", + "integrity": "sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", + "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", + "dev": true + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz", + "integrity": "sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/colorette": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.3.0.tgz", + "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/deprecated-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deprecated-obj/-/deprecated-obj-2.0.0.tgz", + "integrity": "sha512-CkdywZC2rJ8RGh+y3MM1fw1EJ4oO/oNExGbRFv0AQoMS+faTd3nO7slYjkj/6t8OnIMUE+wxh6G97YHhK1ytrw==", + "dev": true, + "dependencies": { + "flat": "^5.0.2", + "lodash": "^4.17.20" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.3.822", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.822.tgz", + "integrity": "sha512-k7jG5oYYHxF4jx6PcqwHX3JVME/OjzolqOZiIogi9xtsfsmTjTdie4x88OakYFPEa8euciTgCCzvVNwvmjHb1Q==", + "dev": true + }, + "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==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fastq": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.12.0.tgz", + "integrity": "sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/generic-pool": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", + "integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-up": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.5.tgz", + "integrity": "sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA==", + "dev": true, + "dependencies": { + "is-ssh": "^1.3.0", + "parse-url": "^6.0.0" + } + }, + "node_modules/git-url-parse": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.5.0.tgz", + "integrity": "sha512-TZYSMDeM37r71Lqg1mbnMlOqlHd7BSij9qN7XwTkRqSAYFMihGLGhfHwgqQob3GUhEneKnV4nskN9rbQw2KGxA==", + "dev": true, + "dependencies": { + "git-up": "^4.0.0" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "11.8.2", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", + "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "dependencies": { + "import-from": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/inquirer": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.1.2.tgz", + "integrity": "sha512-DHLKJwLPNgkfwNmsuEUKSejJFbkv0FMO9SMiQbjI3n5NQuCrSIBqP66ggqyz2a6t2qEolKrMjhQ3+W/xXgUQ+Q==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.3.0", + "run-async": "^2.4.0", + "rxjs": "^7.2.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz", + "integrity": "sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.1.1" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-ssh": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.3.tgz", + "integrity": "sha512-NKzJmQzJfEEma3w5cJNcUMxoXfDjz0Zj0eyCalHn2E6VOwlzjZo0yuO2fcBSf8zhFuVCL/82/r5gRcoi6aEPVQ==", + "dev": true, + "dependencies": { + "protocols": "^1.1.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "dependencies": { + "package-json": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "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=", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "node_modules/macos-release": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", + "integrity": "sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/marked": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.2.tgz", + "integrity": "sha512-TMJQQ79Z0e3rJYazY0tIoMsFzteUGw9fB3FD+gzuIT3zLuG9L9ckIvUfF51apdJkcqc208jJN2KbtPbOvXtbjA==", + "dev": true, + "bin": { + "marked": "bin/marked" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", + "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "dev": true, + "dependencies": { + "mime-db": "1.49.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "node_modules/mocha": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-0wE74YMgOkCgBUj8VyIDwmLUjTsS13WV1Pg7l0SHea2qzZzlq7MDnfbPsHKcELBRk3+izEVkRofjmClpycudCA==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.2", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.7", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.1.23", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.1.5", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/new-github-release-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/new-github-release-url/-/new-github-release-url-1.0.0.tgz", + "integrity": "sha512-dle7yf655IMjyFUqn6Nxkb18r4AOAkzRcgcZv6WZ0IqrOH4QCEZ8Sm6I7XX21zvHdBeeMeTkhR9qT2Z0EJDx6A==", + "dev": true, + "dependencies": { + "type-fest": "^0.4.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/new-github-release-url/node_modules/type-fest": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz", + "integrity": "sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/nise": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "1.1.75", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", + "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/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==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onigasm": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.5.tgz", + "integrity": "sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA==", + "dev": true, + "dependencies": { + "lru-cache": "^5.1.1" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-name": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-4.0.1.tgz", + "integrity": "sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==", + "dev": true, + "dependencies": { + "macos-release": "^2.5.0", + "windows-release": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, + "dependencies": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json/node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json/node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json/node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/package-json/node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "node_modules/package-json/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json/node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/package-json/node_modules/got/node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/package-json/node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "node_modules/package-json/node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/package-json/node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json/node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/package-json/node_modules/responselike/node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-path": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.3.tgz", + "integrity": "sha512-9Cepbp2asKnWTJ9x2kpw6Fe8y9JDbqwahGCTvklzd/cEq5C5JC59x2Xb0Kx+x0QZ8bvNquGO8/BWP0cwBHzSAA==", + "dev": true, + "dependencies": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0", + "qs": "^6.9.4", + "query-string": "^6.13.8" + } + }, + "node_modules/parse-url": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-6.0.0.tgz", + "integrity": "sha512-cYyojeX7yIIwuJzledIHeLUBVJ6COVLeT4eF+2P6aKVzwvgKQPndCBv3+yQ7pcWjqToYwaligxzSYNNmGoMAvw==", + "dev": true, + "dependencies": { + "is-ssh": "^1.3.0", + "normalize-url": "^6.1.0", + "parse-path": "^4.0.0", + "protocols": "^1.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dev": true, + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", + "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", + "dev": true, + "dependencies": { + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "dev": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/release-it": { + "version": "14.11.5", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-14.11.5.tgz", + "integrity": "sha512-9BaPdq7ZKOwtzz3p1mRhg/tOH/cT/y2tUnPYzUwQiVdj42JaGI1Vo2l3WbgK8ICsbFyrhc0tri1+iqI8OvkI1A==", + "dev": true, + "dependencies": { + "@iarna/toml": "2.2.5", + "@octokit/rest": "18.9.0", + "async-retry": "1.3.1", + "chalk": "4.1.2", + "cosmiconfig": "7.0.0", + "debug": "4.3.2", + "deprecated-obj": "2.0.0", + "execa": "5.1.1", + "form-data": "4.0.0", + "git-url-parse": "11.5.0", + "globby": "11.0.4", + "got": "11.8.2", + "import-cwd": "3.0.0", + "inquirer": "8.1.2", + "is-ci": "3.0.0", + "lodash": "4.17.21", + "mime-types": "2.1.32", + "new-github-release-url": "1.0.0", + "open": "7.4.2", + "ora": "5.4.1", + "os-name": "4.0.1", + "parse-json": "5.2.0", + "semver": "7.3.5", + "shelljs": "0.8.4", + "update-notifier": "5.1.0", + "url-join": "4.0.1", + "uuid": "8.3.2", + "yaml": "1.10.2", + "yargs-parser": "20.2.9" + }, + "bin": { + "release-it": "bin/release-it.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/release-it/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/release-it/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/release-it/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/release-it/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/release-it/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/release-it/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz", + "integrity": "sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==", + "dev": true, + "dependencies": { + "tslib": "~2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shiki": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.8.tgz", + "integrity": "sha512-499zQUTjcNTVwwiaPrWldUTXIV3T9HZWxDwE82bY+9GE7P2uD6hpHUTXNbTof3iOG6WT+/062+OMbl0lDoG8WQ==", + "dev": true, + "dependencies": { + "json5": "^2.2.0", + "onigasm": "^2.2.5", + "vscode-textmate": "5.2.0" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true + }, + "node_modules/sinon": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", + "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^7.1.2", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", + "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "0.6.1", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typedoc": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.21.9.tgz", + "integrity": "sha512-VRo7aII4bnYaBBM1lhw4bQFmUcDQV8m8tqgjtc7oXl87jc1Slbhfw2X5MccfcR2YnEClHDWgsiQGgNB8KJXocA==", + "dev": true, + "dependencies": { + "glob": "^7.1.7", + "handlebars": "^4.7.7", + "lunr": "^2.3.9", + "marked": "^3.0.2", + "minimatch": "^3.0.0", + "progress": "^2.0.3", + "shiki": "^0.9.8", + "typedoc-default-themes": "^0.12.10" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 12.10.0" + }, + "peerDependencies": { + "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x" + } + }, + "node_modules/typedoc-default-themes": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz", + "integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/typedoc-github-wiki-theme": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/typedoc-github-wiki-theme/-/typedoc-github-wiki-theme-0.5.1.tgz", + "integrity": "sha512-ED2Uc3WUjbv6xIdCkpMz3yBtcEciJnAhDQdRWLYgw4K+FKY0T3PzbI+ssNsBVgwDnYQP/XuaqfZkeQ3EQsOm9g==", + "dev": true, + "peerDependencies": { + "typedoc": ">=0.20.0", + "typedoc-plugin-markdown": ">=3.4.0" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.10.4.tgz", + "integrity": "sha512-if9w7S9fXLg73AYi/EoRSEhTOZlg3I8mIP8YEmvzSE33VrNXC9/hA0nVcLEwFVJeQY7ay6z67I6kW0KIv7LjeA==", + "dev": true, + "dependencies": { + "handlebars": "^4.7.7" + }, + "peerDependencies": { + "typedoc": ">=0.21.2" + } + }, + "node_modules/typescript": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", + "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uglify-js": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.1.tgz", + "integrity": "sha512-JhS3hmcVaXlp/xSo3PKY5R0JqKs5M3IV+exdLHW99qKvKivPO4Z8qbej6mte17SOPqAOVMjt/XGgWacnFSzM3g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true + }, + "node_modules/update-notifier": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", + "dev": true, + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.1.0", + "pupa": "^2.1.1", + "semver": "^7.3.4", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/update-notifier/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/update-notifier/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/update-notifier/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vscode-textmate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", + "integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==", + "dev": true + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/windows-release": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", + "integrity": "sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==", + "dev": true, + "dependencies": { + "execa": "^4.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/windows-release/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "node_modules/workerpool": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", + "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/compat-data": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", + "dev": true + }, + "@babel/core": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.0.tgz", + "integrity": "sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.0", + "@babel/helper-compilation-targets": "^7.15.0", + "@babel/helper-module-transforms": "^7.15.0", + "@babel/helpers": "^7.14.8", + "@babel/parser": "^7.15.0", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.0.tgz", + "integrity": "sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ==", + "dev": true, + "requires": { + "@babel/types": "^7.15.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz", + "integrity": "sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + } + }, + "@babel/helper-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", + "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", + "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", + "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.0.tgz", + "integrity": "sha512-Jq8H8U2kYiafuj2xMTPQwkTBnEEdGKpT35lJEQsRRjnG0LW3neucsaMWLgKcwu3OHKNeYugfw+Z20BXBSEs2Lg==", + "dev": true, + "requires": { + "@babel/types": "^7.15.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", + "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz", + "integrity": "sha512-RkGiW5Rer7fpXv9m1B3iHIFDZdItnO2/BLfWVW/9q7+KqQSDY5kUfQEbzdXM1MVhJGcugKV7kRrNVzNxmk7NBg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-replace-supers": "^7.15.0", + "@babel/helper-simple-access": "^7.14.8", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.9", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", + "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-replace-supers": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.0.tgz", + "integrity": "sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.15.0", + "@babel/helper-optimise-call-expression": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0" + } + }, + "@babel/helper-simple-access": { + "version": "7.14.8", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz", + "integrity": "sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg==", + "dev": true, + "requires": { + "@babel/types": "^7.14.8" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", + "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "dev": true + }, + "@babel/helpers": { + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.3.tgz", + "integrity": "sha512-HwJiz52XaS96lX+28Tnbu31VeFSQJGOeKHJeaEPQlTl7PnlhFElWPj8tUXtqFIzeN86XxXoBr+WFAyK2PPVz6g==", + "dev": true, + "requires": { + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0" + } + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz", + "integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==", + "dev": true + }, + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/traverse": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.0.tgz", + "integrity": "sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.0", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/parser": "^7.15.0", + "@babel/types": "^7.15.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz", + "integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + } + }, + "@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", + "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "dev": true, + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + }, + "@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "@istanbuljs/nyc-config-typescript": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.1.tgz", + "integrity": "sha512-/gz6LgVpky205LuoOfwEZmnUtaSmdk0QIMcNFj9OvxhiMhPpKftMgZmGN7jNj7jR+lr8IB1Yks3QSSSNSxfoaQ==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@octokit/auth-token": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz", + "integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==", + "dev": true, + "requires": { + "@octokit/types": "^6.0.3" + } + }, + "@octokit/core": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", + "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", + "dev": true, + "requires": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.0", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dev": true, + "requires": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/graphql": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.7.0.tgz", + "integrity": "sha512-diY0qMPyQjfu4rDu3kDhJ9qIZadIm4IISO3RJSv9ajYUWJUCO0AykbgzLcg1xclxtXgzY583u3gAv66M6zz5SA==", + "dev": true, + "requires": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/openapi-types": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-9.7.0.tgz", + "integrity": "sha512-TUJ16DJU8mekne6+KVcMV5g6g/rJlrnIKn7aALG9QrNpnEipFc1xjoarh0PKaAWf2Hf+HwthRKYt+9mCm5RsRg==", + "dev": true + }, + "@octokit/plugin-paginate-rest": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.1.tgz", + "integrity": "sha512-47r52KkhQDkmvUKZqXzA1lKvcyJEfYh3TKAIe5+EzMeyDM3d+/s5v11i2gTk8/n6No6DPi3k5Ind6wtDbo/AEg==", + "dev": true, + "requires": { + "@octokit/types": "^6.24.0" + } + }, + "@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "dev": true, + "requires": {} + }, + "@octokit/plugin-rest-endpoint-methods": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.7.0.tgz", + "integrity": "sha512-G7sgccWRYQMwcHJXkDY/sDxbXeKiZkFQqUtzBCwmrzCNj2GQf3VygQ4T/BFL2crLVpIbenkE/c0ErhYOte2MPw==", + "dev": true, + "requires": { + "@octokit/types": "^6.24.0", + "deprecation": "^2.3.1" + } + }, + "@octokit/request": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.1.tgz", + "integrity": "sha512-Ls2cfs1OfXaOKzkcxnqw5MR6drMA/zWX/LIS/p8Yjdz7QKTPQLMsB3R+OvoxE6XnXeXEE2X7xe4G4l4X0gRiKQ==", + "dev": true, + "requires": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.1", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dev": true, + "requires": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/rest": { + "version": "18.9.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.9.0.tgz", + "integrity": "sha512-VrmrE8gjpuOoDAGjrQq2j9ZhOE6LxaqxaQg0yMrrEnnQZy2ZcAnr5qbVfKsMF0up/48PRV/VFS/2GSMhA7nTdA==", + "dev": true, + "requires": { + "@octokit/core": "^3.5.0", + "@octokit/plugin-paginate-rest": "^2.6.2", + "@octokit/plugin-request-log": "^1.0.2", + "@octokit/plugin-rest-endpoint-methods": "5.7.0" + } + }, + "@octokit/types": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.25.0.tgz", + "integrity": "sha512-bNvyQKfngvAd/08COlYIN54nRgxskmejgywodizQNyiKoXmWRAjKup2/LYwm+T9V0gsKH6tuld1gM0PzmOiB4Q==", + "dev": true, + "requires": { + "@octokit/openapi-types": "^9.5.0" + } + }, + "@sindresorhus/is": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz", + "integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "@types/cacheable-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", + "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", + "dev": true, + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "dev": true + }, + "@types/keyv": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.2.tgz", + "integrity": "sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/mocha": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", + "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "dev": true + }, + "@types/node": { + "version": "16.7.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.8.tgz", + "integrity": "sha512-8upnoQU0OPzbIkm+ZMM0zCeFCkw2s3mS0IWdx0+AAaWqm4fkBb0UJp8Edl7FVKRamYbpJC/aVsHpKWBIbiC7Zg==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/sinon": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.2.tgz", + "integrity": "sha512-BHn8Bpkapj8Wdfxvh2jWIUoaYB/9/XhsL0oOvBfRagJtKlSl9NWPcFOz2lRukI9szwGxFtYZCTejJSqsGDbdmw==", + "dev": true, + "requires": { + "@sinonjs/fake-timers": "^7.1.0" + } + }, + "@types/which": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.1.tgz", + "integrity": "sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==", + "dev": true + }, + "@types/yallist": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/yallist/-/yallist-4.0.1.tgz", + "integrity": "sha512-G3FNJfaYtN8URU6wd6+uwFI62KO79j7n3XTYcwcFncP8gkfoi0b821GoVVt0oqKVnCqKYOMNKIGpakPoFhzAGA==", + "dev": true + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "acorn": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", + "dev": true + }, + "acorn-walk": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.1.tgz", + "integrity": "sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==", + "dev": true + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "dev": true, + "requires": { + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "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==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "async-retry": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz", + "integrity": "sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA==", + "dev": true, + "requires": { + "retry": "0.12.0" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "before-after-hook": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", + "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "boxen": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.0.1.tgz", + "integrity": "sha512-49VBlw+PrWEF51aCmy7QIteYPIFZxSpvqBdP/2itCPPlJ49kj9zg/XPRFrdkne2W+CfwXUls8exMvu1RysZpKA==", + "dev": true, + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.0", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserslist": { + "version": "4.16.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz", + "integrity": "sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001251", + "colorette": "^1.3.0", + "electron-to-chromium": "^1.3.811", + "escalade": "^3.1.1", + "node-releases": "^1.1.75" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001252", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz", + "integrity": "sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "ci-info": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", + "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", + "dev": true + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz", + "integrity": "sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==", + "dev": true + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==" + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "colorette": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.3.0.tgz", + "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + } + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "deprecated-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deprecated-obj/-/deprecated-obj-2.0.0.tgz", + "integrity": "sha512-CkdywZC2rJ8RGh+y3MM1fw1EJ4oO/oNExGbRFv0AQoMS+faTd3nO7slYjkj/6t8OnIMUE+wxh6G97YHhK1ytrw==", + "dev": true, + "requires": { + "flat": "^5.0.2", + "lodash": "^4.17.20" + } + }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.822", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.822.tgz", + "integrity": "sha512-k7jG5oYYHxF4jx6PcqwHX3JVME/OjzolqOZiIogi9xtsfsmTjTdie4x88OakYFPEa8euciTgCCzvVNwvmjHb1Q==", + "dev": true + }, + "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==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fastq": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.12.0.tgz", + "integrity": "sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + } + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=", + "dev": true + }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "generic-pool": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", + "integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==" + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "git-up": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.5.tgz", + "integrity": "sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA==", + "dev": true, + "requires": { + "is-ssh": "^1.3.0", + "parse-url": "^6.0.0" + } + }, + "git-url-parse": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.5.0.tgz", + "integrity": "sha512-TZYSMDeM37r71Lqg1mbnMlOqlHd7BSij9qN7XwTkRqSAYFMihGLGhfHwgqQob3GUhEneKnV4nskN9rbQw2KGxA==", + "dev": true, + "requires": { + "git-up": "^4.0.0" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "dev": true, + "requires": { + "ini": "2.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "got": { + "version": "11.8.2", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", + "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "dev": true, + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true + }, + "hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "requires": { + "import-from": "^3.0.0" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true + }, + "inquirer": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.1.2.tgz", + "integrity": "sha512-DHLKJwLPNgkfwNmsuEUKSejJFbkv0FMO9SMiQbjI3n5NQuCrSIBqP66ggqyz2a6t2qEolKrMjhQ3+W/xXgUQ+Q==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.3.0", + "run-async": "^2.4.0", + "rxjs": "^7.2.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + } + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-ci": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz", + "integrity": "sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==", + "dev": true, + "requires": { + "ci-info": "^3.1.1" + } + }, + "is-core-module": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "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==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "requires": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, + "is-ssh": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.3.tgz", + "integrity": "sha512-NKzJmQzJfEEma3w5cJNcUMxoXfDjz0Zj0eyCalHn2E6VOwlzjZo0yuO2fcBSf8zhFuVCL/82/r5gRcoi6aEPVQ==", + "dev": true, + "requires": { + "protocols": "^1.1.0" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "requires": { + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "requires": { + "package-json": "^6.3.0" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "macos-release": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", + "integrity": "sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==", + "dev": true + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "marked": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.2.tgz", + "integrity": "sha512-TMJQQ79Z0e3rJYazY0tIoMsFzteUGw9fB3FD+gzuIT3zLuG9L9ckIvUfF51apdJkcqc208jJN2KbtPbOvXtbjA==", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mime-db": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "dev": true + }, + "mime-types": { + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", + "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "dev": true, + "requires": { + "mime-db": "1.49.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mocha": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-0wE74YMgOkCgBUj8VyIDwmLUjTsS13WV1Pg7l0SHea2qzZzlq7MDnfbPsHKcELBRk3+izEVkRofjmClpycudCA==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.2", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.7", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.1.23", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.1.5", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "new-github-release-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/new-github-release-url/-/new-github-release-url-1.0.0.tgz", + "integrity": "sha512-dle7yf655IMjyFUqn6Nxkb18r4AOAkzRcgcZv6WZ0IqrOH4QCEZ8Sm6I7XX21zvHdBeeMeTkhR9qT2Z0EJDx6A==", + "dev": true, + "requires": { + "type-fest": "^0.4.1" + }, + "dependencies": { + "type-fest": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz", + "integrity": "sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw==", + "dev": true + } + } + }, + "nise": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true + }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "node-releases": { + "version": "1.1.75", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", + "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "dependencies": { + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.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==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "onigasm": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.5.tgz", + "integrity": "sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA==", + "dev": true, + "requires": { + "lru-cache": "^5.1.1" + } + }, + "open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + } + }, + "os-name": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-4.0.1.tgz", + "integrity": "sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==", + "dev": true, + "requires": { + "macos-release": "^2.5.0", + "windows-release": "^4.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "dependencies": { + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + } + } + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "dev": true + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + }, + "dependencies": { + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + } + } + } + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-path": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.3.tgz", + "integrity": "sha512-9Cepbp2asKnWTJ9x2kpw6Fe8y9JDbqwahGCTvklzd/cEq5C5JC59x2Xb0Kx+x0QZ8bvNquGO8/BWP0cwBHzSAA==", + "dev": true, + "requires": { + "is-ssh": "^1.3.0", + "protocols": "^1.4.0", + "qs": "^6.9.4", + "query-string": "^6.13.8" + } + }, + "parse-url": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-6.0.0.tgz", + "integrity": "sha512-cYyojeX7yIIwuJzledIHeLUBVJ6COVLeT4eF+2P6aKVzwvgKQPndCBv3+yQ7pcWjqToYwaligxzSYNNmGoMAvw==", + "dev": true, + "requires": { + "is-ssh": "^1.3.0", + "normalize-url": "^6.1.0", + "parse-path": "^4.0.0", + "protocols": "^1.4.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "protocols": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz", + "integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dev": true, + "requires": { + "escape-goat": "^2.0.0" + } + }, + "qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "query-string": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", + "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", + "dev": true, + "requires": { + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "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" + } + }, + "registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "dev": true, + "requires": { + "rc": "^1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "requires": { + "rc": "^1.2.8" + } + }, + "release-it": { + "version": "14.11.5", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-14.11.5.tgz", + "integrity": "sha512-9BaPdq7ZKOwtzz3p1mRhg/tOH/cT/y2tUnPYzUwQiVdj42JaGI1Vo2l3WbgK8ICsbFyrhc0tri1+iqI8OvkI1A==", + "dev": true, + "requires": { + "@iarna/toml": "2.2.5", + "@octokit/rest": "18.9.0", + "async-retry": "1.3.1", + "chalk": "4.1.2", + "cosmiconfig": "7.0.0", + "debug": "4.3.2", + "deprecated-obj": "2.0.0", + "execa": "5.1.1", + "form-data": "4.0.0", + "git-url-parse": "11.5.0", + "globby": "11.0.4", + "got": "11.8.2", + "import-cwd": "3.0.0", + "inquirer": "8.1.2", + "is-ci": "3.0.0", + "lodash": "4.17.21", + "mime-types": "2.1.32", + "new-github-release-url": "1.0.0", + "open": "7.4.2", + "ora": "5.4.1", + "os-name": "4.0.1", + "parse-json": "5.2.0", + "semver": "7.3.5", + "shelljs": "0.8.4", + "update-notifier": "5.1.0", + "url-join": "4.0.1", + "uuid": "8.3.2", + "yaml": "1.10.2", + "yargs-parser": "20.2.9" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz", + "integrity": "sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==", + "dev": true, + "requires": { + "tslib": "~2.1.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, + "requires": { + "semver": "^6.3.0" + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "shiki": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.8.tgz", + "integrity": "sha512-499zQUTjcNTVwwiaPrWldUTXIV3T9HZWxDwE82bY+9GE7P2uD6hpHUTXNbTof3iOG6WT+/062+OMbl0lDoG8WQ==", + "dev": true, + "requires": { + "json5": "^2.2.0", + "onigasm": "^2.2.5", + "vscode-textmate": "5.2.0" + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "sinon": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", + "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^7.1.2", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + } + }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "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==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-node": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", + "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "0.6.1", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typedoc": { + "version": "0.21.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.21.9.tgz", + "integrity": "sha512-VRo7aII4bnYaBBM1lhw4bQFmUcDQV8m8tqgjtc7oXl87jc1Slbhfw2X5MccfcR2YnEClHDWgsiQGgNB8KJXocA==", + "dev": true, + "requires": { + "glob": "^7.1.7", + "handlebars": "^4.7.7", + "lunr": "^2.3.9", + "marked": "^3.0.2", + "minimatch": "^3.0.0", + "progress": "^2.0.3", + "shiki": "^0.9.8", + "typedoc-default-themes": "^0.12.10" + } + }, + "typedoc-default-themes": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz", + "integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==", + "dev": true + }, + "typedoc-github-wiki-theme": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/typedoc-github-wiki-theme/-/typedoc-github-wiki-theme-0.5.1.tgz", + "integrity": "sha512-ED2Uc3WUjbv6xIdCkpMz3yBtcEciJnAhDQdRWLYgw4K+FKY0T3PzbI+ssNsBVgwDnYQP/XuaqfZkeQ3EQsOm9g==", + "dev": true, + "requires": {} + }, + "typedoc-plugin-markdown": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.10.4.tgz", + "integrity": "sha512-if9w7S9fXLg73AYi/EoRSEhTOZlg3I8mIP8YEmvzSE33VrNXC9/hA0nVcLEwFVJeQY7ay6z67I6kW0KIv7LjeA==", + "dev": true, + "requires": { + "handlebars": "^4.7.7" + } + }, + "typescript": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", + "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", + "dev": true + }, + "uglify-js": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.1.tgz", + "integrity": "sha512-JhS3hmcVaXlp/xSo3PKY5R0JqKs5M3IV+exdLHW99qKvKivPO4Z8qbej6mte17SOPqAOVMjt/XGgWacnFSzM3g==", + "dev": true, + "optional": true + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true + }, + "update-notifier": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", + "dev": true, + "requires": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.1.0", + "pupa": "^2.1.1", + "semver": "^7.3.4", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "vscode-textmate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", + "integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==", + "dev": true + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "requires": { + "string-width": "^4.0.0" + } + }, + "windows-release": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", + "integrity": "sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==", + "dev": true, + "requires": { + "execa": "^4.0.2" + }, + "dependencies": { + "execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + } + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "workerpool": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", + "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + } + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json index f680639164..be909a2197 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,11 @@ { "name": "redis", - "version": "3.1.2", + "version": "4.0.0-rc.0", "description": "A high performance Redis client.", "keywords": [ "database", "redis", - "transaction", - "pipelining", - "performance", - "queue", - "nodejs", - "pubsub", - "backpressure" + "pubsub" ], "author": "Matt Ranney ", "contributors": [ @@ -25,38 +19,40 @@ } ], "license": "MIT", - "main": "./index.js", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "scripts": { - "coveralls": "nyc report --reporter=text-lcov | coveralls", - "coverage": "nyc report --reporter=html", - "benchmark": "node benchmarks/multi_bench.js", - "test": "nyc --cache mocha ./test/*.spec.js ./test/commands/*.spec.js --timeout=8000 && npm run coverage", - "lint": "eslint .", - "lint:fix": "eslint . --fix", - "lint:report": "eslint --output-file=eslint-report.json --format=json .", - "compare": "node benchmarks/diff_multi_bench_output.js beforeBench.txt afterBench.txt" + "test": "nyc -r text-summary -r html mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'", + "build": "tsc", + "documentation": "typedoc" }, "dependencies": { - "denque": "^1.5.0", - "redis-commands": "^1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0" - }, - "engines": { - "node": ">=10" + "cluster-key-slot": "1.1.0", + "generic-pool": "3.8.2", + "redis-parser": "3.0.0", + "yallist": "4.0.0" }, "devDependencies": { - "bluebird": "^3.7.2", - "coveralls": "^3.1.0", - "cross-spawn": "^7.0.3", - "eslint": "^7.21.0", - "intercept-stdout": "~0.1.2", - "metrics": "^0.1.21", - "mocha": "^8.3.0", + "@istanbuljs/nyc-config-typescript": "^1.0.1", + "@types/mocha": "^9.0.0", + "@types/node": "^16.7.8", + "@types/sinon": "^10.0.2", + "@types/which": "^2.0.1", + "@types/yallist": "^4.0.1", + "mocha": "^9.1.1", "nyc": "^15.1.0", - "prettier": "^2.2.1", - "tcp-port-used": "^1.0.1", - "uuid": "^8.3.2" + "release-it": "^14.11.5", + "sinon": "^11.1.2", + "source-map-support": "^0.5.19", + "ts-node": "^10.2.1", + "typedoc": "^0.21.9", + "typedoc-github-wiki-theme": "^0.5.1", + "typedoc-plugin-markdown": "^3.10.4", + "typescript": "^4.4.2", + "which": "^2.0.2" + }, + "engines": { + "node": ">=12" }, "repository": { "type": "git", @@ -65,13 +61,5 @@ "bugs": { "url": "https://github.com/NodeRedis/node-redis/issues" }, - "homepage": "https://github.com/NodeRedis/node-redis", - "directories": { - "example": "examples", - "test": "test" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-redis" - } + "homepage": "https://github.com/NodeRedis/node-redis/tree/v4" } diff --git a/test/auth.spec.js b/test/auth.spec.js deleted file mode 100644 index d1b596e5ae..0000000000 --- a/test/auth.spec.js +++ /dev/null @@ -1,354 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var helper = require('./helper'); -var errors = require('./errors'); -var redis = config.redis; - -if (process.platform === 'win32') { - // TODO: Fix redis process spawn on windows - return; -} - -describe('client authentication', function () { - before(function (done) { - helper.stopRedis(function () { - helper.startRedis('./conf/password.conf', done); - }); - }); - - helper.allTests({ - allConnections: true - }, function (ip, args) { - - describe('using ' + ip, function () { - var auth = 'porkchopsandwiches'; - var client = null; - - beforeEach(function () { - client = null; - }); - afterEach(function () { - // Explicitly ignore still running commands - // The ready command could still be running - client.end(false); - }); - - it("allows auth to be provided with 'auth' method", function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client = redis.createClient.apply(null, args); - client.auth(auth, function (err, res) { - assert.strictEqual(null, err); - assert.strictEqual('OK', res.toString()); - return done(err); - }); - }); - - it('support redis 2.4 with retrying auth commands if still loading', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client = redis.createClient.apply(null, args); - var time = Date.now(); - client.auth(auth, function (err, res) { - assert.strictEqual('retry worked', res); - var now = Date.now(); - // Hint: setTimeout sometimes triggers early and therefore the value can be like one or two ms to early - assert(now - time >= 98, 'Time should be above 100 ms (the reconnect time) and is ' + (now - time)); - assert(now - time < 225, 'Time should be below 255 ms (the reconnect should only take a bit above 100 ms) and is ' + (now - time)); - done(); - }); - var tmp = client.command_queue.get(0).callback; - client.command_queue.get(0).callback = function (err, res) { - client.auth = function (pass, user, callback) { - callback(null, 'retry worked'); - }; - tmp(new Error('ERR redis is still LOADING')); - }; - }); - - it('emits error when auth is bad without callback', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client = redis.createClient.apply(null, config.configureClient(ip, { - no_ready_check: true - })); - - client.once('error', function (err) { - assert.strictEqual(err.command, 'AUTH'); - assert.ok(errors.invalidPassword.test(err.message)); - return done(); - }); - - client.auth(auth + 'bad'); - }); - - it('returns an error when auth is bad (empty string) with a callback', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client = redis.createClient.apply(null, config.configureClient(ip, { - no_ready_check: true - })); - - client.auth('', function (err, res) { - assert.strictEqual(err.command, 'AUTH'); - assert.ok(errors.invalidPassword.test(err.message)); - done(); - }); - }); - - if (ip === 'IPv4') { - it('allows auth to be provided as part of redis url and do not fire commands before auth is done', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - var end = helper.callFuncAfter(done, 2); - client = redis.createClient('redis://:' + auth + '@' + config.HOST[ip] + ':' + config.PORT); - client.on('ready', function () { - end(); - }); - // The info command may be used while loading but not if not yet authenticated - client.info(function (err, res) { - assert(!err); - end(); - }); - }); - - it('allows auth and database to be provided as part of redis url query parameter', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client = redis.createClient('redis://' + config.HOST[ip] + ':' + config.PORT + '?db=2&password=' + auth); - assert.strictEqual(client.options.db, '2'); - assert.strictEqual(client.options.password, auth); - assert.strictEqual(client.auth_pass, auth); - client.on('ready', function () { - // Set a key so the used database is returned in the info command - client.set('foo', 'bar'); - client.get('foo'); - assert.strictEqual(client.server_info.db2, undefined); - // Using the info command should update the server_info - client.info(function (err, res) { - assert(typeof client.server_info.db2 === 'object'); - }); - client.flushdb(done); - }); - }); - } - - it('allows auth to be provided as config option for client', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - var args = config.configureClient(ip, { - auth_pass: auth - }); - client = redis.createClient.apply(null, args); - client.on('ready', done); - }); - - it('allows auth and no_ready_check to be provided as config option for client', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - var args = config.configureClient(ip, { - password: auth, - no_ready_check: true - }); - client = redis.createClient.apply(null, args); - client.on('ready', done); - }); - - it('allows auth to be provided post-hoc with auth method', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - var args = config.configureClient(ip); - client = redis.createClient.apply(null, args); - client.auth(auth); - client.on('ready', done); - }); - - it('reconnects with appropriate authentication while offline commands are present', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client = redis.createClient.apply(null, args); - client.auth(auth); - client.on('ready', function () { - if (this.times_connected < 3) { - var interval = setInterval(function () { - if (client.commandQueueLength !== 0) { - return; - } - clearInterval(interval); - interval = null; - client.stream.destroy(); - client.set('foo', 'bar'); - client.get('foo'); // Errors would bubble - assert.strictEqual(client.offlineQueueLength, 2); - }, 1); - } else { - done(); - } - }); - client.on('reconnecting', function (params) { - assert.strictEqual(params.error, null); - }); - }); - - it('should return an error if the password is not correct and a callback has been provided', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client = redis.createClient.apply(null, config.configureClient(ip, { - no_ready_check: true - })); - var async = true; - client.auth('undefined', function (err, res) { - assert.ok(errors.invalidPassword.test(err.message)); - assert.strictEqual(err.command, 'AUTH'); - assert.strictEqual(res, undefined); - async = false; - done(); - }); - assert(async); - }); - - it('should emit an error if the password is not correct and no callback has been provided', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client = redis.createClient.apply(null, config.configureClient(ip, { - no_ready_check: true - })); - client.on('error', function (err) { - assert.ok(errors.invalidPassword.test(err.message)); - assert.strictEqual(err.command, 'AUTH'); - done(); - }); - client.auth(234567); - }); - - it('allows auth to be provided post-hoc with auth method again', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - var args = config.configureClient(ip, { - auth_pass: auth - }); - client = redis.createClient.apply(null, args); - client.on('ready', function () { - client.auth(auth, helper.isString('OK', done)); - }); - }); - - it('does not allow any commands to be processed if not authenticated using no_ready_check true', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - var args = config.configureClient(ip, { - no_ready_check: true - }); - client = redis.createClient.apply(null, args); - client.on('ready', function () { - client.set('foo', 'bar', function (err, res) { - assert.ok(/^NOAUTH Authentication required\.(\r\n)?$/.test(err.message)); - assert.equal(err.code, 'NOAUTH'); - assert.equal(err.command, 'SET'); - done(); - }); - }); - }); - - it('does not allow auth to be provided post-hoc with auth method if not authenticated before', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - client = redis.createClient.apply(null, args); - client.on('error', function (err) { - assert.equal(err.code, 'NOAUTH'); - assert.ok(/^Ready check failed: NOAUTH Authentication required\.(\r\n)?$/.test(err.message)); - assert.equal(err.command, 'INFO'); - done(); - }); - }); - - it('should emit an error if the provided password is faulty', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - client = redis.createClient({ - password: 'wrong_password', - no_ready_check: true - }); - client.once('error', function (err) { - assert.ok(errors.invalidPassword.test(err.message)); - done(); - }); - }); - - it('pubsub working with auth', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - var args = config.configureClient(ip, { - password: auth - }); - client = redis.createClient.apply(null, args); - client.set('foo', 'bar'); - client.subscribe('somechannel', 'another channel', function (err, res) { - client.once('ready', function () { - assert.strictEqual(client.pub_sub_mode, 1); - client.get('foo', function (err, res) { - assert.ok(errors.subscribeUnsubscribeOnly.test(err.message)); - done(); - }); - }); - }); - client.once('ready', function () { - // Coherent behavior with all other offline commands fires commands before emitting but does not wait till they return - assert.strictEqual(client.pub_sub_mode, 2); - client.ping(function () { // Make sure all commands were properly processed already - client.stream.destroy(); - }); - }); - }); - - it('individual commands work properly with batch', function (done) { - // quit => might return an error instead of "OK" in the exec callback... (if not connected) - // auth => might return an error instead of "OK" in the exec callback... (if no password is required / still loading on Redis <= 2.4) - // This could be fixed by checking the return value of the callback in the exec callback and - // returning the manipulated [error, result] from the callback. - // There should be a better solution though - - var args = config.configureClient('localhost', { - noReadyCheck: true - }); - client = redis.createClient.apply(null, args); - assert.strictEqual(client.selected_db, undefined); - var end = helper.callFuncAfter(done, 8); - client.on('monitor', function () { - end(); // Should be called for each command after monitor - }); - client.batch() - .auth(auth) - .SELECT(5, function (err, res) { - assert.strictEqual(client.selected_db, 5); - assert.strictEqual(res, 'OK'); - assert.notDeepEqual(client.serverInfo.db5, { avg_ttl: 0, expires: 0, keys: 1 }); - }) - .monitor() - .set('foo', 'bar', helper.isString('OK')) - .INFO('stats', function (err, res) { - assert.strictEqual(res.indexOf('# Stats\r\n'), 0); - assert.strictEqual(client.serverInfo.sync_full, '0'); - }) - .get('foo', helper.isString('bar')) - .subscribe(['foo', 'bar']) - .unsubscribe('foo') - .SUBSCRIBE('/foo', helper.isString('/foo')) - .psubscribe('*') - .quit(helper.isString('OK')) // this might be interesting - .exec(function (err, res) { - res[4] = res[4].substr(0, 9); - assert.deepEqual(res, ['OK', 'OK', 'OK', 'OK', '# Stats\r\n', 'bar', 'bar', 'foo', '/foo', '*', 'OK']); - end(); - }); - }); - }); - }); - - after(function (done) { - if (helper.redisProcess().spawnFailed()) return done(); - helper.stopRedis(function () { - helper.startRedis('./conf/redis.conf', done); - }); - }); -}); diff --git a/test/batch.spec.js b/test/batch.spec.js deleted file mode 100644 index 05cee80681..0000000000 --- a/test/batch.spec.js +++ /dev/null @@ -1,350 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var helper = require('./helper'); -var redis = config.redis; - -describe("The 'batch' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - - describe('when not connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('connect', function () { - client.quit(); - }); - client.on('end', done); - }); - - it('returns an empty array for missing commands', function (done) { - var batch = client.batch(); - batch.exec(function (err, res) { - assert.strictEqual(err, null); - assert.strictEqual(res.length, 0); - done(); - }); - }); - - it('returns an error for batch with commands', function (done) { - var batch = client.batch(); - batch.set('foo', 'bar'); - batch.exec(function (err, res) { - assert.strictEqual(err, null); - assert.strictEqual(res[0].code, 'NR_CLOSED'); - done(); - }); - }); - - it('returns an empty array for missing commands if promisified', function () { - return client.batch().execAsync().then(function (res) { - assert.strictEqual(res.length, 0); - }); - }); - }); - - describe('when connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(function (err) { - return done(err); - }); - }); - }); - - afterEach(function () { - client.end(true); - }); - - it('returns an empty array and keep the execution order in takt', function (done) { - var called = false; - client.set('foo', 'bar', function (err, res) { - called = true; - }); - var batch = client.batch(); - batch.exec(function (err, res) { - assert.strictEqual(err, null); - assert.strictEqual(res.length, 0); - assert(called); - done(); - }); - }); - - it('runs normal calls inbetween batch', function (done) { - var batch = client.batch(); - batch.set('m1', '123'); - client.set('m2', '456', done); - }); - - it('returns an empty array if promisified', function () { - return client.batch().execAsync().then(function (res) { - assert.strictEqual(res.length, 0); - }); - }); - - it('returns an empty result array', function (done) { - var batch = client.batch(); - var async = true; - var notBuffering = batch.exec(function (err, res) { - assert.strictEqual(err, null); - assert.strictEqual(res.length, 0); - async = false; - done(); - }); - assert(async); - assert.strictEqual(notBuffering, true); - }); - - it('fail individually when one command fails using chaining notation', function (done) { - var batch1, batch2; - batch1 = client.batch(); - batch1.mset('batchfoo', '10', 'batchbar', '20', helper.isString('OK')); - - // Provoke an error at queue time - batch1.set('foo2', helper.isError()); - batch1.incr('batchfoo'); - batch1.incr('batchbar'); - batch1.exec(function () { - // Confirm that the previous command, while containing an error, still worked. - batch2 = client.batch(); - batch2.get('foo2', helper.isNull()); - batch2.incr('batchbar', helper.isNumber(22)); - batch2.incr('batchfoo', helper.isNumber(12)); - batch2.exec(function (err, replies) { - assert.strictEqual(null, replies[0]); - assert.strictEqual(22, replies[1]); - assert.strictEqual(12, replies[2]); - return done(); - }); - }); - }); - - it('fail individually when one command fails and emit the error if no callback has been provided', function (done) { - var batch1; - client.on('error', function (err) { - done(err); - }); - batch1 = client.batch(); - batch1.mset('batchfoo', '10', 'batchbar', '20', helper.isString('OK')); - - // Provoke an error at queue time - batch1.set('foo2'); - batch1.incr('batchfoo'); - batch1.incr('batchbar'); - batch1.exec(function (err, res) { - assert.strictEqual(res[1].command, 'SET'); - assert.strictEqual(res[1].code, 'ERR'); - done(); - }); - }); - - it('fail individually when one command in an array of commands fails', function (done) { - // test nested batch-bulk replies - client.batch([ - ['mget', 'batchfoo', 'batchbar', function (err, res) { - assert.strictEqual(2, res.length); - assert.strictEqual(0, +res[0]); - assert.strictEqual(0, +res[1]); - }], - ['set', 'foo2', helper.isError()], - ['incr', 'batchfoo'], - ['incr', 'batchbar'] - ]).exec(function (err, replies) { - assert.strictEqual(2, replies[0].length); - assert.strictEqual(null, replies[0][0]); - assert.strictEqual(null, replies[0][1]); - assert.strictEqual('SET', replies[1].command); - assert.strictEqual('1', replies[2].toString()); - assert.strictEqual('1', replies[3].toString()); - return done(); - }); - }); - - it('handles multiple operations being applied to a set', function (done) { - client.sadd('some set', 'mem 1'); - client.sadd(['some set', 'mem 2']); - client.sadd('some set', 'mem 3'); - client.sadd('some set', 'mem 4'); - - // make sure empty mb reply works - client.del('some missing set'); - client.smembers('some missing set', function (err, reply) { - // make sure empty mb reply works - assert.strictEqual(0, reply.length); - }); - - // test nested batch-bulk replies with empty mb elements. - client.BATCH([ - ['smembers', ['some set']], - ['del', 'some set'], - ['smembers', 'some set', undefined] // The explicit undefined is handled as a callback that is undefined - ]) - .scard('some set') - .exec(function (err, replies) { - assert.strictEqual(4, replies[0].length); - assert.strictEqual(0, replies[2].length); - return done(); - }); - }); - - it('allows multiple operations to be performed using constructor with all kinds of syntax', function (done) { - var now = Date.now(); - var arr = ['batchhmset', 'batchbar', 'batchbaz']; - var arr2 = ['some manner of key', 'otherTypes']; - var arr3 = [5768, 'batchbarx', 'batchfoox']; - var arr4 = ['mset', [578, 'batchbar'], helper.isString('OK')]; - client.batch([ - arr4, - [['mset', 'batchfoo2', 'batchbar2', 'batchfoo3', 'batchbar3'], helper.isString('OK')], - ['hmset', arr], - [['hmset', 'batchhmset2', 'batchbar2', 'batchfoo3', 'batchbar3', 'test'], helper.isString('OK')], - ['hmset', ['batchhmset', 'batchbar', 'batchfoo'], helper.isString('OK')], - ['hmset', arr3, helper.isString('OK')], - ['hmset', now, {123456789: 'abcdefghij', 'some manner of key': 'a type of value', 'otherTypes': 555}], - ['hmset', 'key2', {'0123456789': 'abcdefghij', 'some manner of key': 'a type of value', 'otherTypes': 999}, helper.isString('OK')], - ['HMSET', 'batchhmset', ['batchbar', 'batchbaz']], - ['hmset', 'batchhmset', ['batchbar', 'batchbaz'], helper.isString('OK')], - ]) - .hmget(now, 123456789, 'otherTypes') - .hmget('key2', arr2, function noop () {}) - .hmget(['batchhmset2', 'some manner of key', 'batchbar3']) - .mget('batchfoo2', ['batchfoo3', 'batchfoo'], function (err, res) { - assert.strictEqual(res[0], 'batchbar2'); - assert.strictEqual(res[1], 'batchbar3'); - assert.strictEqual(res[2], null); - }) - .exec(function (err, replies) { - assert.equal(arr.length, 3); - assert.equal(arr2.length, 2); - assert.equal(arr3.length, 3); - assert.equal(arr4.length, 3); - assert.strictEqual(null, err); - assert.equal(replies[10][1], '555'); - assert.equal(replies[11][0], 'a type of value'); - assert.strictEqual(replies[12][0], null); - assert.equal(replies[12][1], 'test'); - assert.equal(replies[13][0], 'batchbar2'); - assert.equal(replies[13].length, 3); - assert.equal(replies.length, 14); - return done(); - }); - }); - - it('converts a non string key to a string', function (done) { - // TODO: Converting the key might change soon again. - client.batch().hmset(true, { - test: 123, - bar: 'baz' - }).exec(done); - }); - - it('runs a batch without any further commands', function (done) { - var buffering = client.batch().exec(function (err, res) { - assert.strictEqual(err, null); - assert.strictEqual(res.length, 0); - done(); - }); - assert(typeof buffering === 'boolean'); - }); - - it('runs a batch without any further commands and without callback', function () { - var buffering = client.batch().exec(); - assert.strictEqual(buffering, true); - }); - - it('allows multiple operations to be performed using a chaining API', function (done) { - client.batch() - .mset('some', '10', 'keys', '20') - .incr('some') - .incr('keys') - .mget('some', 'keys') - .exec(function (err, replies) { - assert.strictEqual(null, err); - assert.equal('OK', replies[0]); - assert.equal(11, replies[1]); - assert.equal(21, replies[2]); - assert.equal(11, replies[3][0].toString()); - assert.equal(21, replies[3][1].toString()); - return done(); - }); - }); - - it('allows multiple commands to work the same as normal to be performed using a chaining API', function (done) { - client.batch() - .mset(['some', '10', 'keys', '20']) - .incr('some', helper.isNumber(11)) - .incr(['keys'], helper.isNumber(21)) - .mget('some', 'keys') - .exec(function (err, replies) { - assert.strictEqual(null, err); - assert.equal('OK', replies[0]); - assert.equal(11, replies[1]); - assert.equal(21, replies[2]); - assert.equal(11, replies[3][0].toString()); - assert.equal(21, replies[3][1].toString()); - return done(); - }); - }); - - it('allows multiple commands to work the same as normal to be performed using a chaining API promisified', function () { - return client.batch() - .mset(['some', '10', 'keys', '20']) - .incr('some', helper.isNumber(11)) - .incr(['keys'], helper.isNumber(21)) - .mget('some', 'keys') - .execAsync() - .then(function (replies) { - assert.equal('OK', replies[0]); - assert.equal(11, replies[1]); - assert.equal(21, replies[2]); - assert.equal(11, replies[3][0].toString()); - assert.equal(21, replies[3][1].toString()); - }); - }); - - it('allows an array to be provided indicating multiple operations to perform', function (done) { - // test nested batch-bulk replies with nulls. - client.batch([ - ['mget', ['batchfoo', 'some', 'random value', 'keys']], - ['incr', 'batchfoo'] - ]) - .exec(function (err, replies) { - assert.strictEqual(replies.length, 2); - assert.strictEqual(replies[0].length, 4); - return done(); - }); - }); - - it('allows multiple operations to be performed on a hash', function (done) { - client.batch() - .hmset('batchhash', 'a', 'foo', 'b', 1) - .hmset('batchhash', { - extra: 'fancy', - things: 'here' - }) - .hgetall('batchhash') - .exec(done); - }); - - it('should work without any callback or arguments', function (done) { - var batch = client.batch(); - batch.set('baz', 'binary'); - batch.set('foo', 'bar'); - batch.ping(); - batch.exec(); - - client.get('foo', helper.isString('bar', done)); - }); - - }); - }); - }); -}); diff --git a/test/commands/blpop.spec.js b/test/commands/blpop.spec.js deleted file mode 100644 index 64aedf81ea..0000000000 --- a/test/commands/blpop.spec.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; -var intercept = require('intercept-stdout'); - -describe("The 'blpop' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - var bclient; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('pops value immediately if list contains values', function (done) { - bclient = redis.createClient.apply(null, args); - redis.debug_mode = true; - var text = ''; - var unhookIntercept = intercept(function (data) { - text += data; - return ''; - }); - client.rpush('blocking list', 'initial value', helper.isNumber(1)); - unhookIntercept(); - assert(/Send 127\.0\.0\.1:6379 id [0-9]+: \*3\r\n\$5\r\nrpush\r\n\$13\r\nblocking list\r\n\$13\r\ninitial value\r\n\n$/.test(text)); - redis.debug_mode = false; - bclient.blpop('blocking list', 0, function (err, value) { - assert.strictEqual(value[0], 'blocking list'); - assert.strictEqual(value[1], 'initial value'); - return done(err); - }); - }); - - it('pops value immediately if list contains values using array notation', function (done) { - bclient = redis.createClient.apply(null, args); - client.rpush(['blocking list', 'initial value'], helper.isNumber(1)); - bclient.blpop(['blocking list', 0], function (err, value) { - assert.strictEqual(value[0], 'blocking list'); - assert.strictEqual(value[1], 'initial value'); - return done(err); - }); - }); - - it('waits for value if list is not yet populated', function (done) { - bclient = redis.createClient.apply(null, args); - bclient.blpop('blocking list 2', 5, function (err, value) { - assert.strictEqual(value[0], 'blocking list 2'); - assert.strictEqual(value[1], 'initial value'); - return done(err); - }); - client.rpush('blocking list 2', 'initial value', helper.isNumber(1)); - }); - - it('times out after specified time', function (done) { - bclient = redis.createClient.apply(null, args); - bclient.BLPOP('blocking list', 1, function (err, res) { - assert.strictEqual(res, null); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - bclient.end(true); - }); - }); - }); -}); diff --git a/test/commands/client.spec.js b/test/commands/client.spec.js deleted file mode 100644 index 3214243107..0000000000 --- a/test/commands/client.spec.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'client' method", function () { - - helper.allTests(function (ip, args) { - var pattern = /addr=/; - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - afterEach(function () { - client.end(true); - }); - - describe('list', function () { - it('lists connected clients', function (done) { - client.client('LIST', helper.match(pattern, done)); - }); - - it("lists connected clients when invoked with multi's chaining syntax", function (done) { - client.multi().client('list', helper.isType.string()).exec(helper.match(pattern, done)); - }); - - it('lists connected clients when invoked with array syntax on client', function (done) { - client.multi().client(['list']).exec(helper.match(pattern, done)); - }); - - it("lists connected clients when invoked with multi's array syntax", function (done) { - client.multi([ - ['client', 'list'] - ]).exec(helper.match(pattern, done)); - }); - }); - - describe('reply', function () { - describe('as normal command', function () { - it('on', function (done) { - helper.serverVersionAtLeast.call(this, client, [3, 2, 0]); - assert.strictEqual(client.reply, 'ON'); - client.client('reply', 'on', helper.isString('OK')); - assert.strictEqual(client.reply, 'ON'); - client.set('foo', 'bar', done); - }); - - it('off', function (done) { - helper.serverVersionAtLeast.call(this, client, [3, 2, 0]); - assert.strictEqual(client.reply, 'ON'); - client.client(Buffer.from('REPLY'), 'OFF', helper.isUndefined()); - assert.strictEqual(client.reply, 'OFF'); - client.set('foo', 'bar', helper.isUndefined(done)); - }); - - it('skip', function (done) { - helper.serverVersionAtLeast.call(this, client, [3, 2, 0]); - assert.strictEqual(client.reply, 'ON'); - client.client('REPLY', Buffer.from('SKIP'), helper.isUndefined()); - assert.strictEqual(client.reply, 'SKIP_ONE_MORE'); - client.set('foo', 'bar', helper.isUndefined()); - client.get('foo', helper.isString('bar', done)); - }); - }); - - describe('in a batch context', function () { - it('on', function (done) { - helper.serverVersionAtLeast.call(this, client, [3, 2, 0]); - var batch = client.batch(); - assert.strictEqual(client.reply, 'ON'); - batch.client('reply', 'on', helper.isString('OK')); - assert.strictEqual(client.reply, 'ON'); - batch.set('foo', 'bar'); - batch.exec(function (err, res) { - assert.deepEqual(res, ['OK', 'OK']); - done(err); - }); - }); - - it('off', function (done) { - helper.serverVersionAtLeast.call(this, client, [3, 2, 0]); - var batch = client.batch(); - assert.strictEqual(client.reply, 'ON'); - batch.set('hello', 'world'); - batch.client(Buffer.from('REPLY'), Buffer.from('OFF'), helper.isUndefined()); - batch.set('foo', 'bar', helper.isUndefined()); - batch.exec(function (err, res) { - assert.strictEqual(client.reply, 'OFF'); - assert.deepEqual(res, ['OK', undefined, undefined]); - done(err); - }); - }); - - it('skip', function (done) { - helper.serverVersionAtLeast.call(this, client, [3, 2, 0]); - assert.strictEqual(client.reply, 'ON'); - client.batch() - .set('hello', 'world') - .client('REPLY', 'SKIP', helper.isUndefined()) - .set('foo', 'bar', helper.isUndefined()) - .get('foo') - .exec(function (err, res) { - assert.strictEqual(client.reply, 'ON'); - assert.deepEqual(res, ['OK', undefined, undefined, 'bar']); - done(err); - }); - }); - }); - }); - - describe('setname / getname', function () { - var client2; - - beforeEach(function (done) { - client2 = redis.createClient.apply(null, args); - client2.once('ready', function () { - done(); - }); - }); - - afterEach(function () { - client2.end(true); - }); - - it('sets the name', function (done) { - // The querys are auto pipelined and the response is a response to all querys of one client - // per chunk. So the execution order is only garanteed on each client - var end = helper.callFuncAfter(done, 2); - - client.client('setname', 'RUTH'); - client2.client('setname', ['RENEE'], helper.isString('OK')); - client2.client(['setname', 'MARTIN'], helper.isString('OK')); - client2.client('getname', function (err, res) { - assert.equal(res, 'MARTIN'); - end(); - }); - client.client('getname', function (err, res) { - assert.equal(res, 'RUTH'); - end(); - }); - }); - - }); - }); - }); -}); diff --git a/test/commands/dbsize.spec.js b/test/commands/dbsize.spec.js deleted file mode 100644 index bd8b146789..0000000000 --- a/test/commands/dbsize.spec.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; -var uuid = require('uuid'); - -describe("The 'dbsize' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var key, value; - - beforeEach(function () { - key = uuid.v4(); - value = uuid.v4(); - }); - - describe('when not connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.quit(); - }); - client.on('end', done); - }); - - it('reports an error', function (done) { - client.dbsize([], function (err, res) { - assert(err.message.match(/The connection is already closed/)); - done(); - }); - }); - }); - - describe('when connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(function (err, res) { - helper.isString('OK')(err, res); - done(); - }); - }); - }); - - afterEach(function () { - client.end(true); - }); - - it('returns a zero db size', function (done) { - client.DBSIZE([], function (err, res) { - helper.isNotError()(err, res); - helper.isType.number()(err, res); - assert.strictEqual(res, 0, 'Initial db size should be 0'); - done(); - }); - }); - - describe('when more data is added to Redis', function () { - var oldSize; - - beforeEach(function (done) { - client.dbsize(function (err, res) { - helper.isType.number()(err, res); - assert.strictEqual(res, 0, 'Initial db size should be 0'); - - oldSize = res; - - client.set(key, value, function (err, res) { - helper.isNotError()(err, res); - done(); - }); - }); - }); - - it('returns a larger db size', function (done) { - client.dbsize([], function (err, res) { - helper.isNotError()(err, res); - helper.isType.positiveNumber()(err, res); - assert.strictEqual(true, (oldSize < res), 'Adding data should increase db size.'); - done(); - }); - }); - }); - }); - }); - }); -}); diff --git a/test/commands/del.spec.js b/test/commands/del.spec.js deleted file mode 100644 index 86c1f4bb3a..0000000000 --- a/test/commands/del.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'del' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('allows a single key to be deleted', function (done) { - client.set('foo', 'bar'); - client.DEL('foo', helper.isNumber(1)); - client.get('foo', helper.isNull(done)); - }); - - it('allows del to be called on a key that does not exist', function (done) { - client.del('foo', helper.isNumber(0, done)); - }); - - it('allows multiple keys to be deleted', function (done) { - client.mset('foo', 'bar', 'apple', 'banana'); - client.del('foo', 'apple', helper.isNumber(2)); - client.get('foo', helper.isNull()); - client.get('apple', helper.isNull(done)); - }); - - it('allows multiple keys to be deleted with the array syntax', function (done) { - client.mset('foo', 'bar', 'apple', 'banana'); - client.del(['foo', 'apple'], helper.isNumber(2)); - client.get('foo', helper.isNull()); - client.get('apple', helper.isNull(done)); - }); - - it('allows multiple keys to be deleted with the array syntax and no callback', function (done) { - client.mset('foo', 'bar', 'apple', 'banana'); - client.del(['foo', 'apple']); - client.get('foo', helper.isNull()); - client.get('apple', helper.isNull(done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/eval.spec.js b/test/commands/eval.spec.js deleted file mode 100644 index db74372db4..0000000000 --- a/test/commands/eval.spec.js +++ /dev/null @@ -1,210 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var crypto = require('crypto'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'eval' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - var source = "return redis.call('set', 'sha', 'test')"; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - afterEach(function () { - client.end(true); - }); - - it('converts a float to an integer when evaluated', function (done) { - client.eval('return 100.5', 0, helper.isNumber(100, done)); - }); - - it('returns a string', function (done) { - client.eval("return 'hello world'", 0, helper.isString('hello world', done)); - }); - - it('converts boolean true to integer 1', function (done) { - client.eval('return true', 0, helper.isNumber(1, done)); - }); - - it('converts boolean false to null', function (done) { - client.eval('return false', 0, helper.isNull(done)); - }); - - it('converts lua status code to string representation', function (done) { - client.eval("return {ok='fine'}", 0, helper.isString('fine', done)); - }); - - it('converts lua error to an error response', function (done) { - client.eval("return {err='this is an error'}", 0, function (err) { - assert(err.code === undefined); - helper.isError()(err); - done(); - }); - }); - - it('represents a lua table appropritely', function (done) { - client.eval("return {1,2,3,'ciao',{1,2}}", 0, function (err, res) { - assert.strictEqual(5, res.length); - assert.strictEqual(1, res[0]); - assert.strictEqual(2, res[1]); - assert.strictEqual(3, res[2]); - assert.strictEqual('ciao', res[3]); - assert.strictEqual(2, res[4].length); - assert.strictEqual(1, res[4][0]); - assert.strictEqual(2, res[4][1]); - return done(); - }); - }); - - it('populates keys and argv correctly', function (done) { - client.eval('return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}', 2, 'a', 'b', 'c', 'd', function (err, res) { - assert.strictEqual(4, res.length); - assert.strictEqual('a', res[0]); - assert.strictEqual('b', res[1]); - assert.strictEqual('c', res[2]); - assert.strictEqual('d', res[3]); - return done(); - }); - }); - - it('allows arguments to be provided in array rather than as multiple parameters', function (done) { - client.eval(['return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}', 2, 'a', 'b', 'c', 'd'], function (err, res) { - assert.strictEqual(4, res.length); - assert.strictEqual('a', res[0]); - assert.strictEqual('b', res[1]); - assert.strictEqual('c', res[2]); - assert.strictEqual('d', res[3]); - return done(); - }); - }); - - it('allows a script to be executed that accesses the redis API without callback', function (done) { - client.eval(source, 0); - client.get('sha', helper.isString('test', done)); - }); - - describe('evalsha', function () { - var sha = crypto.createHash('sha1').update(source).digest('hex'); - - it('allows a script to be executed that accesses the redis API', function (done) { - client.eval(source, 0, helper.isString('OK')); - client.get('sha', helper.isString('test', done)); - }); - - it('can execute a script if the SHA exists', function (done) { - client.evalsha(sha, 0, helper.isString('OK')); - client.get('sha', helper.isString('test', done)); - }); - - it('returns an error if SHA does not exist', function (done) { - client.evalsha('ffffffffffffffffffffffffffffffffffffffff', 0, helper.isError(done)); - }); - - it('emit an error if SHA does not exist without any callback', function (done) { - client.evalsha('ffffffffffffffffffffffffffffffffffffffff', 0); - client.on('error', function (err) { - assert.equal(err.code, 'NOSCRIPT'); - assert(/NOSCRIPT No matching script. Please use EVAL./.test(err.message)); - done(); - }); - }); - - it('emits an error if SHA does not exist and no callback has been provided', function (done) { - client.on('error', function (err) { - assert.equal(err.message, 'NOSCRIPT No matching script. Please use EVAL.'); - done(); - }); - client.evalsha('ffffffffffffffffffffffffffffffffffffffff', 0); - }); - }); - - it('allows a key to be incremented, and performs appropriate conversion from LUA type', function (done) { - client.set('incr key', 0, function (err, reply) { - if (err) return done(err); - client.eval("local foo = redis.call('incr','incr key')\nreturn {type(foo),foo}", 0, function (err, res) { - assert.strictEqual(2, res.length); - assert.strictEqual('number', res[0]); - assert.strictEqual(1, res[1]); - return done(err); - }); - }); - }); - - it('allows a bulk operation to be performed, and performs appropriate conversion from LUA type', function (done) { - client.set('bulk reply key', 'bulk reply value', function (err, res) { - client.eval("local foo = redis.call('get','bulk reply key'); return {type(foo),foo}", 0, function (err, res) { - assert.strictEqual(2, res.length); - assert.strictEqual('string', res[0]); - assert.strictEqual('bulk reply value', res[1]); - return done(err); - }); - }); - }); - - it('allows a multi mulk operation to be performed, with the appropriate type conversion', function (done) { - client.multi() - .del('mylist') - .rpush('mylist', 'a') - .rpush('mylist', 'b') - .rpush('mylist', 'c') - .exec(function (err, replies) { - if (err) return done(err); - client.eval("local foo = redis.call('lrange','mylist',0,-1); return {type(foo),foo[1],foo[2],foo[3],# foo}", 0, function (err, res) { - assert.strictEqual(5, res.length); - assert.strictEqual('table', res[0]); - assert.strictEqual('a', res[1]); - assert.strictEqual('b', res[2]); - assert.strictEqual('c', res[3]); - assert.strictEqual(3, res[4]); - return done(err); - }); - }); - }); - - it('returns an appropriate representation of Lua status reply', function (done) { - client.eval("local foo = redis.call('set','mykey','myval'); return {type(foo),foo['ok']}", 0, function (err, res) { - assert.strictEqual(2, res.length); - assert.strictEqual('table', res[0]); - assert.strictEqual('OK', res[1]); - return done(err); - }); - }); - - it('returns an appropriate representation of a Lua error reply', function (done) { - client.set('error reply key', 'error reply value', function (err, res) { - if (err) return done(err); - client.eval("local foo = redis.pcall('incr','error reply key'); return {type(foo),foo['err']}", 0, function (err, res) { - assert.strictEqual(2, res.length); - assert.strictEqual('table', res[0]); - assert.strictEqual('ERR value is not an integer or out of range', res[1]); - return done(err); - }); - }); - }); - - it('returns an appropriate representation of a Lua nil reply', function (done) { - client.del('nil reply key', function (err, res) { - if (err) return done(err); - client.eval("local foo = redis.call('get','nil reply key'); return {type(foo),foo == false}", 0, function (err, res) { - if (err) throw err; - assert.strictEqual(2, res.length); - assert.strictEqual('boolean', res[0]); - assert.strictEqual(1, res[1]); - return done(err); - }); - }); - }); - }); - }); -}); diff --git a/test/commands/exists.spec.js b/test/commands/exists.spec.js deleted file mode 100644 index 399a0382f4..0000000000 --- a/test/commands/exists.spec.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'exists' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns 1 if the key exists', function (done) { - client.set('foo', 'bar'); - client.EXISTS('foo', helper.isNumber(1, done)); - }); - - it('returns 1 if the key exists with array syntax', function (done) { - client.set('foo', 'bar'); - client.EXISTS(['foo'], helper.isNumber(1, done)); - }); - - it('returns 0 if the key does not exist', function (done) { - client.exists('bar', helper.isNumber(0, done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/expire.spec.js b/test/commands/expire.spec.js deleted file mode 100644 index 2891890edc..0000000000 --- a/test/commands/expire.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'expire' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('expires key after timeout', function (done) { - client.set(['expiry key', 'bar'], helper.isString('OK')); - client.EXPIRE('expiry key', '1', helper.isNumber(1)); - setTimeout(function () { - client.exists(['expiry key'], helper.isNumber(0, done)); - }, 1050); - }); - - it('expires key after timeout with array syntax', function (done) { - client.set(['expiry key', 'bar'], helper.isString('OK')); - client.EXPIRE(['expiry key', '1'], helper.isNumber(1)); - setTimeout(function () { - client.exists(['expiry key'], helper.isNumber(0, done)); - }, 1050); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/flushdb.spec.js b/test/commands/flushdb.spec.js deleted file mode 100644 index a4f761d375..0000000000 --- a/test/commands/flushdb.spec.js +++ /dev/null @@ -1,105 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; -var uuid = require('uuid'); - -describe("The 'flushdb' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var key, key2; - - beforeEach(function () { - key = uuid.v4(); - key2 = uuid.v4(); - }); - - describe('when not connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.quit(); - }); - client.on('end', done); - }); - - it('reports an error', function (done) { - client.flushdb(function (err, res) { - assert(err.message.match(/The connection is already closed/)); - done(); - }); - }); - }); - - describe('when connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - done(); - }); - }); - - afterEach(function () { - client.end(true); - }); - - describe('when there is data in Redis', function () { - - beforeEach(function (done) { - client.mset(key, uuid.v4(), key2, uuid.v4(), helper.isNotError()); - client.dbsize([], function (err, res) { - helper.isType.positiveNumber()(err, res); - assert.equal(res, 2, 'Two keys should have been inserted'); - done(); - }); - }); - - it('deletes all the keys', function (done) { - client.flushdb(function (err, res) { - assert.equal(res, 'OK'); - client.mget(key, key2, function (err, res) { - assert.strictEqual(null, err, 'Unexpected error returned'); - assert.strictEqual(true, Array.isArray(res), 'Results object should be an array.'); - assert.strictEqual(2, res.length, 'Results array should have length 2.'); - assert.strictEqual(null, res[0], 'Redis key should have been flushed.'); - assert.strictEqual(null, res[1], 'Redis key should have been flushed.'); - done(err); - }); - }); - }); - - it('results in a db size of zero', function (done) { - client.flushdb(function (err, res) { - client.dbsize([], function (err, res) { - helper.isNotError()(err, res); - helper.isType.number()(err, res); - assert.strictEqual(0, res, 'Flushing db should result in db size 0'); - done(); - }); - }); - }); - - it('results in a db size of zero without a callback', function (done) { - client.flushdb(); - setTimeout(function (err, res) { - client.dbsize(function (err, res) { - helper.isNotError()(err, res); - helper.isType.number()(err, res); - assert.strictEqual(0, res, 'Flushing db should result in db size 0'); - done(); - }); - }, 25); - }); - }); - }); - }); - }); -}); diff --git a/test/commands/geoadd.spec.js b/test/commands/geoadd.spec.js deleted file mode 100644 index b45df7c83a..0000000000 --- a/test/commands/geoadd.spec.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'geoadd' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns 1 if the key exists', function (done) { - helper.serverVersionAtLeast.call(this, client, [3, 2, 0]); - client.geoadd('mycity:21:0:location', '13.361389', '38.115556', 'COR', function (err, res) { - console.log(err, res); - // geoadd is still in the unstable branch. As soon as it reaches the stable one, activate this test - done(); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/get.spec.js b/test/commands/get.spec.js deleted file mode 100644 index acbfc0d10d..0000000000 --- a/test/commands/get.spec.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; -var uuid = require('uuid'); - -describe("The 'get' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var key, value; - - beforeEach(function () { - key = uuid.v4(); - value = uuid.v4(); - }); - - describe('when not connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.quit(); - }); - client.on('end', done); - }); - - it('reports an error', function (done) { - client.get(key, function (err, res) { - assert(err.message.match(/The connection is already closed/)); - done(); - }); - }); - - it('reports an error promisified', function () { - return client.getAsync(key).then(assert, function (err) { - assert(err.message.match(/The connection is already closed/)); - }); - }); - }); - - describe('when connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - done(); - }); - }); - - afterEach(function () { - client.end(true); - }); - - describe('when the key exists in Redis', function () { - beforeEach(function (done) { - client.set(key, value, function (err, res) { - helper.isNotError()(err, res); - done(); - }); - }); - - it('gets the value correctly', function (done) { - client.GET(key, function (err, res) { - helper.isString(value)(err, res); - done(err); - }); - }); - - it("should not throw on a get without callback (even if it's not useful)", function (done) { - client.GET(key); - client.on('error', function (err) { - throw err; - }); - setTimeout(done, 25); - }); - }); - - describe('when the key does not exist in Redis', function () { - it('gets a null value', function (done) { - client.get(key, function (err, res) { - helper.isNull()(err, res); - done(err); - }); - }); - }); - }); - }); - }); -}); diff --git a/test/commands/getset.spec.js b/test/commands/getset.spec.js deleted file mode 100644 index 48dd7e9d73..0000000000 --- a/test/commands/getset.spec.js +++ /dev/null @@ -1,105 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; -var uuid = require('uuid'); - -describe("The 'getset' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var key, value, value2; - - beforeEach(function () { - key = uuid.v4(); - value = uuid.v4(); - value2 = uuid.v4(); - }); - - describe('when not connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.quit(); - }); - client.on('end', done); - }); - - it('reports an error', function (done) { - client.get(key, function (err, res) { - assert(err.message.match(/The connection is already closed/)); - done(); - }); - }); - }); - - describe('when connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - done(); - }); - }); - - afterEach(function () { - client.end(true); - }); - - describe('when the key exists in Redis', function () { - beforeEach(function (done) { - client.set(key, value, function (err, res) { - helper.isNotError()(err, res); - done(); - }); - }); - - it('gets the value correctly', function (done) { - client.GETSET(key, value2, function (err, res) { - helper.isString(value)(err, res); - client.get(key, function (err, res) { - helper.isString(value2)(err, res); - done(err); - }); - }); - }); - - it('gets the value correctly with array syntax', function (done) { - client.GETSET([key, value2], function (err, res) { - helper.isString(value)(err, res); - client.get(key, function (err, res) { - helper.isString(value2)(err, res); - done(err); - }); - }); - }); - - it('gets the value correctly with array syntax style 2', function (done) { - client.GETSET(key, [value2], function (err, res) { - helper.isString(value)(err, res); - client.get(key, function (err, res) { - helper.isString(value2)(err, res); - done(err); - }); - }); - }); - }); - - describe('when the key does not exist in Redis', function () { - it('gets a null value', function (done) { - client.getset(key, value, function (err, res) { - helper.isNull()(err, res); - done(err); - }); - }); - }); - }); - }); - }); -}); diff --git a/test/commands/hgetall.spec.js b/test/commands/hgetall.spec.js deleted file mode 100644 index 5bfa609d0b..0000000000 --- a/test/commands/hgetall.spec.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'hgetall' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - describe('regular client', function () { - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('handles simple keys and values', function (done) { - client.hmset(['hosts', 'hasOwnProperty', '1', 'another', '23', 'home', '1234'], helper.isString('OK')); - client.HGETALL(['hosts'], function (err, obj) { - assert.strictEqual(3, Object.keys(obj).length); - assert.strictEqual('1', obj.hasOwnProperty.toString()); - assert.strictEqual('23', obj.another.toString()); - assert.strictEqual('1234', obj.home.toString()); - done(err); - }); - }); - - it('handles fetching keys set using an object', function (done) { - client.batch().HMSET('msg_test', { message: 'hello' }, undefined).exec(); - client.hgetall('msg_test', function (err, obj) { - assert.strictEqual(1, Object.keys(obj).length); - assert.strictEqual(obj.message, 'hello'); - done(err); - }); - }); - - it('handles fetching a messing key', function (done) { - client.hgetall('missing', function (err, obj) { - assert.strictEqual(null, obj); - done(err); - }); - }); - }); - - describe('binary client', function () { - var args = config.configureClient(ip, { - return_buffers: true - }); - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns binary results', function (done) { - client.hmset(['bhosts', 'mjr', '1', 'another', '23', 'home', '1234', Buffer.from([0xAA, 0xBB, 0x00, 0xF0]), Buffer.from([0xCC, 0xDD, 0x00, 0xF0])], helper.isString('OK')); - client.HGETALL('bhosts', function (err, obj) { - assert.strictEqual(4, Object.keys(obj).length); - assert.strictEqual('1', obj.mjr.toString()); - assert.strictEqual('23', obj.another.toString()); - assert.strictEqual('1234', obj.home.toString()); - assert.strictEqual((Buffer.from([0xAA, 0xBB, 0x00, 0xF0])).toString('binary'), Object.keys(obj)[3]); - assert.strictEqual((Buffer.from([0xCC, 0xDD, 0x00, 0xF0])).toString('binary'), obj[(Buffer.from([0xAA, 0xBB, 0x00, 0xF0])).toString('binary')].toString('binary')); - return done(err); - }); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/hincrby.spec.js b/test/commands/hincrby.spec.js deleted file mode 100644 index 10b4523b3f..0000000000 --- a/test/commands/hincrby.spec.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'hincrby' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - var hash = 'test hash'; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('increments a key that has already been set', function (done) { - var field = 'field 1'; - - client.HSET(hash, field, 33); - client.hincrby(hash, field, 10, helper.isNumber(43, done)); - }); - - it('increments a key that has not been set', function (done) { - var field = 'field 2'; - - client.HINCRBY(hash, field, 10, helper.isNumber(10, done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/hlen.spec.js b/test/commands/hlen.spec.js deleted file mode 100644 index 874cb2970a..0000000000 --- a/test/commands/hlen.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'hlen' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('reports the count of keys', function (done) { - var hash = 'test hash'; - var field1 = Buffer.from('0123456789'); - var value1 = Buffer.from('abcdefghij'); - var field2 = Buffer.alloc(0); - var value2 = Buffer.alloc(0); - - client.HSET(hash, field1, value1, helper.isNumber(1)); - client.HSET(hash, field2, value2, helper.isNumber(1)); - client.HLEN(hash, helper.isNumber(2, done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/hmget.spec.js b/test/commands/hmget.spec.js deleted file mode 100644 index 3676b5b731..0000000000 --- a/test/commands/hmget.spec.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'hmget' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - var hash = 'test hash'; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('error', done); - client.once('ready', function () { - client.flushdb(); - client.HMSET(hash, {'0123456789': 'abcdefghij', 'some manner of key': 'a type of value'}, helper.isString('OK', done)); - }); - }); - - it('allows keys to be specified using multiple arguments', function (done) { - client.hmget(hash, '0123456789', 'some manner of key', function (err, reply) { - assert.strictEqual('abcdefghij', reply[0].toString()); - assert.strictEqual('a type of value', reply[1].toString()); - return done(err); - }); - }); - - it('allows keys to be specified by passing an array without manipulating the array', function (done) { - var data = ['0123456789', 'some manner of key']; - client.HMGET(hash, data, function (err, reply) { - assert.strictEqual(data.length, 2); - assert.strictEqual('abcdefghij', reply[0].toString()); - assert.strictEqual('a type of value', reply[1].toString()); - return done(err); - }); - }); - - it('allows keys to be specified by passing an array as first argument', function (done) { - client.HMGET([hash, '0123456789', 'some manner of key'], function (err, reply) { - assert.strictEqual('abcdefghij', reply[0].toString()); - assert.strictEqual('a type of value', reply[1].toString()); - return done(err); - }); - }); - - it('allows a single key to be specified in an array', function (done) { - client.HMGET(hash, ['0123456789'], function (err, reply) { - assert.strictEqual('abcdefghij', reply[0].toString()); - return done(err); - }); - }); - - it('allows keys to be specified that have not yet been set', function (done) { - client.HMGET(hash, 'missing thing', 'another missing thing', function (err, reply) { - assert.strictEqual(null, reply[0]); - assert.strictEqual(null, reply[1]); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/hmset.spec.js b/test/commands/hmset.spec.js deleted file mode 100644 index 8ba54ecc3f..0000000000 --- a/test/commands/hmset.spec.js +++ /dev/null @@ -1,117 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'hmset' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - var hash = 'test hash'; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('handles redis-style syntax', function (done) { - client.HMSET(hash, '0123456789', 'abcdefghij', 'some manner of key', 'a type of value', 'otherTypes', 555, helper.isString('OK')); - client.HGETALL(hash, function (err, obj) { - assert.equal(obj['0123456789'], 'abcdefghij'); - assert.equal(obj['some manner of key'], 'a type of value'); - return done(err); - }); - }); - - it('handles object-style syntax', function (done) { - client.hmset(hash, {'0123456789': 'abcdefghij', 'some manner of key': 'a type of value', 'otherTypes': 555}, helper.isString('OK')); - client.HGETALL(hash, function (err, obj) { - assert.equal(obj['0123456789'], 'abcdefghij'); - assert.equal(obj['some manner of key'], 'a type of value'); - return done(err); - }); - }); - - it('handles object-style syntax and the key being a number', function (done) { - client.HMSET(231232, {'0123456789': 'abcdefghij', 'some manner of key': 'a type of value', 'otherTypes': 555}, undefined); - client.HGETALL(231232, function (err, obj) { - assert.equal(obj['0123456789'], 'abcdefghij'); - assert.equal(obj['some manner of key'], 'a type of value'); - return done(err); - }); - }); - - it('allows a numeric key', function (done) { - client.HMSET(hash, 99, 'banana', helper.isString('OK')); - client.HGETALL(hash, function (err, obj) { - assert.equal(obj['99'], 'banana'); - return done(err); - }); - }); - - it('allows a numeric key without callback', function (done) { - client.HMSET(hash, 99, 'banana', 'test', 25); - client.HGETALL(hash, function (err, obj) { - assert.equal(obj['99'], 'banana'); - assert.equal(obj.test, '25'); - return done(err); - }); - }); - - it('allows an array without callback', function (done) { - client.HMSET([hash, 99, 'banana', 'test', 25]); - client.HGETALL(hash, function (err, obj) { - assert.equal(obj['99'], 'banana'); - assert.equal(obj.test, '25'); - return done(err); - }); - }); - - it('allows an array and a callback', function (done) { - client.HMSET([hash, 99, 'banana', 'test', 25], helper.isString('OK')); - client.HGETALL(hash, function (err, obj) { - assert.equal(obj['99'], 'banana'); - assert.equal(obj.test, '25'); - return done(err); - }); - }); - - it('allows a key plus array without callback', function (done) { - client.HMSET(hash, [99, 'banana', 'test', 25]); - client.HGETALL(hash, function (err, obj) { - assert.equal(obj['99'], 'banana'); - assert.equal(obj.test, '25'); - return done(err); - }); - }); - - it('allows a key plus array and a callback', function (done) { - client.HMSET(hash, [99, 'banana', 'test', 25], helper.isString('OK')); - client.HGETALL(hash, function (err, obj) { - assert.equal(obj['99'], 'banana'); - assert.equal(obj.test, '25'); - return done(err); - }); - }); - - it('handles object-style syntax without callback', function (done) { - client.HMSET(hash, {'0123456789': 'abcdefghij', 'some manner of key': 'a type of value'}); - client.HGETALL(hash, function (err, obj) { - assert.equal(obj['0123456789'], 'abcdefghij'); - assert.equal(obj['some manner of key'], 'a type of value'); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/hset.spec.js b/test/commands/hset.spec.js deleted file mode 100644 index 746176f3d8..0000000000 --- a/test/commands/hset.spec.js +++ /dev/null @@ -1,85 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'hset' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - var hash = 'test hash'; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('allows a value to be set in a hash', function (done) { - var field = Buffer.from('0123456789'); - var value = Buffer.from('abcdefghij'); - - client.hset(hash, field, value, helper.isNumber(1)); - client.HGET(hash, field, helper.isString(value.toString(), done)); - }); - - it('handles an empty value', function (done) { - var field = Buffer.from('0123456789'); - var value = Buffer.alloc(0); - - client.HSET(hash, field, value, helper.isNumber(1)); - client.HGET([hash, field], helper.isString('', done)); - }); - - it('handles empty key and value', function (done) { - var field = Buffer.alloc(0); - var value = Buffer.alloc(0); - client.HSET([hash, field, value], function (err, res) { - assert.strictEqual(res, 1); - client.HSET(hash, field, value, helper.isNumber(0, done)); - }); - }); - - it('errors if someone passed a array either as field or as value', function (done) { - var hash = 'test hash'; - var field = 'array'; - var value = ['array contents']; - try { - client.HMSET(hash, field, value); - } catch (error) { - assert(/node_redis: The HMSET command contains a invalid argument type./.test(error.message)); - done(); - } - }); - - it('does not error when a buffer and date are set as values on the same hash', function (done) { - var hash = 'test hash'; - var field1 = 'buffer'; - var value1 = Buffer.from('abcdefghij'); - var field2 = 'date'; - var value2 = new Date(); - - client.HMSET(hash, field1, value1, field2, value2, helper.isString('OK', done)); - }); - - it('does not error when a buffer and date are set as fields on the same hash', function (done) { - var hash = 'test hash'; - var value1 = 'buffer'; - var field1 = Buffer.from('abcdefghij'); - var value2 = 'date'; - var field2 = new Date(); - - client.HMSET(hash, field1, value1, field2, value2, helper.isString('OK', done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/incr.spec.js b/test/commands/incr.spec.js deleted file mode 100644 index 0caab84859..0000000000 --- a/test/commands/incr.spec.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'incr' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - - describe('when connected and a value in Redis', function () { - - var client; - var key = 'ABOVE_SAFE_JAVASCRIPT_INTEGER'; - var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; // Backwards compatible - - afterEach(function () { - client.end(true); - }); - - /* - Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1 === 9007199254740991 - - 9007199254740992 -> 9007199254740992 - 9007199254740993 -> 9007199254740992 - 9007199254740994 -> 9007199254740994 - 9007199254740995 -> 9007199254740996 - 9007199254740996 -> 9007199254740996 - 9007199254740997 -> 9007199254740996 - ... - */ - it('count above the safe integers as numbers', function (done) { - client = redis.createClient.apply(null, args); - // Set a value to the maximum safe allowed javascript number (2^53) - 1 - client.set(key, MAX_SAFE_INTEGER, helper.isNotError()); - client.INCR(key, helper.isNumber(MAX_SAFE_INTEGER + 1)); - client.INCR(key, helper.isNumber(MAX_SAFE_INTEGER + 2)); - client.INCR(key, helper.isNumber(MAX_SAFE_INTEGER + 3)); - client.INCR(key, helper.isNumber(MAX_SAFE_INTEGER + 4)); - client.INCR(key, helper.isNumber(MAX_SAFE_INTEGER + 5)); - client.INCR(key, function (err, res) { - helper.isNumber(MAX_SAFE_INTEGER + 6)(err, res); - assert.strictEqual(typeof res, 'number'); - }); - client.INCR(key, helper.isNumber(MAX_SAFE_INTEGER + 7)); - client.INCR(key, helper.isNumber(MAX_SAFE_INTEGER + 8)); - client.INCR(key, helper.isNumber(MAX_SAFE_INTEGER + 9)); - client.INCR(key, helper.isNumber(MAX_SAFE_INTEGER + 10, done)); - }); - - it('count above the safe integers as strings', function (done) { - args[2].string_numbers = true; - client = redis.createClient.apply(null, args); - // Set a value to the maximum safe allowed javascript number (2^53) - client.set(key, MAX_SAFE_INTEGER, helper.isNotError()); - client.incr(key, helper.isString('9007199254740992')); - client.incr(key, helper.isString('9007199254740993')); - client.incr(key, helper.isString('9007199254740994')); - client.incr(key, helper.isString('9007199254740995')); - client.incr(key, helper.isString('9007199254740996')); - client.incr(key, function (err, res) { - helper.isString('9007199254740997')(err, res); - assert.strictEqual(typeof res, 'string'); - }); - client.incr(key, helper.isString('9007199254740998')); - client.incr(key, helper.isString('9007199254740999')); - client.incr(key, helper.isString('9007199254741000')); - client.incr(key, helper.isString('9007199254741001', done)); - }); - }); - }); - }); -}); diff --git a/test/commands/info.spec.js b/test/commands/info.spec.js deleted file mode 100644 index 4e5bb481fc..0000000000 --- a/test/commands/info.spec.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'info' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushall(done); - }); - }); - - afterEach(function () { - client.end(true); - }); - - it('update serverInfo after a info command', function (done) { - client.set('foo', 'bar'); - client.info(); - client.select(2, function () { - assert.strictEqual(client.serverInfo.db2, undefined); - }); - client.set('foo', 'bar'); - client.info(); - setTimeout(function () { - assert.strictEqual(typeof client.serverInfo.db2, 'object'); - done(); - }, 30); - }); - - it('works with optional section provided with and without callback', function (done) { - client.set('foo', 'bar'); - client.info('keyspace'); - client.select(2, function () { - assert.strictEqual(Object.keys(client.server_info).length, 2, 'Key length should be three'); - assert.strictEqual(typeof client.server_info.db0, 'object', 'db0 keyspace should be an object'); - }); - client.info(['keyspace']); - client.set('foo', 'bar'); - client.info('all', function (err, res) { - assert(Object.keys(client.server_info).length > 3, 'Key length should be way above three'); - assert.strictEqual(typeof client.server_info.redis_version, 'string'); - assert.strictEqual(typeof client.server_info.db2, 'object'); - done(); - }); - }); - - it('check redis v.2.4 support', function (done) { - var end = helper.callFuncAfter(done, 2); - client.internal_send_command = function (command_obj) { - assert.strictEqual(command_obj.args.length, 0); - assert.strictEqual(command_obj.command, 'info'); - end(); - }; - client.info(); - client.info(function () {}); - }); - - it('emit error after a failure', function (done) { - client.info(); - client.once('error', function (err) { - assert.strictEqual(err.code, 'UNCERTAIN_STATE'); - assert.strictEqual(err.command, 'INFO'); - done(); - }); - client.stream.destroy(); - }); - }); - }); -}); diff --git a/test/commands/keys.spec.js b/test/commands/keys.spec.js deleted file mode 100644 index 6ce45790b6..0000000000 --- a/test/commands/keys.spec.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var crypto = require('crypto'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'keys' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushall(done); - }); - }); - - it('returns matching keys', function (done) { - client.mset(['test keys 1', 'test val 1', 'test keys 2', 'test val 2'], helper.isString('OK')); - client.KEYS('test keys*', function (err, results) { - assert.strictEqual(2, results.length); - assert.ok(~results.indexOf('test keys 1')); - assert.ok(~results.indexOf('test keys 2')); - return done(err); - }); - }); - - it('handles a large packet size', function (done) { - var keys_values = []; - - for (var i = 0; i < 200; i++) { - var key_value = [ - 'multibulk:' + crypto.randomBytes(256).toString('hex'), // use long strings as keys to ensure generation of large packet - 'test val ' + i - ]; - keys_values.push(key_value); - } - - client.mset(keys_values.reduce(function (a, b) { - return a.concat(b); - }), helper.isString('OK')); - - client.keys('multibulk:*', function (err, results) { - assert.deepEqual(keys_values.map(function (val) { - return val[0]; - }).sort(), results.sort()); - return done(err); - }); - }); - - it('handles an empty response', function (done) { - client.KEYS(['users:*'], function (err, results) { - assert.strictEqual(results.length, 0); - assert.ok(Array.isArray(results)); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/mget.spec.js b/test/commands/mget.spec.js deleted file mode 100644 index a2c671f683..0000000000 --- a/test/commands/mget.spec.js +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'mget' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('error', done); - client.once('ready', function () { - client.flushdb(); - client.mset(['mget keys 1', 'mget val 1', 'mget keys 2', 'mget val 2', 'mget keys 3', 'mget val 3'], done); - }); - }); - - it('handles fetching multiple keys in argument form', function (done) { - client.mset(['mget keys 1', 'mget val 1', 'mget keys 2', 'mget val 2', 'mget keys 3', 'mget val 3'], helper.isString('OK')); - client.MGET('mget keys 1', 'mget keys 2', 'mget keys 3', function (err, results) { - assert.strictEqual(3, results.length); - assert.strictEqual('mget val 1', results[0].toString()); - assert.strictEqual('mget val 2', results[1].toString()); - assert.strictEqual('mget val 3', results[2].toString()); - return done(err); - }); - }); - - it('handles fetching multiple keys via an array', function (done) { - client.mget(['mget keys 1', 'mget keys 2', 'mget keys 3'], function (err, results) { - assert.strictEqual('mget val 1', results[0].toString()); - assert.strictEqual('mget val 2', results[1].toString()); - assert.strictEqual('mget val 3', results[2].toString()); - return done(err); - }); - }); - - it('handles fetching multiple keys, when some keys do not exist', function (done) { - client.MGET('mget keys 1', ['some random shit', 'mget keys 2', 'mget keys 3'], function (err, results) { - assert.strictEqual(4, results.length); - assert.strictEqual('mget val 1', results[0].toString()); - assert.strictEqual(null, results[1]); - assert.strictEqual('mget val 2', results[2].toString()); - assert.strictEqual('mget val 3', results[3].toString()); - return done(err); - }); - }); - - it('handles fetching multiple keys, when some keys do not exist promisified', function () { - return client.MGETAsync('mget keys 1', ['some random shit', 'mget keys 2', 'mget keys 3']).then(function (results) { - assert.strictEqual(4, results.length); - assert.strictEqual('mget val 1', results[0].toString()); - assert.strictEqual(null, results[1]); - assert.strictEqual('mget val 2', results[2].toString()); - assert.strictEqual('mget val 3', results[3].toString()); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/monitor.spec.js b/test/commands/monitor.spec.js deleted file mode 100644 index 679277ffca..0000000000 --- a/test/commands/monitor.spec.js +++ /dev/null @@ -1,215 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var utils = require('../../lib/utils'); -var redis = config.redis; - -describe("The 'monitor' method", function () { - - helper.allTests(function (ip, args) { - - var client; - - afterEach(function () { - client.end(true); - }); - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('connect', function () { - client.flushdb(done); - }); - }); - - it('monitors commands on all redis clients and works in the correct order', function (done) { - var monitorClient = redis.createClient.apply(null, args); - var responses = [ - ['mget', 'some', 'keys', 'foo', 'bar'], - ['set', 'json', '{"foo":"123","bar":"sdflkdfsjk","another":false}'], - ['eval', "return redis.call('set', 'sha', 'test')", '0'], - ['set', 'sha', 'test'], - ['get', 'baz'], - ['set', 'foo', 'bar" "s are " " good!"'], - ['mget', 'foo', 'baz'], - ['subscribe', 'foo', 'baz'] - ]; - var end = helper.callFuncAfter(done, 5); - - monitorClient.set('foo', 'bar'); - monitorClient.flushdb(); - monitorClient.monitor(function (err, res) { - assert.strictEqual(res, 'OK'); - client.mget('some', 'keys', 'foo', 'bar'); - client.set('json', JSON.stringify({ - foo: '123', - bar: 'sdflkdfsjk', - another: false - })); - client.eval("return redis.call('set', 'sha', 'test')", 0); - monitorClient.get('baz', function (err, res) { - assert.strictEqual(res, null); - end(err); - }); - monitorClient.set('foo', 'bar" "s are " " good!"', function (err, res) { - assert.strictEqual(res, 'OK'); - end(err); - }); - monitorClient.mget('foo', 'baz', function (err, res) { - assert.strictEqual(res[0], 'bar" "s are " " good!"'); - assert.strictEqual(res[1], null); - end(err); - }); - monitorClient.subscribe('foo', 'baz', function (err, res) { - // The return value might change in v.4 - // assert.strictEqual(res, 'baz'); - // TODO: Fix the return value of subscribe calls - end(err); - }); - }); - - monitorClient.on('monitor', function (time, args, rawOutput) { - assert.strictEqual(monitorClient.monitoring, true); - assert.deepEqual(args, responses.shift()); - assert(utils.monitor_regex.test(rawOutput), rawOutput); - if (responses.length === 0) { - monitorClient.quit(end); - } - }); - }); - - it('monitors returns strings in the rawOutput even with return_buffers activated', function (done) { - if (process.platform === 'win32') { - this.skip(); - } - var monitorClient = redis.createClient({ - return_buffers: true, - path: '/tmp/redis.sock' - }); - - monitorClient.MONITOR(function (err, res) { - assert.strictEqual(monitorClient.monitoring, true); - assert.strictEqual(res.inspect(), Buffer.from('OK').inspect()); - monitorClient.mget('hello', Buffer.from('world')); - }); - - monitorClient.on('monitor', function (time, args, rawOutput) { - assert.strictEqual(typeof rawOutput, 'string'); - assert(utils.monitor_regex.test(rawOutput), rawOutput); - assert.deepEqual(args, ['mget', 'hello', 'world']); - // Quit immediatly ends monitoring mode and therefore does not stream back the quit command - monitorClient.quit(done); - }); - }); - - it('monitors reconnects properly and works with the offline queue', function (done) { - var called = false; - client.MONITOR(helper.isString('OK')); - client.mget('hello', 'world'); - client.on('monitor', function (time, args, rawOutput) { - assert.strictEqual(client.monitoring, true); - assert(utils.monitor_regex.test(rawOutput), rawOutput); - assert.deepEqual(args, ['mget', 'hello', 'world']); - if (called) { - // End after a reconnect - return done(); - } - client.stream.destroy(); - client.mget('hello', 'world'); - called = true; - }); - }); - - it('monitors reconnects properly and works with the offline queue in a batch statement', function (done) { - var called = false; - var multi = client.batch(); - multi.MONITOR(helper.isString('OK')); - multi.mget('hello', 'world'); - multi.exec(function (err, res) { - assert.deepEqual(res, ['OK', [null, null]]); - }); - client.on('monitor', function (time, args, rawOutput) { - assert.strictEqual(client.monitoring, true); - assert(utils.monitor_regex.test(rawOutput), rawOutput); - assert.deepEqual(args, ['mget', 'hello', 'world']); - if (called) { - // End after a reconnect - return done(); - } - client.stream.destroy(); - client.mget('hello', 'world'); - called = true; - }); - }); - - it('monitor activates even if the command could not be processed properly after a reconnect', function (done) { - client.MONITOR(function (err, res) { - assert.strictEqual(err.code, 'UNCERTAIN_STATE'); - }); - client.on('error', function (err) {}); // Ignore error here - client.stream.destroy(); - var end = helper.callFuncAfter(done, 2); - client.on('monitor', function (time, args, rawOutput) { - assert.strictEqual(client.monitoring, true); - end(); - }); - client.on('reconnecting', function () { - client.get('foo', function (err, res) { - assert(!err); - assert.strictEqual(client.monitoring, true); - end(); - }); - }); - }); - - it('monitors works in combination with the pub sub mode and the offline queue', function (done) { - var responses = [ - ['subscribe', '/foo', '/bar'], - ['unsubscribe', '/bar'], - ['get', 'foo'], - ['subscribe', '/foo'], - ['subscribe', 'baz'], - ['unsubscribe', 'baz'], - ['publish', '/foo', 'hello world'] - ]; - var pub = redis.createClient(); - pub.on('ready', function () { - client.MONITOR(function (err, res) { - assert.strictEqual(res, 'OK'); - pub.get('foo', helper.isNull()); - }); - client.subscribe('/foo', '/bar'); - client.unsubscribe('/bar'); - setTimeout(function () { - client.stream.destroy(); - client.once('ready', function () { - pub.publish('/foo', 'hello world'); - }); - client.set('foo', 'bar', helper.isError()); - client.subscribe('baz'); - client.unsubscribe('baz'); - }, 150); - var called = false; - client.on('monitor', function (time, args, rawOutput) { - assert.deepEqual(args, responses.shift()); - assert(utils.monitor_regex.test(rawOutput), rawOutput); - if (responses.length === 0) { - // The publish is called right after the reconnect and the monitor is called before the message is emitted. - // Therefore we have to wait till the next tick - process.nextTick(function () { - assert(called); - client.quit(done); - pub.end(false); - }); - } - }); - client.on('message', function (channel, msg) { - assert.strictEqual(channel, '/foo'); - assert.strictEqual(msg, 'hello world'); - called = true; - }); - }); - }); - }); -}); diff --git a/test/commands/mset.spec.js b/test/commands/mset.spec.js deleted file mode 100644 index b60f383134..0000000000 --- a/test/commands/mset.spec.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; -var uuid = require('uuid'); - -describe("The 'mset' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var key, value, key2, value2; - - beforeEach(function () { - key = uuid.v4(); - value = uuid.v4(); - key2 = uuid.v4(); - value2 = uuid.v4(); - }); - - describe('when not connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.quit(); - }); - client.on('end', done); - }); - - it('reports an error', function (done) { - client.mset(key, value, key2, value2, function (err, res) { - assert(err.message.match(/The connection is already closed/)); - done(); - }); - }); - }); - - describe('when connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - done(); - }); - }); - - afterEach(function () { - client.end(true); - }); - - describe('and a callback is specified', function () { - describe('with valid parameters', function () { - it('sets the value correctly', function (done) { - client.mset(key, value, key2, value2, function (err) { - if (err) { - return done(err); - } - client.get(key, helper.isString(value)); - client.get(key2, helper.isString(value2, done)); - }); - }); - }); - - describe("with undefined 'key' parameter and missing 'value' parameter", function () { - it('reports an error', function (done) { - client.mset(undefined, function (err, res) { - helper.isError()(err, null); - done(); - }); - }); - }); - - }); - - describe('and no callback is specified', function () { - describe('with valid parameters', function () { - it('sets the value correctly', function (done) { - client.mset(key, value2, key2, value); - client.get(key, helper.isString(value2)); - client.get(key2, helper.isString(value, done)); - }); - - it('sets the value correctly with array syntax', function (done) { - client.mset([key, value2, key2, value]); - client.get(key, helper.isString(value2)); - client.get(key2, helper.isString(value, done)); - }); - }); - - describe("with undefined 'key' and missing 'value' parameter", function () { - // this behavior is different from the 'set' behavior. - it('emits an error', function (done) { - client.on('error', function (err) { - assert.strictEqual(err.message, "ERR wrong number of arguments for 'mset' command"); - assert.strictEqual(err.name, 'ReplyError'); - done(); - }); - - client.mset(); - }); - }); - }); - }); - }); - }); -}); diff --git a/test/commands/msetnx.spec.js b/test/commands/msetnx.spec.js deleted file mode 100644 index 179f33744e..0000000000 --- a/test/commands/msetnx.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'msetnx' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('if any keys exist entire operation fails', function (done) { - client.mset(['mset1', 'val1', 'mset2', 'val2', 'mset3', 'val3'], helper.isString('OK')); - client.MSETNX(['mset3', 'val3', 'mset4', 'val4'], helper.isNumber(0)); - client.exists(['mset4'], helper.isNumber(0, done)); - }); - - it('sets multiple keys if all keys are not set', function (done) { - client.msetnx(['mset3', 'val3', 'mset4', 'val4'], helper.isNumber(1)); - client.exists(['mset3'], helper.isNumber(1)); - client.exists(['mset3'], helper.isNumber(1, done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/randomkey.test.js b/test/commands/randomkey.test.js deleted file mode 100644 index 226194f921..0000000000 --- a/test/commands/randomkey.test.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'randomkey' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns a random key', function (done) { - client.mset(['test keys 1', 'test val 1', 'test keys 2', 'test val 2'], helper.isString('OK')); - client.RANDOMKEY([], function (err, results) { - assert.strictEqual(true, /test keys.+/.test(results)); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/rename.spec.js b/test/commands/rename.spec.js deleted file mode 100644 index 284fba310e..0000000000 --- a/test/commands/rename.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'rename' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('populates the new key', function (done) { - client.set(['foo', 'bar'], helper.isString('OK')); - client.rename(['foo', 'new foo'], helper.isString('OK')); - client.exists(['new foo'], helper.isNumber(1, done)); - }); - - it('removes the old key', function (done) { - client.set(['foo', 'bar'], helper.isString('OK')); - client.RENAME(['foo', 'new foo'], helper.isString('OK')); - client.exists(['foo'], helper.isNumber(0, done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/renamenx.spec.js b/test/commands/renamenx.spec.js deleted file mode 100644 index b56b0a1a5c..0000000000 --- a/test/commands/renamenx.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'renamenx' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('renames the key if target does not yet exist', function (done) { - client.set('foo', 'bar', helper.isString('OK')); - client.RENAMENX('foo', 'foo2', helper.isNumber(1)); - client.exists('foo', helper.isNumber(0)); - client.exists(['foo2'], helper.isNumber(1, done)); - }); - - it('does not rename the key if the target exists', function (done) { - client.set('foo', 'bar', helper.isString('OK')); - client.set('foo2', 'apple', helper.isString('OK')); - client.renamenx('foo', 'foo2', helper.isNumber(0)); - client.exists('foo', helper.isNumber(1)); - client.exists(['foo2'], helper.isNumber(1, done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/rpush.spec.js b/test/commands/rpush.spec.js deleted file mode 100644 index 793d5d2d80..0000000000 --- a/test/commands/rpush.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; -var assert = require('assert'); - -describe("The 'rpush' command", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('inserts multiple values at a time into a list', function (done) { - client.rpush('test', ['list key', 'should be a list']); - client.lrange('test', 0, -1, function (err, res) { - assert.equal(res[0], 'list key'); - assert.equal(res[1], 'should be a list'); - done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/sadd.spec.js b/test/commands/sadd.spec.js deleted file mode 100644 index 442f391b9d..0000000000 --- a/test/commands/sadd.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'sadd' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('allows a single value to be added to the set', function (done) { - client.SADD('set0', 'member0', helper.isNumber(1)); - client.smembers('set0', function (err, res) { - assert.ok(~res.indexOf('member0')); - return done(err); - }); - }); - - it('does not add the same value to the set twice', function (done) { - client.sadd('set0', 'member0', helper.isNumber(1)); - client.SADD('set0', 'member0', helper.isNumber(0, done)); - }); - - it('allows multiple values to be added to the set', function (done) { - client.sadd('set0', ['member0', 'member1', 'member2'], helper.isNumber(3)); - client.smembers('set0', function (err, res) { - assert.strictEqual(res.length, 3); - assert.ok(~res.indexOf('member0')); - assert.ok(~res.indexOf('member1')); - assert.ok(~res.indexOf('member2')); - return done(err); - }); - }); - - it('allows multiple values to be added to the set with a different syntax', function (done) { - client.sadd(['set0', 'member0', 'member1', 'member2'], helper.isNumber(3)); - client.smembers('set0', function (err, res) { - assert.strictEqual(res.length, 3); - assert.ok(~res.indexOf('member0')); - assert.ok(~res.indexOf('member1')); - assert.ok(~res.indexOf('member2')); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/scard.spec.js b/test/commands/scard.spec.js deleted file mode 100644 index e327eb282a..0000000000 --- a/test/commands/scard.spec.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'scard' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns the number of values in a set', function (done) { - client.sadd('foo', [1, 2, 3], helper.isNumber(3)); - client.scard('foo', helper.isNumber(3, done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/script.spec.js b/test/commands/script.spec.js deleted file mode 100644 index c374f5b5e1..0000000000 --- a/test/commands/script.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var crypto = require('crypto'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'script' method", function () { - - helper.allTests(function (ip, args) { - var command = 'return 99'; - var commandSha = crypto.createHash('sha1').update(command).digest('hex'); - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - afterEach(function () { - client.end(true); - }); - - it("loads script with client.script('load')", function (done) { - client.script('load', command, function (err, result) { - assert.strictEqual(result, commandSha); - return done(); - }); - }); - - it('allows a loaded script to be evaluated', function (done) { - client.evalsha(commandSha, 0, helper.isNumber(99, done)); - }); - - it('allows a script to be loaded as part of a chained transaction', function (done) { - client.multi().script('load', command).exec(function (err, result) { - assert.strictEqual(result[0], commandSha); - return done(); - }); - }); - - it("allows a script to be loaded using a transaction's array syntax", function (done) { - client.multi([['script', 'load', command]]).exec(function (err, result) { - assert.strictEqual(result[0], commandSha); - return done(); - }); - }); - }); - }); -}); diff --git a/test/commands/sdiff.spec.js b/test/commands/sdiff.spec.js deleted file mode 100644 index 95f81f09bd..0000000000 --- a/test/commands/sdiff.spec.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'sdiff' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns set difference', function (done) { - client.sadd('foo', 'x', helper.isNumber(1)); - client.sadd('foo', ['a'], helper.isNumber(1)); - client.sadd('foo', 'b', helper.isNumber(1)); - client.sadd(['foo', 'c'], helper.isNumber(1)); - - client.sadd(['bar', 'c', helper.isNumber(1)]); - - client.sadd('baz', 'a', helper.isNumber(1)); - client.sadd('baz', 'd', helper.isNumber(1)); - - client.sdiff('foo', 'bar', 'baz', function (err, values) { - values.sort(); - assert.equal(values.length, 2); - assert.equal(values[0], 'b'); - assert.equal(values[1], 'x'); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/sdiffstore.spec.js b/test/commands/sdiffstore.spec.js deleted file mode 100644 index fe822b561b..0000000000 --- a/test/commands/sdiffstore.spec.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'sdiffstore' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('calculates set difference ands stores it in a key', function (done) { - client.sadd('foo', 'x', helper.isNumber(1)); - client.sadd('foo', 'a', helper.isNumber(1)); - client.sadd('foo', 'b', helper.isNumber(1)); - client.sadd('foo', 'c', helper.isNumber(1)); - - client.sadd('bar', 'c', helper.isNumber(1)); - - client.sadd('baz', 'a', helper.isNumber(1)); - client.sadd('baz', 'd', helper.isNumber(1)); - - client.sdiffstore('quux', 'foo', 'bar', 'baz', helper.isNumber(2)); - - client.smembers('quux', function (err, values) { - var members = values.sort(); - assert.deepEqual(members, [ 'b', 'x' ]); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/select.spec.js b/test/commands/select.spec.js deleted file mode 100644 index 053496e337..0000000000 --- a/test/commands/select.spec.js +++ /dev/null @@ -1,126 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'select' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - describe('when not connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.quit(); - }); - client.on('end', done); - }); - - it('returns an error if redis is not connected', function (done) { - var buffering = client.select(1, function (err, res) { - assert(err.message.match(/The connection is already closed/)); - done(); - }); - assert(typeof buffering === 'boolean'); - }); - }); - - describe('when connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - afterEach(function () { - client.end(true); - }); - - it('changes the database and calls the callback', function (done) { - // default value of null means database 0 will be used. - assert.strictEqual(client.selected_db, undefined, 'default db should be undefined'); - var buffering = client.SELECT(1, function (err, res) { - helper.isNotError()(err, res); - assert.strictEqual(client.selected_db, 1, 'db should be 1 after select'); - done(); - }); - assert(typeof buffering === 'boolean'); - }); - - describe('and a callback is specified', function () { - describe('with a valid db index', function () { - it('selects the appropriate database', function (done) { - assert.strictEqual(client.selected_db, undefined, 'default db should be undefined'); - client.select(1, function (err) { - assert.equal(err, null); - assert.equal(client.selected_db, 1, 'we should have selected the new valid DB'); - done(); - }); - }); - }); - - describe('with an invalid db index', function () { - it('returns an error', function (done) { - assert.strictEqual(client.selected_db, undefined, 'default db should be undefined'); - client.select(9999, function (err) { - assert.equal(err.code, 'ERR'); - assert((err.message === 'ERR DB index is out of range' || err.message === 'ERR invalid DB index')); - done(); - }); - }); - }); - }); - - describe('and no callback is specified', function () { - describe('with a valid db index', function () { - it('selects the appropriate database', function (done) { - assert.strictEqual(client.selected_db, undefined, 'default db should be undefined'); - client.select(1); - setTimeout(function () { - assert.equal(client.selected_db, 1, 'we should have selected the new valid DB'); - done(); - }, 25); - }); - }); - - describe('with an invalid db index', function () { - it('emits an error when callback not provided', function (done) { - assert.strictEqual(client.selected_db, undefined, 'default db should be undefined'); - - client.on('error', function (err) { - assert.strictEqual(err.command, 'SELECT'); - assert((err.message === 'ERR DB index is out of range' || err.message === 'ERR invalid DB index')); - done(); - }); - - client.select(9999); - }); - }); - }); - - describe('reconnection occurs', function () { - it('selects the appropriate database after a reconnect', function (done) { - assert.strictEqual(client.selected_db, undefined, 'default db should be undefined'); - client.select(3); - client.set('foo', 'bar', function () { - client.stream.destroy(); - }); - client.once('ready', function () { - assert.strictEqual(client.selected_db, 3); - assert(typeof client.server_info.db3 === 'object'); - done(); - }); - }); - }); - }); - }); - }); -}); diff --git a/test/commands/set.spec.js b/test/commands/set.spec.js deleted file mode 100644 index 33a8bfa22c..0000000000 --- a/test/commands/set.spec.js +++ /dev/null @@ -1,178 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; -var uuid = require('uuid'); - -describe("The 'set' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var key, value; - - beforeEach(function () { - key = uuid.v4(); - value = uuid.v4(); - }); - - describe('when not connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.quit(); - }); - client.on('end', done); - }); - - it('reports an error', function (done) { - client.set(key, value, function (err, res) { - assert(err.message.match(/The connection is already closed/)); - done(); - }); - }); - }); - - describe('when connected', function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - afterEach(function () { - client.end(true); - }); - - describe('and a callback is specified', function () { - describe('with valid parameters', function () { - it('sets the value correctly', function (done) { - client.SET(key, value, function (err, res) { - helper.isNotError()(err, res); - client.get(key, function (err, res) { - helper.isString(value)(err, res); - done(); - }); - }); - }); - - it('set expire date in seconds', function (done) { - client.set('foo', 'bar', 'ex', 10, helper.isString('OK')); - client.pttl('foo', function (err, res) { - assert(res >= 10000 - 50); // Max 50 ms should have passed - assert(res <= 10000); // Max possible should be 10.000 - done(err); - }); - }); - - it('set expire date in milliseconds', function (done) { - client.set('foo', 'bar', 'px', 100, helper.isString('OK')); - client.pttl('foo', function (err, res) { - assert(res >= 50); // Max 50 ms should have passed - assert(res <= 100); // Max possible should be 100 - done(err); - }); - }); - - it('only set the key if (not) already set', function (done) { - client.set('foo', 'bar', 'NX', helper.isString('OK')); - client.set('foo', 'bar', 'nx', helper.isNull()); - client.set('foo', 'bar', 'EX', '10', 'XX', helper.isString('OK')); - client.ttl('foo', function (err, res) { - assert(res >= 9); // Min 9s should be left - assert(res <= 10); // Max 10s should be left - done(err); - }); - }); - }); - - describe('reports an error with invalid parameters', function () { - it("undefined 'key' and missing 'value' parameter", function (done) { - client.set(undefined, function (err, res) { - helper.isError()(err, null); - assert.equal(err.command, 'SET'); - done(); - }); - }); - - it('empty array as second parameter', function (done) { - client.set('foo', [], function (err, res) { - assert.strictEqual(err.message, "ERR wrong number of arguments for 'set' command"); - done(); - }); - }); - }); - }); - - describe('and no callback is specified', function () { - describe('with valid parameters', function () { - it('sets the value correctly', function (done) { - client.set(key, value); - client.get(key, helper.isString(value, done)); - }); - - it('sets the value correctly even if the callback is explicitly set to undefined', function (done) { - client.set(key, value, undefined); - client.get(key, helper.isString(value, done)); - }); - - it('sets the value correctly with the array syntax', function (done) { - client.set([key, value]); - client.get(key, helper.isString(value, done)); - }); - }); - - describe("with undefined 'key' and missing 'value' parameter", function () { - it('emits an error without callback', function (done) { - client.on('error', function (err) { - assert.equal(err.message, "ERR wrong number of arguments for 'set' command"); - assert.equal(err.command, 'SET'); - done(); - }); - client.set(undefined); - }); - }); - - it('errors if null value is passed', function (done) { - try { - client.set('foo', null); - assert(false); - } catch (error) { - assert(/The SET command contains a invalid argument type./.test(error.message)); - } - client.get('foo', helper.isNull(done)); - }); - - it('calls callback with error if null value is passed', function (done) { - client.set('foo', null, helper.isError(done)); - }); - - it('emit an error with only the key set', function (done) { - client.on('error', function (err) { - assert.equal(err.message, "ERR wrong number of arguments for 'set' command"); - done(); - }); - - client.set('foo'); - }); - - it('emit an error without any parameters', function (done) { - client.once('error', function (err) { - assert.equal(err.message, "ERR wrong number of arguments for 'set' command"); - assert.equal(err.command, 'SET'); - done(); - }); - client.set(); - }); - }); - }); - }); - }); -}); diff --git a/test/commands/setex.spec.js b/test/commands/setex.spec.js deleted file mode 100644 index a2126e6dbb..0000000000 --- a/test/commands/setex.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'setex' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('sets a key with an expiry', function (done) { - client.setex(['setex key', '100', 'setex val'], helper.isString('OK')); - var buffering = client.exists(['setex key'], helper.isNumber(1)); - assert(typeof buffering === 'boolean'); - client.ttl(['setex key'], function (err, ttl) { - assert(ttl > 0); - return done(); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/setnx.spec.js b/test/commands/setnx.spec.js deleted file mode 100644 index 4b4688c0a6..0000000000 --- a/test/commands/setnx.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'setnx' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('sets key if it does not have a value', function (done) { - client.SETNX('foo', 'banana', helper.isNumber(1)); - client.get('foo', helper.isString('banana', done)); - }); - - it('does not set key if it already has a value', function (done) { - client.set('foo', 'bar', helper.isString('OK')); - client.setnx('foo', 'banana', helper.isNumber(0)); - client.get('foo', helper.isString('bar', done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/sinter.spec.js b/test/commands/sinter.spec.js deleted file mode 100644 index c4fc775955..0000000000 --- a/test/commands/sinter.spec.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'sinter' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('handles two sets being intersected', function (done) { - client.sadd('sa', 'a', helper.isNumber(1)); - client.sadd('sa', 'b', helper.isNumber(1)); - client.sadd('sa', 'c', helper.isNumber(1)); - - client.sadd('sb', 'b', helper.isNumber(1)); - client.sadd('sb', 'c', helper.isNumber(1)); - client.sadd('sb', 'd', helper.isNumber(1)); - - client.SINTER('sa', 'sb', function (err, intersection) { - assert.equal(intersection.length, 2); - assert.deepEqual(intersection.sort(), [ 'b', 'c' ]); - return done(err); - }); - }); - - it('handles three sets being intersected', function (done) { - client.sadd('sa', 'a', helper.isNumber(1)); - client.sadd('sa', 'b', helper.isNumber(1)); - client.sadd('sa', 'c', helper.isNumber(1)); - - client.sadd('sb', 'b', helper.isNumber(1)); - client.sadd('sb', 'c', helper.isNumber(1)); - client.sadd('sb', 'd', helper.isNumber(1)); - - client.sadd('sc', 'c', helper.isNumber(1)); - client.sadd('sc', 'd', helper.isNumber(1)); - client.sadd('sc', 'e', helper.isNumber(1)); - - client.sinter('sa', 'sb', 'sc', function (err, intersection) { - assert.equal(intersection.length, 1); - assert.equal(intersection[0], 'c'); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/sinterstore.spec.js b/test/commands/sinterstore.spec.js deleted file mode 100644 index 1ea4c4b109..0000000000 --- a/test/commands/sinterstore.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'sinterstore' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('calculates set intersection and stores it in a key', function (done) { - client.sadd('sa', 'a', helper.isNumber(1)); - client.sadd('sa', 'b', helper.isNumber(1)); - client.sadd('sa', 'c', helper.isNumber(1)); - - client.sadd('sb', 'b', helper.isNumber(1)); - client.sadd('sb', 'c', helper.isNumber(1)); - client.sadd('sb', 'd', helper.isNumber(1)); - - client.sadd('sc', 'c', helper.isNumber(1)); - client.sadd('sc', 'd', helper.isNumber(1)); - client.sadd('sc', 'e', helper.isNumber(1)); - - client.sinterstore('foo', 'sa', 'sb', 'sc', helper.isNumber(1)); - - client.smembers('foo', function (err, members) { - assert.deepEqual(members, [ 'c' ]); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/sismember.spec.js b/test/commands/sismember.spec.js deleted file mode 100644 index 37ac1c466a..0000000000 --- a/test/commands/sismember.spec.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'sismember' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns 0 if the value is not in the set', function (done) { - client.sismember('foo', 'banana', helper.isNumber(0, done)); - }); - - it('returns 1 if the value is in the set', function (done) { - client.sadd('foo', 'banana', helper.isNumber(1)); - client.SISMEMBER('foo', 'banana', helper.isNumber(1, done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/slowlog.spec.js b/test/commands/slowlog.spec.js deleted file mode 100644 index 21f3f8007a..0000000000 --- a/test/commands/slowlog.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'slowlog' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('logs operations in slowlog', function (done) { - client.config('set', 'slowlog-log-slower-than', 0, helper.isString('OK')); - client.slowlog('reset', helper.isString('OK')); - client.set('foo', 'bar', helper.isString('OK')); - client.get('foo', helper.isString('bar')); - client.SLOWLOG('get', function (err, res) { - assert.equal(res.length, 3); - assert.equal(res[0][3].length, 2); - assert.deepEqual(res[1][3], ['set', 'foo', 'bar']); - assert.deepEqual(res[2][3], ['slowlog', 'reset']); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/smembers.spec.js b/test/commands/smembers.spec.js deleted file mode 100644 index 0bc8143719..0000000000 --- a/test/commands/smembers.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'smembers' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns all values in a set', function (done) { - client.sadd('foo', 'x', helper.isNumber(1)); - client.sadd('foo', 'y', helper.isNumber(1)); - client.smembers('foo', function (err, values) { - assert.equal(values.length, 2); - var members = values.sort(); - assert.deepEqual(members, [ 'x', 'y' ]); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/smove.spec.js b/test/commands/smove.spec.js deleted file mode 100644 index 969c264b75..0000000000 --- a/test/commands/smove.spec.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'smove' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('moves a value to a set that does not yet exist', function (done) { - client.sadd('foo', 'x', helper.isNumber(1)); - client.smove('foo', 'bar', 'x', helper.isNumber(1)); - client.sismember('foo', 'x', helper.isNumber(0)); - client.sismember('bar', 'x', helper.isNumber(1, done)); - }); - - it('does not move a value if it does not exist in the first set', function (done) { - client.sadd('foo', 'x', helper.isNumber(1)); - client.SMOVE('foo', 'bar', 'y', helper.isNumber(0)); - client.sismember('foo', 'y', helper.isNumber(0)); - client.sismember('bar', 'y', helper.isNumber(0, done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/sort.spec.js b/test/commands/sort.spec.js deleted file mode 100644 index 2ee08c44e2..0000000000 --- a/test/commands/sort.spec.js +++ /dev/null @@ -1,130 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -function setupData (client, done) { - client.rpush('y', 'd'); - client.rpush('y', 'b'); - client.rpush('y', 'a'); - client.rpush('y', 'c'); - - client.rpush('x', '3'); - client.rpush('x', '9'); - client.rpush('x', '2'); - client.rpush('x', '4'); - - client.set('w3', '4'); - client.set('w9', '5'); - client.set('w2', '12'); - client.set('w4', '6'); - - client.set('o2', 'buz'); - client.set('o3', 'foo'); - client.set('o4', 'baz'); - client.set('o9', 'bar'); - - client.set('p2', 'qux'); - client.set('p3', 'bux'); - client.set('p4', 'lux'); - client.set('p9', 'tux', done); -} - -describe("The 'sort' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('error', done); - client.once('connect', function () { - client.flushdb(); - setupData(client, done); - }); - }); - - describe('alphabetical', function () { - it('sorts in ascending alphabetical order', function (done) { - client.sort('y', 'asc', 'alpha', function (err, sorted) { - assert.deepEqual(sorted, ['a', 'b', 'c', 'd']); - return done(err); - }); - }); - - it('sorts in descending alphabetical order', function (done) { - client.SORT('y', 'desc', 'alpha', function (err, sorted) { - assert.deepEqual(sorted, ['d', 'c', 'b', 'a']); - return done(err); - }); - }); - }); - - describe('numeric', function () { - it('sorts in ascending numeric order', function (done) { - client.sort('x', 'asc', function (err, sorted) { - assert.deepEqual(sorted, [2, 3, 4, 9]); - return done(err); - }); - }); - - it('sorts in descending numeric order', function (done) { - client.sort('x', 'desc', function (err, sorted) { - assert.deepEqual(sorted, [9, 4, 3, 2]); - return done(err); - }); - }); - }); - - describe('pattern', function () { - it('handles sorting with a pattern', function (done) { - client.sort('x', 'by', 'w*', 'asc', function (err, sorted) { - assert.deepEqual(sorted, [3, 9, 4, 2]); - return done(err); - }); - }); - - it("handles sorting with a 'by' pattern and 1 'get' pattern", function (done) { - client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', function (err, sorted) { - assert.deepEqual(sorted, ['foo', 'bar', 'baz', 'buz']); - return done(err); - }); - }); - - it("handles sorting with a 'by' pattern and 2 'get' patterns", function (done) { - client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*', function (err, sorted) { - assert.deepEqual(sorted, ['foo', 'bux', 'bar', 'tux', 'baz', 'lux', 'buz', 'qux']); - return done(err); - }); - }); - - it("handles sorting with a 'by' pattern and 2 'get' patterns with the array syntax", function (done) { - client.sort(['x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*'], function (err, sorted) { - assert.deepEqual(sorted, ['foo', 'bux', 'bar', 'tux', 'baz', 'lux', 'buz', 'qux']); - return done(err); - }); - }); - - it("sorting with a 'by' pattern and 2 'get' patterns and stores results", function (done) { - client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*', 'store', 'bacon', function (err) { - if (err) return done(err); - }); - - client.lrange('bacon', 0, -1, function (err, values) { - assert.deepEqual(values, ['foo', 'bux', 'bar', 'tux', 'baz', 'lux', 'buz', 'qux']); - return done(err); - }); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); - -}); diff --git a/test/commands/spop.spec.js b/test/commands/spop.spec.js deleted file mode 100644 index ec3e93fda3..0000000000 --- a/test/commands/spop.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'spop' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns a random element from the set', function (done) { - client.sadd('zzz', 'member0', helper.isNumber(1)); - client.scard('zzz', helper.isNumber(1)); - - client.spop('zzz', function (err, value) { - if (err) return done(err); - assert.equal(value, 'member0'); - client.scard('zzz', helper.isNumber(0, done)); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/srem.spec.js b/test/commands/srem.spec.js deleted file mode 100644 index d325cb5715..0000000000 --- a/test/commands/srem.spec.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'srem' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('removes a value', function (done) { - client.sadd('set0', 'member0', helper.isNumber(1)); - client.srem('set0', 'member0', helper.isNumber(1)); - client.scard('set0', helper.isNumber(0, done)); - }); - - it('handles attempting to remove a missing value', function (done) { - client.SREM('set0', 'member0', helper.isNumber(0, done)); - }); - - it('allows multiple values to be removed', function (done) { - client.sadd('set0', ['member0', 'member1', 'member2'], helper.isNumber(3)); - client.SREM('set0', ['member1', 'member2'], helper.isNumber(2)); - client.smembers('set0', function (err, res) { - assert.strictEqual(res.length, 1); - assert.ok(~res.indexOf('member0')); - return done(err); - }); - }); - - it('allows multiple values to be removed with send_command', function (done) { - client.send_command('sadd', ['set0', 'member0', 'member1', 'member2'], helper.isNumber(3)); - client.send_command('srem', ['set0', 'member1', 'member2'], helper.isNumber(2)); - client.smembers('set0', function (err, res) { - assert.strictEqual(res.length, 1); - assert.ok(~res.indexOf('member0')); - return done(err); - }); - }); - - it('handles a value missing from the set of values being removed', function (done) { - client.sadd(['set0', 'member0', 'member1', 'member2'], helper.isNumber(3)); - client.SREM(['set0', 'member3', 'member4'], helper.isNumber(0)); - client.smembers('set0', function (err, res) { - assert.strictEqual(res.length, 3); - assert.ok(~res.indexOf('member0')); - assert.ok(~res.indexOf('member1')); - assert.ok(~res.indexOf('member2')); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/sunion.spec.js b/test/commands/sunion.spec.js deleted file mode 100644 index cc8eb62475..0000000000 --- a/test/commands/sunion.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'sunion' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns the union of a group of sets', function (done) { - client.sadd('sa', 'a', helper.isNumber(1)); - client.sadd('sa', 'b', helper.isNumber(1)); - client.sadd('sa', 'c', helper.isNumber(1)); - - client.sadd('sb', 'b', helper.isNumber(1)); - client.sadd('sb', 'c', helper.isNumber(1)); - client.sadd('sb', 'd', helper.isNumber(1)); - - client.sadd('sc', 'c', helper.isNumber(1)); - client.sadd('sc', 'd', helper.isNumber(1)); - client.sadd('sc', 'e', helper.isNumber(1)); - - client.sunion('sa', 'sb', 'sc', function (err, union) { - assert.deepEqual(union.sort(), ['a', 'b', 'c', 'd', 'e']); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/sunionstore.spec.js b/test/commands/sunionstore.spec.js deleted file mode 100644 index bd64c6f6b7..0000000000 --- a/test/commands/sunionstore.spec.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'sunionstore' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('stores the result of a union', function (done) { - client.sadd('sa', 'a', helper.isNumber(1)); - client.sadd('sa', 'b', helper.isNumber(1)); - client.sadd('sa', 'c', helper.isNumber(1)); - - client.sadd('sb', 'b', helper.isNumber(1)); - client.sadd('sb', 'c', helper.isNumber(1)); - client.sadd('sb', 'd', helper.isNumber(1)); - - client.sadd('sc', 'c', helper.isNumber(1)); - client.sadd('sc', 'd', helper.isNumber(1)); - client.sadd('sc', 'e', helper.isNumber(1)); - - client.sunionstore('foo', 'sa', 'sb', 'sc', helper.isNumber(5)); - - client.smembers('foo', function (err, members) { - assert.equal(members.length, 5); - assert.deepEqual(members.sort(), ['a', 'b', 'c', 'd', 'e']); - return done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/ttl.spec.js b/test/commands/ttl.spec.js deleted file mode 100644 index e176d41cb8..0000000000 --- a/test/commands/ttl.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'ttl' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('returns the current ttl on a key', function (done) { - client.set(['ttl key', 'ttl val'], helper.isString('OK')); - client.expire(['ttl key', '100'], helper.isNumber(1)); - client.TTL(['ttl key'], function (err, ttl) { - assert(ttl >= 99); - assert(ttl <= 100); - done(err); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/commands/type.spec.js b/test/commands/type.spec.js deleted file mode 100644 index f70d79e9ef..0000000000 --- a/test/commands/type.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'type' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('reports string type', function (done) { - client.set(['string key', 'should be a string'], helper.isString('OK')); - client.TYPE(['string key'], helper.isString('string', done)); - }); - - it('reports list type', function (done) { - client.rpush(['list key', 'should be a list'], helper.isNumber(1)); - client.type(['list key'], helper.isString('list', done)); - }); - - it('reports set type', function (done) { - client.sadd(['set key', 'should be a set'], helper.isNumber(1)); - client.TYPE(['set key'], helper.isString('set', done)); - }); - - it('reports zset type', function (done) { - client.zadd('zset key', ['10.0', 'should be a zset'], helper.isNumber(1)); - client.TYPE(['zset key'], helper.isString('zset', done)); - }); - - it('reports hash type', function (done) { - client.hset('hash key', 'hashtest', 'should be a hash', helper.isNumber(1)); - client.TYPE(['hash key'], helper.isString('hash', done)); - }); - - it('reports none for null key', function (done) { - client.TYPE('not here yet', helper.isString('none', done)); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); - -}); diff --git a/test/commands/watch.spec.js b/test/commands/watch.spec.js deleted file mode 100644 index 52a9b26f75..0000000000 --- a/test/commands/watch.spec.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('../lib/config'); -var helper = require('../helper'); -var redis = config.redis; - -describe("The 'watch' method", function () { - - helper.allTests(function (ip, args) { - - var watched = 'foobar'; - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - afterEach(function () { - client.end(true); - }); - - it('does not execute transaction if watched key was modified prior to execution', function (done) { - client.WATCH(watched); - client.incr(watched); - var multi = client.multi(); - multi.incr(watched); - multi.exec(helper.isNull(done)); - }); - - it('successfully modifies other keys independently of transaction', function (done) { - client.set('unwatched', 200); - - client.set(watched, 0); - client.watch(watched); - client.incr(watched); - - client.multi().incr(watched).exec(function (err, replies) { - assert.strictEqual(replies, null, 'Aborted transaction multi-bulk reply should be null.'); - - client.get('unwatched', function (err, reply) { - assert.equal(reply, 200, 'Expected 200, got ' + reply); - return done(err); - }); - }); - }); - }); - }); -}); diff --git a/test/commands/zadd.spec.js b/test/commands/zadd.spec.js deleted file mode 100644 index f22963416c..0000000000 --- a/test/commands/zadd.spec.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var assert = require('assert'); -var redis = config.redis; - -describe("The 'zadd' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('reports an error', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - client.zadd('infinity', [+'5t', 'should not be possible'], helper.isError(done)); - }); - - it('return inf / -inf', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - helper.serverVersionAtLeast.call(this, client, [3, 0, 2]); - client.zadd('infinity', [+Infinity, 'should be inf'], helper.isNumber(1)); - client.zadd('infinity', ['inf', 'should be also be inf'], helper.isNumber(1)); - client.zadd('infinity', -Infinity, 'should be negative inf', helper.isNumber(1)); - client.zadd('infinity', [99999999999999999999999, 'should not be inf'], helper.isNumber(1)); - client.zrange('infinity', 0, -1, 'WITHSCORES', function (err, res) { - assert.equal(res[5], 'inf'); - assert.equal(res[1], '-inf'); - if (process.platform !== 'win32') { - assert.equal(res[3], '9.9999999999999992e+22'); - } else { - assert.equal(res[3], '9.9999999999999992e+022'); - } - done(); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); - -}); diff --git a/test/commands/zscan.spec.js b/test/commands/zscan.spec.js deleted file mode 100644 index eb8acf44db..0000000000 --- a/test/commands/zscan.spec.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var assert = require('assert'); -var redis = config.redis; - -describe("The 'zscan' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('return values', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - helper.serverVersionAtLeast.call(this, client, [2, 8, 0]); - var hash = {}; - var set = []; - var zset = ['zset:1']; - for (var i = 0; i < 500; i++) { - hash['key_' + i] = 'value_' + i; - set.push('member_' + i); - zset.push(i, 'z_member_' + i); - } - client.hmset('hash:1', hash); - client.sadd('set:1', set); - client.zadd(zset); - client.zscan('zset:1', 0, 'MATCH', '*', 'COUNT', 500, function (err, res) { - assert(!err); - assert.strictEqual(res.length, 2); - assert.strictEqual(res[1].length, 1000); - done(); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); - -}); diff --git a/test/commands/zscore.spec.js b/test/commands/zscore.spec.js deleted file mode 100644 index 8b95e52764..0000000000 --- a/test/commands/zscore.spec.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -var config = require('../lib/config'); -var helper = require('../helper'); -var assert = require('assert'); -var redis = config.redis; - -describe("The 'zscore' method", function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client; - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(done); - }); - }); - - it('should return the score of member in the sorted set at key', function (done) { - client.zadd('myzset', 1, 'one'); - client.zscore('myzset', 'one', function (err, res) { - assert.equal(res, 1); - done(); - }); - }); - - afterEach(function () { - client.end(true); - }); - }); - }); -}); diff --git a/test/conect.slave.spec.js b/test/conect.slave.spec.js deleted file mode 100644 index 5264a125e6..0000000000 --- a/test/conect.slave.spec.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var helper = require('./helper'); -var RedisProcess = require('./lib/redis-process'); -var rp; -var path = require('path'); -var redis = config.redis; - -if (process.platform === 'win32') { - // TODO: Fix redis process spawn on windows - return; -} - -describe('master slave sync', function () { - var master = null; - var slave = null; - - before(function (done) { - helper.stopRedis(function () { - helper.startRedis('./conf/password.conf', done); - }); - }); - - before(function (done) { - if (helper.redisProcess().spawnFailed()) return done(); - master = redis.createClient({ - password: 'porkchopsandwiches' - }); - var multi = master.multi(); - var i = 0; - while (i < 1000) { - i++; - // Write some data in the redis instance, so there's something to sync - multi.set('foo' + i, 'bar' + new Array(500).join(Math.random())); - } - multi.exec(done); - }); - - it('sync process and no master should delay ready being emitted for slaves', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - var port = 6381; - var firstInfo; - slave = redis.createClient({ - port: port, - retry_strategy: function (options) { - // Try to reconnect in very small intervals to catch the master_link_status down before the sync completes - return 10; - } - }); - - var tmp = slave.info.bind(slave); - var i = 0; - slave.info = function (err, res) { - i++; - tmp(err, res); - if (!firstInfo || Object.keys(firstInfo).length === 0) { - firstInfo = slave.server_info; - } - }; - - slave.on('connect', function () { - assert.strictEqual(i, 0); - }); - - var end = helper.callFuncAfter(done, 2); - - slave.on('ready', function () { - assert.strictEqual(this.server_info.master_link_status, 'up'); - assert.strictEqual(firstInfo.master_link_status, 'down'); - assert(i > 1); - this.get('foo300', function (err, res) { - assert.strictEqual(res.substr(0, 3), 'bar'); - end(err); - }); - }); - - RedisProcess.start(function (err, _rp) { - rp = _rp; - end(err); - }, path.resolve(__dirname, './conf/slave.conf'), port); - }); - - after(function (done) { - if (helper.redisProcess().spawnFailed()) return done(); - var end = helper.callFuncAfter(done, 3); - rp.stop(end); - slave.end(true); - master.flushdb(function (err) { - end(err); - master.end(true); - }); - helper.stopRedis(function () { - helper.startRedis('./conf/redis.conf', end); - }); - }); -}); diff --git a/test/conf/faulty.cert b/test/conf/faulty.cert deleted file mode 100644 index 30c9879006..0000000000 --- a/test/conf/faulty.cert +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDATCCAemgAwIBAgIJALkMmVkQOERnMA0GCSqGSIb3DQEBBQUAMBcxFTATBgNV -BAMMDHJlZGlzLmpzLm9yZzAeFw0xNTEwMTkxMjIzMjRaFw0yNTEwMTYxMjIzMjRa -MBcxFTATBgNVBAMMDHJlZGlzLmpzLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAJ/DmMTJHf7kyspxI1A/JmOc+KI9vxEcN5qn7IiZuGN7ghE43Q3q -XB2GUkMAuW1POkmM5yi3SuT1UXDR/4Gk7KlbHKMs37AV6PgJXX6oX0zu12LTAT7V -5byNrYtehSo42l1188dGEMCGaaf0cDntc7A3aW0ZtzrJt+2pu31Uatl2SEJCMra6 -+v6O0c9aHMF1cArKeawGqR+jHw6vXFZQbUd06nW5nQlUA6wVt1JjlLPwBwYsWLsi -YQxMC8NqpgAIg5tULSCpKwx5isL/CeotVVGDNZ/G8R1nTrxuygPlc3Qskj57hmV4 -tZK4JJxQFi7/9ehvjAvHohKrEPeqV5XL87cCAwEAAaNQME4wHQYDVR0OBBYEFCn/ -5hB+XY4pVOnaqvrmZMxrLFjLMB8GA1UdIwQYMBaAFCn/5hB+XY4pVOnaqvrmZMxr -LFjLMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAEduPyTHpXkCVZRQ -v6p+Ug4iVeXpxGCVr34y7EDUMgmuDdqsz1SrmqeDd0VmjZT8htbWw7QBKDPEBsbi -wl606aAn01iM+oUrwbtXxid1xfZj/j6pIhQVkGu7e/8A7Pr4QOP4OMdHB7EmqkAo -d/OLHa9LdKv2UtJHD6U7oVQbdBHrRV62125GMmotpQuSkEfZM6edKNzHPlqV/zJc -2kGCw3lZC21mTrsSMIC/FQiobPnig4kAvfh0of2rK/XAntlwT8ie1v1aK+jERsfm -uzMihl6XXBdzheq6KdIlf+5STHBIIRcvBoRKr5Va7EhnO03tTzeJowtqDv47yPC6 -w4kLcP8= ------END CERTIFICATE----- diff --git a/test/conf/password.conf b/test/conf/password.conf deleted file mode 100644 index 3b3c02f346..0000000000 --- a/test/conf/password.conf +++ /dev/null @@ -1,6 +0,0 @@ -requirepass porkchopsandwiches -port 6379 -bind ::1 127.0.0.1 -unixsocket /tmp/redis.sock -unixsocketperm 700 -save "" diff --git a/test/conf/redis.conf b/test/conf/redis.conf deleted file mode 100644 index 9bf706c654..0000000000 --- a/test/conf/redis.conf +++ /dev/null @@ -1,5 +0,0 @@ -port 6379 -bind ::1 127.0.0.1 -unixsocket /tmp/redis.sock -unixsocketperm 700 -save "" \ No newline at end of file diff --git a/test/conf/redis.js.org.cert b/test/conf/redis.js.org.cert deleted file mode 100644 index 6747b744ac..0000000000 --- a/test/conf/redis.js.org.cert +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDATCCAemgAwIBAgIJALkMmVkQOERnMA0GCSqGSIb3DQEBBQUAMBcxFTATBgNV -BAMMDHJlZGlzLmpzLm9yZzAeFw0xNTEwMTkxMjIzMjRaFw0yNTEwMTYxMjIzMjRa -MBcxFTATBgNVBAMMDHJlZGlzLmpzLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAJ/DmMTJHf7kyspxI1A/JmOc+KI9vxEcN5qn7IiZuGN7ghE43Q3q -XB2GUkMAuW1POkmM5yi3SuT1UXDR/4Gk7KlbHKMs37AV6PgJXX6oX0zu12LTAT7V -5byNrYtehSo42l1188dGEMCGaaf0cDntc7A3aW0ZtzrJt+2pu31Uatl2SEJCMra6 -+v6O0c9aHMF1cArKeawGqR+jHw6vXFZQbUd05nW5nQlUA6wVt1JjlLPwBwYsWLsi -YQxMC8NqpgAIg5tULSCpKwx5isL/CeotVVGDNZ/G8R1nTrxuygPlc3Qskj57hmV4 -tZK4JJxQFi7/9ehvjAvHohKrEPeqV5XL87cCAwEAAaNQME4wHQYDVR0OBBYEFCn/ -5hB+XY4pVOnaqvrmZMxrLFjLMB8GA1UdIwQYMBaAFCn/5hB+XY4pVOnaqvrmZMxr -LFjLMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAEduPyTHpXkCVZRQ -v6p+Ug4iVeXpxGCVr34y7EDUMgmuDdqsz1SrmqeDd0VmjZT8htbWw7QBKDPEBsbi -wl606aAn01iM+oUrwbtXxid1xfZj/j6pIhQVkGu7e/8A7Pr4QOP4OMdHB7EmqkAo -d/OLHa9LdKv2UtJHD6U7oVQbdBHrRV62125GMmotpQuSkEfZM6edKNzHPlqV/zJc -2kGCw3lZC21mTrsSMIC/FQiobPnig4kAvfh0of2rK/XAntlwT8ie1v1aK+jERsfm -uzMihl6XXBdzheq6KdIlf+5STHBIIRcvBoRKr5Va7EhnO03tTzeJowtqDv47yPC6 -w4kLcP8= ------END CERTIFICATE----- diff --git a/test/conf/redis.js.org.key b/test/conf/redis.js.org.key deleted file mode 100644 index 9376fc1eee..0000000000 --- a/test/conf/redis.js.org.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAn8OYxMkd/uTKynEjUD8mY5z4oj2/ERw3mqfsiJm4Y3uCETjd -DepcHYZSQwC5bU86SYznKLdK5PVRcNH/gaTsqVscoyzfsBXo+AldfqhfTO7XYtMB -PtXlvI2ti16FKjjaXXXzx0YQwIZpp/RwOe1zsDdpbRm3Osm37am7fVRq2XZIQkIy -trr6/o7Rz1ocwXVwCsp5rAapH6MfDq9cVlBtR3TmdbmdCVQDrBW3UmOUs/AHBixY -uyJhDEwLw2qmAAiDm1QtIKkrDHmKwv8J6i1VUYM1n8bxHWdOvG7KA+VzdCySPnuG -ZXi1krgknFAWLv/16G+MC8eiEqsQ96pXlcvztwIDAQABAoIBAGx4kLCLHCKDlGv+ -hMtnFNltKiJ9acxkLByFBsN4GwjwQk8PHIbmJ8Sj/hYf18WvlRN65zdtuxvYs4K2 -EZQkNcqGYdsoDHexaIt/UEs+ZfYF85bVTHMtJt3uE3Ycpq0UDK6H9wvFNnqAyBuQ -iuHJplJuTNYWL6Fqc8aZBwMA3crmwWTelgS+IXLH06E298+KIxbYrWSgrbcmV/Pj -Iwek4CPS0apoJnXxbZDDhAEYGOTxDNXGm+r7BaX/ePM2x1PPib2X9F2XqFV+A4T8 -Z91axKJwMrVuTrJkaLPDx9lNUskvvV6KgjZAtYRGpLQTN1AqXJZ09IoK9sNPE4rX -9fm4awECgYEAzMJkABL0UOoGJhdRf/R0aUOQMO7vYetX5SK9QXcEI04XYFieSaPm -71st+R/JlJ+LhrTrzGXvyU0tFAQaQZtwaGj/JhbptIpLlGrVf3mqSvxkNi/wzQnn -jBJrrf1ZkDiqtSy7AxGAefWblgK3R1ZU5+0a5jubDkmOltIlbULf0skCgYEAx76l -+5KhWOJPvrjNGB1a8oVXiFzoCpaVVZIhSdl0AtvkKollm5Ou+CKYpE3fKrejRXTD -zmr5bJFXT3VlmIa010cgXJ2btlFa1RiNzgretsOmMcHxLkpAu2/a0L4psHlCrWVK -fxbUW0BYEFVXBDe/4JhFw41YqohdPkFAyo5OUn8CgYBQZGYkzUxVVHzTicY66bym -85ryS217UY5x7WDHCjZ6shdlgYWsPgjWo0L6k+tuSfHbEr+dwcwSihWPzUiNx7yr -kcXTq51YgA/KluN6KEefJ1clG099AU2C5lyWtGjswgLsHULTopSBzdenXyuce53c -bXBpQq/PPTwZpSqCqoX8WQKBgGe+nsk+jGz1BoRBycyHmrAyD5e04ZR2R9PtFTsd -JYNCoIxzVoHqv8sDdRKJm6q9PKEbl4PDzg7UomuTxxPki1LxD17rQW/9a1cY7LYi -sTBuCAj5+YGYcWypGRaoXlDZeodC/+Fogx1uGw9Is+xt5EwL6tg5tt7D+uIV1Egg -h4+TAoGBAKYA/jn9v93bzPi+w1rlZrlPufRSr4k3mcHae165N/1PnjSguTFIF5DW -+1f5S+XioNyTcfx5gKI8f6wRn1j5zbB24GXgu8dXCzRHC2gzrwq2D9v1od4zP/o7 -xFxyiNGOMUJ7uW9d/nEL5Eg4CQKZEkZNmzHhuKNr8wDSr16DhXVK ------END RSA PRIVATE KEY----- diff --git a/test/conf/rename.conf b/test/conf/rename.conf deleted file mode 100644 index 207fe15622..0000000000 --- a/test/conf/rename.conf +++ /dev/null @@ -1,8 +0,0 @@ -port 6379 -bind ::1 127.0.0.1 -unixsocket /tmp/redis.sock -unixsocketperm 700 -save "" -rename-command SET 807081f5afa96845a02816a28b7258c3 -rename-command GET f397808a43ceca3963e22b4e13deb672 -rename-command GETRANGE 9e3102b15cf231c4e9e940f284744fe0 diff --git a/test/conf/slave.conf b/test/conf/slave.conf deleted file mode 100644 index f5632bbffc..0000000000 --- a/test/conf/slave.conf +++ /dev/null @@ -1,7 +0,0 @@ -port 6381 -bind ::1 127.0.0.1 -unixsocket /tmp/redis6381.sock -unixsocketperm 700 -slaveof localhost 6379 -masterauth porkchopsandwiches -save "" diff --git a/test/conf/stunnel.conf.template b/test/conf/stunnel.conf.template deleted file mode 100644 index 17ee3e879c..0000000000 --- a/test/conf/stunnel.conf.template +++ /dev/null @@ -1,11 +0,0 @@ -pid = __dirname/stunnel.pid -; output = __dirname/stunnel.log -CAfile = __dirname/redis.js.org.cert -cert = __dirname/redis.js.org.cert -key = __dirname/redis.js.org.key -client = no -foreground = yes -debug = 7 -[redis] -accept = 127.0.0.1:6380 -connect = 127.0.0.1:6379 diff --git a/test/connection.spec.js b/test/connection.spec.js deleted file mode 100644 index 827ff69c8a..0000000000 --- a/test/connection.spec.js +++ /dev/null @@ -1,637 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var helper = require('./helper'); -var redis = config.redis; -var intercept = require('intercept-stdout'); -var net = require('net'); -var client; - -describe('connection tests', function () { - - beforeEach(function () { - client = null; - }); - afterEach(function () { - if (!client) return; - - client.end(true); - }); - - it('unofficially support for a private stream', function () { - // While using a private stream, reconnection and other features are not going to work properly. - // Besides that some functions also have to be monkey patched to be safe from errors in this case. - // Therefore this is not officially supported! - var socket = new net.Socket(); - client = new redis.RedisClient({ - prefix: 'test' - }, socket); - assert.strictEqual(client.stream, socket); - assert.strictEqual(client.stream.listeners('error').length, 1); - assert.strictEqual(client.address, '"Private stream"'); - // Pretent a reconnect event - client.create_stream(); - assert.strictEqual(client.stream, socket); - assert.strictEqual(client.stream.listeners('error').length, 1); - }); - - describe('quit on lost connections', function () { - - it('calling quit while the connection is down should not end in reconnecting version a', function (done) { - var called = 0; - client = redis.createClient({ - port: 9999, - retry_strategy: function (options) { - var bool = client.quit(function (err, res) { - assert.strictEqual(res, 'OK'); - assert.strictEqual(err, null); - assert.strictEqual(called++, -1); - setTimeout(done, 25); - }); - assert.strictEqual(bool, false); - assert.strictEqual(called++, 0); - return 5; - } - }); - client.set('foo', 'bar', function (err, res) { - assert.strictEqual(err.message, 'Stream connection ended and command aborted.'); - called = -1; - }); - }); - - it('calling quit while the connection is down should not end in reconnecting version b', function (done) { - var called = false; - client = redis.createClient(9999); - client.set('foo', 'bar', function (err, res) { - assert.strictEqual(err.message, 'Stream connection ended and command aborted.'); - called = true; - }); - var bool = client.quit(function (err, res) { - assert.strictEqual(res, 'OK'); - assert.strictEqual(err, null); - assert(called); - done(); - }); - assert.strictEqual(bool, false); - }); - - it('calling quit while the connection is down without offline queue should end the connection right away', function (done) { - var called = false; - client = redis.createClient(9999, { - enable_offline_queue: false - }); - client.set('foo', 'bar', function (err, res) { - assert.strictEqual(err.message, 'SET can\'t be processed. The connection is not yet established and the offline queue is deactivated.'); - called = true; - }); - var bool = client.quit(function (err, res) { - assert.strictEqual(res, 'OK'); - assert.strictEqual(err, null); - assert(called); - done(); - }); - // TODO: In v.3 the quit command would be fired right away, so bool should be true - assert.strictEqual(bool, false); - }); - - it('calling quit while connected without offline queue should end the connection when all commands have finished', function (done) { - var called = false; - client = redis.createClient({ - enable_offline_queue: false - }); - client.on('ready', function () { - client.set('foo', 'bar', function (err, res) { - assert.strictEqual(res, 'OK'); - called = true; - }); - var bool = client.quit(function (err, res) { - assert.strictEqual(res, 'OK'); - assert.strictEqual(err, null); - assert(called); - done(); - }); - // TODO: In v.3 the quit command would be fired right away, so bool should be true - assert.strictEqual(bool, true); - }); - }); - - it('do not quit before connected or a connection issue is detected', function (done) { - client = redis.createClient(); - client.set('foo', 'bar', helper.isString('OK')); - var bool = client.quit(done); - assert.strictEqual(bool, false); - }); - - it('quit "succeeds" even if the client connection is closed while doing so', function (done) { - client = redis.createClient(); - client.set('foo', 'bar', function (err, res) { - assert.strictEqual(res, 'OK'); - client.quit(function (err, res) { - assert.strictEqual(res, 'OK'); - done(err); - }); - client.end(true); // Flushing the quit command should result in a success - }); - }); - - it('quit right away if connection drops while quit command is on the fly', function (done) { - client = redis.createClient(); - client.once('ready', function () { - client.set('foo', 'bar', helper.isError()); - var bool = client.quit(done); - assert.strictEqual(bool, true); - process.nextTick(function () { - client.stream.destroy(); - }); - }); - }); - - }); - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - - describe('on lost connection', function () { - it('emit an error after max retry timeout and do not try to reconnect afterwards', function (done) { - // TODO: Investigate why this test fails with windows. Reconnect is only triggered once - if (process.platform === 'win32') this.skip(); - - var connect_timeout = 600; // in ms - client = redis.createClient({ - connect_timeout: connect_timeout - }); - var time = 0; - - client.once('ready', function () { - helper.killConnection(client); - }); - - client.on('reconnecting', function (params) { - time += params.delay; - }); - - client.on('error', function (err) { - if (/Redis connection in broken state: connection timeout.*?exceeded./.test(err.message)) { - process.nextTick(function () { // End is called after the error got emitted - assert.strictEqual(client.emitted_end, true); - assert.strictEqual(client.connected, false); - assert.strictEqual(client.ready, false); - assert.strictEqual(client.closing, true); - assert.strictEqual(client.retry_totaltime, connect_timeout); - assert.strictEqual(time, connect_timeout); - done(); - }); - } - }); - }); - - it('end connection while retry is still ongoing', function (done) { - var connect_timeout = 1000; // in ms - client = redis.createClient({ - connect_timeout: connect_timeout - }); - - client.once('ready', function () { - helper.killConnection(client); - }); - - client.on('reconnecting', function (params) { - client.end(true); - assert.strictEqual(params.times_connected, 1); - setTimeout(done, 5); - }); - }); - - it('can not connect with wrong host / port in the options object', function (done) { - var options = { - host: 'somewhere', - port: 6379, - family: ip, - max_attempts: 1 - }; - client = redis.createClient(options); - assert.strictEqual(client.connection_options.family, ip === 'IPv6' ? 6 : 4); - assert.strictEqual(Object.keys(options).length, 4); - var end = helper.callFuncAfter(done, 2); - - client.on('error', function (err) { - assert(/CONNECTION_BROKEN|ENOTFOUND|EAI_AGAIN/.test(err.code)); - end(); - }); - - }); - - it('emits error once if reconnecting after command has been executed but not yet returned without callback', function (done) { - client = redis.createClient.apply(null, args); - - client.on('ready', function () { - client.set('foo', 'bar', function (err) { - assert.strictEqual(err.code, 'UNCERTAIN_STATE'); - done(); - }); - // Abort connection before the value returned - client.stream.destroy(); - }); - }); - - it('retryStrategy used to reconnect with individual error', function (done) { - client = redis.createClient({ - retryStrategy: function (options) { - if (options.totalRetryTime > 150) { - client.set('foo', 'bar'); - client.once('error', function (err) { - assert.strictEqual(err.message, 'Redis connection in broken state: retry aborted.'); - assert.strictEqual(err.origin.message, 'Connection timeout'); - done(); - }); - // Pass a individual error message to the error handler - return new Error('Connection timeout'); - } - return Math.min(options.attempt * 25, 200); - }, - port: 9999 - }); - }); - - it('retry_strategy used to reconnect', function (done) { - client = redis.createClient({ - retry_strategy: function (options) { - if (options.total_retry_time > 150) { - client.set('foo', 'bar'); - client.once('error', function (err) { - assert.strictEqual(err.message, 'Redis connection in broken state: retry aborted.'); - assert.strictEqual(err.code, 'CONNECTION_BROKEN'); - assert.strictEqual(err.origin.code, 'ECONNREFUSED'); - done(); - }); - return false; - } - return Math.min(options.attempt * 25, 200); - }, - port: 9999 - }); - }); - - it('retryStrategy used to reconnect with defaults', function (done) { - var unhookIntercept = intercept(function () { - return ''; - }); - redis.debugMode = true; - client = redis.createClient({ - retryStrategy: function (options) { - client.set('foo', 'bar'); - assert(redis.debugMode); - return null; - } - }); - setTimeout(function () { - client.stream.destroy(); - }, 50); - client.on('error', function (err) { - if (err instanceof redis.AbortError) { - assert.strictEqual(err.message, 'Redis connection in broken state: retry aborted.'); - assert.strictEqual(err.code, 'CONNECTION_BROKEN'); - unhookIntercept(); - redis.debugMode = false; - done(); - } - }); - }); - }); - - describe('when not connected', function () { - - it('emit an error after the socket timeout exceeded the connect_timeout time', function (done) { - var connect_timeout = 500; // in ms - client = redis.createClient({ - // Auto detect ipv4 and use non routable ip to trigger the timeout - host: '10.255.255.1', - connect_timeout: connect_timeout - }); - process.nextTick(function () { - assert.strictEqual(client.stream.listeners('timeout').length, 1); - }); - assert.strictEqual(client.address, '10.255.255.1:6379'); - assert.strictEqual(client.connection_options.family, 4); - - client.on('reconnecting', function (params) { - throw new Error('No reconnect, since no connection was ever established'); - }); - - var time = Date.now(); - client.on('error', function (err) { - if (err.code === 'ENETUNREACH') { // The test is run without a internet connection. Pretent it works - return done(); - } - assert(/Redis connection in broken state: connection timeout.*?exceeded./.test(err.message), err.message); - // The code execution on windows is very slow at times - var add = process.platform !== 'win32' ? 15 : 200; - var now = Date.now(); - assert(now - time < connect_timeout + add, 'The real timeout time should be below ' + (connect_timeout + add) + 'ms but is: ' + (now - time)); - // Timers sometimes trigger early (e.g. 1ms to early) - assert(now - time >= connect_timeout - 5, 'The real timeout time should be above ' + connect_timeout + 'ms, but it is: ' + (now - time)); - done(); - }); - }); - - it('use the system socket timeout if the connect_timeout has not been provided', function (done) { - client = redis.createClient({ - host: '0:0:0:0:0:0:0:1', // auto detect ip v6 - no_ready_check: true - }); - assert.strictEqual(client.address, '0:0:0:0:0:0:0:1:6379'); - assert.strictEqual(client.connection_options.family, 6); - process.nextTick(function () { - assert.strictEqual(client.stream.listeners('timeout').length, 0); - done(); - }); - }); - - it('clears the socket timeout after a connection has been established', function (done) { - client = redis.createClient({ - connect_timeout: 1000 - }); - process.nextTick(function () { - // node > 6 - var timeout = client.stream.timeout; - // node <= 6 - if (timeout === undefined) timeout = client.stream._idleTimeout; - assert.strictEqual(timeout, 1000); - }); - client.on('connect', function () { - // node > 6 - var expected = 0; - var timeout = client.stream.timeout; - // node <= 6 - if (timeout === undefined) { - timeout = client.stream._idleTimeout; - expected = -1; - } - assert.strictEqual(timeout, expected); - assert.strictEqual(client.stream.listeners('timeout').length, 0); - client.on('ready', done); - }); - }); - - it('connect with host and port provided in the options object', function (done) { - client = redis.createClient({ - host: 'localhost', - port: '6379', - connect_timeout: 1000 - }); - - client.once('ready', done); - }); - - it('connect with path provided in the options object', function (done) { - if (process.platform === 'win32') { - this.skip(); - } - client = redis.createClient({ - path: '/tmp/redis.sock', - connect_timeout: 1000 - }); - - var end = helper.callFuncAfter(done, 2); - - client.once('ready', end); - client.set('foo', 'bar', end); - }); - - it('connects correctly with args', function (done) { - client = redis.createClient.apply(null, args); - client.on('error', done); - - client.once('ready', function () { - client.removeListener('error', done); - client.get('recon 1', done); - }); - }); - - it('connects correctly with default values', function (done) { - client = redis.createClient(); - client.on('error', done); - - client.once('ready', function () { - client.removeListener('error', done); - client.get('recon 1', done); - }); - }); - - it('connects with a port only', function (done) { - client = redis.createClient(6379); - assert.strictEqual(client.connection_options.family, 4); - client.on('error', done); - - client.once('ready', function () { - client.removeListener('error', done); - client.get('recon 1', done); - }); - }); - - it('connects correctly to localhost', function (done) { - client = redis.createClient(null, null); - client.on('error', done); - - client.once('ready', function () { - client.removeListener('error', done); - client.get('recon 1', done); - }); - }); - - it('connects correctly to the provided host with the port set to null', function (done) { - client = redis.createClient(null, 'localhost'); - client.on('error', done); - assert.strictEqual(client.address, 'localhost:6379'); - - client.once('ready', function () { - client.set('foo', 'bar'); - client.get('foo', function (err, res) { - assert.strictEqual(res, 'bar'); - done(err); - }); - }); - }); - - it('connects correctly to localhost and no ready check', function (done) { - client = redis.createClient(undefined, undefined, { - no_ready_check: true - }); - client.on('error', done); - - client.once('ready', function () { - client.set('foo', 'bar'); - client.get('foo', function (err, res) { - assert.strictEqual(res, 'bar'); - done(err); - }); - }); - }); - - it('connects correctly to the provided host with the port set to undefined', function (done) { - client = redis.createClient(undefined, 'localhost', { - no_ready_check: true - }); - client.on('error', done); - assert.strictEqual(client.address, 'localhost:6379'); - - client.once('ready', function () { - client.set('foo', 'bar'); - client.get('foo', function (err, res) { - assert.strictEqual(res, 'bar'); - done(err); - }); - }); - }); - - it('connects correctly even if the info command is not present on the redis server', function (done) { - client = redis.createClient.apply(null, args); - client.info = function (cb) { - // Mock the result - cb(new Error("ERR unknown command 'info'")); - }; - client.once('ready', function () { - assert.strictEqual(Object.keys(client.server_info).length, 0); - done(); - }); - }); - - it('fake the stream to mock redis', function () { - // This is needed for libraries that want to mock the stream like fakeredis - var temp = redis.RedisClient.prototype.create_stream; - var create_stream_string = String(temp); - redis.RedisClient.prototype.create_stream = function () { - this.connected = true; - this.ready = true; - }; - client = new redis.RedisClient(); - assert.strictEqual(client.stream, undefined); - assert.strictEqual(client.ready, true); - assert.strictEqual(client.connected, true); - client.end = function () {}; - assert(create_stream_string !== String(redis.RedisClient.prototype.create_stream)); - redis.RedisClient.prototype.create_stream = temp; - assert(create_stream_string === String(redis.RedisClient.prototype.create_stream)); - }); - - if (ip === 'IPv4') { - it('allows connecting with the redis url to the default host and port, select db 3 and warn about duplicate db option', function (done) { - client = redis.createClient('redis:///3?db=3'); - assert.strictEqual(client.selected_db, '3'); - client.on('ready', done); - }); - - it('allows connecting with the redis url and the default port and auth provided even though it is not required', function (done) { - client = redis.createClient('redis://:porkchopsandwiches@' + config.HOST[ip] + '/'); - var end = helper.callFuncAfter(done, 2); - client.on('warning', function (msg) { - assert.strictEqual(msg, 'Warning: Redis server does not require a password, but a password was supplied.'); - end(); - }); - client.on('ready', end); - }); - - it('allows connecting with the redis url as first parameter and the options as second parameter', function (done) { - client = redis.createClient('//127.0.0.1', { - connect_timeout: 1000 - }); - assert.strictEqual(client.options.connect_timeout, 1000); - client.on('ready', done); - }); - - it('allows connecting with the redis url in the options object and works with protocols other than the redis protocol (e.g. http)', function (done) { - client = redis.createClient({ - url: 'http://:porkchopsandwiches@' + config.HOST[ip] + '/3' - }); - assert.strictEqual(client.auth_pass, 'porkchopsandwiches'); - assert.strictEqual(+client.selected_db, 3); - assert(!client.options.port); - assert.strictEqual(client.options.host, config.HOST[ip]); - client.on('ready', done); - }); - - it('allows connecting with the redis url and no auth and options as second parameter', function (done) { - var options = { - detect_buffers: false - }; - client = redis.createClient('redis://' + config.HOST[ip] + ':' + config.PORT, options); - assert.strictEqual(Object.keys(options).length, 1); - client.on('ready', done); - }); - - it('allows connecting with the redis url and no auth and options as third parameter', function (done) { - client = redis.createClient('redis://' + config.HOST[ip] + ':' + config.PORT, null, { - detect_buffers: false - }); - client.on('ready', done); - }); - } - - it('redis still loading <= 500', function (done) { - client = redis.createClient.apply(null, args); - var tmp = client.info.bind(client); - var end = helper.callFuncAfter(done, 3); - var delayed = false; - var time; - // Mock original function and pretent redis is still loading - client.info = function (cb) { - tmp(function (err, res) { - if (!delayed) { - assert(!err); - client.server_info.loading = 1; - client.server_info.loading_eta_seconds = 0.5; - delayed = true; - time = Date.now(); - } - end(); - cb(err, res); - }); - }; - client.on('ready', function () { - var rest = Date.now() - time; - assert(rest >= 495, 'Rest should be equal or above 500 ms but is: ' + rest); // setTimeout might trigger early - // Be on the safe side and accept 200ms above the original value - assert(rest - 250 < 500, 'Rest - 250 should be below 500 ms but is: ' + (rest - 250)); - assert(delayed); - end(); - }); - }); - - it('redis still loading > 1000ms', function (done) { - client = redis.createClient.apply(null, args); - var tmp = client.info.bind(client); - var end = helper.callFuncAfter(done, 3); - var delayed = false; - var time; - // Mock original function and pretent redis is still loading - client.info = function (cb) { - tmp(function (err, res) { - if (!delayed) { - assert(!err); - // Try reconnecting after one second even if redis tells us the time needed is above one second - client.server_info.loading = 1; - client.server_info.loading_eta_seconds = 2.5; - delayed = true; - time = Date.now(); - } - end(); - cb(err, res); - }); - }; - client.on('ready', function () { - var rest = Date.now() - time; - assert(rest >= 998, '`rest` should be equal or above 1000 ms but is: ' + rest); // setTimeout might trigger early - // Be on the safe side and accept 200ms above the original value - assert(rest - 250 < 1000, '`rest` - 250 should be below 1000 ms but is: ' + (rest - 250)); - assert(delayed); - end(); - }); - }); - - }); - - }); - }); -}); diff --git a/test/custom_errors.spec.js b/test/custom_errors.spec.js deleted file mode 100644 index 90a5aaeb66..0000000000 --- a/test/custom_errors.spec.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var errors = require('../lib/customErrors'); - -describe('errors', function () { - - describe('AbortError', function () { - it('should inherit from Error', function () { - var e = new errors.AbortError({}); - assert.strictEqual(e.message, ''); - assert.strictEqual(e.name, 'AbortError'); - assert.strictEqual(Object.keys(e).length, 0); - assert(e instanceof Error); - assert(e instanceof errors.AbortError); - }); - - it('should list options properties but not name and message', function () { - var e = new errors.AbortError({ - name: 'weird', - message: 'hello world', - property: true - }); - assert.strictEqual(e.message, 'hello world'); - assert.strictEqual(e.name, 'weird'); - assert.strictEqual(e.property, true); - assert.strictEqual(Object.keys(e).length, 2); - assert(e instanceof Error); - assert(e instanceof errors.AbortError); - assert(delete e.name); - assert.strictEqual(e.name, 'AbortError'); - }); - - it('should change name and message', function () { - var e = new errors.AbortError({ - message: 'hello world', - property: true - }); - assert.strictEqual(e.name, 'AbortError'); - assert.strictEqual(e.message, 'hello world'); - e.name = 'foo'; - e.message = 'foobar'; - assert.strictEqual(e.name, 'foo'); - assert.strictEqual(e.message, 'foobar'); - }); - }); - - describe('AggregateError', function () { - it('should inherit from Error and AbortError', function () { - var e = new errors.AggregateError({}); - assert.strictEqual(e.message, ''); - assert.strictEqual(e.name, 'AggregateError'); - assert.strictEqual(Object.keys(e).length, 0); - assert(e instanceof Error); - assert(e instanceof errors.AggregateError); - assert(e instanceof errors.AbortError); - }); - - it('should list options properties but not name and message', function () { - var e = new errors.AggregateError({ - name: 'weird', - message: 'hello world', - property: true - }); - assert.strictEqual(e.message, 'hello world'); - assert.strictEqual(e.name, 'weird'); - assert.strictEqual(e.property, true); - assert.strictEqual(Object.keys(e).length, 2); - assert(e instanceof Error); - assert(e instanceof errors.AggregateError); - assert(e instanceof errors.AbortError); - assert(delete e.name); - assert.strictEqual(e.name, 'AggregateError'); - }); - - it('should change name and message', function () { - var e = new errors.AggregateError({ - message: 'hello world', - property: true - }); - assert.strictEqual(e.name, 'AggregateError'); - assert.strictEqual(e.message, 'hello world'); - e.name = 'foo'; - e.message = 'foobar'; - assert.strictEqual(e.name, 'foo'); - assert.strictEqual(e.message, 'foobar'); - }); - }); -}); diff --git a/test/detect_buffers.spec.js b/test/detect_buffers.spec.js deleted file mode 100644 index faa63efb1f..0000000000 --- a/test/detect_buffers.spec.js +++ /dev/null @@ -1,268 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var helper = require('./helper'); -var redis = config.redis; - -describe('detect_buffers', function () { - - var client; - var args = config.configureClient('localhost', { - detect_buffers: true - }); - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('error', done); - client.once('connect', function () { - client.flushdb(function (err) { - client.hmset('hash key 2', 'key 1', 'val 1', 'key 2', 'val 2'); - client.set('string key 1', 'string value'); - return done(err); - }); - }); - }); - - afterEach(function () { - client.end(true); - }); - - describe('get', function () { - describe('first argument is a string', function () { - it('returns a string', function (done) { - client.get('string key 1', helper.isString('string value', done)); - }); - - it('returns a string when executed as part of transaction', function (done) { - client.multi().get('string key 1').exec(function (err, res) { - helper.isString('string value', done)(err, res[0]); - }); - }); - }); - - describe('first argument is a buffer', function () { - it('returns a buffer', function (done) { - client.get(Buffer.from('string key 1'), function (err, reply) { - assert.strictEqual(true, Buffer.isBuffer(reply)); - assert.strictEqual('', reply.inspect()); - return done(err); - }); - }); - - it('returns a bufffer when executed as part of transaction', function (done) { - client.multi().get(Buffer.from('string key 1')).exec(function (err, reply) { - assert.strictEqual(1, reply.length); - assert.strictEqual(true, Buffer.isBuffer(reply[0])); - assert.strictEqual('', reply[0].inspect()); - return done(err); - }); - }); - }); - }); - - describe('multi.hget', function () { - it('can interleave string and buffer results', function (done) { - client.multi() - .hget('hash key 2', 'key 1') - .hget(Buffer.from('hash key 2'), 'key 1') - .hget('hash key 2', Buffer.from('key 2')) - .hget('hash key 2', 'key 2') - .exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(4, reply.length); - assert.strictEqual('val 1', reply[0]); - assert.strictEqual(true, Buffer.isBuffer(reply[1])); - assert.strictEqual('', reply[1].inspect()); - assert.strictEqual(true, Buffer.isBuffer(reply[2])); - assert.strictEqual('', reply[2].inspect()); - assert.strictEqual('val 2', reply[3]); - return done(err); - }); - }); - }); - - describe('batch.hget', function () { - it('can interleave string and buffer results', function (done) { - client.batch() - .hget('hash key 2', 'key 1') - .hget(Buffer.from('hash key 2'), 'key 1') - .hget('hash key 2', Buffer.from('key 2')) - .hget('hash key 2', 'key 2') - .exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(4, reply.length); - assert.strictEqual('val 1', reply[0]); - assert.strictEqual(true, Buffer.isBuffer(reply[1])); - assert.strictEqual('', reply[1].inspect()); - assert.strictEqual(true, Buffer.isBuffer(reply[2])); - assert.strictEqual('', reply[2].inspect()); - assert.strictEqual('val 2', reply[3]); - return done(err); - }); - }); - }); - - describe('hmget', function () { - describe('first argument is a string', function () { - it('returns strings for keys requested', function (done) { - client.hmget('hash key 2', 'key 1', 'key 2', function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(2, reply.length); - assert.strictEqual('val 1', reply[0]); - assert.strictEqual('val 2', reply[1]); - return done(err); - }); - }); - - it('returns strings for keys requested in transaction', function (done) { - client.multi().hmget('hash key 2', 'key 1', 'key 2').exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(1, reply.length); - assert.strictEqual(2, reply[0].length); - assert.strictEqual('val 1', reply[0][0]); - assert.strictEqual('val 2', reply[0][1]); - return done(err); - }); - }); - - it('handles array of strings with undefined values (repro #344)', function (done) { - client.hmget('hash key 2', 'key 3', 'key 4', function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(2, reply.length); - assert.equal(null, reply[0]); - assert.equal(null, reply[1]); - return done(err); - }); - }); - - it('handles array of strings with undefined values in transaction (repro #344)', function (done) { - client.multi().hmget('hash key 2', 'key 3', 'key 4').exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(1, reply.length); - assert.strictEqual(2, reply[0].length); - assert.equal(null, reply[0][0]); - assert.equal(null, reply[0][1]); - return done(err); - }); - }); - }); - - describe('first argument is a buffer', function () { - it('returns buffers for keys requested', function (done) { - client.hmget(Buffer.from('hash key 2'), 'key 1', 'key 2', function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(2, reply.length); - assert.strictEqual(true, Buffer.isBuffer(reply[0])); - assert.strictEqual(true, Buffer.isBuffer(reply[1])); - assert.strictEqual('', reply[0].inspect()); - assert.strictEqual('', reply[1].inspect()); - return done(err); - }); - }); - - it('returns buffers for keys requested in transaction', function (done) { - client.multi().hmget(Buffer.from('hash key 2'), 'key 1', 'key 2').exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(1, reply.length); - assert.strictEqual(2, reply[0].length); - assert.strictEqual(true, Buffer.isBuffer(reply[0][0])); - assert.strictEqual(true, Buffer.isBuffer(reply[0][1])); - assert.strictEqual('', reply[0][0].inspect()); - assert.strictEqual('', reply[0][1].inspect()); - return done(err); - }); - }); - - it('returns buffers for keys requested in .batch', function (done) { - client.batch().hmget(Buffer.from('hash key 2'), 'key 1', 'key 2').exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(1, reply.length); - assert.strictEqual(2, reply[0].length); - assert.strictEqual(true, Buffer.isBuffer(reply[0][0])); - assert.strictEqual(true, Buffer.isBuffer(reply[0][1])); - assert.strictEqual('', reply[0][0].inspect()); - assert.strictEqual('', reply[0][1].inspect()); - return done(err); - }); - }); - }); - }); - - describe('hgetall', function (done) { - describe('first argument is a string', function () { - it('returns string values', function (done) { - client.hgetall('hash key 2', function (err, reply) { - assert.strictEqual('object', typeof reply); - assert.strictEqual(2, Object.keys(reply).length); - assert.strictEqual('val 1', reply['key 1']); - assert.strictEqual('val 2', reply['key 2']); - return done(err); - }); - }); - - it('returns string values when executed in transaction', function (done) { - client.multi().hgetall('hash key 2').exec(function (err, reply) { - assert.strictEqual(1, reply.length); - assert.strictEqual('object', typeof reply[0]); - assert.strictEqual(2, Object.keys(reply[0]).length); - assert.strictEqual('val 1', reply[0]['key 1']); - assert.strictEqual('val 2', reply[0]['key 2']); - return done(err); - }); - }); - - it('returns string values when executed in .batch', function (done) { - client.batch().hgetall('hash key 2').exec(function (err, reply) { - assert.strictEqual(1, reply.length); - assert.strictEqual('object', typeof reply[0]); - assert.strictEqual(2, Object.keys(reply[0]).length); - assert.strictEqual('val 1', reply[0]['key 1']); - assert.strictEqual('val 2', reply[0]['key 2']); - return done(err); - }); - }); - }); - - describe('first argument is a buffer', function () { - it('returns buffer values', function (done) { - client.hgetall(Buffer.from('hash key 2'), function (err, reply) { - assert.strictEqual(null, err); - assert.strictEqual('object', typeof reply); - assert.strictEqual(2, Object.keys(reply).length); - assert.strictEqual(true, Buffer.isBuffer(reply['key 1'])); - assert.strictEqual(true, Buffer.isBuffer(reply['key 2'])); - assert.strictEqual('', reply['key 1'].inspect()); - assert.strictEqual('', reply['key 2'].inspect()); - return done(err); - }); - }); - - it('returns buffer values when executed in transaction', function (done) { - client.multi().hgetall(Buffer.from('hash key 2')).exec(function (err, reply) { - assert.strictEqual(1, reply.length); - assert.strictEqual('object', typeof reply[0]); - assert.strictEqual(2, Object.keys(reply[0]).length); - assert.strictEqual(true, Buffer.isBuffer(reply[0]['key 1'])); - assert.strictEqual(true, Buffer.isBuffer(reply[0]['key 2'])); - assert.strictEqual('', reply[0]['key 1'].inspect()); - assert.strictEqual('', reply[0]['key 2'].inspect()); - return done(err); - }); - }); - - it('returns buffer values when executed in .batch', function (done) { - client.batch().hgetall(Buffer.from('hash key 2')).exec(function (err, reply) { - assert.strictEqual(1, reply.length); - assert.strictEqual('object', typeof reply[0]); - assert.strictEqual(2, Object.keys(reply[0]).length); - assert.strictEqual(true, Buffer.isBuffer(reply[0]['key 1'])); - assert.strictEqual(true, Buffer.isBuffer(reply[0]['key 2'])); - assert.strictEqual('', reply[0]['key 1'].inspect()); - assert.strictEqual('', reply[0]['key 2'].inspect()); - return done(err); - }); - }); - }); - }); -}); diff --git a/test/errors.js b/test/errors.js deleted file mode 100644 index 060afab585..0000000000 --- a/test/errors.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports = { - invalidPassword: /^(ERR invalid password|WRONGPASS invalid username-password pair)/, - subscribeUnsubscribeOnly: /^ERR( Can't execute 'get':)? only \(P\)SUBSCRIBE \/ \(P\)UNSUBSCRIBE/ -}; diff --git a/test/good_traces.spec.js b/test/good_traces.spec.js deleted file mode 100644 index d8759b0f9a..0000000000 --- a/test/good_traces.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var fork = require('child_process').fork; -var redis = config.redis; - -describe('stack traces', function () { - - it('should return good traces with NODE_ENV=development set', function (done) { - var external = fork('./test/lib/good-traces.js', { - env: { - NODE_ENV: 'development' - } - }); - - var id = setTimeout(function () { - external.kill(); - done(new Error('Timeout')); - }, 6000); - - external.on('close', function (code) { - clearTimeout(id); - assert.strictEqual(code, 0); - done(); - }); - }); - - it('should return good traces with NODE_DEBUG=redis env set', function (done) { - var external = fork('./test/lib/good-traces.js', { - env: { - NODE_DEBUG: 'redis' - }, - silent: true - }); - - var id = setTimeout(function () { - external.kill(); - done(new Error('Timeout')); - }, 6000); - - external.on('close', function (code) { - clearTimeout(id); - assert.strictEqual(code, 0); - done(); - }); - }); - - // This is always going to return good stack traces - it('should always return good stack traces for rejected offline commands', function (done) { - var client = redis.createClient({ - enable_offline_queue: false - }); - client.set('foo', function (err, res) { - assert(/good_traces.spec.js/.test(err.stack)); - client.quit(done); - }); - }); -}); diff --git a/test/helper.js b/test/helper.js deleted file mode 100644 index 3e1437d14a..0000000000 --- a/test/helper.js +++ /dev/null @@ -1,220 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var path = require('path'); -var config = require('./lib/config'); -var RedisProcess = require('./lib/redis-process'); -var StunnelProcess = require('./lib/stunnel-process'); -var rp; -var stunnel_process; - -function startRedis (conf, done, port) { - RedisProcess.start(function (err, _rp) { - rp = _rp; - return done(err); - }, path.resolve(__dirname, conf), port); -} - -// don't start redis every time we -// include this helper file! -if (!process.env.REDIS_TESTS_STARTED) { - process.env.REDIS_TESTS_STARTED = true; - - before(function (done) { - startRedis('./conf/redis.conf', done); - }); - - after(function (done) { - if (rp) rp.stop(done); - }); -} - -function arrayHelper (results) { - if (results instanceof Array) { - assert.strictEqual(results.length, 1, 'The array length may only be one element'); - return results[0]; - } - return results; -} - -module.exports = { - redisProcess: function () { - return rp; - }, - stopRedis: function (done) { - rp.stop(done); - }, - startRedis: startRedis, - stopStunnel: function (done) { - if (stunnel_process) { - StunnelProcess.stop(stunnel_process, done); - } else { - done(); - } - }, - startStunnel: function (done) { - StunnelProcess.start(function (err, _stunnel_process) { - stunnel_process = _stunnel_process; - return done(err); - }, path.resolve(__dirname, './conf')); - }, - isNumber: function (expected, done) { - return function (err, results) { - assert.strictEqual(err, null, 'expected ' + expected + ', got error: ' + err); - results = arrayHelper(results); - assert.strictEqual(results, expected, expected + ' !== ' + results); - assert.strictEqual(typeof results, 'number', 'expected a number, got ' + typeof results); - if (done) done(); - }; - }, - isString: function (str, done) { - str = '' + str; // Make sure it's a string - return function (err, results) { - assert.strictEqual(err, null, "expected string '" + str + "', got error: " + err); - results = arrayHelper(results); - if (Buffer.isBuffer(results)) { // If options are passed to return either strings or buffers... - results = results.toString(); - } - assert.strictEqual(results, str, str + ' does not match ' + results); - if (done) done(); - }; - }, - isNull: function (done) { - return function (err, results) { - assert.strictEqual(err, null, 'expected null, got error: ' + err); - results = arrayHelper(results); - assert.strictEqual(results, null, results + ' is not null'); - if (done) done(); - }; - }, - isUndefined: function (done) { - return function (err, results) { - assert.strictEqual(err, null, 'expected null, got error: ' + err); - results = arrayHelper(results); - assert.strictEqual(results, undefined, results + ' is not undefined'); - if (done) done(); - }; - }, - isError: function (done) { - return function (err, results) { - assert(err instanceof Error, "err is not instance of 'Error', but an error is expected here."); - if (done) done(); - }; - }, - isNotError: function (done) { - return function (err, results) { - assert.strictEqual(err, null, 'expected success, got an error: ' + err); - if (done) done(); - }; - }, - isType: { - number: function (done) { - return function (err, results) { - assert.strictEqual(err, null, 'expected any number, got error: ' + err); - assert.strictEqual(typeof results, 'number', results + ' is not a number'); - if (done) done(); - }; - }, - string: function (done) { - return function (err, results) { - assert.strictEqual(err, null, 'expected any string, got error: ' + err); - assert.strictEqual(typeof results, 'string', results + ' is not a string'); - if (done) done(); - }; - }, - positiveNumber: function (done) { - return function (err, results) { - assert.strictEqual(err, null, 'expected positive number, got error: ' + err); - assert(results > 0, results + ' is not a positive number'); - if (done) done(); - }; - } - }, - match: function (pattern, done) { - return function (err, results) { - assert.strictEqual(err, null, 'expected ' + pattern.toString() + ', got error: ' + err); - results = arrayHelper(results); - assert(pattern.test(results), "expected string '" + results + "' to match " + pattern.toString()); - if (done) done(); - }; - }, - serverVersionAtLeast: function (connection, desired_version) { - // Wait until a connection has established (otherwise a timeout is going to be triggered at some point) - if (Object.keys(connection.server_info).length === 0) { - throw new Error('Version check not possible as the client is not yet ready or did not expose the version'); - } - // Return true if the server version >= desired_version - var version = connection.server_info.versions; - for (var i = 0; i < 3; i++) { - if (version[i] > desired_version[i]) { - return true; - } - if (version[i] < desired_version[i]) { - if (this.skip) this.skip(); - return false; - } - } - return true; - }, - allTests: function (opts, cb) { - if (!cb) { - cb = opts; - opts = {}; - } - var protocols = ['IPv4']; - if (process.platform !== 'win32') { - protocols.push('IPv6', '/tmp/redis.sock'); - } - var options = [{ - detect_buffers: true - }, { - detect_buffers: false - }]; - options.forEach(function (options) { - var strOptions = ''; - var key; - for (key in options) { - if (options.hasOwnProperty(key)) { - strOptions += key + ': ' + options[key] + '; '; - } - } - describe('using options: ' + strOptions, function () { - protocols.forEach(function (ip, i) { - if (i !== 0 && !opts.allConnections) { - return; - } - cb(ip, config.configureClient(ip, options)); - }); - }); - }); - }, - removeMochaListener: function () { - var mochaListener = process.listeners('uncaughtException').pop(); - process.removeListener('uncaughtException', mochaListener); - return mochaListener; - }, - callFuncAfter: function (func, max) { - var i = 0; - return function (err) { - if (err) { - throw err; - } - i++; - if (i >= max) { - func(); - return true; - } - return false; - }; - }, - killConnection: function (client) { - // Change the connection option to a non existing one and destroy the stream - client.connection_options = { - port: 65535, - host: '127.0.0.1', - family: 4 - }; - client.address = '127.0.0.1:65535'; - client.stream.destroy(); - } -}; diff --git a/test/lib/config.js b/test/lib/config.js deleted file mode 100644 index d363f83e04..0000000000 --- a/test/lib/config.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -// helpers for configuring a redis client in -// its various modes, ipV6, ipV4, socket. -var redis = require('../../index'); -var bluebird = require('bluebird'); - -// Promisify everything -bluebird.promisifyAll(redis.RedisClient.prototype); -bluebird.promisifyAll(redis.Multi.prototype); - -var config = { - redis: redis, - PORT: 6379, - HOST: { - IPv4: '127.0.0.1', - IPv6: '::1' - }, - configureClient: function (ip, opts) { - var args = []; - // Do not manipulate the opts => copy them each time - opts = opts ? JSON.parse(JSON.stringify(opts)) : {}; - - if (ip.match(/\.sock/)) { - args.push(ip); - } else { - args.push(config.PORT); - args.push(config.HOST[ip]); - opts.family = ip; - } - args.push(opts); - - return args; - } -}; - -module.exports = config; diff --git a/test/lib/good-traces.js b/test/lib/good-traces.js deleted file mode 100644 index c583da2ae2..0000000000 --- a/test/lib/good-traces.js +++ /dev/null @@ -1,20 +0,0 @@ -// Spawned by the good_stacks.spec.js tests -'use strict'; - -var assert = require('assert'); -var redis = require('../../index'); -var client = redis.createClient(); - -// Both error cases would normally return bad stack traces -client.set('foo', function (err, res) { - assert(/good-traces.js:9:8/.test(err.stack)); - client.set('foo', 'bar', function (err, res) { - assert(/good-traces.js:11:12/.test(err.stack)); - client.quit(function () { - process.exit(0); - }); - }); - process.nextTick(function () { - client.stream.destroy(); - }); -}); diff --git a/test/lib/redis-process.js b/test/lib/redis-process.js deleted file mode 100644 index 23ff2e1815..0000000000 --- a/test/lib/redis-process.js +++ /dev/null @@ -1,100 +0,0 @@ -'use strict'; - -// helper to start and stop the redis process. -var config = require('./config'); -var fs = require('fs'); -var path = require('path'); -var spawn = require('cross-spawn'); -var tcpPortUsed = require('tcp-port-used'); -var bluebird = require('bluebird'); - -// wait for redis to be listening in -// all three modes (ipv4, ipv6, socket). -function waitForRedis (available, cb, port) { - if (process.platform === 'win32') return cb(); - - var time = Date.now(); - var running = false; - var socket = '/tmp/redis.sock'; - if (port) { - // We have to distinguish the redis sockets if we have more than a single redis instance running - socket = '/tmp/redis' + port + '.sock'; - } - port = port || config.PORT; - var id = setInterval(function () { - if (running) return; - running = true; - bluebird.join( - tcpPortUsed.check(port, '127.0.0.1'), - tcpPortUsed.check(port, '::1'), - function (ipV4, ipV6) { - if (ipV6 === available && ipV4 === available) { - if (fs.existsSync(socket) === available) { - clearInterval(id); - return cb(); - } - // The same message applies for can't stop but we ignore that case - throw new Error('Port ' + port + ' is already in use. Tests can\'t start.\n'); - } - if (Date.now() - time > 6000) { - throw new Error('Redis could not start on port ' + (port || config.PORT) + '\n'); - } - running = false; - } - ).catch(function (err) { - console.error('\x1b[31m' + err.stack + '\x1b[0m\n'); - process.exit(1); - }); - }, 100); -} - -module.exports = { - start: function (done, conf, port) { - var spawnFailed = false; - if (process.platform === 'win32') return done(null, { - spawnFailed: function () { - return spawnFailed; - }, - stop: function (done) { - return done(); - } - }); - // spawn redis with our testing configuration. - var confFile = conf || path.resolve(__dirname, '../conf/redis.conf'); - var rp = spawn('redis-server', [confFile], {}); - - // capture a failure booting redis, and give - // the user running the test some directions. - rp.once('exit', function (code) { - if (code !== 0) { - spawnFailed = true; - throw new Error('TESTS: Redis Spawn Failed'); - } - }); - - // wait for redis to become available, by - // checking the port we bind on. - waitForRedis(true, function () { - // return an object that can be used in - // an after() block to shutdown redis. - return done(null, { - spawnFailed: function () { - return spawnFailed; - }, - stop: function (done) { - if (spawnFailed) return done(); - rp.once('exit', function (code) { - var error = null; - if (code !== null && code !== 0) { - error = new Error('Redis shutdown failed with code ' + code); - } - waitForRedis(false, function () { - return done(error); - }, port); - }); - rp.kill('SIGTERM'); - } - }); - }, port); - } -}; diff --git a/test/lib/stunnel-process.js b/test/lib/stunnel-process.js deleted file mode 100644 index d773f4f1de..0000000000 --- a/test/lib/stunnel-process.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict'; - -// helper to start and stop the stunnel process. -var spawn = require('child_process').spawn; -var EventEmitter = require('events'); -var fs = require('fs'); -var path = require('path'); -var util = require('util'); - -// Newer Node.js versions > 0.10 return the EventEmitter right away and using .EventEmitter was deprecated -if (typeof EventEmitter !== 'function') { - EventEmitter = EventEmitter.EventEmitter; -} - -function once (cb) { - var called = false; - return function () { - if (called) return; - called = true; - cb.apply(this, arguments); - }; -} - -function StunnelProcess (conf_dir) { - EventEmitter.call(this); - - // set up an stunnel to redis; edit the conf file to include required absolute paths - var conf_file = path.resolve(conf_dir, 'stunnel.conf'); - var conf_text = fs.readFileSync(conf_file + '.template').toString().replace(/__dirname/g, conf_dir); - - fs.writeFileSync(conf_file, conf_text); - var stunnel = this.stunnel = spawn('stunnel', [conf_file]); - - // handle child process events, and failure to set up tunnel - var self = this; - this.timer = setTimeout(function () { - self.emit('error', new Error('Timeout waiting for stunnel to start')); - }, 8000); - - stunnel.on('error', function (err) { - self.clear(); - self.emit('error', err); - }); - - stunnel.on('exit', function (code) { - self.clear(); - if (code === 0) { - self.emit('stopped'); - } else { - self.emit('error', new Error('Stunnel exited unexpectedly; code = ' + code)); - } - }); - - // wait to stunnel to start - stunnel.stderr.on('data', function (data) { - if (data.toString().match(/Service.+redis.+bound/)) { - clearTimeout(this.timer); - self.emit('started'); - } - }); -} -util.inherits(StunnelProcess, EventEmitter); - -StunnelProcess.prototype.clear = function () { - this.stunnel = null; - clearTimeout(this.timer); -}; - -StunnelProcess.prototype.stop = function (done) { - if (this.stunnel) { - this.stunnel.kill(); - } -}; - -module.exports = { - start: function (done, conf_dir) { - done = once(done); - var stunnel = new StunnelProcess(conf_dir); - stunnel.once('error', done.bind(done)); - stunnel.once('started', done.bind(done, null, stunnel)); - }, - stop: function (stunnel, done) { - stunnel.removeAllListeners(); - stunnel.stop(); - stunnel.once('error', done.bind(done)); - stunnel.once('stopped', done.bind(done, null)); - } -}; diff --git a/test/lib/unref.js b/test/lib/unref.js deleted file mode 100644 index 5ad176238e..0000000000 --- a/test/lib/unref.js +++ /dev/null @@ -1,17 +0,0 @@ -// spawned by the unref tests in node_redis.spec.js. -// when configured, unref causes the client to exit -// as soon as there are no outstanding commands. -'use strict'; - -var redis = require('../../index'); -var HOST = process.argv[2] || '127.0.0.1'; -var PORT = process.argv[3]; -var args = PORT ? [PORT, HOST] : [HOST]; - -var c = redis.createClient.apply(redis, args); -c.info(function (err, reply) { - if (err) process.exit(-1); - if (!reply.length) process.exit(-1); - process.stdout.write(reply.length.toString()); -}); -c.unref(); diff --git a/test/multi.spec.js b/test/multi.spec.js deleted file mode 100644 index 5b0e801c87..0000000000 --- a/test/multi.spec.js +++ /dev/null @@ -1,737 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var helper = require('./helper'); -var utils = require('../lib/utils'); -var redis = config.redis; -var zlib = require('zlib'); -var client; - -describe("The 'multi' method", function () { - - afterEach(function () { - client.end(true); - }); - - describe('regression test', function () { - it('saved buffers with charsets different than utf-8 (issue #913)', function (done) { - this.timeout(12000); // Windows tests on 0.10 are slow - client = redis.createClient(); - - var end = helper.callFuncAfter(done, 100); - - // Some random object created from http://beta.json-generator.com/ - var test_obj = { - '_id': '5642c4c33d4667c4a1fefd99', 'index': 0, 'guid': '5baf1f1c-7621-41e7-ae7a-f8c6f3199b0f', 'isActive': true, - 'balance': '$1,028.63', 'picture': 'http://placehold.it/32x32', 'age': 31, 'eyeColor': 'green', 'name': {'first': 'Shana', 'last': 'Long'}, - 'company': 'MANGLO', 'email': 'shana.long@manglo.us', 'phone': '+1 (926) 405-3105', 'address': '747 Dank Court, Norfolk, Ohio, 1112', - 'about': 'Eu pariatur in nisi occaecat enim qui consequat nostrud cupidatat id. ' + - 'Commodo commodo dolore esse irure minim quis deserunt anim laborum aute deserunt et est. Quis nisi laborum deserunt nisi quis.', - 'registered': 'Friday, April 18, 2014 9:56 AM', 'latitude': '74.566613', 'longitude': '-11.660432', 'tags': [7, 'excepteur'], - 'range': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'friends': [3, {'id': 1, 'name': 'Schultz Dyer'}], - 'greeting': 'Hello, Shana! You have 5 unread messages.', 'favoriteFruit': 'strawberry' - }; - - function run () { - if (end() === true) { - return; - } - // To demonstrate a big payload for hash set field values, let's create a big array - var test_arr = []; - var i = 0; - for (; i < 80; i++) { - var new_obj = JSON.parse(JSON.stringify(test_obj)); - test_arr.push(new_obj); - } - - var json = JSON.stringify(test_arr); - zlib.deflate(Buffer.from(json), function (err, buffer) { - if (err) { - done(err); - return; - } - - var multi = client.multi(); - multi.del('SOME_KEY'); - - for (i = 0; i < 100; i++) { - multi.hset('SOME_KEY', 'SOME_FIELD' + i, buffer); - } - multi.exec(function (err, res) { - if (err) { - done(err); - return; - } - run(); - }); - }); - } - run(); - }); - }); - - describe('pipeline limit', function () { - - it('do not exceed maximum string size', function (done) { - this.timeout(process.platform !== 'win32' ? 10000 : 35000); // Windows tests are horribly slow - // Triggers a RangeError: Invalid string length if not handled properly - client = redis.createClient(); - var multi = client.multi(); - var i = Math.pow(2, 28); - while (i > 0) { - i -= 10230; - multi.set('foo' + i, 'bar' + new Array(1024).join('1234567890')); - } - client.on('ready', function () { - multi.exec(function (err, res) { - assert.strictEqual(res.length, 26241); - }); - client.flushdb(done); - }); - }); - - }); - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - - describe('when not connected', function () { - - beforeEach(function (done) { - var end = helper.callFuncAfter(done, 2); - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.quit(end); - }); - client.once('end', end); - }); - - it('reports an error', function (done) { - var multi = client.multi(); - var notBuffering = multi.exec(function (err, res) { - assert(err.message.match(/The connection is already closed/)); - done(); - }); - assert.strictEqual(notBuffering, false); - }); - - it('reports an error if promisified', function () { - return client.multi().execAsync().catch(function (err) { - assert(err.message.match(/The connection is already closed/)); - }); - }); - }); - - describe('when connected', function () { - - beforeEach(function () { - client = redis.createClient.apply(null, args); - }); - - describe('monitor and transactions do not work together', function () { - - it('results in a execabort', function (done) { - // Check that transactions in combination with monitor result in an error - client.monitor(function (e) { - client.on('error', function (err) { - assert.strictEqual(err.code, 'EXECABORT'); - client.end(false); - done(); - }); - var multi = client.multi(); - multi.set('hello', 'world'); - multi.exec(); - }); - }); - - it('results in a execabort #2', function (done) { - // Check that using monitor with a transactions results in an error - client.multi().set('foo', 'bar').monitor().exec(function (err, res) { - assert.strictEqual(err.code, 'EXECABORT'); - client.end(false); - done(); - }); - }); - - it('sanity check', function (done) { - // Remove the listener and add it back again after the error - var mochaListener = helper.removeMochaListener(); - process.on('uncaughtException', function (err) { - helper.removeMochaListener(); - process.on('uncaughtException', mochaListener); - done(); - }); - // Check if Redis still has the error - client.monitor(); - client.send_command('multi'); - client.send_command('set', ['foo', 'bar']); - client.send_command('get', ['foo']); - client.send_command('exec', function (err, res) { - // res[0] is going to be the monitor result of set - // res[1] is going to be the result of the set command - assert(utils.monitor_regex.test(res[0])); - assert.strictEqual(res[1], 'OK'); - assert.strictEqual(res.length, 2); - client.end(false); - }); - }); - }); - - it('executes a pipelined multi properly in combination with the offline queue', function (done) { - var multi1 = client.multi(); - multi1.set('m1', '123'); - multi1.get('m1'); - multi1.exec(done); - assert.strictEqual(client.offline_queue.length, 4); - }); - - it('executes a pipelined multi properly after a reconnect in combination with the offline queue', function (done) { - client.once('ready', function () { - client.stream.destroy(); - var called = false; - var multi1 = client.multi(); - multi1.set('m1', '123'); - multi1.get('m1'); - multi1.exec(function (err, res) { - assert(!err); - called = true; - }); - client.once('ready', function () { - var multi1 = client.multi(); - multi1.set('m2', '456'); - multi1.get('m2'); - multi1.exec(function (err, res) { - assert(called); - assert(!err); - assert.strictEqual(res[1], '456'); - done(); - }); - }); - }); - }); - }); - - describe('when connection is broken', function () { - - it('return an error even if connection is in broken mode if callback is present', function (done) { - client = redis.createClient({ - host: 'somewhere', - port: 6379, - retry_strategy: function (options) { - if (options.attempt > 1) { - // End reconnecting with built in error - return undefined; - } - } - }); - - client.on('error', function (err) { - if (/Redis connection to somewhere:6379 failed/.test(err.origin.message)) { - done(); - } - }); - - client.multi([['set', 'foo', 'bar'], ['get', 'foo']]).exec(function (err, res) { - assert(/Redis connection in broken state: retry aborted/.test(err.message)); - assert.strictEqual(err.errors.length, 2); - assert.strictEqual(err.errors[0].args.length, 2); - }); - }); - - it('does not emit an error twice if connection is in broken mode with no callback', function (done) { - client = redis.createClient({ - host: 'somewhere', - port: 6379, - retry_strategy: function (options) { - if (options.attempt > 1) { - // End reconnecting with built in error - return undefined; - } - } - }); - - client.on('error', function (err) { - // Results in multiple done calls if test fails - if (/Redis connection to somewhere:6379 failed/.test(err.origin.message)) { - done(); - } - }); - - client.multi([['set', 'foo', 'bar'], ['get', 'foo']]).exec(); - }); - }); - - describe('when ready', function () { - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('ready', function () { - client.flushdb(function (err) { - return done(err); - }); - }); - }); - - it('returns an empty result array', function (done) { - var multi = client.multi(); - var notBuffering = multi.exec(function (err, res) { - assert.strictEqual(err, null); - assert.strictEqual(res.length, 0); - done(); - }); - assert.strictEqual(notBuffering, true); - }); - - it('runs normal calls in-between multis', function (done) { - var multi1 = client.multi(); - multi1.set('m1', '123'); - client.set('m2', '456', done); - }); - - it('runs simultaneous multis with the same client', function (done) { - var end = helper.callFuncAfter(done, 2); - - var multi1 = client.multi(); - multi1.set('m1', '123'); - multi1.get('m1'); - - var multi2 = client.multi(); - multi2.set('m2', '456'); - multi2.get('m2'); - - multi1.exec(end); - multi2.exec(function (err, res) { - assert.strictEqual(res[1], '456'); - end(); - }); - }); - - it('runs simultaneous multis with the same client version 2', function (done) { - var end = helper.callFuncAfter(done, 2); - var multi2 = client.multi(); - var multi1 = client.multi(); - - multi2.set('m2', '456'); - multi1.set('m1', '123'); - multi1.get('m1'); - multi2.get('m1'); - multi2.ping(); - - multi1.exec(end); - multi2.exec(function (err, res) { - assert.strictEqual(res[1], '123'); - end(); - }); - }); - - it('roles back a transaction when one command in a sequence of commands fails', function (done) { - var multi1, multi2; - // Provoke an error at queue time - multi1 = client.MULTI(); - multi1.mset('multifoo', '10', 'multibar', '20', helper.isString('OK')); - - multi1.set('foo2', helper.isError()); - multi1.incr('multifoo'); - multi1.incr('multibar'); - multi1.exec(function () { - // Redis 2.6.5+ will abort transactions with errors - // see: http://redis.io/topics/transactions - var multibar_expected = 1; - var multifoo_expected = 1; - // Confirm that the previous command, while containing an error, still worked. - multi2 = client.multi(); - multi2.incr('multibar', helper.isNumber(multibar_expected)); - multi2.incr('multifoo', helper.isNumber(multifoo_expected)); - multi2.exec(function (err, replies) { - assert.strictEqual(multibar_expected, replies[0]); - assert.strictEqual(multifoo_expected, replies[1]); - return done(); - }); - }); - }); - - it('roles back a transaction when one command in an array of commands fails', function (done) { - // test nested multi-bulk replies - client.multi([ - ['mget', 'multifoo', 'multibar', function (err, res) { - assert.strictEqual(2, res.length); - assert.strictEqual(0, +res[0]); - assert.strictEqual(0, +res[1]); - }], - ['set', 'foo2', helper.isError()], - ['incr', 'multifoo'], - ['incr', 'multibar'] - ]).exec(function (err, replies) { - assert.notEqual(err, null); - assert.equal(replies, undefined); - return done(); - }); - }); - - it('handles multiple operations being applied to a set', function (done) { - client.sadd('some set', 'mem 1'); - client.sadd(['some set', 'mem 2']); - client.sadd('some set', 'mem 3'); - client.sadd('some set', 'mem 4'); - - // make sure empty mb reply works - client.del('some missing set'); - client.smembers('some missing set', function (err, reply) { - // make sure empty mb reply works - assert.strictEqual(0, reply.length); - }); - - // test nested multi-bulk replies with empty mb elements. - client.multi([ - ['smembers', ['some set']], - ['del', 'some set'], - ['smembers', 'some set'] - ]) - .scard('some set') - .exec(function (err, replies) { - assert.strictEqual(4, replies[0].length); - assert.strictEqual(0, replies[2].length); - return done(); - }); - }); - - it('allows multiple operations to be performed using constructor with all kinds of syntax', function (done) { - var now = Date.now(); - var arr = ['multihmset', 'multibar', 'multibaz']; - var arr2 = ['some manner of key', 'otherTypes']; - var arr3 = [5768, 'multibarx', 'multifoox']; - var arr4 = ['mset', [578, 'multibar'], helper.isString('OK')]; - var called = false; - client.multi([ - arr4, - [['mset', 'multifoo2', 'multibar2', 'multifoo3', 'multibar3'], helper.isString('OK')], - ['hmset', arr], - [['hmset', 'multihmset2', 'multibar2', 'multifoo3', 'multibar3', 'test'], helper.isString('OK')], - ['hmset', ['multihmset', 'multibar', 'multifoo'], helper.isString('OK')], - ['hmset', arr3, helper.isString('OK')], - ['hmset', now, {123456789: 'abcdefghij', 'some manner of key': 'a type of value', 'otherTypes': 555}], - ['hmset', 'key2', {'0123456789': 'abcdefghij', 'some manner of key': 'a type of value', 'otherTypes': 999}, helper.isString('OK')], - ['HMSET', 'multihmset', ['multibar', 'multibaz'], undefined], // undefined is used as a explicit not set callback variable - ['hmset', 'multihmset', ['multibar', 'multibaz'], helper.isString('OK')], - ]) - .hmget(now, 123456789, 'otherTypes') - .hmget('key2', arr2, function noop () {}) - .hmget(['multihmset2', 'some manner of key', 'multibar3']) - .mget('multifoo2', ['multifoo3', 'multifoo'], function (err, res) { - assert(res[0], 'multifoo3'); - assert(res[1], 'multifoo'); - called = true; - }) - .exec(function (err, replies) { - assert(called); - assert.equal(arr.length, 3); - assert.equal(arr2.length, 2); - assert.equal(arr3.length, 3); - assert.equal(arr4.length, 3); - assert.strictEqual(null, err); - assert.equal(replies[10][1], '555'); - assert.equal(replies[11][0], 'a type of value'); - assert.strictEqual(replies[12][0], null); - assert.equal(replies[12][1], 'test'); - assert.equal(replies[13][0], 'multibar2'); - assert.equal(replies[13].length, 3); - assert.equal(replies.length, 14); - return done(); - }); - }); - - it('converts a non string key to a string', function (done) { - // TODO: Converting the key might change soon again. - client.multi().hmset(true, { - test: 123, - bar: 'baz' - }).exec(done); - }); - - it('runs a multi without any further commands', function (done) { - var buffering = client.multi().exec(function (err, res) { - assert.strictEqual(err, null); - assert.strictEqual(res.length, 0); - done(); - }); - assert(typeof buffering === 'boolean'); - }); - - it('allows multiple operations to be performed using a chaining API', function (done) { - client.multi() - .mset('some', '10', 'keys', '20') - .incr('some') - .incr('keys') - .mget('some', ['keys']) - .exec(function (err, replies) { - assert.strictEqual(null, err); - assert.equal('OK', replies[0]); - assert.equal(11, replies[1]); - assert.equal(21, replies[2]); - assert.equal(11, replies[3][0].toString()); - assert.equal(21, replies[3][1].toString()); - return done(); - }); - }); - - it('allows multiple commands to work the same as normal to be performed using a chaining API', function (done) { - client.multi() - .mset(['some', '10', 'keys', '20']) - .incr('some', helper.isNumber(11)) - .incr(['keys'], helper.isNumber(21)) - .mget('some', 'keys') - .exec(function (err, replies) { - assert.strictEqual(null, err); - assert.equal('OK', replies[0]); - assert.equal(11, replies[1]); - assert.equal(21, replies[2]); - assert.equal(11, replies[3][0].toString()); - assert.equal(21, replies[3][1].toString()); - return done(); - }); - }); - - it('allows multiple commands to work the same as normal to be performed using a chaining API promisified', function () { - return client.multi() - .mset(['some', '10', 'keys', '20']) - .incr('some', helper.isNumber(11)) - .incr(['keys'], helper.isNumber(21)) - .mget('some', 'keys') - .execAsync() - .then(function (replies) { - assert.equal('OK', replies[0]); - assert.equal(11, replies[1]); - assert.equal(21, replies[2]); - assert.equal(11, replies[3][0].toString()); - assert.equal(21, replies[3][1].toString()); - }); - }); - - it('allows an array to be provided indicating multiple operations to perform', function (done) { - // test nested multi-bulk replies with nulls. - client.multi([ - ['mget', ['multifoo', 'some', 'random value', 'keys']], - ['incr', 'multifoo'] - ]) - .exec(function (err, replies) { - assert.strictEqual(replies.length, 2); - assert.strictEqual(replies[0].length, 4); - return done(); - }); - }); - - it('allows multiple operations to be performed on a hash', function (done) { - client.multi() - .hmset('multihash', 'a', 'foo', 'b', 1) - .hmset('multihash', { - extra: 'fancy', - things: 'here' - }) - .hgetall('multihash') - .exec(function (err, replies) { - assert.strictEqual(null, err); - assert.equal('OK', replies[0]); - assert.equal(Object.keys(replies[2]).length, 4); - assert.equal('foo', replies[2].a); - assert.equal('1', replies[2].b); - assert.equal('fancy', replies[2].extra); - assert.equal('here', replies[2].things); - return done(); - }); - }); - - it('reports EXECABORT exceptions when they occur (while queueing)', function (done) { - client.multi().config('bar').set('foo').set('bar').exec(function (err, reply) { - assert.equal(err.code, 'EXECABORT'); - assert.equal(reply, undefined, 'The reply should have been discarded'); - assert(err.message.match(/^EXECABORT/), 'Error message should begin with EXECABORT'); - assert.equal(err.errors.length, 2, 'err.errors should have 2 items'); - assert.strictEqual(err.errors[0].command, 'SET'); - assert.strictEqual(err.errors[0].code, 'ERR'); - assert.strictEqual(err.errors[0].position, 1); - assert(/^ERR/.test(err.errors[0].message), 'Actuall error message should begin with ERR'); - return done(); - }); - }); - - it('reports multiple exceptions when they occur (while EXEC is running)', function (done) { - client.multi().config('bar').debug('foo').eval("return {err='this is an error'}", 0).exec(function (err, reply) { - assert.strictEqual(reply.length, 3); - assert.equal(reply[0].code, 'ERR'); - assert.equal(reply[0].command, 'CONFIG'); - assert.equal(reply[2].code, undefined); - assert.equal(reply[2].command, 'EVAL'); - assert(/^this is an error/.test(reply[2].message)); - assert(/^ERR/.test(reply[0].message), 'Error message should begin with ERR'); - assert(/^ERR/.test(reply[1].message), 'Error message should begin with ERR'); - return done(); - }); - }); - - it('reports multiple exceptions when they occur (while EXEC is running) promisified', function () { - return client.multi().config('bar').debug('foo').eval("return {err='this is an error'}", 0).execAsync().then(function (reply) { - assert.strictEqual(reply.length, 3); - assert.equal(reply[0].code, 'ERR'); - assert.equal(reply[0].command, 'CONFIG'); - assert.equal(reply[2].code, undefined); - assert.equal(reply[2].command, 'EVAL'); - assert(/^this is an error/.test(reply[2].message)); - assert(/^ERR/.test(reply[0].message), 'Error message should begin with ERR'); - assert(/^ERR/.test(reply[1].message), 'Error message should begin with ERR'); - }); - }); - - it('reports multiple exceptions when they occur (while EXEC is running) and calls cb', function (done) { - var multi = client.multi(); - multi.config('bar', helper.isError()); - multi.set('foo', 'bar', helper.isString('OK')); - multi.debug('foo').exec(function (err, reply) { - assert.strictEqual(reply.length, 3); - assert.strictEqual(reply[0].code, 'ERR'); - assert(/^ERR/.test(reply[0].message), 'Error message should begin with ERR'); - assert(/^ERR/.test(reply[2].message), 'Error message should begin with ERR'); - assert.strictEqual(reply[1], 'OK'); - client.get('foo', helper.isString('bar', done)); - }); - }); - - it('emits an error if no callback has been provided and execabort error occured', function (done) { - var multi = client.multi(); - multi.config('bar'); - multi.set('foo'); - multi.exec(); - - client.on('error', function (err) { - assert.equal(err.code, 'EXECABORT'); - done(); - }); - }); - - it('should work without any callback', function (done) { - var multi = client.multi(); - multi.set('baz', 'binary'); - multi.set('foo', 'bar'); - multi.exec(); - - client.get('foo', helper.isString('bar', done)); - }); - - it('should not use a transaction with exec_atomic if no command is used', function () { - var multi = client.multi(); - var test = false; - multi.exec_batch = function () { - test = true; - }; - multi.exec_atomic(); - assert(test); - }); - - it('should not use a transaction with exec_atomic if only one command is used', function () { - var multi = client.multi(); - var test = false; - multi.exec_batch = function () { - test = true; - }; - multi.set('baz', 'binary'); - multi.EXEC_ATOMIC(); - assert(test); - }); - - it('should use transaction with exec_atomic and more than one command used', function (done) { - var multi = client.multi(); - var test = false; - multi.exec_batch = function () { - test = true; - }; - multi.set('baz', 'binary'); - multi.get('baz'); - multi.exec_atomic(done); - assert(!test); - }); - - it('do not mutate arguments in the multi constructor', function (done) { - var input = [['set', 'foo', 'bar'], ['get', 'foo']]; - client.multi(input).exec(function (err, res) { - assert.strictEqual(input.length, 2); - assert.strictEqual(input[0].length, 3); - assert.strictEqual(input[1].length, 2); - done(); - }); - }); - - it('works properly after a reconnect. issue #897', function (done) { - client.stream.destroy(); - client.on('error', function (err) { - assert.strictEqual(err.code, 'ECONNREFUSED'); - }); - client.on('ready', function () { - client.multi([['set', 'foo', 'bar'], ['get', 'foo']]).exec(function (err, res) { - assert(!err); - assert.strictEqual(res[1], 'bar'); - done(); - }); - }); - }); - - it('emits error once if reconnecting after multi has been executed but not yet returned without callback', function (done) { - // NOTE: If uncork is called async by postponing it to the next tick, this behavior is going to change. - // The command won't be processed anymore two errors are returned instead of one - client.on('error', function (err) { - assert.strictEqual(err.code, 'UNCERTAIN_STATE'); - client.get('foo', function (err, res) { - assert.strictEqual(res, 'bar'); - done(); - }); - }); - - // The commands should still be fired, no matter that the socket is destroyed on the same tick - client.multi().set('foo', 'bar').get('foo').exec(); - // Abort connection before the value returned - client.stream.destroy(); - }); - - it('indivdual commands work properly with multi', function (done) { - // Neither of the following work properly in a transactions: - // (This is due to Redis not returning the reply as expected / resulting in undefined behavior) - // (Likely there are more commands that do not work with a transaction) - // - // auth => can't be called after a multi command - // monitor => results in faulty return values e.g. multi().monitor().set('foo', 'bar').get('foo') - // returns ['OK, 'OK', 'monitor reply'] instead of ['OK', 'OK', 'bar'] - // quit => ends the connection before the exec - // client reply skip|off => results in weird return values. Not sure what exactly happens - // subscribe => enters subscribe mode and this does not work in combination with exec (the same for psubscribe, unsubscribe...) - // - - // Make sure send_command is not called - client.send_command = function () { - throw new Error('failed'); - }; - - assert.strictEqual(client.selected_db, undefined); - var multi = client.multi(); - multi.select(5, function (err, res) { - assert.strictEqual(client.selected_db, 5); - assert.strictEqual(res, 'OK'); - assert.notDeepEqual(client.server_info.db5, { avg_ttl: 0, expires: 0, keys: 1 }); - }); - // multi.client('reply', 'on', helper.isString('OK')); // Redis v.3.2 - multi.set('foo', 'bar', helper.isString('OK')); - multi.info(function (err, res) { - assert.strictEqual(res.indexOf('# Server\r\nredis_version:'), 0); - assert.deepEqual(client.server_info.db5, { avg_ttl: 0, expires: 0, keys: 1 }); - }); - multi.get('foo', helper.isString('bar')); - multi.exec(function (err, res) { - res[2] = res[2].substr(0, 10); - assert.deepEqual(res, ['OK', 'OK', '# Server\r\n', 'bar']); - client.flushdb(done); - }); - }); - - }); - }); - }); -}); diff --git a/test/node_redis.spec.js b/test/node_redis.spec.js deleted file mode 100644 index 12ad70d5ef..0000000000 --- a/test/node_redis.spec.js +++ /dev/null @@ -1,1043 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var fs = require('fs'); -var util = require('util'); -var path = require('path'); -var intercept = require('intercept-stdout'); -var config = require('./lib/config'); -var helper = require('./helper'); -var fork = require('child_process').fork; -var redis = config.redis; -var client; - -// Currently GitHub Actions on Windows (and event travis) builds hang after completing if -// any processes are still running, we shutdown redis-server after all tests complete (can't do this in a -// `after_script` hook as it hangs before the `after` life cycles) to workaround the issue. -after(function (done) { - if (process.platform !== 'win32' || !process.env.GITHUB_ACTION) { - return done(); - } - setTimeout(function () { - require('cross-spawn').sync('redis-server', ['--service-stop'], {}); - done(); - }, 2000); -}); - -describe('The node_redis client', function () { - - describe("The 'add_command' method", function () { - - var Redis = redis.RedisClient; - - it('camel case and snakeCase version exists', function () { - assert.strictEqual(typeof redis.addCommand, 'function'); - assert.strictEqual(typeof redis.add_command, 'function'); - }); - - it('converts special characters in functions names to lowercase', function () { - var command = 'really-new.command'; - assert.strictEqual(Redis.prototype[command], undefined); - redis.addCommand(command); - if (Redis.prototype[command].name !== '') { - assert.strictEqual(Redis.prototype[command].name, 'really_new_command'); - assert.strictEqual(Redis.prototype[command.toUpperCase()].name, 'really_new_command'); - assert.strictEqual(Redis.prototype.really_new_command.name, 'really_new_command'); - assert.strictEqual(Redis.prototype.REALLY_NEW_COMMAND.name, 'really_new_command'); - } - }); - }); - - it('individual commands sanity check', function (done) { - // All commands should work the same in multi context or without - // Therefor individual commands always have to be handled in both cases - fs.readFile(path.resolve(__dirname, '../lib/individualCommands.js'), 'utf8', function (err, data) { - var client_prototype = data.match(/(\n| = )RedisClient\.prototype.[a-zA-Z_]+/g); - var multi_prototype = data.match(/(\n| = )Multi\.prototype\.[a-zA-Z_]+/g); - // Check that every entry RedisClient entry has a correspondend Multi entry - assert.strictEqual(client_prototype.filter(function (entry) { - return multi_prototype.indexOf(entry.replace('RedisClient', 'Multi')) === -1; - }).length, 4); // multi and batch are included too - assert.strictEqual(client_prototype.length, multi_prototype.length + 4); - // Check that all entries exist in uppercase and in lowercase variants - assert.strictEqual(data.match(/(\n| = )RedisClient\.prototype.[a-z_]+/g).length * 2, client_prototype.length); - done(); - }); - }); - - it('convert minus to underscore in Redis function names', function (done) { - var names = Object.keys(redis.RedisClient.prototype); - client = redis.createClient(); - for (var i = 0; i < names.length; i++) { - assert(/^([a-zA-Z_][a-zA-Z_0-9]*)?$/.test(client[names[i]].name)); - } - client.quit(done); - }); - - it('reset the parser while reconnecting (See #1190)', function (done) { - var client = redis.createClient({ - retryStrategy: function () { - return 5; - } - }); - client.once('reconnecting', function () { - process.nextTick(function () { - assert.strictEqual(client.reply_parser.buffer, null); - client.end(true); - done(); - }); - }); - var partialInput = Buffer.from('$100\r\nabcdef'); - client.reply_parser.execute(partialInput); - assert.strictEqual(client.reply_parser.buffer.inspect(), partialInput.inspect()); - client.stream.destroy(); - }); - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - - afterEach(function () { - client.end(true); - }); - - describe('when connected', function () { - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - client.once('connect', function () { - client.flushdb(done); - }); - }); - - describe('duplicate', function () { - it('check if all options got copied properly', function (done) { - client.selected_db = 2; - var client2 = client.duplicate(); - assert.strictEqual(client.connectionId + 1, client2.connection_id); - assert.strictEqual(client2.selected_db, 2); - assert(client.connected); - assert(!client2.connected); - for (var elem in client.options) { - if (client.options.hasOwnProperty(elem)) { - assert.strictEqual(client2.options[elem], client.options[elem]); - } - } - client2.on('error', function (err) { - assert.strictEqual(err.message, 'Connection forcefully ended and command aborted. It might have been processed.'); - assert.strictEqual(err.command, 'SELECT'); - assert(err instanceof Error); - assert.strictEqual(err.name, 'AbortError'); - }); - client2.on('ready', function () { - client2.end(true); - done(); - }); - }); - - it('check if all new options replaced the old ones', function (done) { - client.selected_db = 1; - var client2 = client.duplicate({ - db: 2, - no_ready_check: true - }); - assert(client.connected); - assert(!client2.connected); - assert.notEqual(client.selected_db, client2.selected_db); - assert.strictEqual(client.options.no_ready_check, undefined); - assert.strictEqual(client2.options.no_ready_check, true); - assert.notDeepEqual(client.options, client2.options); - for (var elem in client.options) { - if (client.options.hasOwnProperty(elem)) { - if (elem !== 'no_ready_check') { - assert.strictEqual(client2.options[elem], client.options[elem]); - } - } - } - client2.on('ready', function () { - client2.end(true); - done(); - }); - }); - - it('works with a callback', function (done) { - client.duplicate(function (err, client) { - assert(!err); - assert.strictEqual(client.ready, true); - client.quit(done); - }); - }); - - it('works with a callback and errors out', function (done) { - client.duplicate({ - port: '9999' - }, function (err, client) { - assert.strictEqual(err.code, 'ECONNREFUSED'); - done(client); - }); - }); - - it('works with a promises', function () { - return client.duplicateAsync().then(function (client) { - assert.strictEqual(client.ready, true); - return client.quitAsync(); - }); - }); - - it('works with a promises and errors', function () { - return client.duplicateAsync({ - port: 9999 - }).catch(function (err) { - assert.strictEqual(err.code, 'ECONNREFUSED'); - }); - }); - }); - - describe('big data', function () { - - // Check if the fast mode for big strings is working correct - it('safe strings that are bigger than 30000 characters', function (done) { - var str = 'foo ಠ_ಠ bar '; - while (str.length < 111111) { - str += str; - } - client.set('foo', str); - client.get('foo', function (err, res) { - assert.strictEqual(res, str); - done(); - }); - }); - - it('safe strings that are bigger than 30000 characters with multi', function (done) { - var str = 'foo ಠ_ಠ bar '; - while (str.length < 111111) { - str += str; - } - var called = false; - var temp = client.write_buffers.bind(client); - assert(client.fire_strings); - client.write_buffers = function (data) { - called = true; - // To increase write performance for strings the value is converted to a buffer - assert(!client.fire_strings); - temp(data); - }; - client.multi().set('foo', str).get('foo', function (err, res) { - assert.strictEqual(res, str); - }).exec(function (err, res) { - assert(called); - assert.strictEqual(res[1], str); - done(); - }); - assert(client.fire_strings); - }); - }); - - describe('send_command', function () { - - it('omitting args should be fine', function (done) { - client.server_info = {}; - client.send_command('info'); - client.send_command('ping', function (err, res) { - assert.strictEqual(res, 'PONG'); - // Check if the previous info command used the internal individual info command - assert.notDeepEqual(client.server_info, {}); - client.server_info = {}; - }); - client.send_command('info', null, undefined); - client.send_command('ping', null, function (err, res) { - assert.strictEqual(res, 'PONG'); - // Check if the previous info command used the internal individual info command - assert.notDeepEqual(client.server_info, {}); - client.server_info = {}; - }); - client.send_command('info', undefined, undefined); - client.send_command('ping', function (err, res) { - assert.strictEqual(res, 'PONG'); - // Check if the previous info command used the internal individual info command - assert.notDeepEqual(client.server_info, {}); - client.server_info = {}; - }); - client.send_command('info', undefined, function (err, res) { - assert(/redis_version/.test(res)); - // The individual info command should also be called by using send_command - // console.log(info, client.server_info); - assert.notDeepEqual(client.server_info, {}); - done(); - }); - }); - - it('using multi with sendCommand should work as individual command instead of using the internal multi', function (done) { - // This is necessary to keep backwards compatibility and it is the only way to handle multis as you want in node_redis - client.sendCommand('multi'); - client.sendCommand('set', ['foo', 'bar'], helper.isString('QUEUED')); - client.get('foo'); - client.exec(function (err, res) { // exec is not manipulated if not fired by the individual multi command - // As the multi command is handled individually by the user he also has to handle the return value - assert.strictEqual(res[0].toString(), 'OK'); - assert.strictEqual(res[1].toString(), 'bar'); - done(); - }); - }); - - it('multi should be handled special', function (done) { - client.send_command('multi', undefined, helper.isString('OK')); - var args = ['test', 'bla']; - client.send_command('set', args, helper.isString('QUEUED')); - assert.deepEqual(args, ['test', 'bla']); // Check args manipulation - client.get('test', helper.isString('QUEUED')); - client.exec(function (err, res) { - // As the multi command is handled individually by the user he also has to handle the return value - assert.strictEqual(res[0].toString(), 'OK'); - assert.strictEqual(res[1].toString(), 'bla'); - done(); - }); - }); - - it('using another type as cb should throw', function () { - try { - client.send_command('set', ['test', 'bla'], [true]); - throw new Error('failed'); - } catch (err) { - assert.strictEqual(err.message, 'Wrong input type "Array" for callback function'); - } - try { - client.send_command('set', ['test', 'bla'], null); - throw new Error('failed'); - } catch (err) { - assert.strictEqual(err.message, 'Wrong input type "null" for callback function'); - } - }); - - it('command argument has to be of type string', function () { - try { - client.send_command(true, ['test', 'bla'], function () {}); - throw new Error('failed'); - } catch (err) { - assert.strictEqual(err.message, 'Wrong input type "Boolean" for command name'); - } - try { - client.send_command(undefined, ['test', 'bla'], function () {}); - throw new Error('failed'); - } catch (err) { - assert.strictEqual(err.message, 'Wrong input type "undefined" for command name'); - } - try { - client.send_command(null, ['test', 'bla'], function () {}); - throw new Error('failed'); - } catch (err) { - assert.strictEqual(err.message, 'Wrong input type "null" for command name'); - } - }); - - it('args may only be of type Array or undefined', function () { - try { - client.send_command('info', 123); - throw new Error('failed'); - } catch (err) { - assert.strictEqual(err.message, 'Wrong input type "Number" for args'); - } - }); - - it('passing a callback as args and as callback should throw', function () { - try { - client.send_command('info', function a () {}, function b () {}); - throw new Error('failed'); - } catch (err) { - assert.strictEqual(err.message, 'Wrong input type "Function" for args'); - } - }); - - it('multi should be handled special', function (done) { - client.send_command('multi', undefined, helper.isString('OK')); - var args = ['test', 'bla']; - client.send_command('set', args, helper.isString('QUEUED')); - assert.deepEqual(args, ['test', 'bla']); // Check args manipulation - client.get('test', helper.isString('QUEUED')); - client.exec(function (err, res) { - // As the multi command is handled individually by the user he also has to handle the return value - assert.strictEqual(res[0].toString(), 'OK'); - assert.strictEqual(res[1].toString(), 'bla'); - done(); - }); - }); - - it('the args array may contain a arbitrary number of arguments', function (done) { - client.send_command('mset', ['foo', 1, 'bar', 2, 'baz', 3], helper.isString('OK')); - client.mget(['foo', 'bar', 'baz'], function (err, res) { - // As the multi command is handled individually by the user he also has to handle the return value - assert.strictEqual(res[0].toString(), '1'); - assert.strictEqual(res[1].toString(), '2'); - assert.strictEqual(res[2].toString(), '3'); - done(); - }); - }); - - it('send_command with callback as args', function (done) { - client.send_command('abcdef', function (err, res) { - if (process.platform === 'win32') { - assert.strictEqual(err.message, "ERR unknown command 'abcdef'"); - } else { - assert.strictEqual(err.message, 'ERR unknown command `abcdef`, with args beginning with: '); - } - done(); - }); - }); - - }); - - describe('retry_unfulfilled_commands', function () { - - it('should retry all commands instead of returning an error if a command did not yet return after a connection loss', function (done) { - var bclient = redis.createClient({ - retry_unfulfilled_commands: true - }); - bclient.blpop('blocking list 2', 5, function (err, value) { - assert.strictEqual(value[0], 'blocking list 2'); - assert.strictEqual(value[1], 'initial value'); - bclient.end(true); - done(err); - }); - bclient.once('ready', function () { - setTimeout(function () { - bclient.stream.destroy(); - client.rpush('blocking list 2', 'initial value', helper.isNumber(1)); - }, 100); - }); - }); - - it('should retry all commands even if the offline queue is disabled', function (done) { - var bclient = redis.createClient({ - enableOfflineQueue: false, - retryUnfulfilledCommands: true - }); - bclient.once('ready', function () { - bclient.blpop('blocking list 2', 5, function (err, value) { - assert.strictEqual(value[0], 'blocking list 2'); - assert.strictEqual(value[1], 'initial value'); - bclient.end(true); - done(err); - }); - setTimeout(function () { - bclient.stream.destroy(); - client.rpush('blocking list 2', 'initial value', helper.isNumber(1)); - }, 100); - }); - }); - - }); - - describe('.end', function () { - - it('used without flush / flush set to false', function (done) { - var finished = false; - var end = helper.callFuncAfter(function () { - if (!finished) { - done(new Error('failed')); - } - }, 20); - var cb = function (err, res) { - assert(/Connection forcefully ended|The connection is already closed./.test(err.message)); - assert.strictEqual(err.code, 'NR_CLOSED'); - end(); - }; - for (var i = 0; i < 20; i++) { - if (i === 10) { - client.end(); - } - client.set('foo', 'bar', cb); - } - client.on('warning', function () {}); // Ignore deprecation message - setTimeout(function () { - finished = true; - done(); - }, 25); - }); - - it('used with flush set to true', function (done) { - var end = helper.callFuncAfter(function () { - done(); - }, 20); - var cb = function (err, res) { - assert(/Connection forcefully ended|The connection is already closed./.test(err.message)); - end(); - }; - for (var i = 0; i < 20; i++) { - if (i === 10) { - client.end(true); - client.stream.write('foo'); // Trigger an error on the closed stream that we ignore - } - client.set('foo', 'bar', cb); - } - }); - - it('emits an aggregate error if no callback was present for multiple commands in debug_mode', function (done) { - redis.debug_mode = true; - var unhookIntercept = intercept(function (data) { - return ''; // Don't print the debug messages - }); - client.set('foo', 'bar'); - client.set('baz', 'hello world'); - client.on('error', function (err) { - assert(err instanceof Error); - assert(err instanceof redis.AbortError); - assert(err instanceof redis.AggregateError); - assert.strictEqual(err.name, 'AggregateError'); - assert.strictEqual(err.errors.length, 2); - assert.strictEqual(err.message, 'Connection forcefully ended and commands aborted.'); - assert.strictEqual(err.code, 'NR_CLOSED'); - assert.strictEqual(err.errors[0].message, 'Connection forcefully ended and command aborted. It might have been processed.'); - assert.strictEqual(err.errors[0].command, 'SET'); - assert.strictEqual(err.errors[0].code, 'NR_CLOSED'); - assert.deepEqual(err.errors[0].args, ['foo', 'bar']); - done(); - }); - client.end(true); - unhookIntercept(); - redis.debug_mode = false; - }); - - it('emits an abort error if no callback was present for a single commands', function (done) { - redis.debug_mode = true; - var unhookIntercept = intercept(function (data) { - return ''; // Don't print the debug messages - }); - client.set('foo', 'bar'); - client.on('error', function (err) { - assert(err instanceof Error); - assert(err instanceof redis.AbortError); - assert(!(err instanceof redis.AggregateError)); - assert.strictEqual(err.message, 'Connection forcefully ended and command aborted. It might have been processed.'); - assert.strictEqual(err.command, 'SET'); - assert.strictEqual(err.code, 'NR_CLOSED'); - assert.deepEqual(err.args, ['foo', 'bar']); - done(); - }); - client.end(true); - unhookIntercept(); - redis.debug_mode = false; - }); - - it('does not emit abort errors if no callback was present while not being in debug_mode ', function (done) { - client.set('foo', 'bar'); - client.end(true); - setTimeout(done, 100); - }); - - }); - - describe('commands after using .quit should fail', function () { - - it('return an error in the callback', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - // TODO: Investigate why this test is failing hard and killing mocha if using '/tmp/redis.sock'. - // Seems like something is wrong with nyc while passing a socket connection to create client! - var client2 = redis.createClient(); - client2.quit(function () { - client2.get('foo', function (err, res) { - assert.strictEqual(err.message, 'Stream connection ended and command aborted. It might have been processed.'); - assert.strictEqual(client2.offline_queue.length, 0); - done(); - }); - }); - }); - - it('return an error in the callback version two', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client.quit(); - setTimeout(function () { - client.get('foo', function (err, res) { - assert.strictEqual(err.message, 'GET can\'t be processed. The connection is already closed.'); - assert.strictEqual(err.command, 'GET'); - assert.strictEqual(client.offline_queue.length, 0); - done(); - }); - }, 50); - }); - - it('emit an error', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - client.quit(); - client.on('error', function (err) { - assert.strictEqual(err.message, 'SET can\'t be processed. The connection is already closed.'); - assert.strictEqual(err.command, 'SET'); - assert.strictEqual(client.offline_queue_length, 0); - done(); - }); - setTimeout(function () { - client.set('foo', 'bar'); - }, 50); - }); - - }); - - describe('when redis closes unexpectedly', function () { - it('reconnects and can retrieve the pre-existing data', function (done) { - client.on('reconnecting', function on_recon (params) { - client.on('connect', function on_connect () { - var end = helper.callFuncAfter(function () { - client.removeListener('connect', on_connect); - client.removeListener('reconnecting', on_recon); - assert.strictEqual(client.server_info.db0.keys, 2); - assert.strictEqual(Object.keys(client.server_info.db0).length, 3); - done(); - }, 4); - client.get('recon 1', helper.isString('one', end)); - client.get('recon 1', helper.isString('one', end)); - client.get('recon 2', helper.isString('two', end)); - client.get('recon 2', helper.isString('two', end)); - }); - }); - - client.set('recon 1', 'one'); - client.set('recon 2', 'two', function (err, res) { - // Do not do this in normal programs. This is to simulate the server closing on us. - // For orderly shutdown in normal programs, do client.quit() - client.stream.destroy(); - }); - }); - - it('reconnects properly when monitoring', function (done) { - client.on('reconnecting', function on_recon (params) { - client.on('ready', function on_ready () { - assert.strictEqual(client.monitoring, true, 'monitoring after reconnect'); - client.removeListener('ready', on_ready); - client.removeListener('reconnecting', on_recon); - done(); - }); - }); - - assert.strictEqual(client.monitoring, false, 'monitoring off at start'); - client.set('recon 1', 'one'); - client.monitor(function (err, res) { - assert.strictEqual(client.monitoring, true, 'monitoring on after monitor()'); - client.set('recon 2', 'two', function (err, res) { - // Do not do this in normal programs. This is to simulate the server closing on us. - // For orderly shutdown in normal programs, do client.quit() - client.stream.destroy(); - }); - }); - }); - - describe("and it's subscribed to a channel", function () { - // "Connection in subscriber mode, only subscriber commands may be used" - it('reconnects, unsubscribes, and can retrieve the pre-existing data', function (done) { - client.on('ready', function on_connect () { - client.unsubscribe(helper.isNotError()); - - client.on('unsubscribe', function (channel, count) { - // we should now be out of subscriber mode. - assert.strictEqual(channel, 'recon channel'); - assert.strictEqual(count, 0); - client.set('foo', 'bar', helper.isString('OK', done)); - }); - }); - - client.set('recon 1', 'one'); - client.subscribe('recon channel', function (err, res) { - // Do not do this in normal programs. This is to simulate the server closing on us. - // For orderly shutdown in normal programs, do client.quit() - client.stream.destroy(); - }); - }); - - it('reconnects, unsubscribes, and can retrieve the pre-existing data of a explicit channel', function (done) { - client.on('ready', function on_connect () { - client.unsubscribe('recon channel', helper.isNotError()); - - client.on('unsubscribe', function (channel, count) { - // we should now be out of subscriber mode. - assert.strictEqual(channel, 'recon channel'); - assert.strictEqual(count, 0); - client.set('foo', 'bar', helper.isString('OK', done)); - }); - }); - - client.set('recon 1', 'one'); - client.subscribe('recon channel', function (err, res) { - // Do not do this in normal programs. This is to simulate the server closing on us. - // For orderly shutdown in normal programs, do client.quit() - client.stream.destroy(); - }); - }); - }); - - describe('domain', function () { - it('allows client to be executed from within domain', function (done) { - var domain = require('domain').create(); - - domain.run(function () { - client.set('domain', 'value', function (err, res) { - assert.ok(process.domain); - throw new Error('ohhhh noooo'); - }); - }); - - // this is the expected and desired behavior - domain.on('error', function (err) { - assert.strictEqual(err.message, 'ohhhh noooo'); - domain.exit(); - done(); - }); - }); - - it('keeps the same domain by using the offline queue', function (done) { - client.end(true); - client = redis.createClient(); - var testDomain = require('domain').create(); - testDomain.run(function () { - client.set('FOOBAR', 'def', function () { - assert.strictEqual(process.domain, testDomain); - done(); - }); - }); - }); - - it('catches all errors from within the domain', function (done) { - var domain = require('domain').create(); - - domain.run(function () { - if (process.versions.node.split('.')[0] >= 13) { - // Node >= 13 - // Recreate client in domain so error handlers run this domain - // Changed in: "error handler runs outside of its domain" - // https://github.com/nodejs/node/pull/26211 - client.end(true); // make sure to close current client - client = redis.createClient(); - } - client.end(true); - // Trigger an error within the domain - client.set('domain', 'value'); - }); - - domain.on('error', function (err) { - assert.strictEqual(err.message, 'SET can\'t be processed. The connection is already closed.'); - domain.exit(); - done(); - }); - }); - }); - }); - - describe('utf8', function () { - it('handles utf-8 keys', function (done) { - var utf8_sample = 'ಠ_ಠ'; - client.set(['utf8test', utf8_sample], helper.isString('OK')); - client.get(['utf8test'], function (err, obj) { - assert.strictEqual(utf8_sample, obj); - done(err); - }); - }); - }); - }); - - describe('unref', function () { - it('exits subprocess as soon as final command is processed', function (done) { - this.timeout(12000); - var args = config.HOST[ip] ? [config.HOST[ip], config.PORT] : [ip]; - var external = fork('./test/lib/unref.js', args); - - var id = setTimeout(function () { - external.kill(); - done(new Error('unref subprocess timed out')); - }, 8000); - - external.on('close', function (code) { - clearTimeout(id); - assert.strictEqual(code, 0); - done(); - }); - }); - }); - - describe('execution order / fire query while loading', function () { - it('keep execution order for commands that may fire while redis is still loading', function (done) { - client = redis.createClient.apply(null, args); - var fired = false; - client.set('foo', 'bar', function (err, res) { - assert(fired === false); - done(); - }); - client.info(function (err, res) { - fired = true; - }); - }); - - // TODO: consider allowing loading commands in v.4 - // it('should fire early', function (done) { - // client = redis.createClient.apply(null, args); - // var fired = false; - // client.info(function (err, res) { - // fired = true; - // }); - // client.set('foo', 'bar', function (err, res) { - // assert(fired); - // done(); - // }); - // assert.strictEqual(client.offline_queue.length, 1); - // assert.strictEqual(client.command_queue.length, 1); - // client.on('connect', function () { - // assert.strictEqual(client.offline_queue.length, 1); - // assert.strictEqual(client.command_queue.length, 1); - // }); - // client.on('ready', function () { - // assert.strictEqual(client.offline_queue.length, 0); - // }); - // }); - }); - - describe('protocol error', function () { - - it('should gracefully recover and only fail on the already send commands', function (done) { - client = redis.createClient.apply(null, args); - var error; - client.on('error', function (err) { - assert.strictEqual(err.message, 'Protocol error, got "a" as reply type byte. Please report this.'); - assert.strictEqual(err, error); - assert(err instanceof redis.ParserError); - // After the hard failure work properly again. The set should have been processed properly too - client.get('foo', function (err, res) { - assert.strictEqual(res, 'bar'); - done(); - }); - }); - client.once('ready', function () { - client.set('foo', 'bar', function (err, res) { - assert.strictEqual(err.message, 'Fatal error encountered. Command aborted. It might have been processed.'); - assert.strictEqual(err.code, 'NR_FATAL'); - assert(err instanceof redis.AbortError); - error = err.origin; - }); - // Make sure we call execute out of the reply - // ready is called in a reply - process.nextTick(function () { - // Fail the set answer. Has no corresponding command obj and will therefore land in the error handler and set - client.reply_parser.execute(Buffer.from('a*1\r*1\r$1`zasd\r\na')); - }); - }); - }); - }); - - describe('enable_offline_queue', function () { - describe('true', function () { - - it('does not return an error and enqueues operation', function (done) { - client = redis.createClient(9999, null); - var finished = false; - client.on('error', function (e) { - // ignore, b/c expecting a "can't connect" error - }); - - setTimeout(function () { - client.set('foo', 'bar', function (err, result) { - if (!finished) done(err); - assert.strictEqual(err.message, 'Connection forcefully ended and command aborted.'); - }); - - setTimeout(function () { - assert.strictEqual(client.offline_queue.length, 1); - finished = true; - done(); - }, 25); - }, 50); - }); - - it('enqueues operation and keep the queue while trying to reconnect', function (done) { - client = redis.createClient(9999, null, { - retry_strategy: function (options) { - if (options.attempt > 4) { - return undefined; - } - return 100; - }, - }); - var i = 0; - - client.on('error', function (err) { - if (err.code === 'CONNECTION_BROKEN') { - assert(i, 3); - assert.strictEqual(client.offline_queue.length, 0); - assert.strictEqual(err.origin.code, 'ECONNREFUSED'); - if (!(err instanceof redis.AbortError)) { - done(); - } else { - assert.strictEqual(err.command, 'SET'); - } - } else { - assert.equal(err.code, 'ECONNREFUSED'); - - if (typeof err.errno === 'number') { - // >= Node 13 - assert.equal(util.getSystemErrorName(err.errno), 'ECONNREFUSED'); - } else { - // < Node 13 - assert.equal(err.errno, 'ECONNREFUSED'); - } - assert.equal(err.syscall, 'connect'); - } - }); - - client.on('reconnecting', function (params) { - i++; - assert.equal(params.attempt, i); - assert.strictEqual(params.times_connected, 0); - assert(params.error instanceof Error); - assert(typeof params.total_retry_time === 'number'); - assert.strictEqual(client.offline_queue.length, 2); - }); - - // Should work with either a callback or without - client.set('baz', 13); - client.set('foo', 'bar', function (err, result) { - assert(i, 3); - assert(err); - assert.strictEqual(client.offline_queue.length, 0); - }); - }); - - it('flushes the command queue if connection is lost', function (done) { - client = redis.createClient(); - - client.once('ready', function () { - var multi = client.multi(); - multi.config('bar'); - var cb = function (err, reply) { - assert.equal(err.code, 'UNCERTAIN_STATE'); - }; - for (var i = 0; i < 12; i += 3) { - client.set('foo' + i, 'bar' + i); - multi.set('foo' + (i + 1), 'bar' + (i + 1), cb); - multi.set('foo' + (i + 2), 'bar' + (i + 2)); - } - multi.exec(); - assert.equal(client.command_queue_length, 15); - helper.killConnection(client); - }); - - var end = helper.callFuncAfter(done, 3); - client.on('error', function (err) { - if (err.command === 'EXEC') { - assert.strictEqual(client.command_queue.length, 0); - assert.strictEqual(err.errors.length, 9); - assert.strictEqual(err.errors[1].command, 'SET'); - assert.deepEqual(err.errors[1].args, ['foo1', 'bar1']); - end(); - } else if (err.code === 'UNCERTAIN_STATE') { - assert.strictEqual(client.command_queue.length, 0); - assert.strictEqual(err.errors.length, 4); - assert.strictEqual(err.errors[0].command, 'SET'); - assert.deepEqual(err.errors[0].args, ['foo0', 'bar0']); - end(); - } else { - assert.equal(err.code, 'ECONNREFUSED'); - if (typeof err.errno === 'number') { - // >= Node 13 - assert.equal(util.getSystemErrorName(err.errno), 'ECONNREFUSED'); - } else { - // < Node 13 - assert.equal(err.errno, 'ECONNREFUSED'); - } - assert.equal(err.syscall, 'connect'); - end(); - } - }); - }); - }); - - describe('false', function () { - - it('stream not writable', function (done) { - client = redis.createClient({ - enable_offline_queue: false - }); - client.on('ready', function () { - client.stream.destroy(); - client.set('foo', 'bar', function (err, res) { - assert.strictEqual(err.message, "SET can't be processed. Stream not writeable."); - done(); - }); - }); - }); - - it('emit an error and does not enqueues operation', function (done) { - client = redis.createClient(9999, null, { - max_attempts: 0, - enable_offline_queue: false - }); - var end = helper.callFuncAfter(done, 3); - - client.on('error', function (err) { - assert(/offline queue is deactivated|ECONNREFUSED/.test(err.message)); - assert.equal(client.command_queue.length, 0); - end(); - }); - - client.set('foo', 'bar'); - - assert.doesNotThrow(function () { - client.set('foo', 'bar', function (err) { - // should callback with an error - assert.ok(err); - setTimeout(end, 50); - }); - }); - }); - - it('flushes the command queue if connection is lost', function (done) { - client = redis.createClient({ - enable_offline_queue: false - }); - - redis.debug_mode = true; - var unhookIntercept = intercept(function () { - return ''; - }); - client.once('ready', function () { - var multi = client.multi(); - multi.config('bar'); - var cb = function (err, reply) { - assert.equal(err.code, 'UNCERTAIN_STATE'); - }; - for (var i = 0; i < 12; i += 3) { - client.set('foo' + i, 'bar' + i); - multi.set('foo' + (i + 1), 'bar' + (i + 1), cb); - multi.set('foo' + (i + 2), 'bar' + (i + 2)); - } - multi.exec(); - assert.equal(client.command_queue.length, 15); - helper.killConnection(client); - }); - - var end = helper.callFuncAfter(done, 3); - client.on('error', function (err) { - assert.equal(client.command_queue.length, 0); - if (err.command === 'EXEC') { - assert.equal(err.errors.length, 9); - end(); - } else if (err.code === 'UNCERTAIN_STATE') { - assert.equal(err.errors.length, 4); - end(); - } else { - assert.equal(err.code, 'ECONNREFUSED'); - if (typeof err.errno === 'number') { - // >= Node 13 - assert.equal(util.getSystemErrorName(err.errno), 'ECONNREFUSED'); - } else { - // < Node 13 - assert.equal(err.errno, 'ECONNREFUSED'); - } - assert.equal(err.syscall, 'connect'); - redis.debug_mode = false; - client.end(true); - unhookIntercept(); - end(); - } - }); - }); - }); - }); - - }); - }); -}); diff --git a/test/prefix.spec.js b/test/prefix.spec.js deleted file mode 100644 index 52fd39c1cc..0000000000 --- a/test/prefix.spec.js +++ /dev/null @@ -1,118 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var helper = require('./helper'); -var redis = config.redis; - -describe('prefix key names', function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client = null; - - beforeEach(function (done) { - client = redis.createClient({ - prefix: 'test:prefix:' - }); - client.on('ready', function () { - client.flushdb(function (err) { - done(err); - }); - }); - }); - - afterEach(function () { - client.end(true); - }); - - it('auto prefix set / get', function (done) { - client.set('key', 'value', function (err, reply) { - assert.strictEqual(reply, 'OK'); - }); - client.get('key', function (err, reply) { - assert.strictEqual(reply, 'value'); - }); - client.getrange('key', 1, -1, function (err, reply) { - assert.strictEqual(reply, 'alue'); - assert.strictEqual(err, null); - }); - client.exists('key', function (err, res) { - assert.strictEqual(res, 1); - }); - client.exists('test:prefix:key', function (err, res) { - // The key will be prefixed itself - assert.strictEqual(res, 0); - }); - client.mset('key2', 'value2', 'key3', 'value3'); - client.keys('*', function (err, res) { - assert.strictEqual(res.length, 3); - assert(res.indexOf('test:prefix:key') !== -1); - assert(res.indexOf('test:prefix:key2') !== -1); - assert(res.indexOf('test:prefix:key3') !== -1); - done(); - }); - }); - - it('auto prefix set / get with .batch', function (done) { - var batch = client.batch(); - batch.set('key', 'value', function (err, reply) { - assert.strictEqual(reply, 'OK'); - }); - batch.get('key', function (err, reply) { - assert.strictEqual(reply, 'value'); - }); - batch.getrange('key', 1, -1, function (err, reply) { - assert.strictEqual(reply, 'alue'); - assert.strictEqual(err, null); - }); - batch.exists('key', function (err, res) { - assert.strictEqual(res, 1); - }); - batch.exists('test:prefix:key', function (err, res) { - // The key will be prefixed itself - assert.strictEqual(res, 0); - }); - batch.mset('key2', 'value2', 'key3', 'value3'); - batch.keys('*', function (err, res) { - assert.strictEqual(res.length, 3); - assert(res.indexOf('test:prefix:key') !== -1); - assert(res.indexOf('test:prefix:key2') !== -1); - assert(res.indexOf('test:prefix:key3') !== -1); - }); - batch.exec(done); - }); - - it('auto prefix set / get with .multi', function (done) { - var multi = client.multi(); - multi.set('key', 'value', function (err, reply) { - assert.strictEqual(reply, 'OK'); - }); - multi.get('key', function (err, reply) { - assert.strictEqual(reply, 'value'); - }); - multi.getrange('key', 1, -1, function (err, reply) { - assert.strictEqual(reply, 'alue'); - assert.strictEqual(err, null); - }); - multi.exists('key', function (err, res) { - assert.strictEqual(res, 1); - }); - multi.exists('test:prefix:key', function (err, res) { - // The key will be prefixed itself - assert.strictEqual(res, 0); - }); - multi.mset('key2', 'value2', 'key3', 'value3'); - multi.keys('*', function (err, res) { - assert.strictEqual(res.length, 3); - assert(res.indexOf('test:prefix:key') !== -1); - assert(res.indexOf('test:prefix:key2') !== -1); - assert(res.indexOf('test:prefix:key3') !== -1); - }); - multi.exec(done); - }); - - }); - }); -}); diff --git a/test/pubsub.spec.js b/test/pubsub.spec.js deleted file mode 100644 index 34e93f37f2..0000000000 --- a/test/pubsub.spec.js +++ /dev/null @@ -1,679 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var helper = require('./helper'); -var errors = require('./errors'); -var redis = config.redis; - -describe('publish/subscribe', function () { - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var pub = null; - var sub = null; - var channel = 'test channel'; - var channel2 = 'test channel 2'; - var message = 'test message'; - - beforeEach(function (done) { - var end = helper.callFuncAfter(done, 2); - - pub = redis.createClient.apply(null, args); - sub = redis.createClient.apply(null, args); - pub.once('connect', function () { - pub.flushdb(function () { - end(); - }); - }); - sub.once('connect', function () { - end(); - }); - }); - - describe('disable resubscribe', function () { - beforeEach(function (done) { - sub.end(false); - sub = redis.createClient({ - disable_resubscribing: true - }); - sub.once('connect', function () { - done(); - }); - }); - - it('does not fire subscribe events after reconnecting', function (done) { - var a = false; - sub.on('subscribe', function (chnl, count) { - if (chnl === channel2) { - if (a) { - return done(new Error('Test failed')); - } - assert.equal(2, count); - sub.stream.destroy(); - } - }); - - sub.on('reconnecting', function () { - a = true; - sub.on('ready', function () { - assert.strictEqual(sub.command_queue.length, 0); - done(); - }); - }); - - sub.subscribe(channel, channel2); - }); - }); - - describe('string_numbers and pub sub', function () { - beforeEach(function (done) { - sub.end(false); - sub = redis.createClient({ - string_numbers: true - }); - sub.once('connect', function () { - done(); - }); - }); - - it('does not fire subscribe events after reconnecting', function (done) { - var i = 0; - var end = helper.callFuncAfter(done, 2); - sub.on('subscribe', function (chnl, count) { - assert.strictEqual(typeof count, 'number'); - assert.strictEqual(++i, count); - }); - sub.on('unsubscribe', function (chnl, count) { - assert.strictEqual(typeof count, 'number'); - assert.strictEqual(--i, count); - }); - sub.subscribe(channel, channel2); - sub.unsubscribe(function (err, res) { // Do not pass a channel here! - assert.strictEqual(sub.pub_sub_mode, 2); - assert.deepEqual(sub.subscription_set, {}); - end(); - }); - sub.set('foo', 'bar', helper.isString('OK')); - sub.subscribe(channel2, end); - }); - }); - - describe('subscribe', function () { - it('fires a subscribe event for each channel subscribed to even after reconnecting', function (done) { - var a = false; - sub.on('subscribe', function (chnl, count) { - if (chnl === channel2) { - assert.equal(2, count); - if (a) return done(); - sub.stream.destroy(); - } - }); - - sub.on('reconnecting', function () { - a = true; - }); - - sub.subscribe(channel, channel2); - }); - - it('fires a subscribe event for each channel as buffer subscribed to even after reconnecting', function (done) { - var a = false; - sub.end(true); - sub = redis.createClient({ - detect_buffers: true - }); - sub.on('subscribe', function (chnl, count) { - if (chnl.inspect() === Buffer.from([0xAA, 0xBB, 0x00, 0xF0]).inspect()) { - assert.equal(1, count); - if (a) { - return done(); - } - sub.stream.destroy(); - } - }); - - sub.on('reconnecting', function () { - a = true; - }); - - sub.subscribe(Buffer.from([0xAA, 0xBB, 0x00, 0xF0]), channel2); - }); - - it('receives messages on subscribed channel', function (done) { - var end = helper.callFuncAfter(done, 2); - sub.on('subscribe', function (chnl, count) { - pub.publish(channel, message, function (err, res) { - helper.isNumber(1)(err, res); - end(); - }); - }); - - sub.on('message', function (chnl, msg) { - assert.equal(chnl, channel); - assert.equal(msg, message); - end(); - }); - - sub.subscribe(channel); - }); - - it('receives messages if subscribe is called after unsubscribe', function (done) { - var end = helper.callFuncAfter(done, 2); - sub.once('subscribe', function (chnl, count) { - pub.publish(channel, message, function (err, res) { - helper.isNumber(1)(err, res); - end(); - }); - }); - - sub.on('message', function (chnl, msg) { - assert.equal(chnl, channel); - assert.equal(msg, message); - end(); - }); - - sub.subscribe(channel); - sub.unsubscribe(channel); - sub.subscribe(channel); - }); - - it('handles SUB_UNSUB_MSG_SUB', function (done) { - sub.subscribe('chan8'); - sub.subscribe('chan9'); - sub.unsubscribe('chan9'); - pub.publish('chan8', 'something'); - sub.subscribe('chan9', done); - }); - - it('handles SUB_UNSUB_MSG_SUB 2', function (done) { - sub.psubscribe('abc*', helper.isString('abc*')); - sub.subscribe('xyz'); - sub.unsubscribe('xyz'); - pub.publish('abcd', 'something'); - sub.subscribe('xyz', done); - }); - - it('emits end event if quit is called from within subscribe', function (done) { - sub.on('end', done); - sub.on('subscribe', function (chnl, count) { - sub.quit(); - }); - sub.subscribe(channel); - }); - - it('subscribe; close; resubscribe with prototype inherited property names', function (done) { - var count = 0; - var channels = ['channel 1', 'channel 2']; - var msg = ['hi from channel 1', 'hi from channel 2']; - - sub.on('message', function (channel, message) { - var n = Math.max(count - 1, 0); - assert.strictEqual(channel, channels[n]); - assert.strictEqual(message, msg[n]); - if (count === 2) return done(); - sub.stream.end(); - }); - - sub.select(3); - sub.subscribe(channels); - - sub.on('ready', function (err, results) { - pub.publish(channels[count], msg[count]); - count++; - }); - - pub.publish(channels[count], msg[count]); - }); - }); - - describe('multiple subscribe / unsubscribe commands', function () { - - it('reconnects properly with pub sub and select command', function (done) { - var end = helper.callFuncAfter(done, 2); - sub.select(3); - sub.set('foo', 'bar'); - sub.set('failure', helper.isError()); // Triggering a warning while subscribing should work - sub.mget('foo', 'bar', 'baz', 'hello', 'world', function (err, res) { - assert.deepEqual(res, ['bar', null, null, null, null]); - }); - sub.subscribe('somechannel', 'another channel', function (err, res) { - end(); - sub.stream.destroy(); - }); - assert(sub.ready); - sub.on('ready', function () { - sub.unsubscribe(); - sub.del('foo'); - sub.info(end); - }); - }); - - it('should not go into pubsub mode with unsubscribe commands', function (done) { - sub.on('unsubscribe', function (msg) { - // The unsubscribe should not be triggered, as there was no corresponding channel - throw new Error('Test failed'); - }); - sub.set('foo', 'bar'); - sub.unsubscribe(function (err, res) { - assert.strictEqual(res, null); - }); - sub.del('foo', done); - }); - - it('handles multiple channels with the same channel name properly, even with buffers', function (done) { - var channels = ['a', 'b', 'a', Buffer.from('a'), 'c', 'b']; - var subscribed_channels = [1, 2, 2, 2, 3, 3]; - var i = 0; - sub.subscribe(channels); - sub.on('subscribe', function (channel, count) { - if (Buffer.isBuffer(channel)) { - assert.strictEqual(channel.inspect(), Buffer.from(channels[i]).inspect()); - } else { - assert.strictEqual(channel, channels[i].toString()); - } - assert.strictEqual(count, subscribed_channels[i]); - i++; - }); - sub.unsubscribe('a', 'c', 'b'); - sub.get('foo', done); - }); - - it('should only resubscribe to channels not unsubscribed earlier on a reconnect', function (done) { - sub.subscribe('/foo', '/bar'); - sub.batch().unsubscribe(['/bar'], function () { - pub.pubsub('channels', function (err, res) { - assert.deepEqual(res, ['/foo']); - sub.stream.destroy(); - sub.once('ready', function () { - pub.pubsub('channels', function (err, res) { - assert.deepEqual(res, ['/foo']); - sub.unsubscribe('/foo', done); - }); - }); - }); - }).exec(); - }); - - it('unsubscribes, subscribes, unsubscribes... single and multiple entries mixed. Withouth callbacks', function (done) { - function subscribe (channels) { - sub.unsubscribe(helper.isNull); - sub.subscribe(channels, helper.isNull); - } - var all = false; - var subscribeMsg = ['1', '3', '2', '5', 'test', 'bla']; - sub.on('subscribe', function (msg, count) { - subscribeMsg.splice(subscribeMsg.indexOf(msg), 1); - if (subscribeMsg.length === 0 && all) { - assert.strictEqual(count, 3); - done(); - } - }); - var unsubscribeMsg = ['1', '3', '2']; - sub.on('unsubscribe', function (msg, count) { - unsubscribeMsg.splice(unsubscribeMsg.indexOf(msg), 1); - if (unsubscribeMsg.length === 0) { - assert.strictEqual(count, 0); - all = true; - } - }); - - subscribe(['1', '3']); - subscribe(['2']); - subscribe(['5', 'test', 'bla']); - }); - - it('unsubscribes, subscribes, unsubscribes... single and multiple entries mixed. Without callbacks', function (done) { - function subscribe (channels) { - sub.unsubscribe(); - sub.subscribe(channels); - } - var all = false; - var subscribeMsg = ['1', '3', '2', '5', 'test', 'bla']; - sub.on('subscribe', function (msg, count) { - subscribeMsg.splice(subscribeMsg.indexOf(msg), 1); - if (subscribeMsg.length === 0 && all) { - assert.strictEqual(count, 3); - done(); - } - }); - var unsubscribeMsg = ['1', '3', '2']; - sub.on('unsubscribe', function (msg, count) { - unsubscribeMsg.splice(unsubscribeMsg.indexOf(msg), 1); - if (unsubscribeMsg.length === 0) { - assert.strictEqual(count, 0); - all = true; - } - }); - - subscribe(['1', '3']); - subscribe(['2']); - subscribe(['5', 'test', 'bla']); - }); - - it('unsubscribes, subscribes, unsubscribes... single and multiple entries mixed. Without callback and concret channels', function (done) { - function subscribe (channels) { - sub.unsubscribe(channels); - sub.unsubscribe(channels); - sub.subscribe(channels); - } - var all = false; - var subscribeMsg = ['1', '3', '2', '5', 'test', 'bla']; - sub.on('subscribe', function (msg, count) { - subscribeMsg.splice(subscribeMsg.indexOf(msg), 1); - if (subscribeMsg.length === 0 && all) { - assert.strictEqual(count, 6); - done(); - } - }); - var unsubscribeMsg = ['1', '3', '2', '5', 'test', 'bla']; - sub.on('unsubscribe', function (msg, count) { - var pos = unsubscribeMsg.indexOf(msg); - if (pos !== -1) - unsubscribeMsg.splice(pos, 1); - if (unsubscribeMsg.length === 0) { - all = true; - } - }); - - subscribe(['1', '3']); - subscribe(['2']); - subscribe(['5', 'test', 'bla']); - }); - - it('unsubscribes, subscribes, unsubscribes... with pattern matching', function (done) { - function subscribe (channels, callback) { - sub.punsubscribe('prefix:*', helper.isNull); - sub.psubscribe(channels, function (err, res) { - helper.isNull(err); - if (callback) callback(err, res); - }); - } - var all = false; - var end = helper.callFuncAfter(done, 8); - var subscribeMsg = ['prefix:*', 'prefix:3', 'prefix:2', '5', 'test:a', 'bla']; - sub.on('psubscribe', function (msg, count) { - subscribeMsg.splice(subscribeMsg.indexOf(msg), 1); - if (subscribeMsg.length === 0) { - assert.strictEqual(count, 5); - all = true; - } - }); - var rest = 1; - var unsubscribeMsg = ['prefix:*', 'prefix:*', 'prefix:*', '*']; - sub.on('punsubscribe', function (msg, count) { - unsubscribeMsg.splice(unsubscribeMsg.indexOf(msg), 1); - if (all) { - assert.strictEqual(unsubscribeMsg.length, 0); - assert.strictEqual(count, rest--); // Print the remaining channels - end(); - } else { - assert.strictEqual(msg, 'prefix:*'); - assert.strictEqual(count, rest++ - 1); - } - }); - sub.on('pmessage', function (pattern, channel, msg) { - assert.strictEqual(msg, 'test'); - assert.strictEqual(pattern, 'prefix:*'); - assert.strictEqual(channel, 'prefix:1'); - end(); - }); - - subscribe(['prefix:*', 'prefix:3'], function () { - pub.publish('prefix:1', Buffer.from('test'), function () { - subscribe(['prefix:2']); - subscribe(['5', 'test:a', 'bla'], function () { - assert(all); - }); - sub.punsubscribe(function (err, res) { - assert(!err); - assert.strictEqual(res, 'bla'); - assert(all); - all = false; // Make sure the callback is actually after the emit - end(); - }); - sub.pubsub('channels', function (err, res) { - assert.strictEqual(res.length, 0); - end(); - }); - }); - }); - }); - }); - - describe('unsubscribe', function () { - it('fires an unsubscribe event', function (done) { - sub.on('subscribe', function (chnl, count) { - sub.unsubscribe(channel); - }); - - sub.subscribe(channel); - - sub.on('unsubscribe', function (chnl, count) { - assert.equal(chnl, channel); - assert.strictEqual(count, 0); - return done(); - }); - }); - - it('puts client back into write mode', function (done) { - sub.on('subscribe', function (chnl, count) { - sub.unsubscribe(channel); - }); - - sub.subscribe(channel); - - sub.on('unsubscribe', function (chnl, count) { - pub.incr('foo', helper.isNumber(1, done)); - }); - }); - - it('does not complain when unsubscribe is called and there are no subscriptions', function (done) { - sub.unsubscribe(function (err, res) { - assert.strictEqual(err, null); - assert.strictEqual(res, null); - done(); - }); - }); - - it('executes callback when unsubscribe is called and there are no subscriptions', function (done) { - pub.unsubscribe(function (err, results) { - assert.strictEqual(null, results); - done(err); - }); - }); - }); - - describe('psubscribe', function () { - it('allows all channels to be subscribed to using a * pattern', function (done) { - sub.subscribe('/foo'); - var sub2 = redis.createClient({ - return_buffers: true - }); - sub2.on('ready', function () { - sub2.batch().psubscribe('*', helper.isString('*')).exec(); - sub2.subscribe('/foo'); - sub2.on('pmessage', function (pattern, channel, message) { - assert.strictEqual(pattern.inspect(), Buffer.from('*').inspect()); - assert.strictEqual(channel.inspect(), Buffer.from('/foo').inspect()); - assert.strictEqual(message.inspect(), Buffer.from('hello world').inspect()); - sub2.quit(done); - }); - pub.pubsub('numsub', '/foo', function (err, res) { - assert.deepEqual(res, ['/foo', 2]); - }); - // sub2 is counted twice as it subscribed with psubscribe and subscribe - pub.publish('/foo', 'hello world', helper.isNumber(3)); - }); - }); - - it('allows to listen to pmessageBuffer and pmessage', function (done) { - var end = helper.callFuncAfter(done, 6); - var data = Array(10000).join('äüs^öéÉÉ`e'); - sub.set('foo', data, function () { - sub.get('foo'); - sub.stream.once('data', function () { - assert.strictEqual(sub.message_buffers, false); - assert.strictEqual(sub.shouldBuffer, false); - sub.on('pmessageBuffer', function (pattern, channel) { - if (typeof pattern === 'string') { - pattern = Buffer.from(pattern); - channel = Buffer.from(channel); - } - assert.strictEqual(pattern.inspect(), Buffer.from('*').inspect()); - assert.strictEqual(channel.inspect(), Buffer.from('/foo').inspect()); - sub.quit(end); - }); - // Either message_buffers or buffers has to be true, but not both at the same time - assert.notStrictEqual(sub.message_buffers, sub.buffers); - }); - var batch = sub.batch(); - batch.psubscribe('*'); - batch.subscribe('/foo'); - batch.unsubscribe('/foo'); - batch.unsubscribe(helper.isNull()); - batch.subscribe(['/foo'], helper.isString('/foo')); - batch.exec(function () { - pub.pubsub('numsub', '/foo', function (err, res) { - // There's one subscriber to this channel - assert.deepEqual(res, ['/foo', 1]); - end(); - }); - pub.pubsub('channels', function (err, res) { - // There's exactly one channel that is listened too - assert.deepEqual(res, ['/foo']); - end(); - }); - pub.pubsub('numpat', function (err, res) { - // One pattern is active - assert.strictEqual(res, 1); - end(); - }); - pub.publish('/foo', 'hello world', helper.isNumber(2)); - }); - // Either message_buffers or buffers has to be true, but not both at the same time - sub.on('pmessage', function (pattern, channel, message) { - assert.strictEqual(pattern, '*'); - assert.strictEqual(channel, '/foo'); - assert.strictEqual(message, 'hello world'); - end(); - }); - sub.on('message', function (channel, message) { - assert.strictEqual(channel, '/foo'); - assert.strictEqual(message, 'hello world'); - end(); - }); - }); - }); - }); - - describe('punsubscribe', function () { - it('does not complain when punsubscribe is called and there are no subscriptions', function () { - sub.punsubscribe(); - }); - - it('executes callback when punsubscribe is called and there are no subscriptions', function (done) { - pub.batch().punsubscribe(helper.isNull()).exec(done); - }); - }); - - describe('fail for other commands while in pub sub mode', function () { - it('return error if only pub sub commands are allowed', function (done) { - sub.subscribe('channel'); - // Ping is allowed even if not listed as such! - sub.ping(function (err, res) { - assert.strictEqual(err, null); - assert.strictEqual(res[0], 'pong'); - }); - // Get is forbidden - sub.get('foo', function (err, res) { - assert.ok(errors.subscribeUnsubscribeOnly.test(err.message)); - assert.strictEqual(err.command, 'GET'); - }); - // Quit is allowed - sub.quit(done); - }); - - it('emit error if only pub sub commands are allowed without callback', function (done) { - sub.subscribe('channel'); - sub.on('error', function (err) { - assert.ok(errors.subscribeUnsubscribeOnly.test(err.message)); - assert.strictEqual(err.command, 'GET'); - done(); - }); - sub.get('foo'); - }); - }); - - it('should not publish a message multiple times per command', function (done) { - var published = {}; - - function subscribe (message) { - sub.removeAllListeners('subscribe'); - sub.removeAllListeners('message'); - sub.removeAllListeners('unsubscribe'); - sub.on('subscribe', function () { - pub.publish('/foo', message); - }); - sub.on('message', function (channel, message) { - if (published[message]) { - done(new Error('Message published more than once.')); - } - published[message] = true; - }); - sub.on('unsubscribe', function (channel, count) { - assert.strictEqual(count, 0); - }); - sub.subscribe('/foo'); - } - - subscribe('hello'); - - setTimeout(function () { - sub.unsubscribe(); - setTimeout(function () { - subscribe('world'); - setTimeout(done, 50); - }, 40); - }, 40); - }); - - it('should not publish a message without any publish command', function (done) { - pub.set('foo', 'message'); - pub.set('bar', 'hello'); - pub.mget('foo', 'bar'); - pub.subscribe('channel', function () { - setTimeout(done, 50); - }); - pub.on('message', function (msg) { - done(new Error('This message should not have been published: ' + msg)); - }); - }); - - it('arguments variants', function (done) { - sub.batch() - .info(['stats']) - .info() - .client('KILL', ['type', 'pubsub']) - .client('KILL', ['type', 'pubsub'], function () {}) - .unsubscribe() - .psubscribe(['pattern:*']) - .punsubscribe('unkown*') - .punsubscribe(['pattern:*']) - .exec(function (err, res) { - sub.client('kill', ['type', 'pubsub']); - sub.psubscribe('*'); - sub.punsubscribe('pa*'); - sub.punsubscribe(['a', '*'], done); - }); - }); - - afterEach(function () { - // Explicitly ignore still running commands - pub.end(false); - sub.end(false); - }); - }); - }); -}); diff --git a/test/rename.spec.js b/test/rename.spec.js deleted file mode 100644 index 68adc5699f..0000000000 --- a/test/rename.spec.js +++ /dev/null @@ -1,147 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var helper = require('./helper'); -var redis = config.redis; - -if (process.platform === 'win32') { - // TODO: Fix redis process spawn on windows - return; -} - -// TODO these tests are causing flakey tests - looks like redis-server is not -// being started with new configuration after or before these tests -xdescribe('rename commands', function () { - before(function (done) { - helper.stopRedis(function () { - helper.startRedis('./conf/rename.conf', done); - }); - }); - - helper.allTests(function (ip, args) { - - describe('using ' + ip, function () { - var client = null; - - beforeEach(function (done) { - if (helper.redisProcess().spawnFailed()) return done(); - client = redis.createClient({ - rename_commands: { - set: '807081f5afa96845a02816a28b7258c3', - GETRANGE: '9e3102b15cf231c4e9e940f284744fe0' - }, - }); - - client.on('ready', function () { - client.flushdb(done); - }); - }); - - afterEach(function () { - if (helper.redisProcess().spawnFailed()) return; - client.end(true); - }); - - it('allows to use renamed functions', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client.set('key', 'value', function (err, reply) { - assert.strictEqual(reply, 'OK'); - }); - - client.get('key', function (err, reply) { - assert.strictEqual(err.message, 'ERR unknown command `get`, with args beginning with: `key`, '); - assert.strictEqual(err.command, 'GET'); - assert.strictEqual(reply, undefined); - }); - - client.getrange('key', 1, -1, function (err, reply) { - assert.strictEqual(reply, 'alue'); - assert.strictEqual(err, null); - done(); - }); - }); - - it('should also work with batch', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client.batch([['set', 'key', 'value']]).exec(function (err, res) { - assert.strictEqual(res[0], 'OK'); - }); - - var batch = client.batch(); - batch.getrange('key', 1, -1); - batch.exec(function (err, res) { - assert(!err); - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0], 'alue'); - done(); - }); - }); - - it('should also work with multi', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client.multi([['set', 'key', 'value']]).exec(function (err, res) { - assert.strictEqual(res[0], 'OK'); - }); - - var multi = client.multi(); - multi.getrange('key', 1, -1); - multi.exec(function (err, res) { - assert(!err); - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0], 'alue'); - done(); - }); - }); - - it('should also work with multi and abort transaction', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - var multi = client.multi(); - multi.get('key'); - multi.getrange('key', 1, -1, function (err, reply) { - assert.strictEqual(reply, 'alue'); - assert.strictEqual(err, null); - }); - multi.exec(function (err, res) { - assert(err); - assert.strictEqual(err.message, 'EXECABORT Transaction discarded because of previous errors.'); - assert.strictEqual(err.errors[0].message, 'ERR unknown command `get`, with args beginning with: `key`, '); - assert.strictEqual(err.errors[0].command, 'GET'); - assert.strictEqual(err.code, 'EXECABORT'); - assert.strictEqual(err.errors[0].code, 'ERR'); - done(); - }); - }); - - it('should also work prefixed commands', function (done) { - if (helper.redisProcess().spawnFailed()) this.skip(); - - client.end(true); - client = redis.createClient({ - rename_commands: { - set: '807081f5afa96845a02816a28b7258c3' - }, - prefix: 'baz' - }); - client.set('foo', 'bar'); - client.keys('*', function (err, reply) { - assert.strictEqual(reply[0], 'bazfoo'); - assert.strictEqual(err, null); - done(); - }); - }); - - }); - }); - - after(function (done) { - if (helper.redisProcess().spawnFailed()) return done(); - helper.stopRedis(function () { - helper.startRedis('./conf/redis.conf', done); - }); - }); -}); diff --git a/test/return_buffers.spec.js b/test/return_buffers.spec.js deleted file mode 100644 index 22efb31a04..0000000000 --- a/test/return_buffers.spec.js +++ /dev/null @@ -1,297 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var helper = require('./helper'); -var redis = config.redis; - -describe('return_buffers', function () { - - helper.allTests(function (ip, basicArgs) { - - describe('using ' + ip, function () { - var client; - var args = config.configureClient(ip, { - return_buffers: true, - detect_buffers: true - }); - - beforeEach(function (done) { - client = redis.createClient.apply(null, args); - var i = 1; - if (args[2].detect_buffers) { - // Test if detect_buffer option was deactivated - assert.strictEqual(client.options.detect_buffers, false); - args[2].detect_buffers = false; - i++; - } - var end = helper.callFuncAfter(done, i); - client.on('warning', function (msg) { - assert.strictEqual(msg, 'WARNING: You activated return_buffers and detect_buffers at the same time. The return value is always going to be a buffer.'); - end(); - }); - client.once('error', done); - client.once('connect', function () { - client.flushdb(function (err) { - client.hmset('hash key 2', 'key 1', 'val 1', 'key 2', 'val 2'); - client.set('string key 1', 'string value'); - end(err); - }); - }); - }); - - afterEach(function () { - client.end(true); - }); - - describe('get', function () { - describe('first argument is a string', function () { - it('returns a buffer', function (done) { - client.get('string key 1', function (err, reply) { - assert.strictEqual(true, Buffer.isBuffer(reply)); - assert.strictEqual('', reply.inspect()); - return done(err); - }); - }); - - it('returns a bufffer when executed as part of transaction', function (done) { - client.multi().get('string key 1').exec(function (err, reply) { - assert.strictEqual(1, reply.length); - assert.strictEqual(true, Buffer.isBuffer(reply[0])); - assert.strictEqual('', reply[0].inspect()); - return done(err); - }); - }); - }); - }); - - describe('multi.hget', function () { - it('returns buffers', function (done) { - client.multi() - .hget('hash key 2', 'key 1') - .hget(Buffer.from('hash key 2'), 'key 1') - .hget('hash key 2', Buffer.from('key 2')) - .hget('hash key 2', 'key 2') - .exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(4, reply.length); - assert.strictEqual('', reply[0].inspect()); - assert.strictEqual(true, Buffer.isBuffer(reply[1])); - assert.strictEqual('', reply[1].inspect()); - assert.strictEqual(true, Buffer.isBuffer(reply[2])); - assert.strictEqual('', reply[2].inspect()); - assert.strictEqual(true, Buffer.isBuffer(reply[3])); - assert.strictEqual('', reply[3].inspect()); - return done(err); - }); - }); - }); - - describe('batch.hget', function () { - it('returns buffers', function (done) { - client.batch() - .hget('hash key 2', 'key 1') - .hget(Buffer.from('hash key 2'), 'key 1') - .hget('hash key 2', Buffer.from('key 2')) - .hget('hash key 2', 'key 2') - .exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(4, reply.length); - assert.strictEqual('', reply[0].inspect()); - assert.strictEqual(true, Buffer.isBuffer(reply[1])); - assert.strictEqual('', reply[1].inspect()); - assert.strictEqual(true, Buffer.isBuffer(reply[2])); - assert.strictEqual('', reply[2].inspect()); - assert.strictEqual(true, Buffer.isBuffer(reply[3])); - assert.strictEqual('', reply[3].inspect()); - return done(err); - }); - }); - }); - - describe('hmget', function () { - describe('first argument is a string', function () { - it('handles array of strings with undefined values in transaction (repro #344)', function (done) { - client.multi().hmget('hash key 2', 'key 3', 'key 4').exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(1, reply.length); - assert.strictEqual(2, reply[0].length); - assert.equal(null, reply[0][0]); - assert.equal(null, reply[0][1]); - return done(err); - }); - }); - }); - - describe('first argument is a buffer', function () { - it('returns buffers for keys requested', function (done) { - client.hmget(Buffer.from('hash key 2'), 'key 1', 'key 2', function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(2, reply.length); - assert.strictEqual(true, Buffer.isBuffer(reply[0])); - assert.strictEqual(true, Buffer.isBuffer(reply[1])); - assert.strictEqual('', reply[0].inspect()); - assert.strictEqual('', reply[1].inspect()); - return done(err); - }); - }); - - it('returns buffers for keys requested in transaction', function (done) { - client.multi().hmget(Buffer.from('hash key 2'), 'key 1', 'key 2').exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(1, reply.length); - assert.strictEqual(2, reply[0].length); - assert.strictEqual(true, Buffer.isBuffer(reply[0][0])); - assert.strictEqual(true, Buffer.isBuffer(reply[0][1])); - assert.strictEqual('', reply[0][0].inspect()); - assert.strictEqual('', reply[0][1].inspect()); - return done(err); - }); - }); - - it('returns buffers for keys requested in .batch', function (done) { - client.batch().hmget(Buffer.from('hash key 2'), 'key 1', 'key 2').exec(function (err, reply) { - assert.strictEqual(true, Array.isArray(reply)); - assert.strictEqual(1, reply.length); - assert.strictEqual(2, reply[0].length); - assert.strictEqual(true, Buffer.isBuffer(reply[0][0])); - assert.strictEqual(true, Buffer.isBuffer(reply[0][1])); - assert.strictEqual('', reply[0][0].inspect()); - assert.strictEqual('', reply[0][1].inspect()); - return done(err); - }); - }); - }); - }); - - describe('hgetall', function (done) { - describe('first argument is a string', function () { - it('returns buffer values', function (done) { - client.hgetall('hash key 2', function (err, reply) { - assert.strictEqual('object', typeof reply); - assert.strictEqual(2, Object.keys(reply).length); - assert.strictEqual('', reply['key 1'].inspect()); - assert.strictEqual('', reply['key 2'].inspect()); - return done(err); - }); - }); - - it('returns buffer values when executed in transaction', function (done) { - client.multi().hgetall('hash key 2').exec(function (err, reply) { - assert.strictEqual(1, reply.length); - assert.strictEqual('object', typeof reply[0]); - assert.strictEqual(2, Object.keys(reply[0]).length); - assert.strictEqual('', reply[0]['key 1'].inspect()); - assert.strictEqual('', reply[0]['key 2'].inspect()); - return done(err); - }); - }); - - it('returns buffer values when executed in .batch', function (done) { - client.batch().hgetall('hash key 2').exec(function (err, reply) { - assert.strictEqual(1, reply.length); - assert.strictEqual('object', typeof reply[0]); - assert.strictEqual(2, Object.keys(reply[0]).length); - assert.strictEqual('', reply[0]['key 1'].inspect()); - assert.strictEqual('', reply[0]['key 2'].inspect()); - return done(err); - }); - }); - }); - - describe('first argument is a buffer', function () { - it('returns buffer values', function (done) { - client.hgetall(Buffer.from('hash key 2'), function (err, reply) { - assert.strictEqual(null, err); - assert.strictEqual('object', typeof reply); - assert.strictEqual(2, Object.keys(reply).length); - assert.strictEqual(true, Buffer.isBuffer(reply['key 1'])); - assert.strictEqual(true, Buffer.isBuffer(reply['key 2'])); - assert.strictEqual('', reply['key 1'].inspect()); - assert.strictEqual('', reply['key 2'].inspect()); - return done(err); - }); - }); - - it('returns buffer values when executed in transaction', function (done) { - client.multi().hgetall(Buffer.from('hash key 2')).exec(function (err, reply) { - assert.strictEqual(1, reply.length); - assert.strictEqual('object', typeof reply[0]); - assert.strictEqual(2, Object.keys(reply[0]).length); - assert.strictEqual(true, Buffer.isBuffer(reply[0]['key 1'])); - assert.strictEqual(true, Buffer.isBuffer(reply[0]['key 2'])); - assert.strictEqual('', reply[0]['key 1'].inspect()); - assert.strictEqual('', reply[0]['key 2'].inspect()); - return done(err); - }); - }); - - it('returns buffer values when executed in .batch', function (done) { - client.batch().hgetall(Buffer.from('hash key 2')).exec(function (err, reply) { - assert.strictEqual(1, reply.length); - assert.strictEqual('object', typeof reply[0]); - assert.strictEqual(2, Object.keys(reply[0]).length); - assert.strictEqual(true, Buffer.isBuffer(reply[0]['key 1'])); - assert.strictEqual(true, Buffer.isBuffer(reply[0]['key 2'])); - assert.strictEqual('', reply[0]['key 1'].inspect()); - assert.strictEqual('', reply[0]['key 2'].inspect()); - return done(err); - }); - }); - }); - }); - - describe('publish/subscribe', function (done) { - var pub; - var sub; - var channel = 'test channel'; - var message = Buffer.from('test message'); - - var args = config.configureClient(ip, { - return_buffers: true - }); - - beforeEach(function (done) { - var pubConnected; - var subConnected; - - pub = redis.createClient.apply(redis.createClient, basicArgs); - sub = redis.createClient.apply(null, args); - pub.once('connect', function () { - pub.flushdb(function () { - pubConnected = true; - if (subConnected) { - done(); - } - }); - }); - sub.once('connect', function () { - subConnected = true; - if (pubConnected) { - done(); - } - }); - }); - - it('receives buffer messages', function (done) { - sub.on('subscribe', function (chnl, count) { - pub.publish(channel, message); - }); - - sub.on('message', function (chnl, msg) { - assert.strictEqual(true, Buffer.isBuffer(msg)); - assert.strictEqual('', msg.inspect()); - return done(); - }); - - sub.subscribe(channel); - }); - - afterEach(function () { - sub.end(true); - pub.end(true); - }); - }); - }); - }); -}); diff --git a/test/tls.spec.js b/test/tls.spec.js deleted file mode 100644 index 127a2cfb8d..0000000000 --- a/test/tls.spec.js +++ /dev/null @@ -1,151 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var config = require('./lib/config'); -var fs = require('fs'); -var helper = require('./helper'); -var path = require('path'); -var redis = config.redis; -var utils = require('../lib/utils'); -var tls = require('tls'); - -var tls_options = { - servername: 'redis.js.org', - rejectUnauthorized: true, - ca: [ String(fs.readFileSync(path.resolve(__dirname, './conf/redis.js.org.cert'))) ] -}; - -var tls_port = 6380; -// Use skip instead of returning to indicate what tests really got skipped -var skip = false; - -describe('TLS connection tests', function () { - - before(function (done) { - // Print the warning when the tests run instead of while starting mocha - if (process.platform === 'win32') { - skip = true; - console.warn('\nStunnel tests do not work on windows atm. If you think you can fix that, it would be warmly welcome.\n'); - } - if (skip) return done(); - helper.stopStunnel(function () { - helper.startStunnel(done); - }); - }); - - after(function (done) { - if (skip) return done(); - helper.stopStunnel(done); - }); - - var client; - - afterEach(function () { - if (skip) return; - client.end(true); - }); - - describe('on lost connection', function () { - it('emit an error after max retry timeout and do not try to reconnect afterwards', function (done) { - if (skip) this.skip(); - var connect_timeout = 500; // in ms - client = redis.createClient({ - connect_timeout: connect_timeout, - port: tls_port, - tls: utils.clone(tls_options) - }); - var time = 0; - assert.strictEqual(client.address, '127.0.0.1:' + tls_port); - - client.once('ready', function () { - helper.killConnection(client); - }); - - client.on('reconnecting', function (params) { - time += params.delay; - }); - - client.on('error', function (err) { - if (/Redis connection in broken state: connection timeout.*?exceeded./.test(err.message)) { - process.nextTick(function () { - assert.strictEqual(time, connect_timeout); - assert.strictEqual(client.emitted_end, true); - assert.strictEqual(client.connected, false); - assert.strictEqual(client.ready, false); - assert.strictEqual(client.closing, true); - assert.strictEqual(time, connect_timeout); - done(); - }); - } - }); - }); - }); - - describe('when not connected', function () { - - it('connect with host and port provided in the tls object', function (done) { - if (skip) this.skip(); - var tls_opts = utils.clone(tls_options); - tls_opts.port = tls_port; - tls_opts.host = 'localhost'; - client = redis.createClient({ - connect_timeout: 1000, - tls: tls_opts - }); - - // verify connection is using TCP, not UNIX socket - assert.strictEqual(client.connection_options.host, 'localhost'); - assert.strictEqual(client.connection_options.port, tls_port); - assert.strictEqual(client.address, 'localhost:' + tls_port); - assert(client.stream.encrypted); - - client.set('foo', 'bar'); - client.get('foo', helper.isString('bar', done)); - }); - - describe('using rediss as url protocol', function () { - var tls_connect = tls.connect; - beforeEach(function () { - tls.connect = function (options) { - options = utils.clone(options); - options.ca = tls_options.ca; - options.servername = 'redis.js.org'; - options.rejectUnauthorized = true; - return tls_connect.call(tls, options); - }; - }); - afterEach(function () { - tls.connect = tls_connect; - }); - it('connect with tls when rediss is used as the protocol', function (done) { - if (skip) this.skip(); - client = redis.createClient('rediss://localhost:' + tls_port); - // verify connection is using TCP, not UNIX socket - assert(client.stream.encrypted); - client.set('foo', 'bar'); - client.get('foo', helper.isString('bar', done)); - }); - }); - - it('fails to connect because the cert is not correct', function (done) { - if (skip) this.skip(); - var faulty_cert = utils.clone(tls_options); - faulty_cert.ca = [ String(fs.readFileSync(path.resolve(__dirname, './conf/faulty.cert'))) ]; - client = redis.createClient({ - host: 'localhost', - connect_timeout: 1000, - port: tls_port, - tls: faulty_cert - }); - assert.strictEqual(client.address, 'localhost:' + tls_port); - client.on('error', function (err) { - assert(/DEPTH_ZERO_SELF_SIGNED_CERT/.test(err.code || err.message), err); - client.end(true); - }); - client.set('foo', 'bar', function (err, res) { - done(res); - }); - }); - - }); -}); diff --git a/test/unify_options.spec.js b/test/unify_options.spec.js deleted file mode 100644 index dcdd46d330..0000000000 --- a/test/unify_options.spec.js +++ /dev/null @@ -1,241 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var unifyOptions = require('../lib/createClient'); -var intercept = require('intercept-stdout'); - -describe('createClient options', function () { - - describe('port as first parameter', function () { - it('pass the options in the second parameter after a port', function () { - var options = unifyOptions(1234, { - option1: true, - option2: function () {} - }); - assert.strictEqual(Object.keys(options).length, 4); - assert(options.option1); - assert.strictEqual(options.port, 1234); - assert.strictEqual(options.host, undefined); - assert.strictEqual(typeof options.option2, 'function'); - }); - - it('pass the options in the third parameter after a port and host being set to null', function () { - var options = unifyOptions(1234, null, { - option1: true, - option2: function () {} - }); - assert.strictEqual(Object.keys(options).length, 4); - assert(options.option1); - assert.strictEqual(options.port, 1234); - assert.strictEqual(options.host, undefined); - assert.strictEqual(typeof options.option2, 'function'); - }); - - it('pass the options in the third parameter after a port and host being set to undefined', function () { - var options = unifyOptions(1234, undefined, { - option1: true, - option2: function () {} - }); - assert.strictEqual(Object.keys(options).length, 4); - assert(options.option1); - assert.strictEqual(options.port, 1234); - assert.strictEqual(options.host, undefined); - assert.strictEqual(typeof options.option2, 'function'); - }); - - it('pass the options in the third parameter after a port and host', function () { - var options = unifyOptions('1234', 'localhost', { - option1: true, - option2: function () {} - }); - assert.strictEqual(Object.keys(options).length, 4); - assert(options.option1); - assert.strictEqual(options.port, '1234'); - assert.strictEqual(options.host, 'localhost'); - assert.strictEqual(typeof options.option2, 'function'); - }); - - it('should throw with three parameters all set to a truthy value', function () { - try { - unifyOptions(1234, {}, {}); - throw new Error('failed'); - } catch (err) { - assert.strictEqual(err.message, 'Unknown type of connection in createClient()'); - } - }); - }); - - describe('unix socket as first parameter', function () { - it('pass the options in the second parameter after a port', function () { - var options = unifyOptions('/tmp/redis.sock', { - option1: true, - option2: function () {}, - option3: [1, 2, 3] - }); - assert.strictEqual(Object.keys(options).length, 4); - assert(options.option1); - assert.strictEqual(options.path, '/tmp/redis.sock'); - assert.strictEqual(typeof options.option2, 'function'); - assert.strictEqual(options.option3.length, 3); - }); - - it('pass the options in the third parameter after a port and host being set to null', function () { - var options = unifyOptions('/tmp/redis.sock', null, { - option1: true, - option2: function () {} - }); - assert.strictEqual(Object.keys(options).length, 3); - assert(options.option1); - assert.strictEqual(options.path, '/tmp/redis.sock'); - assert.strictEqual(typeof options.option2, 'function'); - }); - }); - - describe('redis url as first parameter', function () { - it('empty redis url including options as second parameter', function () { - var options = unifyOptions('redis://', { - option: [1, 2, 3] - }); - assert.strictEqual(Object.keys(options).length, 1); - assert.strictEqual(options.option.length, 3); - }); - - it('begin with two slashes including options as third parameter', function () { - var options = unifyOptions('//:abc@/3?port=123', { - option: [1, 2, 3] - }); - assert.strictEqual(Object.keys(options).length, 4); - assert.strictEqual(options.option.length, 3); - assert.strictEqual(options.port, '123'); - assert.strictEqual(options.db, '3'); - assert.strictEqual(options.password, 'abc'); - }); - - it('duplicated, identical query options including options obj', function () { - var text = ''; - var unhookIntercept = intercept(function (data) { - text += data; - return ''; - }); - var options = unifyOptions('//:abc@localhost:123/3?db=3&port=123&password=abc', null, { - option: [1, 2, 3] - }); - unhookIntercept(); - assert.strictEqual(text, - 'node_redis: WARNING: You passed the db option twice!\n' + - 'node_redis: WARNING: You passed the port option twice!\n' + - 'node_redis: WARNING: You passed the password option twice!\n' - ); - assert.strictEqual(Object.keys(options).length, 5); - assert.strictEqual(options.option.length, 3); - assert.strictEqual(options.host, 'localhost'); - assert.strictEqual(options.port, '123'); - assert.strictEqual(options.db, '3'); - assert.strictEqual(options.password, 'abc'); - }); - - it('should throw on duplicated, non-identical query options', function () { - try { - unifyOptions('//:abc@localhost:1234/3?port=123&password=abc'); - throw new Error('failed'); - } catch (err) { - assert.equal(err.message, 'The port option is added twice and does not match'); - } - }); - - it('should throw without protocol slashes', function () { - try { - unifyOptions('redis:abc@localhost:123/3?db=3&port=123&password=abc'); - throw new Error('failed'); - } catch (err) { - assert.equal(err.message, 'The redis url must begin with slashes "//" or contain slashes after the redis protocol'); - } - }); - - it('warns on protocol other than redis in the redis url', function () { - var text = ''; - var unhookIntercept = intercept(function (data) { - text += data; - return ''; - }); - var options = unifyOptions('http://abc'); - unhookIntercept(); - assert.strictEqual(Object.keys(options).length, 1); - assert.strictEqual(options.host, 'abc'); - assert.strictEqual(text, 'node_redis: WARNING: You passed "http" as protocol instead of the "redis" protocol!\n'); - }); - }); - - describe('no parameters or set to null / undefined', function () { - it('no parameters', function () { - var options = unifyOptions(); - assert.strictEqual(Object.keys(options).length, 1); - assert.strictEqual(options.host, undefined); - }); - - it('set to null', function () { - var options = unifyOptions(null, null); - assert.strictEqual(Object.keys(options).length, 1); - assert.strictEqual(options.host, null); - }); - - it('set to undefined', function () { - var options = unifyOptions(undefined, undefined); - assert.strictEqual(Object.keys(options).length, 1); - assert.strictEqual(options.host, undefined); - }); - }); - - describe('only an options object is passed', function () { - it('with options', function () { - var options = unifyOptions({ - option: true - }); - assert.strictEqual(Object.keys(options).length, 2); - assert.strictEqual(options.host, undefined); - assert.strictEqual(options.option, true); - }); - - it('without options', function () { - var options = unifyOptions({}); - assert.strictEqual(Object.keys(options).length, 1); - assert.strictEqual(options.host, undefined); - }); - - it('should throw with more parameters', function () { - try { - unifyOptions({ - option: true - }, undefined); - throw new Error('failed'); - } catch (err) { - assert.strictEqual(err.message, 'Too many arguments passed to createClient. Please only pass the options object'); - } - }); - - it('including url as option', function () { - var options = unifyOptions({ - option: [1, 2, 3], - url: '//hm:abc@localhost:123/3' - }); - assert.strictEqual(Object.keys(options).length, 7); - assert.strictEqual(options.option.length, 3); - assert.strictEqual(options.host, 'localhost'); - assert.strictEqual(options.port, '123'); - assert.strictEqual(options.db, '3'); - assert.strictEqual(options.url, '//hm:abc@localhost:123/3'); - assert.strictEqual(options.password, 'abc'); - }); - }); - - describe('faulty data', function () { - it('throws on strange connection info', function () { - try { - unifyOptions(true); - throw new Error('failed'); - } catch (err) { - assert.equal(err.message, 'Unknown type of connection in createClient()'); - } - }); - }); -}); diff --git a/test/utils.spec.js b/test/utils.spec.js deleted file mode 100644 index 592600fb6d..0000000000 --- a/test/utils.spec.js +++ /dev/null @@ -1,185 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var Queue = require('denque'); -var utils = require('../lib/utils'); -var intercept = require('intercept-stdout'); - -describe('utils.js', function () { - - describe('clone', function () { - it('ignore the object prototype and clone a nested array / object', function () { - var obj = { - a: [null, 'foo', ['bar'], { - "i'm special": true - }], - number: 5, - fn: function noop () {} - }; - var clone = utils.clone(obj); - assert.deepEqual(clone, obj); - assert.strictEqual(obj.fn, clone.fn); - assert(typeof clone.fn === 'function'); - }); - - it('replace falsy values with an empty object as return value', function () { - var a = utils.clone(); - var b = utils.clone(null); - assert.strictEqual(Object.keys(a).length, 0); - assert.strictEqual(Object.keys(b).length, 0); - }); - - it('transform camelCase options to snake_case and add the camel_case option', function () { - var a = utils.clone({ - optionOneTwo: true, - retryStrategy: false, - nested: { - onlyContainCamelCaseOnce: true - }, - tls: { - rejectUnauthorized: true - } - }); - assert.strictEqual(Object.keys(a).length, 5); - assert.strictEqual(a.option_one_two, true); - assert.strictEqual(a.retry_strategy, false); - assert.strictEqual(a.camel_case, true); - assert.strictEqual(a.tls.rejectUnauthorized, true); - assert.strictEqual(Object.keys(a.nested).length, 1); - }); - - it('throws on circular data', function () { - try { - var a = {}; - a.b = a; - utils.clone(a); - throw new Error('failed'); - } catch (e) { - assert(e.message !== 'failed'); - } - }); - }); - - describe('print helper', function () { - it('callback with reply', function () { - var text = ''; - var unhookIntercept = intercept(function (data) { - text += data; - return ''; - }); - utils.print(null, 'abc'); - unhookIntercept(); - assert.strictEqual(text, 'Reply: abc\n'); - }); - - it('callback with error', function () { - var text = ''; - var unhookIntercept = intercept(function (data) { - text += data; - return ''; - }); - utils.print(new Error('Wonderful exception')); - unhookIntercept(); - assert.strictEqual(text, 'Error: Wonderful exception\n'); - }); - }); - - describe('reply_in_order', function () { - - var err_count = 0; - var res_count = 0; - var emitted = false; - var clientMock = { - emit: function () { emitted = true; }, - offline_queue: new Queue(), - command_queue: new Queue() - }; - var create_command_obj = function () { - return { - callback: function (err, res) { - if (err) err_count++; - else res_count++; - } - }; - }; - - beforeEach(function () { - clientMock.offline_queue.clear(); - clientMock.command_queue.clear(); - err_count = 0; - res_count = 0; - emitted = false; - }); - - it('no elements in either queue. Reply in the next tick with callback', function (done) { - var called = false; - utils.reply_in_order(clientMock, function () { - called = true; - done(); - }, null, null); - assert(!called); - }); - - it('no elements in either queue. Reply in the next tick without callback', function (done) { - assert(!emitted); - utils.reply_in_order(clientMock, null, new Error('tada')); - assert(!emitted); - setTimeout(function () { - assert(emitted); - done(); - }, 1); - }); - - it('elements in the offline queue. Reply after the offline queue is empty and respect the command_obj callback', function (done) { - clientMock.offline_queue.push(create_command_obj()); - clientMock.offline_queue.push(create_command_obj()); - utils.reply_in_order(clientMock, function () { - assert.strictEqual(clientMock.offline_queue.length, 0); - assert.strictEqual(res_count, 2); - done(); - }, null, null); - while (clientMock.offline_queue.length) { - clientMock.offline_queue.shift().callback(null, 'foo'); - } - }); - - it('elements in the offline queue. Reply after the offline queue is empty and respect the command_obj error emit', function (done) { - clientMock.command_queue.push({}); - clientMock.command_queue.push(create_command_obj()); - clientMock.command_queue.push({}); - utils.reply_in_order(clientMock, function () { - assert.strictEqual(clientMock.command_queue.length, 0); - assert(emitted); - assert.strictEqual(err_count, 1); - assert.strictEqual(res_count, 0); - done(); - }, null, null); - while (clientMock.command_queue.length) { - var command_obj = clientMock.command_queue.shift(); - if (command_obj.callback) { - command_obj.callback(new Error('tada')); - } - } - }); - - it('elements in the offline queue and the command_queue. Reply all other commands got handled respect the command_obj', function (done) { - clientMock.command_queue.push(create_command_obj()); - clientMock.command_queue.push(create_command_obj()); - clientMock.command_queue.push(create_command_obj()); - clientMock.offline_queue.push({}); - utils.reply_in_order(clientMock, function (err, res) { - assert.strictEqual(clientMock.command_queue.length, 0); - assert.strictEqual(clientMock.offline_queue.length, 0); - assert(!emitted); - assert.strictEqual(res_count, 3); - done(); - }, null, null); - while (clientMock.offline_queue.length) { - clientMock.command_queue.push(clientMock.offline_queue.shift()); - } - while (clientMock.command_queue.length) { - clientMock.command_queue.shift().callback(null, 'hello world'); - } - }); - }); -}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..deebc9f125 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2019", + "lib": ["ES2019", "ES2020.BigInt", "ES2020.String", "ES2020.Symbol.WellKnown"], + "module": "CommonJS", + "moduleResolution": "Node", + "esModuleInterop": true, + "outDir": "./dist", + "declaration": true, + "useDefineForClassFields": true, + "allowJs": true + }, + "files": [ + "./lib/ts-declarations/cluster-key-slot.d.ts", + "./lib/ts-declarations/redis-parser.d.ts" + ], + "include": [ + "./index.ts", + "./lib/**/*.ts" + ], + "ts-node": { + "files": true + }, + "typedocOptions": { + "entryPoints": [ + "./index.ts", + "./lib" + ], + "exclude": [ + "./lib/ts-declarations", + "./lib/test-utils.ts" + ], + "theme": "./node_modules/typedoc-github-wiki-theme/dist", + "out": "documentation" + } +}